DEV Community

Discussion on: Using abstract models in Django

Collapse
 
nemrtvej profile image
Marek Makovec

In my opinion, this article describes a really easy way how to painfully shoot yourself in the foot when you store your data in SQL databases.

In my experience, introducing abstract models in Django leads to unsolvable N+1 query issues in the long run.

Imagine following scenario:

  • You have an abstract model Person with field full_name.
  • You have a class Student inheriting from Person.
  • You have a class Teacher inheriting from Person.

Imagine you have a model called AccessCard.
This AccessCard has properties value (int) and owner (Person). When you create a table listing 100 AccessCards per page, showing both the AccessCard.value and AccessCard.owner.full_name, is there a way, how to provide these data this using just 1 SQL query instead of 1+100?

If I remember correctly, it is easy when abstract classes are not used. See QuerySet.prefetch_related / QuerySet.select_related. However when I was trying to find a solution for this with inheritance in play, I failed miserably.

In my experience with ORM mappers in Django and with Doctrine from the PHP world, using inheritance in conjunction with SQL is a bad idea.
The time you save by not typing again the definitions are definitely lost when you try to optimize the horribly inefficient queries.

I do not want to degrade the value of your article, I just want to point out the downsides and that this approach has, in my opinion, significant drawbacks which should be pointed out.

Collapse
 
guzmanojero profile image
guzmanojero • Edited

You can use QuerySet.prefetch_related or QuerySet.select_related with Abstract Base Classes in Django.

Let's say we have these models:


class Country(models.Model):
    name = models.CharField(max_length=200)

    def __str__(self):
        return self.name

class AbstractPerson(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

    class Meta:
        abstract = True

class AbstractAddress(models.Model):
    street = models.CharField(max_length=100)
    city = models.CharField(max_length=50)
    state = models.CharField(max_length=2)
    zip_code = models.CharField(max_length=10)
    country = models.ForeignKey(Country, on_delete=models.CASCADE)

    class Meta:
        abstract = True

class AbstractPhoneNumber(models.Model):
    phone_number = models.CharField(max_length=15)

    class Meta:
        abstract = True

class Contact(AbstractPerson, AbstractAddress, AbstractPhoneNumber):
    email = models.EmailField()

    def __str__(self):
        return f"{self.first_name} {self.last_name} - {self.country}"
Enter fullscreen mode Exit fullscreen mode

If you make this query you wouldn't have a N+1 issue:

> Contact.objects.select_related("country").get(id=1).country
Enter fullscreen mode Exit fullscreen mode

You have one query only. The generated SQL is:

SELECT app_contact.id,
       app_contact.first_name,
       app_contact.last_name,
       app_contact.street,
       app_contact.city,
       app_contact.state,
       app_contact.zip_code,
       app_contact.country_id,
       app_contact.phone_number,
       app_contact.email,
       app_country.id,
       app_country.name
  FROM app_contact
INNER JOIN app_country
    ON (app_contact.country_id = app_country.id)
 WHERE app_contact.id = '1'
Enter fullscreen mode Exit fullscreen mode

Maybe you were referring to something else?