Created
March 10, 2025 23:08
-
-
Save powderflask/151f60a6115d3c362cee62a59d2ab53e to your computer and use it in GitHub Desktop.
Django One-to-Many: data models
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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