While the existing ManyToManyField
is suitable for basic relationships, some projects find need to tie objects together along with some information about their relationship. The common example is the role an actor played in a movie, but it could be used for many other things, including what "base" a dating couple has gotten to, for instance. For these cases, the common recommendation is to simply create an intermediary model with ForeignKey
s to each of the connected models, along with any extra fields that are appropriate for the relationship.
In the Hollywood example, something like this:
class Actor(models.Model): name = models.CharField(maxlength=255) class Film(models.Model): title = models.CharField(maxlength=255) actors = models.ManyToManyField(Actor)
would become something like this instead, with the role
field added:
class Actor(models.Model): name = models.CharField(maxlength=255) class Film(models.Model): title = models.CharField(maxlength=255) class Role(models.Model): actor = models.ForeignKey(Actor, related_name='roles') film = models.ForeignKey(Film, related_name='roles') role = models.CharField(maxlength=255)
Unfortunately, the database API then goes from this:
>>> for actor in film.actors.all(): ... print actor.name Graham Chapman Terry Gilliam John Cleese Eric Idle
to this:
>>> for role in film.roles.all(): ... print '%s played %s' % (role.actor.name, role.role) Graham Chapman played King Arthur Terry Gilliam played Sir Bors John Cleese played Sir Lancelot the Brave Eric Idle played Sir Robin the Not-Quite-So-Brave-as-Sir Launcelot
While programmers may not have a problem with this change, it is very counter-intuitive for template authors who may not have (nor should they need) an intimate understanding of how relational databases work. I'd like to propose an alternative, using a custom manager for these intermediary models, that will enable a more natural API.
By simply adding one line and renaming the related_name
s:
class Actor(models.Model): name = models.CharField(maxlength=255) class Film(models.Model): title = models.CharField(maxlength=255) class Role(models.Model): actor = models.ForeignKey(Actor, related_name='films') film = models.ForeignKey(Film, related_name='actors') role = models.CharField(maxlength=255) objects = models.ManyToManyManager()
we could get a much more intuitive API:
>>> for actor in film.actors.all(): ... print '%s played %s' % (actor.name, actor.role) Graham Chapman played King Arthur Terry Gilliam played Sir Bors John Cleese played Sir Lancelot the Brave Eric Idle played Sir Robin the Not-Quite-So-Brave-as-Sir Launcelot
This would work just as well in both directions:
>>> for film in john.films.all(): ... print '%s in %s' % (film.role, film.title) Sir Lancelot the Brave in Monty Python and the Holy Grail Sir Nicholas de Mimsy-Porpington in Harry Potter and the Sorceror's Stone
Ideally, this API would also extend the add
method of the manager, allowing it to take keyword attributes for the relationship meta-data:
>>> film = Film.objects.create(title='And Now for Something Completely Different') >>> john.films.add(film, role='Sir George Head') >>> for film in john.films.all(): ... print '%s in %s' % (film.role, film.title) Sir Lancelot the Brave in Monty Python and the Holy Grail Sir Nicholas de Mimsy-Porpington in Harry Potter and the Sorceror's Stone Sir George Head in And Now for Something Completely Different
And without the intermediary model, there's need for an update
function on the manager, which would handle modifying the meta-data:
>>> film.actors.update(john, role='Mungo the Cook')