| | 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 | }}} |