Search code examples
djangodjango-modelsdjango-ormdjango-migrations

Django model abstract model with constrains not being inherited: 'constraints' refers to field 'xxx' which is not local to model 'Foo'


I am using Django 3.2

I have come across a strange behaviour which appears to be a bug; Fields used in constraints defined in a ABC are not inherited by child classes.

myapp/models.py

class BaseFoo(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,related_name='%(class)s_content_type')
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    user = models.ForeignKey(User, blank=False, null=False, on_delete=models.CASCADE,related_name='%(class)s')
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True
        constraints = [
            models.UniqueConstraint(fields=['content_object', 'user'], name="'%(class)s_unique"),
        ]
    
class Foo(BaseFoo):
    class Meta(BaseFoo.Meta):
        abstract = False

No problem when I makemigrations, but, When I attempt to migrate the schema (`python manage.py migrate), I get the following error:

myapp.Foo: (models.E016) 'constraints' refers to field 'content_object' which is not local to model 'Foo'. HINT: This issue may be caused by multi-table inheritance.

Why is Foo not inheriting the fields clearly defined in the parent ABC? - and how do I fix this (assuming it's not a bug)?


Solution

  • 1. You can't use GenericForeignKey in UniqueConstraint, because a GenericForeignKey isn’t a normal field object. Instead in UniqueConstraint use content_type and object_id, this is equivalent to content_object:

    
    class Meta:
        abstract = True
        constraints = [
            models.UniqueConstraint(
                fields=['content_type', 'object_id', 'user'], 
                name="%(app_label)s_%(class)s_user_unique"),
        ]

    See Django Docs:

    Due to the way GenericForeignKey is implemented, you cannot use such fields directly with filters (filter() and exclude(), for example) via the database API. Because a GenericForeignKey isn’t a normal field object.

    Likewise, GenericForeignKey does not appear in ModelForms.

    2. According to Django Docs in abstract base classes related_name must contain '%(app_label)s' and '%(class)s'. And you should always specify a unique reverse name and query name for the field.

    content_type = models.ForeignKey(
       ContentType, on_delete=models.CASCADE,
       related_name='%(app_label)s_%(class)s_content_types',
       related_query_name='%(app_label)s_%(class)s_content_type',
    )
    
    user = models.ForeignKey(
        User, blank=False, null=False,
        on_delete=models.CASCADE,
        related_name='%(app_label)s_%(class)s_users',
        related_query_name='%(app_label)s_%(class)s_user',
    )
    

    If you are using related_name or related_query_name on a ForeignKey or ManyToManyField, you must always specify a unique reverse name and query name for the field. This would normally cause a problem in abstract base classes, since the fields on this class are included into each of the child classes, with exactly the same values for the attributes (including related_name and related_query_name) each time.

    To work around this problem, when you are using related_name or related_query_name in an abstract base class (only), part of the value should contain '%(app_label)s' and '%(class)s'.