| 1 | 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. |
| 2 | |
| 3 | In the Hollywood example, something like this: |
| 4 | |
| 5 | {{{ |
| 6 | #!python |
| 7 | class Actor(models.Model): |
| 8 | name = models.CharField(maxlength=255) |
| 9 | |
| 10 | class Movie(models.Model): |
| 11 | title = models.CharField(maxlength=255) |
| 12 | actors = models.ManyToManyField(Actor) |
| 13 | }}} |
| 14 | |
| 15 | would become something like this instead, with the `role` field added: |
| 16 | |
| 17 | {{{ |
| 18 | #!python |
| 19 | class Actor(models.Model): |
| 20 | name = models.CharField(maxlength=255) |
| 21 | |
| 22 | class Film(models.Model): |
| 23 | title = models.CharField(maxlength=255) |
| 24 | |
| 25 | class Role(models.Model): |
| 26 | actor = models.ForeignKey(Actor, related_name='roles') |
| 27 | film = models.ForeignKey(Film, related_name='roles') |
| 28 | role = models.CharField(maxlength=255) |
| 29 | }}} |
| 30 | |
| 31 | Unfortunately, the database API then goes from this: |
| 32 | |
| 33 | {{{ |
| 34 | #!python |
| 35 | >>> for actor in film.actors.all(): |
| 36 | ... print actor.name |
| 37 | Graham Chapman |
| 38 | Terry Gilliam |
| 39 | John Cleese |
| 40 | Eric Idle |
| 41 | }}} |
| 42 | |
| 43 | to this: |
| 44 | |
| 45 | {{{ |
| 46 | #!python |
| 47 | >>> for role in film.roles.all(): |
| 48 | ... print '%s played %s' % (role.actor.name, role.role) |
| 49 | Graham Chapman played King Arthur |
| 50 | Terry Gilliam played Sir Bors |
| 51 | John Cleese played Sir Lancelot the Brave |
| 52 | Eric Idle played Sir Robin the Not-Quite-So-Brave-as-Sir Launcelot |
| 53 | }}} |
| 54 | |
| 55 | 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. |
| 56 | |
| 57 | By simply adding one line and renaming the `related_name`s: |
| 58 | |
| 59 | {{{ |
| 60 | #!python |
| 61 | class Actor(models.Model): |
| 62 | name = models.CharField(maxlength=255) |
| 63 | |
| 64 | class Film(models.Model): |
| 65 | title = models.CharField(maxlength=255) |
| 66 | |
| 67 | class Role(models.Model): |
| 68 | actor = models.ForeignKey(Actor, related_name='films') |
| 69 | film = models.ForeignKey(Film, related_name='actors') |
| 70 | role = models.CharField(maxlength=255) |
| 71 | |
| 72 | objects = models.ManyToManyManager() |
| 73 | }}} |
| 74 | |
| 75 | we could get a much more intuitive API: |
| 76 | |
| 77 | {{{ |
| 78 | #!python |
| 79 | >>> for actor in film.actors.all(): |
| 80 | ... print '%s played %s' % (actor.name, actor.role) |
| 81 | Graham Chapman played King Arthur |
| 82 | Terry Gilliam played Sir Bors |
| 83 | John Cleese played Sir Lancelot the Brave |
| 84 | Eric Idle played Sir Robin the Not-Quite-So-Brave-as-Sir Launcelot |
| 85 | }}} |
| 86 | |
| 87 | This would work just as well in both directions: |
| 88 | |
| 89 | {{{ |
| 90 | #!python |
| 91 | >>> for film in john.films.all(): |
| 92 | ... print '%s in %s' % (film.role, film.title) |
| 93 | Sir Lancelot the Brave in Monty Python and the Holy Grail |
| 94 | Sir Nicholas de Mimsy-Porpington in Harry Potter and the Sorceror's Stone |
| 95 | }}} |
| 96 | |
| 97 | Ideally, this API would also extend the `add` method of the manager, allowing it to take keyword attributes for the relationship meta-data: |
| 98 | |
| 99 | {{{ |
| 100 | #!python |
| 101 | >>> film = Film.objects.create(title='And Now for Something Completely Different') |
| 102 | >>> john.films.add(film, role='Sir George Head') |
| 103 | >>> for film in john.films.all(): |
| 104 | ... print '%s in %s' % (film.role, film.title) |
| 105 | Sir Lancelot the Brave in Monty Python and the Holy Grail |
| 106 | Sir Nicholas de Mimsy-Porpington in Harry Potter and the Sorceror's Stone |
| 107 | Sir George Head in And Now for Something Completely Different |
| 108 | }}} |
| 109 | |
| 110 | And without the intermediary model, there's need for an `update` function on the manager, which would handle modifying the meta-data: |
| 111 | |
| 112 | {{{ |
| 113 | #!python |
| 114 | >>> film.actors.update(john, role='Mungo the Cook') |
| 115 | }}} |