简体   繁体   中英

Altering unique fields of inherited Django model

In Django, I'm trying to create a base model that can be used to track the different version of some other model's data in a transparent way. Something like:

class Warehouse(models.Model):

    version_number = models.IntegerField()
    objects = CustomModelManagerFilteringOnVersionNumber()

    class Meta:
        abstract=True

    def save(self, *args, **kwargs):
        # handling here version number incrementation
        # never overwrite data but create separate records,
        # don't delete but mark as deleted, etc.
        pass

class SomeData(Warehouse):
    id = models.IntegerField( primary_key=True )
    name = models.CharField(unique=True)

The problem I have is that SomeData.name is actually not unique, the tuple ('version_number', 'name' ) is.

I know I can use the Meta class in SomeData with unique_together but I was wondering whether this could be done in a more transparent way. That is, dynamically modifying/creating this unique_together field.

Final note: maybe handling this with model inheritance is not the correct approach but it looked pretty appealing to me if I can handle this field uniqueness problem.

EDIT: Obviously the word 'dynamic' in this context is misleading. I do want the uniqueness to be preserved at the database level. The idea is to have different version of the data.

A give here a small example with the above model (assuming an abstract model inheritance, so that everything is in the same table):

The database could contain:

version_number | id | name  | #comment
0              | 1  | bob   | first insert
0              | 2  | nicky | first insert
1              | 1  | bobby | bob changed is name on day x.y.z
0              | 3  | malcom| first insert
1              | 3  | malco | name change
2              | 3  | momo  | again...

and my custom model manager would filter the data (taking the max on version number for every unique id) so that: SomeData.objects.all() would return

id | name
1  | bobby
2  | nicky
3  | momo

and would also offer others method that can return data as in version n-1.

Obviously I will use a timestamp instead of a version number so that I can retrieve the data at a given date but the principle remains the same.

Now the problem is that when I do ./manage.py makemigrations with the models above, it will enforce uniqueness on SomeData.name and on SomeData.id when what I need is uniqueness on ('SomeData.id','version_number') and ('SomeData.name', 'version_number') . Explicitly, I need to append some fields from the base models into the fields of the inherited models declared as unique in the db and this any times the manage.py command is run and/or the production server runs.

By "dynamically modifying/creating this unique_together field", I assume you mean you want to enforce this at the code level, not at the database level.

You can do it in the save() method of your model:

from django.core.exceptions import ValidationError

def save(self, *args, **kwargs):
    if SomeData.objects.filter(name=self.name, version_number=self.version_number).exclude(pk=self.pk).exists():
        raise ValidationError('Such thing exists')

    return super(SomeData, self).save(*args, **kwargs)

Hope it helps!

Ok, I ended subclassing the ModelBase from django.db.models.base so that I can insert into 'unique_together' any fields declared as unique. Here is my current code. It does not yet implements the manager and save methods but the db uniqueness constraints are handled correctly.

from django.db.models.options import normalize_together
from django.db.models.base import ModelBase
from django.db.models.fields import Field

class WarehouseManager( models.Manager ):
    def get_queryset( self ):
        """Default queryset is filtered to reflect the current status of the db."""

        qs = super( WarehouseManager, self ).\
             get_queryset().\
             filter( wh_version = 0 )

class WarehouseModel( models.Model ):
    class Meta:
        abstract = True

    class __metaclass__(ModelBase):
        def __new__(cls, name, bases, attrs):
            super_new = ModelBase.__new__

            meta = attrs.get( 'Meta', None )
            try:
                if attrs['Meta'].abstract == True:
                    return super_new(cls, name, bases, attrs )
            except:
                pass

            if meta is not None:
                ut = getattr( meta, 'unique_together', () )
                ut = normalize_together( ut )
                attrs['Meta'].unique_together = tuple( k+('wh_version',) for k in ut )

            unique_togethers = ()
            for fname,f in attrs.items():
                if fname.startswith( 'wh_' ) or not isinstance( f, Field ):
                    continue

                if f.primary_key:
                    if not isinstance( f, models.AutoField ):
                        raise AttributeError( "Warehouse inherited models cannot "
                                "define a primary_key=True field which is not an "
                                "django.db.models.AutoField. Use unique=True instead." )
                    continue

                if f.unique:
                    f._unique = False
                    unique_togethers += ( (fname,'wh_version'), )

            if unique_togethers:
                if 'Meta' in attrs:
                    attrs['Meta'].unique_together += unique_togethers
                else:
                    class DummyMeta: pass
                    attrs['Meta'] = DummyMeta
                    attrs['Meta'].unique_together = unique_togethers

            return super_new(cls, name, bases, attrs )

    wh_date = models.DateTimeField(
        editable=False,
        auto_now=True,
        db_index=True
    )
    wh_version = models.IntegerField(
        editable=False,
        default=0,
        db_index=True,
    )

    def save( self, *args, **kwargs ):
        # handles here special save behavior...
        return super( WarehouseModel, self ).save( *args, **kwargs )

    objects = WarehouseManager()

class SomeData( WarehouseModel ):
    pid = models.IntegerField( unique=True )

class SomeOtherData( WarehouseModel ):
    class Meta:
        unique_together = ('pid','chars')
    pid = models.IntegerField()
    chars = models.CharField()

Just to add to @Bob's answer you may need to use the __metaclass__ class as following

class ModelBaseMeta(BaseModel):
    
    def __new__(cls, name, bases, attrs):
        ...
    ...

class YourAbstractClass(models.Model, metaclass=ModelBaseMeta):
    
    class Meta:
        abstract = True

Instead of putting the __metaclass__ definition directly inside the abstract class.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM