[Doctrine ORM] OneToMany met ManyToOne terugkoppeling

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • egonolieux
  • Registratie: Mei 2009
  • Laatst online: 06-01-2024

egonolieux

Professionele prutser

Topicstarter
Ik heb een applicatie met 3 subscription plans. Elke gebruiker heeft steeds 1 subscription actief. De gebruikers en hun subscriptions worden beschreven door het volgende model (MySQL):

Afbeeldingslocatie: https://image.prntscr.com/image/ieGxe6fMR5SkKJU3bxJTHA.png

De reden voor het bestaan van de user_subscription tabel is het bijhouden of een gebruiker zijn trial reeds heeft gebruikt of niet. In de user_subscription tabel zit dus maximaal hetzelfde aantal rijen als er subscription types zijn. Om de huidig actieve user_subscription aan te duiden is er een terugkoppeling naar de user tabel (subscription_id). Ook is er een terugkoppeling voor de vorige subscription die de user had (previous_subscription_id).

Dit werkt prima, maar bij het mappen/gebruiken van Doctrine ORM zit ik met een aantal problemen. De User entity bevat een OneToMany relatie naar user_subscription genaamd subscriptions. Dezelfde User entity bevat daarnaast ook 2 ManyToOne relaties om de huidige en vorige subscription aan te duiden. Als een user zijn subscription veranderd moet worden, doe ik dit telkens door de UserSubscriptionType mee te geven aan de changeSubscription() methode van de User entity:

Indien de UserSubscription voor het gegeven type reeds bestaat, is er geen probleem; dan haal ik de bestaande UserSubscription gewoonweg uit de subscriptions arraycollection en assign ik hem aan de subscription property om hem actief te zetten.

Indien de UserSubscription voor het gegeven type nog niet bestaat, moet ik deze eerst aanmaken en hier zit het probleem: ik moet de nieuwe UserSubscription instantie zowel aan de subscriptions arraycollection toevoegen als aan de subscription property toe kennen om hem actief te zetten. Doctrine lijkt hier niet goed mee overweg te kunnen: het maakt niet uit of ik enkel op de subscriptions arraycollection cascade persist zet of ook op de subscription property: ik krijg telkens de exception dat subscription_id null is bij het inserten in de tabel.

Hoe kan ik dit datamodel laten werken in Doctrine?

Beste antwoord (via egonolieux op 20-02-2018 19:53)


  • Joep
  • Registratie: December 2005
  • Laatst online: 11:42
egonolieux schreef op maandag 19 februari 2018 @ 21:40:
Niets te maken met Doctrine. De user_subscription_type tabel is gewoon een opsomming van alle mogelijke subscription types. Er kunnen bvb duizenden user_subscriptions zijn van 1 bepaald type.
Derp, duidelijk. Ik snap de structuur nu :>
egonolieux schreef op maandag 19 februari 2018 @ 21:36:
Als het echt niet lukt kan ik inderdaad 2 boolean kolommen is_current en is_previous toevoegen. Ik heb dan wel geen garantie dat eenzelfde rij niet zowel current als previous is. Ook heb ik dan geen garantie dat er telkens maar 1 current en previous waarde is, tenzij ik de boolean kolommen nullable maak met een unique index wat eigenlijk een vuile hack is.

Een andere iets mooiere oplossing is ipv met de user_subscription tabel een terugkoppeling te doen, rechtstreeks met user_subscription_type te werken. Los van dit specifieke voorbeeld zou ik in het algemeen willen weten of mijn probleemstelling werkend te krijgen is.
Ik zou de FK subscription_id niet gebruiken in de tabel user, aangezien het een 1-op-1-relatie is zoals @ikvanwinsum al zegt.

Als het de bedoeling is dat elke gebruiker maximaal maar 1 actieve subscription kan hebben en je daarnaast alleen maar de voorgaande subscription wilt weten, zou je zelfs zonder booleans, subscription_id en previous_subscription_id kunnen. Als je wilt weten wat de actieve subscription is zou dat met onderstaande query kunnen:
SQL:
1
2
3
4
5
6
SELECT user_subscription.id 
FROM user_subscription 
INNER JOIN user ON user_subscription.user_id = user.id 
INNER JOIN user_subscription_type ON user_subscription.type_id = user_subscription_type.id 
WHERE user_subscription.end_date_time > NOW() 
ORDER BY user_subscription_type.tier ASC, user_subscription.end_date_time DESC, user_subscription.trial_end_date_time DESC LIMIT 1

Deze query gaat er vanuit dat de duurste subscription voorrang heeft op goedkopere tiers, zelfs als die langer geldig zijn. Daarnaast heeft de goedkoopste actieve (einddatum later dan nu) betaalde subscription voorrang op welke trial dan ook. Het maakt met deze query niet uit hoeveel records een user heeft in de tabel user_subscription, wat je er ook in pleurt.

Wil je weten wat de vorige subscription is:
SQL:
1
2
3
4
5
6
SELECT user_subscription.id 
FROM user_subscription 
INNER JOIN user ON user_subscription.user_id = user.id 
INNER JOIN user_subscription_type ON user_subscription.type_id = user_subscription_type.id 
WHERE user_subscription.end_date_time > NOW() 
ORDER BY user_subscription_type.tier ASC, user_subscription.end_date_time DESC, user_subscription.trial_end_date_time DESC LIMIT 1,1


Ik heb de code niet kunnen testen, maar ik dacht dat het klopt.

Alle reacties


Acties:
  • 0 Henk 'm!

  • Crazy-
  • Registratie: Januari 2002
  • Laatst online: 00:35

Crazy-

Best life ever

Gooi je entititeiten eens hier neer; die code zegt meer dan een tabel relatie :)

Met name het gedeelte van je user entity en alles geralateerd aan Subscription.

Je dient namelijk inderdaad wel je actieve subscription te koppelen aan je User. Doctrine weet uiteraard niet wat de actieve is

[ Voor 90% gewijzigd door Crazy- op 19-02-2018 21:05 ]

12,85kWp - ZB 7,5m2/400l - 5kW Pana H WP (CV&SWW) - 13,8kWh accu


Acties:
  • 0 Henk 'm!

  • egonolieux
  • Registratie: Mei 2009
  • Laatst online: 06-01-2024

egonolieux

Professionele prutser

Topicstarter
Zo, heb even alle niet relevante properties en methodes weggelaten ;)

De User entity:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<?php

namespace AppBundle\Entity;

use AppBundle\Entity\Entity;
use AppBundle\Entity\UserSubscription;
use AppBundle\Entity\UserSubscriptionType;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
 * @ORM\Table(name="user")
 */
class User extends Entity
{
    /**
     * @ORM\Id
     * @ORM\Column(name="id", type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\OneToMany(
     *     targetEntity="AppBundle\Entity\UserSubscription",
     *     mappedBy="user",
     *     cascade={"persist", "merge", "detach"}
     * )
     */
    private $subscriptions;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\UserSubscription", cascade="merge")
     * @ORM\JoinColumn(name="subscription_id", referencedColumnName="id")
     */
    private $subscription;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\UserSubscription", cascade="merge")
     * @ORM\JoinColumn(name="previous_subscription_id", referencedColumnName="id", nullable=true)
     */
    private $previousSubscription;

    public function __construct()
    {
        $this->subscriptions = new ArrayCollection();
    }

    public function getId() : ?int
    {
        return $this->id;
    }

    public function getSubscription(?UserSubscriptionType $userSubscriptionType = null) : ?UserSubscription
    {
        return ($userSubscriptionType === null)
            ? $this->subscription
            : $this->getSubscriptionByType($userSubscriptionType);
    }

    public function getPreviousSubscription() : ?UserSubscription
    {
        return $this->previousSubscription;
    }

    public function changeSubscription(
        UserSubscriptionType $userSubscriptionType,
        ?DateTime $endDateTime = null,
        ?DateTime $trialEndDateTime = null
    ) : User
    {
        $userSubscription = $this->getSubscriptionByType($userSubscriptionType);

        // The current user subscription is already of the given user subscription type.
        if (
            $userSubscription !== null &&
            $userSubscription->getType()->hasSameIdentity($this->subscription->getType())
        )
        {
            return $this;
        }

        // End the trial of the current subscription.
        if ($this->isSubscriptionTrialStarted())
        {
            $this->expireSubscriptionTrial();
        }

        $newUserSubscription = $userSubscription;

        // Create new user subscription.
        if ($newUserSubscription === null)
        {
            $newUserSubscription = new UserSubscription();
            $newUserSubscription
                ->setUser($this)
                ->setType($userSubscriptionType);

            $this->subscriptions->add($newUserSubscription);
        }

        $newUserSubscription
            ->setEndDateTime($endDateTime)
            ->setTrialEndDateTime($trialEndDateTime);

        $this->previousSubscription = $this->subscription;
        $this->subscription = $newUserSubscription;

        return $this;
    }

    public function isSubscriptionExpired(?UserSubscriptionType $userSubscriptionType = null) : bool
    {
        $userSubscription = $this->subscription;

        if ($userSubscriptionType !== null)
        {
            $userSubscription = $this->getSubscriptionByType($userSubscriptionType);
        }

        if ($userSubscription !== null)
        {
            $endDateTime = $userSubscription->getEndDateTime();
            return $endDateTime !== null && $endDateTime <= new DateTime('now');
        }

        return false;
    }

    public function expireSubscription(?DateTime $endDateTime = null) : User
    {
        if (!$this->isSubscriptionExpired())
        {
            $this->subscription->setEndDateTime($endDateTime ?? new DateTime('now'));
        }
        return $this;
    }

    public function isSubscriptionTrialStarted(?UserSubscriptionType $userSubscriptionType = null) : bool
    {
        $userSubscription = $this->subscription;

        if ($userSubscriptionType !== null)
        {
            $userSubscription = $this->getSubscriptionByType($userSubscriptionType);
        }

        if ($userSubscription !== null)
        {
            return $userSubscription->getTrialEndDateTime() !== null;
        }

        return false;
    }

    public function isSubscriptionTrialExpired(?UserSubscriptionType $userSubscriptionType = null) : bool
    {
        $userSubscription = $this->subscription;

        if ($userSubscriptionType !== null)
        {
            $userSubscription = $this->getSubscriptionByType($userSubscriptionType);
        }

        if ($userSubscription !== null)
        {
            $trialEndDateTime = $userSubscription->getTrialEndDateTime();
            return $trialEndDateTime !== null && $trialEndDateTime <= new DateTime('now');
        }

        return false;
    }

    public function expireSubscriptionTrial(?DateTime $trialEndDateTime = null) : User
    {
        if (!$this->isSubscriptionTrialStarted())
        {
            throw new Exception('Cannot expire the subscription trial because the trial has not been started yet.');
        }

        if (!$this->isSubscriptionTrialExpired())
        {
            $this->subscription->setTrialEndDateTime($trialEndDateTime ?? new DateTime('now'));
        }

        return $this;
    }

    public function isCardCollectionConversionNeeded() : bool
    {
        return
            $this->previousSubscription !== null &&
            $this->subscription->getType()->getTier() < $this->previousSubscription->getType()->getTier();
    }

    private function getSubscriptionByType(UserSubscriptionType $userSubscriptionType) : ?UserSubscription
    {
        foreach ($this->subscriptions as $userSubscription)
        {
            if ($userSubscription->getType()->hasSameIdentity($userSubscriptionType))
            {
                return $userSubscription;
            }
        }

        return null;
    }
}


De UserSubscription entity:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?php

namespace AppBundle\Entity;

use AppBundle\Entity\Entity;
use AppBundle\Entity\User;
use AppBundle\Entity\UserSubscriptionType;
use DateTime;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="user_subscription")
 */
class UserSubscription extends Entity
{
    /**
     * @ORM\Id
     * @ORM\Column(name="id", type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="subscriptions")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
     */
    private $user;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\UserSubscriptionType")
     * @ORM\JoinColumn(name="type_id", referencedColumnName="id")
     */
    private $type;

    /**
     * @ORM\Column(name="end_date_time", type="datetime", nullable=true)
     */
    private $endDateTime;

    /**
     * @ORM\Column(name="trial_end_date_time", type="datetime", nullable=true)
     */
    private $trialEndDateTime;

    public function getId() : ?int
    {
        return $this->id;
    }

    public function getUser() : ?User
    {
        return $this->user;
    }

    public function setUser(User $user) : UserSubscription
    {
        $this->user = $user;
        return $this;
    }

    public function getType() : ?UserSubscriptionType
    {
        return $this->type;
    }

    public function setType(UserSubscriptionType $userSubscriptionType) : UserSubscription
    {
        $this->type = $userSubscriptionType;
        return $this;
    }

    public function getEndDateTime() : ?DateTime
    {
        return $this->endDateTime;
    }

    public function setEndDateTime(?DateTime $endDateTime) : UserSubscription
    {
        $this->endDateTime = $endDateTime;
        return $this;
    }

    public function getTrialEndDateTime() : ?DateTime
    {
        return $this->trialEndDateTime;
    }

    public function setTrialEndDateTime(?DateTime $trialEndDateTime) : UserSubscription
    {
        $this->trialEndDateTime = $trialEndDateTime;
        return $this;
    }

    public function hasIdentity() : bool
    {
        return $this->id !== null;
    }

    public function hasSameIdentity(Entity $entity) : bool
    {
        if (!($entity instanceof UserSubscription))
        {
            return false;
        }
        return $this->id === $entity->getId();
    }
}


En de UserSubscriptionType entity:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php

namespace AppBundle\Entity;

use AppBundle\Entity\Entity;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(readOnly=true)
 * @ORM\Table(name="user_subscription_type")
 */
class UserSubscriptionType extends Entity
{
    /**
     * @ORM\Id
     * @ORM\Column(name="id", type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    
    /**
     * @ORM\Column(name="name", type="string", length=50)
     */
    private $name;

    /**
     * @ORM\Column(name="tier", type="integer")
     */
    private $tier;

    public function getId() : int
    {
        return $this->id;
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function getTier() : int
    {
        return $this->tier;
    }

    public function hasIdentity() : bool
    {
        return $this->id !== null;
    }

    public function hasSameIdentity(Entity $entity) : bool
    {
        if (!($entity instanceof UserSubscriptionType))
        {
            return false;
        }
        return $this->id === $entity->getId();
    }
}


Ik denk dat de meeste code wel voor zich spreekt bij het gedeelte waarin de subscriptions beheerd/veranderd worden, maar indien niet, laat het me gerust weten ;)

Acties:
  • 0 Henk 'm!

  • ikvanwinsum
  • Registratie: Februari 2011
  • Laatst online: 05-10 18:06

ikvanwinsum

/dev/null

egonolieux schreef op maandag 19 februari 2018 @ 20:46:
In de user_subscription tabel zit dus maximaal hetzelfde aantal rijen als er subscription types zijn.
Je bedoelt maximaal types*users rijen?
egonolieux schreef op maandag 19 februari 2018 @ 20:46:
Dezelfde User entity bevat daarnaast ook 2 ManyToOne relaties om de huidige en vorige subscription aan te duiden.
Waarom is dit geen one to one relatie? elke user heeft toch maar 1 actieve subscribtion en elk subscription entity hoort toch maar bij 1 user?
egonolieux schreef op maandag 19 februari 2018 @ 20:46:
Indien de UserSubscription voor het gegeven type reeds bestaat, is er geen probleem; dan haal ik de bestaande UserSubscription gewoonweg uit de subscriptions arraycollection en assign ik hem aan de subscription property om hem actief te zetten.

Indien de UserSubscription voor het gegeven type nog niet bestaat, moet ik deze eerst aanmaken en hier zit het probleem: ik moet de nieuwe UserSubscription instantie zowel aan de subscriptions arraycollection toevoegen als aan de subscription property toe kennen om hem actief te zetten. Doctrine lijkt hier niet goed mee overweg te kunnen: het maakt niet uit of ik enkel op de subscriptions arraycollection cascade persist zet of ook op de subscription property: ik krijg telkens de exception dat subscription_id null is bij het inserten in de tabel.

Hoe kan ik dit datamodel laten werken in Doctrine?
Kun je niet beter in je user_subscription tabel een soort 'active' boolean zetten? Dan ben je van die dubbele foreign key af. Nu maak je als het ware een cirkeltje :)

edit: als je in plaats van [code] tags nu [code=php] gebruikt hebben we ook nog syntax highlighting. :*)

[ Voor 3% gewijzigd door ikvanwinsum op 19-02-2018 21:25 ]

U zegt: ‘Alles is toegestaan.’ Zeker, maar niet alles is goed. Alles is toegestaan, maar niet alles is opbouwend.


Acties:
  • 0 Henk 'm!

  • Joep
  • Registratie: December 2005
  • Laatst online: 11:42
Ik heb geen verstand van doctrine, maar waarom heb je de tabel user_subscription_type überhaupt nodig? Om bij te houden welke subscriptions een user heeft/had kun je toch gewoon de tabel user_subscription gebruiken? Heeft het soms te maken met de manier waarop doctrine werkt?

Acties:
  • 0 Henk 'm!

  • egonolieux
  • Registratie: Mei 2009
  • Laatst online: 06-01-2024

egonolieux

Professionele prutser

Topicstarter
Je bedoelt maximaal types*users rijen?
Klopt, ik sprak in functie van een enkele user.
Waarom is dit geen one to one relatie? elke user heeft toch maar 1 actieve subscribtion en elk subscription entity hoort toch maar bij 1 user?
Daar heb je gelijk in. Ik denk dat ik me uit gewoonte vergist heb ;)
Kun je niet beter in je user_subscription tabel een soort 'active' boolean zetten? Dan ben je van die dubbele foreign key af. Nu maak je als het ware een cirkeltje
Ik heb mijn model juist zo gebouwd dat ik geen boolean kolom moet gebruiken; wat ik nu doe is de meest efficiente semantisch correcte manier van werken bij relationele databases. De vraag is nu of dit werkend te krijgen is in Doctrine.

Als het echt niet lukt kan ik inderdaad 2 boolean kolommen is_current en is_previous toevoegen. Ik heb dan wel geen garantie dat eenzelfde rij niet zowel current als previous is. Ook heb ik dan geen garantie dat er telkens maar 1 current en previous waarde is, tenzij ik de boolean kolommen nullable maak met een unique index wat eigenlijk een vuile hack is.

Een andere iets mooiere oplossing is ipv met de user_subscription tabel een terugkoppeling te doen, rechtstreeks met user_subscription_type te werken. Los van dit specifieke voorbeeld zou ik in het algemeen willen weten of mijn probleemstelling werkend te krijgen is.

[ Voor 5% gewijzigd door egonolieux op 19-02-2018 21:47 ]


Acties:
  • 0 Henk 'm!

  • egonolieux
  • Registratie: Mei 2009
  • Laatst online: 06-01-2024

egonolieux

Professionele prutser

Topicstarter
Joep schreef op maandag 19 februari 2018 @ 21:36:
Ik heb geen verstand van doctrine, maar waarom heb je de tabel user_subscription_type überhaupt nodig? Om bij te houden welke subscriptions een user heeft/had kun je toch gewoon de tabel user_subscription gebruiken? Heeft het soms te maken met de manier waarop doctrine werkt?
Niets te maken met Doctrine. De user_subscription_type tabel is gewoon een opsomming van alle mogelijke subscription types. Er kunnen bvb duizenden user_subscriptions zijn van 1 bepaald type.

Acties:
  • Beste antwoord
  • 0 Henk 'm!

  • Joep
  • Registratie: December 2005
  • Laatst online: 11:42
egonolieux schreef op maandag 19 februari 2018 @ 21:40:
Niets te maken met Doctrine. De user_subscription_type tabel is gewoon een opsomming van alle mogelijke subscription types. Er kunnen bvb duizenden user_subscriptions zijn van 1 bepaald type.
Derp, duidelijk. Ik snap de structuur nu :>
egonolieux schreef op maandag 19 februari 2018 @ 21:36:
Als het echt niet lukt kan ik inderdaad 2 boolean kolommen is_current en is_previous toevoegen. Ik heb dan wel geen garantie dat eenzelfde rij niet zowel current als previous is. Ook heb ik dan geen garantie dat er telkens maar 1 current en previous waarde is, tenzij ik de boolean kolommen nullable maak met een unique index wat eigenlijk een vuile hack is.

Een andere iets mooiere oplossing is ipv met de user_subscription tabel een terugkoppeling te doen, rechtstreeks met user_subscription_type te werken. Los van dit specifieke voorbeeld zou ik in het algemeen willen weten of mijn probleemstelling werkend te krijgen is.
Ik zou de FK subscription_id niet gebruiken in de tabel user, aangezien het een 1-op-1-relatie is zoals @ikvanwinsum al zegt.

Als het de bedoeling is dat elke gebruiker maximaal maar 1 actieve subscription kan hebben en je daarnaast alleen maar de voorgaande subscription wilt weten, zou je zelfs zonder booleans, subscription_id en previous_subscription_id kunnen. Als je wilt weten wat de actieve subscription is zou dat met onderstaande query kunnen:
SQL:
1
2
3
4
5
6
SELECT user_subscription.id 
FROM user_subscription 
INNER JOIN user ON user_subscription.user_id = user.id 
INNER JOIN user_subscription_type ON user_subscription.type_id = user_subscription_type.id 
WHERE user_subscription.end_date_time > NOW() 
ORDER BY user_subscription_type.tier ASC, user_subscription.end_date_time DESC, user_subscription.trial_end_date_time DESC LIMIT 1

Deze query gaat er vanuit dat de duurste subscription voorrang heeft op goedkopere tiers, zelfs als die langer geldig zijn. Daarnaast heeft de goedkoopste actieve (einddatum later dan nu) betaalde subscription voorrang op welke trial dan ook. Het maakt met deze query niet uit hoeveel records een user heeft in de tabel user_subscription, wat je er ook in pleurt.

Wil je weten wat de vorige subscription is:
SQL:
1
2
3
4
5
6
SELECT user_subscription.id 
FROM user_subscription 
INNER JOIN user ON user_subscription.user_id = user.id 
INNER JOIN user_subscription_type ON user_subscription.type_id = user_subscription_type.id 
WHERE user_subscription.end_date_time > NOW() 
ORDER BY user_subscription_type.tier ASC, user_subscription.end_date_time DESC, user_subscription.trial_end_date_time DESC LIMIT 1,1


Ik heb de code niet kunnen testen, maar ik dacht dat het klopt.

Acties:
  • 0 Henk 'm!

  • egonolieux
  • Registratie: Mei 2009
  • Laatst online: 06-01-2024

egonolieux

Professionele prutser

Topicstarter
Dat is inderdaad ook een mogelijke oplossing, alleen zou ik dan een extra niet-nullable kolom started_date_time toevoegen en die gebruiken ipv de end datetimes.

De reden is dat zowel end_date_time als trial_end_date_time praktisch altijd op null staan tenzij de gebruiker zijn subscription heeft stopgezet (en nog krediet heeft tot het einde van de periode) of een trial gestart heeft. Ik zou dan aflopend sorteren op die startdatum en dan de tier.

Het enige probleem dat ik met deze aanpak heb, is dat het niet direct duidelijk is vanuit het databasemodel dat een gebruiker maar 1 subscription actief mag hebben.

Acties:
  • 0 Henk 'm!

  • Joep
  • Registratie: December 2005
  • Laatst online: 11:42
Tsja, de structuur zoals je die nu hebt maakt ook niet duidelijk dat de tabel user_subscription per user maar 3 records mag bevatten, 1 per type subscription. Op een gegeven moment zul je bepaalde dingen toch moeten oplossen in de applicatie i.p.v. de database. Ik zou trial_end_date_time trouwens wegdoen en de trials gewoon als records toevoegen in de tabel user_subscription_type, met desnoods een boolean om aan te duiden of een record in de tabel user_subscription_type een trial betreft of niet.

Acties:
  • 0 Henk 'm!

  • egonolieux
  • Registratie: Mei 2009
  • Laatst online: 06-01-2024

egonolieux

Professionele prutser

Topicstarter
Er staat een gecombineerde unique key op type_id en user_id, dus die max 3 records per gebruiker is wel vastgelegd in de database. Wat het 1 type per subscription betreft, heb je wel gelijk in. Ik denk dat ik het bij jouw suggestie zal houden dus.

Ik zou de trials als apart type kunnen beschouwen, maar welk verschil zou dit maken?
Persoonlijk vind ik het correcter het type gescheiden te houden van de trials.

[ Voor 77% gewijzigd door egonolieux op 20-02-2018 06:22 ]


Acties:
  • +1 Henk 'm!

  • DJMaze
  • Registratie: Juni 2002
  • Niet online
Hmmm ik begrijp je insteek, maar hypothetisch gezien kan niet het volgende?
usertypeendend_trial
1basic2019-01-01null
1pronull2018-03-01

Dus een basic account + een pro trial?


Dan zou ik gewoon voor een subscriptions tabel gaan met een one-to-many en het hele gedoe uit de user tabel verwijderen.
Dan heb je ook altijd de volledige historie van alles.
usertypeendis_trialcreate date
1basic2017-01-0112016-12-01
1basic2018-01-0102017-01-01
1basic2019-01-0102018-01-01
1pro2018-03-0112018-02-01


SQL:
1
SELECT type, end, is_trial FROM subscriptions WHERE user = 1 AND end > CURRENT_DATE ORDER BY end ASC LIMIT 1

[ Voor 53% gewijzigd door DJMaze op 20-02-2018 01:13 ]

Maak je niet druk, dat doet de compressor maar


Acties:
  • 0 Henk 'm!

  • egonolieux
  • Registratie: Mei 2009
  • Laatst online: 06-01-2024

egonolieux

Professionele prutser

Topicstarter
Hmmm ik begrijp je insteek, maar hypothetisch gezien kan niet het volgende?
usertypeendend_trial
1basic2019-01-01null
1pronull2018-03-01

Dus een basic account + een pro trial?
Binnen de database is dit in principe mogelijk, maar binnen de applicatie zou dit nooit het geval mogen zijn: een trial is altijd gebonden met de user subscription type.

Overschakelen van een trial subscription naar een niet-trial subscription is ook nooit een nieuwe record: indien er een trial is, wordt de trial_end_date_time op een datum in de toekomst gezet, anders blijft die null.

Standard is een user_subscription oneindig in lengte (end_date_time = null), tenzij de user zijn subscription cancelled. Dan wordt de end_date_time op de datum gezet tot wanneer de user betaald heeft.

Jouw voorbeeld zou er dan zo uitzien:

user_idtype_idend_date_timetrial_end_date_timestart_date_time
1basic2017-01-012016-12-142016-12-01
1pronull2017-01-142017-01-01


De geschiedenis van alle subscriptions is mooi meegenomen, maar brengt wel een groot probleem met zich mee: de oneToMany relatie binnen Doctrine laadt alle records voor de user. Aangezien ik uit deze subscriptions de meest recente en de vorige subscription moet halen, is dit niet schaalbaar op termijn qua performance omdat er steeds meer subscriptions zullen bijkomen.

Ik zit dan eerder te denken aan gewoonweg een gecombineerde unique index te gebruiken op user_id en type_id. Als de gebruiker van subscription verandert, haal ik gewoon de bestaande record op en reset/verander de datum waarden (end en start).

Mijn eisen zijn eigenlijk redelijk simpel; ik wil volgende zaken in mijn applicatie uit de database kunnen halen:
  1. Een user heeft steeds 1 subscription actief, niet meer of minder
  2. Een user mag nooit meer dan 1x een trial van een bepaald type starten
  3. Ik moet een (optionele) einddatum kunnen instellen voor de actieve subscription
  4. Ik moet een (optionele) einddatum kunnen instellen voor de trial van een subscription
  5. Ik moet de vorige subscription kunnen achterhalen

Acties:
  • 0 Henk 'm!

  • DJMaze
  • Registratie: Juni 2002
  • Niet online
egonolieux schreef op dinsdag 20 februari 2018 @ 06:55:
De geschiedenis van alle subscriptions is mooi meegenomen, maar brengt wel een groot probleem met zich mee: de oneToMany relatie binnen Doctrine laadt alle records voor de user. Aangezien ik uit deze subscriptions de meest recente en de vorige subscription moet halen, is dit niet schaalbaar op termijn qua performance omdat er steeds meer subscriptions zullen bijkomen.
Ik was om verschillende redenen nooit fan van Doctrine en dus nooit echt gebruikt.
Maar als dit een groot Doctrine probleem is, dan ben ik blij dat ik het nooit gebruik.
Er zijn vast mensen die hier omheen werken met een querybuilder ofzo.

[ Voor 5% gewijzigd door DJMaze op 20-02-2018 13:18 ]

Maak je niet druk, dat doet de compressor maar


Acties:
  • 0 Henk 'm!

  • egonolieux
  • Registratie: Mei 2009
  • Laatst online: 06-01-2024

egonolieux

Professionele prutser

Topicstarter
Je kan met Doctrine evengoed filteren als met native SQL hoor. Het probleem in mijn geval is dat ik de oneToMany relatie direct aanspreek binnen een entity. Binnen de entity zelf zijn er weinig mogelijkheden tot filteren (tenzij eerst alles ophalen en dan filteren in de getter). Dat is ook logisch want een entity/domein object hoeft het concept "database" niet te kennen. Moest ik de logica in een aparte service class doen was dit geen enkel probleem, maar dit is in mijn geval niet mogelijk. Ik denk dat dit een typisch geval is waar je de database en de ORM op elkaar moet afstellen.

[ Voor 23% gewijzigd door egonolieux op 20-02-2018 19:58 ]

Pagina: 1