Setting up Models with multiple Foreign Key relationships

I’m setting up a database which initially had three models (Team, Person, Report) with relationships as shown below:

from django.db import models

class Team(models.Model):
    pass


class Person(models.Model):
    team = models.ForeignKey(Team, on_delete=models.CASCADE)


class Report(models.Model):
    team = models.ForeignKey(Team, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)

But now I need to distinguish between two types of Person: “Members” and “Leaders”. Each Team can only have one “Leader”. Members and Leaders will have one or two data fields that differ from each other, and some slightly different business logic. Otherwise they’ll be 90% the same as the current Person model.

I’m considering two options:

  1. Create a role field on Person with possible values of "Member" and "Leader" (and also add additional fields which would only be used for Members or Leaders, but not both)

  2. Create two new models, Member and Leader, that inherit from Person.

Both approaches have drawbacks. Option #2 almost seems better, except I’m not sure how to handle the foreign key relationship from Report to Members and Leaders (maybe use a generic foreign key, since it would be a single foreign key to two different models?). Is there a good way to handle the foreign key relationship here?

Or is there maybe a better, different option to distinguish between two types of Person (“Member” and “Leader”) in this set of models?

The preferred answer is to some degree going to depend upon what’s different between the two.

For clarity regarding #2 - if Member and Leader inherit from Person, and Person is abstract=False, then you don’t need to define the relationship between them. Django will create the OneToOneRelationship between them. See the docs for Multi-table inheritance.

But either one of these will work. What’s better depends upon the specific.

Thank you for the feedback. I had assumed that concretely inheriting from Person to create new Leader and Member classes wouldn’t work without also using a generic foreign key to relate both of these new classes to Team. But I just tried it and it seems to work if I simply leave Person as the foreign key. By “work”, I mean that creating a Report and calling report.person will return a Leader or Member instead of a Person, if a Leader or Member was assigned to the person field in the Report.

Below are the new class definitions, updated with some additional fields to distinguish between classes:

from django.db import models


class Team(models.Model):
    name = models.CharField(max_length=255)


class Person(models.Model):
    name = models.CharField(max_length=255)


class Leader(Person):
    leader_field = models.CharField(max_length=255)
    team = models.OneToOneField(Team, on_delete=models.CASCADE)


class Member(Person):
    member_field = models.CharField(max_length=255)
    team = models.ForeignKey(Team, on_delete=models.CASCADE)


class Report(models.Model):
    team = models.ForeignKey(Team, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)

Going into the shell with python manage.py shell and creating some objects like this…

from myapp.models import Team, Person, Leader, Member, Report
team = Team.objects.create(name="A")
alice = Leader.objects.create(name="Alice", leader_field="Alice Leader", team=team)
bob = Member.objects.create(name="Bob", member_field="Bob Member", team=team)
charlie = Member.objects.create(name="Charlie", member_field="Charlie Member", team=team)
report_a = Report.objects.create(team=team, person=alice)
report_b = Report.objects.create(team=team, person=bob)
report_c = Report.objects.create(team=team, person=charlie)

…then Alice is a Leader, and is returned as a Leader from report_a, as shown below. This means that fields defined in Person (the name field) and Leader (the leader_field) can both be accessed, even though the foreign key in Report is to a Person instead of a Leader (I didn’t expect this behavior):

>>> report_a.person
<Leader: Leader object (1)>
>>> report_a.person.name
'Alice'
>>> report_a.person.leader_field
'Alice Leader'

Similarly for Bob, a Member:

>>> report_b.person.name               
'Bob'
>>> report_b.person.member_field
'Bob Member'

The team leader (as an object) and members (as a query set) can be accessed directly from a Team instance:

>>> team.leader
<Leader: Leader object (1)>
>>> team.leader.name
'Alice'
>>> team.member_set.all()
<QuerySet [<Member: Member object (2)>, <Member: Member object (3)>]>
>>> for member in team.member_set.all():
...   print(member.name)
... 
Bob
Charlie

Trying to add a second leader fails, as expected, due to the one-to-one constraint from Leader to Team:

>>> Leader.objects.create(name="Dave", team=team)
django.db.utils.IntegrityError: UNIQUE constraint failed: myapp_leader.team_id

Adding more members works, as expected:

>>> Member.objects.create(name="Dave", team=team) 
<Member: Member object (4)>

I thought about doing this the other way, too, without concrete inheritance… If instead of doing inheritance as above, I added a role field to the Person class with options of “Leader” and “Member”, I think I would need to add a custom save() method to prevent more than one Person with a role of Leader from being added to a Team. Additionally, I think I’d need to add methods (or @property) to the Team class to be able to directly return team.leader and team.member_set as an object and a query set, respectively, i.e., instead of writing queries to access them each time, just put the queries in methods or @property in Team. (accessing them directly from team would make templating easier, etc.)

All of that behavior comes automatically when using concrete inheritance, though, so inheritance seems like a more direct approach in this case. I’ve seen people say that concrete inheritance is not a great idea, though. What are the potential pitfalls?