Reputation: 14594
I'm trying to model something in Django and it doesn't seem quite right. A Book
and a Movie
can have one or more Person
s involved in it, each of whom has a Role
(like "Author" or "Director") on that Book
or Movie
.
I'm doing this with Generic relations something like this:
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=255)
class Role(models.Model):
person = models.ForeignKey('Person')
role_name = models.CharField(max_length=255)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
class Book(models.Model):
title = models.CharField(max_length=255)
roles = GenericRelation('Role', related_query_name='books')
class Movie(models.Model):
title = models.CharField(max_length=255)
roles = GenericRelation('Role', related_query_name='movies')
Which seems to work, but it seems odd when getting all the Book
s a Person
has worked on:
person = Person.objects.get(pk=1)
for role in person.role_set.all():
print(role.role_name)
for book in role.books.all():
print(book.title)
The for book in role.books.all()
feels wrong - like a role can have multiple books. I think I want a more direct relationship between a person working on a particular book/movie.
Is there a better way to model this?
Upvotes: 4
Views: 3006
Reputation: 14255
Django's generic relations may not be the best choice for this case. Instead, it looks like some form of inheritance is more appropriate.
Django works with relational databases. Much of the power, efficiency, and reliability of a relational database originates from the fact that the relations between tables are fixed by the designer (or developer, or whatever): Just by looking at the model definitions, you know how the tables are related. The user cannot influence this.
A GenericForeignKey
, on the other hand, breaks this basic concept, by allowing the user to create new relations to any other table in the database. These relations exist only in the data, not in the schema. The designer cannot know which relations will exist at any point in time, nor can the model definitions tell us how the tables are related. For that, we need to look at the actual data from the database. Needless to say this clever trick complicates matters greatly, and makes relational integrity difficult, if not impossible, to maintain.
Still, there are some use-cases where the flexibility of GenericForeignKey
is exactly what is needed. Django's own example of a TaggedItem illustrates this well.
However, the OP's case is not one of them, as far as I can see.
The answer by @tdsymonds points in the right direction, which is, to use some form of inheritance from a Media
class. However, I believe using django-polymorphic
overcomplicates the matter, as there are several ways of implementing this using plain Django.
Probably the simplest method, although maybe not the most query-efficient one, is to use Django's multi-table inheritance (a.k.a. class-table inheritance).
Applied to the OP's example, this becomes:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=255)
roles = models.ManyToManyField(to='Media', through='Role')
class Role(models.Model):
person = models.ForeignKey('Person', on_delete=models.CASCADE)
media = models.ForeignKey('Media', on_delete=models.CASCADE)
role_name = models.CharField(max_length=255)
class Media(models.Model):
title = models.CharField(max_length=255)
class Book(Media):
pass
class Movie(Media):
pass
Note that Role
has a ForeignKey
directly to Media
, which can be set using either a Media
object, a Book
object, a Movie
object, or any other subclass you decide to add. The subclasses can have additional attributes. We can easily get a list of all roles a Person
played, regardless of the type of Media
, via person.roles.all()
(a basic many-to-many relation).
To view the roles in the admin, add an inline, as explained in the docs:
from django.contrib import admin
from mediaroles.models import Person, Role, Book, Movie, Media
class RoleInline(admin.TabularInline):
model = Role
extra = 0
class PersonAdmin(admin.ModelAdmin):
inlines = (RoleInline,)
admin.site.register(Person, PersonAdmin)
admin.site.register([Role, Book, Movie])
Upvotes: 2
Reputation: 1709
If it was me, I'd try an approach as follows using the many to many relationship through the role model. (See https://docs.djangoproject.com/en/1.10/topics/db/models/#s-extra-fields-on-many-to-many-relationships)
As you mentioned above in the comments, the trouble comes because you have several models (book and movie) that would require this relationship. The solution to this, is to use a BaseRole model and create separate models for movie roles and book roles. Something like this:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=255)
class BaseRole(models.Model):
role_name = models.CharField(max_length=255)
person = models.ForeignKey('Person', on_delete=models.CASCADE)
class Meta:
abstract = True
class MovieRole(BaseRole):
movie = models.ForeignKey('Movie', on_delete=models.CASCADE)
class BookRole(BaseRole):
book = models.ForeignKey('Book', on_delete=models.CASCADE)
class Book(models.Model):
title = models.CharField(max_length=255)
roles = models.ManyToManyField(Person, through='BookRole')
class Movie(models.Model):
title = models.CharField(max_length=255)
roles = models.ManyToManyField(Person, through='MovieRole')
That way when you'd like to filter on all the Books a Person has worked on, you can do that as follows:
person = Person.objects.get(pk=1)
books = Book.objects.filter(roles=person).distinct()
The distinct()
call is necessary, as a person could have more than one role on a book/movie (e.g. they could be a producer and the director), so without it, the filter would return the an instance of the book for each role that the person had in the movie/book.
In response to you comment, perhaps a better solution would be to use Django Polymorphic (https://django-polymorphic.readthedocs.io/en/stable/quickstart.html).
So you'd set your models up something like this:
from django.db import models
from polymorphic.models import PolymorphicModel
class Person(models.Model):
name = models.CharField(max_length=255)
class Role(models.Model):
role_name = models.CharField(max_length=255)
person = models.ForeignKey('Person', on_delete=models.CASCADE)
media = models.ForeignKey('Media', on_delete=models.CASCADE)
class Media(PolymorphicModel):
title = models.CharField(max_length=255)
roles = models.ManyToManyField(Person, through='Role')
class Book(Media):
pass
class Movie(Media):
pass
That way you can still get all the books a person has worked on by:
person = Person.objects.get(pk=1)
books = Book.objects.filter(roles=person).distinct()
But you can also get all of the media that a person has worked on by:
media = person.media_set.all()
And yes you're right, there is no point in having the content_type and object_id, so I've removed these fields.
Upvotes: 2