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.
Example Problem
For instance, consider the following two Model definitions:
|
|
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
.
Automatic Propagation
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 is achieved in the following straightforward way:
def propagate_field_update(reference, field_name,
targets):
value = getattr(reference, field_name)
for target in targets:
setattr(target, field_name, value)
Alternate Method
Another option is to update all fields together (for instance, after a save signal, as done below); this is achieved in the following way:
def propagate_field_updates(reference, targets):
mapping = [(f.attname,
getattr(reference, f.attname)) for f in \
reference._meta.fields]
for target in targets:
for (f_attname, v) in mapping:
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.
Mini Framework
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:
from django.db.models.signals import post_init, \
post_save, post_delete
from collections import defaultdict
from weakref import WeakValueDictionary
MODEL_INSTANCES_ALIASES = defaultdict(
WeakValueDictionary)
def record_alias(sender, instance, **kwargs):
"""
Keeps a weak reference to all aliases of a Model
instance, using the class and primary key as a key
to a default dictionary of weak value dictionary
entries, which in turn use the id of aliases
mapped to weak references to the aliases
themselves, in the following manner:
{ (Class, PK) : { id : instance,
id : instance, ... }, ... }
Recording will happen post initialization, but
also post save in the event that a Model instance
is created directly from a class (and not the
create Manager method), and hence does not yet
feature a primary key.
"""
if instance.pk is not None:
aliases = MODEL_INSTANCES_ALIASES[(sender,
instance.pk)]
if not aliases.has_key(id(instance)):
aliases[id(instance)] = instance
post_init.connect(record_alias)
post_save.connect(record_alias)
def propagate_updates_to_aliases(sender, instance,
**kwargs):
"""
Propagates all field values of a given Model
instance to all its aliases post save and delete.
Can also be invoked explicitly, or used in a
decorator for methods which update fields.
"""
mapping = [(f.attname,
getattr(instance, f.attname)) for f in \
instance._meta.fields]
# Propagate field values to all aliases.
for target in MODEL_INSTANCES_ALIASES[
(instance.__class__, instance.pk)
].itervalues():
# Ignore the given instance.
if not id(target) == id(instance):
for (f_attname, v) in mapping:
setattr(target, f_attname, v)
post_save.connect(propagate_updates_to_aliases)
post_delete.connect(propagate_updates_to_aliases)
Using the above technique, our example changed 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.