Propagating field changes in Django Model instance aliases

Django's ORM creates multiple aliases for the same Model instance. In certain situations this may result in problematic behavior, as multiple operations on the same instance are interleaved, however, field access happens on different aliases of the instance, ultimately resulting in loss of updates.

For instance, consider the following two Models:

1 class A(Model):
2     pass
3 
4 class B(Model):
5     a = OneToOneField(A)
6     value = IntegerField()
7     
8     def __unicode__(self):
9         return str(self.value)

And the following sequence of statements:

>>> a = A.objects.create()
>>> b = B.objects.create(a = a, value = 69)
>>> b.value
69
>>> a.b.value
69
>>> b.value = 42
>>> b.save()
>>> b.value
42
>>> a.b.value
69

Predictably, the two instances of B are different objects, and updating one has no effect on the rest, as witnessed by the different values of 42 and 69

It is possible to automatically propagate field changes to all aliases of a Model by (a) keeping track of aliases, and (b) propagating upon assignment. Assuming you have a list of all aliases of a Model instance (see below), then field changes from a reference alias to all the remainder target aliases can be achieved in the following straightforward way.

1 def propagate_field_update(reference, field_name,
2     targets):
3     value = getattr(reference, field_name)
4     for target in targets:
5         setattr(target, field_name, value)

Another option is to update all fields together (for instance, after a save signal, as done below); this can be achieved in the following way.

1 def propagate_field_updates(reference, targets):
2     mapping = [(f.attname,
3         getattr(reference, f.attname)) for f in \
4             reference._meta.fields]
5     for target in targets:
6         for (f_attname, v) in mapping:
7             setattr(target, f_attname, v)

Note, however, that updates to fields which are not handled through Django's Model fields will not be captured using the above technique.

The following mini-framework uses post initialization and post save signal listeners to maintain weak references of aliases, as well as propagates field changes to all aliases using post save and post delete signals.

 1 from django.db.models.signals import post_init, \
 2     post_save, post_delete
 3 from collections import defaultdict
 4 from weakref import WeakValueDictionary
 5 
 6 MODEL_INSTANCES_ALIASES = defaultdict(
 7     WeakValueDictionary)
 8 
 9 def record_alias(sender, instance, **kwargs):
10     """
11     Keeps a weak reference to all aliases of a Model
12     instance, using the class and primary key as a key
13     to a default dictionary of weak value dictionary
14     entries, which in turn use the id of aliases
15     mapped to weak references to the aliases
16     themselves, in the following manner:
17 
18     { (Class, PK) : { id : instance,
19         id : instance, ... }, ... }
20 
21     Recording will happen post initialization, but
22     also post save in the event that a Model instance
23     is created directly from a class (and not the
24     create Manager method), and hence does not yet
25     feature a primary key.
26     """
27     if instance.pk is not None:
28         aliases = MODEL_INSTANCES_ALIASES[(sender,
29             instance.pk)]
30         if not aliases.has_key(id(instance)):
31             aliases[id(instance)] = instance
32 
33 post_init.connect(record_alias)
34 post_save.connect(record_alias)
35 
36 def propagate_updates_to_aliases(sender, instance,
37     **kwargs):
38     """
39     Propagates all field values of a given Model
40     instance to all its aliases post save and delete.
41     Can also be invoked explicitly, or used in a
42     decorator for methods which update fields.
43     """
44 
45     mapping = [(f.attname,
46         getattr(instance, f.attname)) for f in \
47             instance._meta.fields]
48 
49     # Propagate field values to all aliases.
50     for target in MODEL_INSTANCES_ALIASES[
51         (instance.__class__, instance.pk)
52     ].itervalues():
53         # Ignore the given instance.
54         if not id(target) == id(instance):
55             for (f_attname, v) in mapping:
56                 setattr(target, f_attname, v)
57 
58 post_save.connect(propagate_updates_to_aliases)
59 post_delete.connect(propagate_updates_to_aliases)

Using the above technique, the example becomes as follows.

>>> a = A.objects.create()
>>> b = B.objects.create(a = a, value = 69)
>>> b.value
69
>>> a.b.value
69
>>> b.value = 42
>>> b.save()
>>> b.value
42
>>> a.b.value
42

Which demonstrates that the two aliases have consistent field values.

I'm currently experimenting with a mini-framework for propagating field updates as they happen. I am also planning on determining the kind of performance and memory impact this has on realistic Django production code; more updates in the future.