Overview¶
What its all about is connecting models together and, if you want, creating some metadata about the meaning of that relationship (i.e. a tag).
To this end, django-generic-m2m does three things to make this behavior easier:
- wraps up all querying and connecting logic in a single attribute that acts on both model instances and the model class
- allows any model to be used as the intermediary “through” model
- provides an optimized lookup when
GenericForeignKeys
are used
Adding to a model¶
Before you start creating relationships, you’ll need to add a RelatedObjectsDescriptor
to any model you plan on relating to other models.
Here’s a quick example:
from django.db import models
from genericm2m.models import RelatedObjectsDescriptor
class Food(models.Model):
name = models.CharField(max_length=255)
related = RelatedObjectsDescriptor()
def __unicode__(self):
return self.name
class Beverage(models.Model):
name = models.CharField(max_length=255)
related = RelatedObjectsDescriptor()
def __unicode__(self):
return self.name
If you’d like to add relationships to a model that you don’t control (for example
the User
model from django.contrib.auth
), you can use the monkey_patch
utility:
from django.contrib.auth.models import User
from genericm2m.utils import monkey_patch
monkey_patch(User, name='related')
Creating and querying relationships¶
A custom model manager is exposed on each model via the RelatedObjectsDescriptor
.
The API for creating and querying relationships is exposed via this descriptor.
Here is a sample interactive terminal session:
>>> # create a handful of objects to use in our demo
>>> pizza = Food.objects.create(name='pizza')
>>> cereal = Food.objects.create(name='cereal')
>>> beer = Beverage.objects.create(name='beer')
>>> soda = Beverage.objects.create(name='soda')
>>> milk = Beverage.objects.create(name='milk')
>>> healthy_eater = User.objects.create_user('healthy_eater', 'healthy@health.com', 'secret')
>>> chocula = User.objects.create_user('chocula', 'chocula@postcereal.com', 'garlic')
Now that we have some Food, Beverage and User objects, create some connections between them:
>>> rel_obj = pizza.related.connect(beer, alias='Beer and pizza are good')
>>> type(rel_obj) # what did we just create?
<class 'genericm2m.models.RelatedObject'>
The object that represents the connection is an instance of whatever is passed to
the RelatedObjectDescriptor
when it is added to a model, but the default
is genericm2m.models.RelatedObject
. Here are the interesting properties of the
new related object:
>>> rel_obj.parent
<Food: pizza>
>>> rel_obj.object
<Beverage: beer>
>>> rel_obj.alias
'Beer and pizza are good'
These relationships can be queried:
>>> pizza.related.all() # find all objects that pizza has been related to
[<RelatedObject: pizza related to beer ("Beer and pizza are good")>]
When the RelatedObject is a GFK, as is the case here, the RelatedObjectsDescriptor
will
return a special QuerySet
class that provides an optimized lookup of any GFK-ed objects:
>>> type(pizza.related.all())
<class 'genericm2m.models.GFKOptimizedQuerySet'>
>>> pizza.related.all().generic_objects() # traverse the GFK relationships
[<Beverage: beer>]
If the object on the back-side of the relationship also has a RelatedObjectsDescriptor
with
the same intermediary model, reverse lookups are possible:
>>> beer.related.related_to() # query the back-side of the relationship
[<RelatedObject: pizza related to beer ("Beer and pizza are good")>]
Create some more connections - any combination of models can be used. Below I’m connectiong a Food (cereal) to both Beverage objects (milk) and User objects (Chocula):
>>> cereal.related.connect(milk) # connecting to a beverage
<RelatedObject: cereal related to milk ("")>
>>> cereal.related.connect(chocula) # connecting to a user
<RelatedObject: cereal related to chocula ("")>
>>> cereal.related.all() # show what cereal is related to
[<RelatedObject: cereal related to chocula ("")>,
<RelatedObject: cereal related to milk ("")>]
>>> chocula.related.all() # relationships are ONE WAY
[]
>>> chocula.related.related_to() # querying the backside shows what has been connected to chocula
[<RelatedObject: cereal related to chocula ("")>]
Also worth noting is that the RelatedObjectsDescriptor
works on both the
instance-level and the class-level, so if we wanted to see all objects related to foods:
>>> Food.related.all() # anything that has been related to a food
[<RelatedObject: cereal related to chocula ("")>,
<RelatedObject: cereal related to milk ("")>,
<RelatedObject: pizza related to beer ("Beer and pizza are good")>]
Using a custom “through” model¶
It’s possible to use a custom “through” model in place of the default RelatedObject
.
If you know you’re only going to be using a couple models, this can be a handy way
to save queries. Looking at the tests, here’s another silly example where we
have a RelatedBeverage
model that our Food model will use:
class RelatedBeverage(models.Model):
food = models.ForeignKey('Food')
beverage = models.ForeignKey('Beverage')
class Meta:
ordering = ('-id',)
class Food(models.Model):
# ... same as above except for this new attribute:
related_beverages = RelatedObjectsDescriptor(RelatedBeverage, 'food', 'beverage')
The “related_beverages” attribute is an instance of RelatedObjectsDescriptor
,
but it is instantiated with a couple of arguments:
RelatedBeverage
: the model to be used to hold the “connections”- ‘food’: the field name on the above model which maps to the “from” object
- ‘beverage’: the field name which maps to the “to” object
Continuing the shell session from above with the same models, foods can be connected to beverages using the new “related_beverages” attribute:
>>> pizza.related_beverages.connect(soda)
<RelatedBeverage: RelatedBeverage object>
Querying provides the same interface, but since the “to” object is a direct
ForeignKey
to Beverage, a normal django QuerySet
is used:
>>> pizza.related_beverages.all()
[<RelatedBeverage: RelatedBeverage object>]
>>> type(pizza.related_beverages.all())
<class 'django.db.models.query.QuerySet'>
A TypeError
will be raised if you try to connect an invalid object, such as
a Person to the “related_beverages”:
>>> pizza.related_beverages.connect(mario)
*** TypeError: Unable to query ...
And lastly, just like before, its possible to query on the class to get all the
RelatedBeverage
objects for our foods:
>>> Food.related_beverages.all()
[<RelatedBeverage: RelatedBeverage object>]