Aliasing Fields In Django

Aliasing Fields in Django

Occasionally you have a situation where you want to deprecate fields from a Django model either because you are replacing it with a different field or you are planning on removing it at some point.

If you have a large codebase, it can be hard to identify all the place where the field is used and synchronize the changes required all in one go. This is especially true if the changes need to be completed by a number of different people.

One solution to deprecating Django fields is to use an "AliasField". This is where you rename the field that you want to deprecate to something else and in its place insert a virtual field which acts the same as the old field, but allows you to intercept the __set__ and __get__ in order to customize behaviour.

For example if you have a model with a separate start_date and start_time but you now want to combine these two fields into a single datetime.

class MyClass(models.Model):  
    start_date = models.DateField()
    start_time = models.TimeField()

You can start by renaming the fields to something else where your existing codebase can't access it:

class MyClass(models.Model):  
    _start_date = models.DateField()
    _start_time = models.TimeField()

Be careful not to change any properties on this field, because ideally you want Django to make a RenameField operations rather than drop the old column and make a new one (loosing all your data!)

You can now create a dynamic AliasField which behaves like the original field except that it sets and gets to the new field instead.

class AliasFieldMixin(object):  
    def contribute_to_class(self, cls, name, virtual_only=False):
        super(AliasField, self).contribute_to_class(cls, name, virtual_only=True)
        setattr(cls, name, self)

    def __set__(self, instance, value):
        warnings.warn("This field is deprecated", DeprecationWarning)
        setattr(instance, self.db_column, value)

    def __get__(self, instance, instance_type=None):
        warnings.warn("This field is deprecated", DeprecationWarning)
        return getattr(instance, self.db_column)

def AliasField(field_class_aliased, *args, **kwargs):  
    return type("CustomAliasField", (AliasFieldMixin, field_class_aliased.__class__), {})(db_column=field_class_aliased.name, *args, **kwargs)

In the model you can then use the generated AliasField by giving it a reference to the field that you want to alias.

class MyClass(models.Model):  
    _start_date = models.DateField()
    _start_time = models.TimeField()

    start_date = AliasField(_start_date)
    start_time = AliasField(_start_time)

The big benefit of this is that the AliasField is still just a normal Django field, so it supports the same operations that any other field does; including indexes, filtering and joining through the ORM.

Anyone who now uses the old field sees the DeprecationWarning if they try and set or get to the field as well as when the field is used in the ORM.