Skip to content

Instantly share code, notes, and snippets.

@powderflask
Created March 10, 2025 23:08
Show Gist options
  • Save powderflask/151f60a6115d3c362cee62a59d2ab53e to your computer and use it in GitHub Desktop.
Save powderflask/151f60a6115d3c362cee62a59d2ab53e to your computer and use it in GitHub Desktop.
Django One-to-Many: data models
"""
Many-to-One relations are modelled in django by adding a `ForeignKey` field to the "Many" side model.
But what if the "Many" side model can't be modified? e.g., modelling legacy DB; model is defined by 3rd party app.
Wouldn't it be nice if there were a "OneToManyField", similar to django's `ManyToManyField` that could be added to the "One" side model?
This gist explores a possible data model for this and comes to a surprising conclusion...
"""
# Idea: Use a ManyToManyField and define a OneToOneField on the through-table to enforce One-to-Many. It works!
class Agreement(models.Model):
""" One side - one "Agreement" has many "Projects", a "Project" has zero or one "Agreement" """
ordinal = models.IntegerField(blank=False, null=False, )
# What I want:
# projects = models.OneToManyField("Project", related_name="agreement")
# What django offers:
projects = models.ManyToManyField("Project", through="ThroughModel", related_name="agreement_set")
class ThroughModel(models.Model):
""" A "Through" model for the Many-to-One relation - use a OneToOneField to enforce One-to-Many relation """
agreement = models.ForeignKey(Agreement, on_delete=models.CASCADE, related_name='agreement_prjs')
project = models.OneToOneField(Project, on_delete=models.CASCADE, related_name='prj_agreement')
class Project(models.Model):
""" Model on the "Many" side of many-to-one relation - restriction: can't modify the DB table for this model. """
name = models.CharField(max_length=50, blank=False, null=False,)
@property
def agreement(self):
""" Simulate One side of relation. (Optional - decouples relation access from this specific implementation.) """
return self.prj_agreement.agreement
# Usage: works well, but a little akward b/c queries are coupled to the implementation and need to "know" about through model...
agreements = [Agreement.objects.create(ordinal=i) for i in range(2)]
projects = [Project.objects.create(name=f"Prj {i}") for i in range(6)]
throughs = [ThroughModel.objects.create(project=prj, agreement=agr)
for agr, group in zip(agreements, (projects[:3], projects[3:])) for prj in group]
prj = Project.objects.select_related("prj_agreement__agreement").order_by("name").first()
assert prj.agreement.ordinal == agreements[0].ordinal
"""
The relational structure these models create in the DB:
- 3 tables
- through-table enforces Many-to-One relation
"""
# But consider the following alternate implementation...
class ProjectWithAgreement(Project):
agreement = models.ForeignKey(Agreement, on_delete=models.CASCADE, related_name='project_set')
# Usage: exactly as we would like:
mti_projects = [ProjectWithAgreement.objects.create(name=f"Prj w/ Agreement {i}", agreement=agreements[0]) for i in range(3)]
prj2 = ProjectWithAgreement.objects.select_related("agreement").order_by("name").first()
assert prj2.agreement.pk == agreements[0].pk
"""
The relational structure this Multi-table Inheritance creates in the DB:
- 3 tables
- Multi-table inheritance model enforces Many-to-One relation between Project and Agreement
Boom! Adding the FK to Multi-table Inherited Model creates an near-identical DB structure as the OneToMany through model!
but django support for MTI now makes syntax very natural with clean queries and no need to add a property to the "Many" side model.
Conclusion: Multi-table inheritance is often panned, even "considered harmful", but when used with intention and to solve
specific problems it provides a powerful syntax for creating structures in the DB that otherwise require more complex syntax / coupling.
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment