3

I currently have to make the following decision for my model Order: Either I use GenericForeignKey which refers either to the models DiscountModel or AnotherDiscountModel. There can only be one of those, so from the idea GenericForeignKey could make sense.

However, I am implementing it somewhere, where performance matters. The alternative approach would be to have to ForeignKey fields in my model: discount_model and another_discount_model. One of them will always be empty.

I now wonder which path you would go before I add the "other discount model". Do you have any insights you can share with me? Currently, GenericForeignKey seems a bit more complex to me and I would have to change several parts in my code.

Additional to Risadinha's comment I share my current model structure here:

class AbstractDiscount(TimeStampedModel):


    value = models.PositiveIntegerField(
        verbose_name=_("Value"),
        validators=[
            MinValueValidator(0),
        ],
        null=True,
        blank=True,
    )
    percentage = models.DecimalField(
        verbose_name=_("Percentage"),
        max_digits=5,
        decimal_places=4,
        validators=[
            MinValueValidator(0),
            MaxValueValidator(1),
        ],
        null=True,
        blank=True,
    )
    type = models.CharField(
        verbose_name=_("Type"),
        max_length=10,
        choices=TYPE_CHOICES,
    )
    redeemed_amount = models.PositiveIntegerField(
        verbose_name=_("Amount of redeems"),
        default=0,
    )

    class Meta:
        abstract = True


class Discount(AbstractDiscount):
    available_amount = models.PositiveIntegerField(
        verbose_name=_("Available amount"),
    )
    valid_from = models.DateTimeField(
        verbose_name=_("Valid from"),
        help_text=_("Choose local time of event location. Leave empty and discount will be valid right away."),
        null=True,
        blank=True,
    )
    valid_until = models.DateTimeField(
        verbose_name=_("Valid until"),
        help_text=_("Choose local time of event location. Leave empty to keep discount valid till the event."),
        null=True,
        blank=True,
    )
    comment = models.TextField(
        verbose_name=_("Comment"),
        help_text=_("Leave some notes for this discount code."),
        null=True,
        blank=True,
    )
    status = models.CharField(
        verbose_name=_("Status"),
        max_length=12,
        choices=STATUS_CHOICES,
        default=STATUS_ACTIVE,
    )
    code = models.CharField(
        verbose_name=_("Discount code"),
        max_length=20,
    )
    event = models.ForeignKey(
        Event,
        related_name='discounts',
        on_delete=models.CASCADE,
    )  # CASCADE = delete the discount if the event is deleted
    tickets = models.ManyToManyField(
        Ticket,
        related_name='discounts',
        blank=True,
        help_text=_("Leave empty to apply this discount to all tickets"),
        verbose_name=_("Tickets"),
    )

    class Meta:
        verbose_name = _("Discount")
        verbose_name_plural = _("Discounts")
        ordering = ['code']


class SocialDiscount(AbstractDiscount):
    event = models.OneToOneField(
        Event,
        related_name='social_ticketing_discount',
        on_delete=models.CASCADE,
    )  # CASCADE = delete the discount if the event is deleted
    tickets = models.ManyToManyField(
        Ticket,
        related_name='social_ticketing_discount',
        blank=True,
        help_text=_("Leave empty to apply this discount to all tickets"),
        verbose_name=_("Tickets"),
    )

    class Meta:
        verbose_name = _("SocialDiscount")
        verbose_name_plural = _("SocialDiscount")
Joey Coder
  • 3,199
  • 8
  • 28
  • 60
  • 2
    And yet another option: if `DiscountModel` and `AnotherDiscountModel` would share the same non-abstract parent model you could use a regular `ForeignKey` on that parent model. See https://docs.djangoproject.com/en/2.1/topics/db/models/#model-inheritance – Risadinha Feb 05 '19 at 15:48
  • You say performance matters, but are you sure the choice will have that much impact ? If so you can just test it in a basic implementation, and if either choice is ok, choose `GenericForeignKey` or @Risadinha 's solution. – Andy Feb 05 '19 at 16:16
  • @Risadinha I actually have a (currently) abstract class where both models are built off. However, `AbstractDiscount` 'can't' exist alone as the `foreign_key` to the event would be missing. Did I understood it correct that your suggestion would include making `AbstractDiscount` to a non-abstract model? – Joey Coder Feb 05 '19 at 16:25
  • 1
    Other things to consider : 1) You can't use GenericForeignKey in query filters ; 2) a GenericForeignKey won't appear in a ModelForm. These can be disqualifying. See https://docs.djangoproject.com/en/2.1/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericForeignKey – Andy Feb 05 '19 at 16:28

1 Answers1

4

There is no generic answer to this, just considerations. The decision depends on the business logic you need to implement with this solution.

Two Columns

order.discount = ForeignKey(Discount, null=True)
order.social_discount = ForeignKey(SocialDiscount, null=True)

When checking in subsequent code:

if order.discount:
    # do this based on Discount model
elif order.social_discount:
    # do that based on SocialDiscount model

This is a solution in favor of two very different Discount behaviours.

Use this:

  • if there are only those two and no more in the future,
  • if you would call very different fields and methods on them (they have different business logic surrounding them).

Non-Abstract Parent

# renamed from AbstractDiscount to ParentDiscount for obvious reasons
order.discount = ForeignKey(ParentDiscount, null=True)

Subsequent code:

if order.discount:
    # do things that apply to either discount
    if isinstance(order.discount, 'Discount'):
        # do things that only apply to Discount
    elif isinstance(order.discount, 'SocialDiscount'):
        # do things that only apply to SocialDiscount

Use this:

  • if there might be more children of ParentDiscount in the future,
  • if there is general business logic that applies to any type of ParentDiscount that would be shared between all children.

GenericForeignKey

Querying on GenericForeignKeys requires a bit of work. As @Andy remarked it is not directly supported, but you can of course query on content_type and object_id together. The __in lookup won't work unless you can rely on object_id only.

It won't work out of the box in forms. For the Django Admin, there might be some solution, though, see GenericForeignKey and Admin in Django.

Use this:

  • if there might be more discounts of various types in the future (ask the product owner and make sure this is not just some far far away future),
  • if there is general business logic that applies to any those types,
  • if you don't need a no-work quick Admin solution.
Risadinha
  • 16,058
  • 2
  • 88
  • 91
  • Great explanation, thank you so much. I have one more question to `Non-Abstract Parent`. Case 1: I create a discount. That would create a field in `ParentDiscount` as well as in `Discount` that refers back to `ParentDiscount` in a OnetoOne-Relation. Case 2: I create a `social_discount`. Same as Case 1 (OnetoOne-Relation to `PartentDiscount`), just that it's `SocialDiscount` instead `Discount`. – Joey Coder Feb 05 '19 at 17:18
  • @JoeyCoder - I can't follow you exactly but if you are wondering what's happening underneath: just try it out and look into your database. For each child that your create, one row will be added to the child table and one row will be added to the parent table (so actually, creating a child obj does two inserts, one row each into two tables). – Risadinha Feb 05 '19 at 17:27