Opened 13 years ago

Last modified 5 years ago

#16732 assigned Bug

Unable to have abstract model with unique_together

Reported by: Mitar Owned by: Can Sarıgöl
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords:
Cc: mmitar@…, Giacomo Graziosi, direx Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: yes Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

I have such model definition:

class SlugVersion(models.Model):
    class Meta:
        abstract = True
        unique_together = (('slug', 'version'),)

    slug = models.CharField(db_index=True, max_length=10, editable=False)
    version = models.IntegerField(db_index=True, editable=False)

class Base(SlugVersion):
    name = models.CharField(max_length=10)

class Test(Base):
    test = models.IntegerField()

And I am getting:

Error: One or more models did not validate:
test.test: "unique_together" refers to slug. This is not
in the same model as the unique_together statement.
test.test: "unique_together" refers to version. This is not
in the same model as the unique_together statement.

I see this is as a bug. Why there could not be a table for class Base with unique constraints on its slug and version fields, and then 1-1 relationship with additional table for model Test with field test.

Change History (10)

comment:1 by Mitar, 13 years ago

But this does work:

class SlugVersion(models.Model):
    class Meta:
        abstract = True
        unique_together = (('slug', 'version'),)

    slug = models.CharField(db_index=True, max_length=10, editable=False)
    version = models.IntegerField(db_index=True, editable=False)

class Base(SlugVersion):
    name = models.CharField(max_length=10)

class Test(Base):
    class Meta:
        unique_together = ()

    test = models.IntegerField()

And also creates tables as wanted.

comment:2 by Mitar, 13 years ago

Using such metaclass for Base class above solves the problem:

class ModelBaseFix(models.base.ModelBase):
    def __new__(cls, name, bases, attrs):
        # We simply remove all unique_together values which we find in a parent
        new_class = super(ModelBaseFix, cls).__new__(cls, name, bases, attrs)
        unique_together = list(new_class._meta.unique_together)
        for parent_class in new_class._meta.parents.keys():
            for parent_unique_together in parent_class._meta.unique_together:
                try:
                    unique_together.remove(parent_unique_together)
                except ValueError:
                    pass
        new_class._meta.unique_together = tuple(unique_together)
        return new_class

comment:3 by Aymeric Augustin, 13 years ago

Triage Stage: UnreviewedAccepted

If I add this code below the models definition in the original report:

print SlugVersion._meta.abstract, SlugVersion._meta.unique_together
print Base._meta.abstract, Base._meta.unique_together
print Test._meta.abstract, Test._meta.unique_together

I get:

% ./manage.py validate
True (('slug', 'version'),)
False (('slug', 'version'),)
False (('slug', 'version'),)
Error: One or more models did not validate:
selftest.test: "unique_together" refers to slug. This is not in the same model as the unique_together statement.
selftest.test: "unique_together" refers to version. This is not in the same model as the unique_together statement.

This shows that Test inherits the Meta of Base, which is wrong, because Base isn't abstract.

comment:4 by anonymous, 11 years ago

Would this ever be fixed?

I have been avoiding the use of abstract models because of this and it prevents the model declarations from being DRY...

comment:5 by Giacomo Graziosi, 10 years ago

Cc: Giacomo Graziosi added

comment:6 by direx, 10 years ago

Cc: direx added

comment:7 by Can Sarıgöl, 6 years ago

Has patch: set
Owner: changed from nobody to Can Sarıgöl
Status: newassigned
Version: 1.3master

PR
When I read the document about meta-inheritance, I didn't see the topic that in abstract = False case What happens to child meta? as far as I could see from code, ordering and get_latest_by always moving in child meta in every situation. Should we change the code like my PR + document about {{{ abstract = False }} and fix the blown up test?

comment:8 by Mariusz Felisiak, 5 years ago

Patch needs improvement: set

Topic for abstract = False is not a part of "Abstract base classes" documentation (see meta-and-multi-table-inheritance). Falling tests show that it is not a proper solution. ('suffix1', 'suffix2') should be inherited from an abstract class BookXtra. I'm afraid that proposed change is strongly backward incompatible.

comment:9 by Can Sarıgöl, 5 years ago

Patch needs improvement: unset

I got your backward-incompatible concern. I added a case for classes that have more than one inherited classes and abstract at least one. this can solve BookXtra test.

comment:10 by Mariusz Felisiak, 5 years ago

Needs tests: set
Note: See TracTickets for help on using tickets.
Back to Top