Add a specialization of CounterField for tracking model relations.
Review Request #6248 — Created Aug. 20, 2014 and submitted
RelationCounterField
tracks the counts on one end of a ForeignKey or
ManyToManyField relation, providing a convenient and efficient way of
operating based on how many objects are related to the model.This takes a single parameter, which is the relation name. That can be
the field name of a localForeignKey
/ManyToManyField
, or it can be
therelated_name
on the model that's provided by another model's
ForeignKey
/ManyToManyField
. For example:reviews_count = RelationCounterField('reviews')
When there are Django-initiated state changes on the opposite end of
the relation, such as newly added or deleted models, the counters will
update with the new count.These counts should be accurate so long as the operations are performed
on model instances. Raw SQL queries will cause the models to get out of
sync. When this happens, the counts can be recomputed by issuing a
reinit_<fieldname>
call.
RelationCounterField
itself doesn't actually do much, aside from set
things up. All the magic happens inInstanceState
andRelationTracker
.
InstanceState
holds information on a loaded model instance/relation name
pair, and allRelationCounterFields
that follow that pair. It has
functions for operation on all the fields on a model following that
relation. (Normally there will be 1, but the design works out better to
just allow for more than one, since otherwise we actually have to track
more things to deal with conflicting state.)
RelationTracker
is the real heart ofRelationCounterField
. There's one
RelationTracker
for every model class/relation name pair. It figures out
information on the relation, listens to signals, and makes the
appropriate database and instance updates (usingInstanceState
).Since Django's signals and API for all this are a little less useful
than I'd like, there's a lot of calculations that have to go on and some
state tracking we have to do. I've tried to document how it all works,
but there's likely room for improvement.Unit tests were written to handle every case I could throw at it, along
with ensuring all query counts are consistent with what I'd expect.
Wrote a lot of unit tests. They all pass.
For the query counts in the unit tests, I added up all the queries for
the operation in both Django's code paths and ours. I then saved those as
constants and referenced them everywhere they made sense. The unit tests
ended up matching those query counts.
Description | From | Last Updated |
---|---|---|
Col: 80 E501 line too long (80 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (83 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (80 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (83 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (80 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (81 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (83 > 79 characters) |
reviewbot | |
Col: 25 E127 continuation line over-indented for visual indent |
reviewbot | |
Col: 5 E303 too many blank lines (2) |
reviewbot | |
Col: 9 E265 block comment should start with '# ' |
reviewbot | |
Col: 5 E303 too many blank lines (2) |
reviewbot | |
Col: 5 E303 too many blank lines (2) |
reviewbot | |
Col: 5 E303 too many blank lines (2) |
reviewbot | |
local variable 'rel_model' is assigned to but never used |
reviewbot | |
Leftover debug code? |
david | |
Remove this line? |
david | |
Col: 25 E127 continuation line over-indented for visual indent |
reviewbot | |
Col: 25 E127 continuation line over-indented for visual indent |
reviewbot |