A few times in the last year, I've run into the need to add some metadata to a Django many-to-many relationship. By default, there's no explicit model to add fields to, but - if you're working on an active project - you probably have existing data in the default 'through' table that you don't want to lose. So what are you to do if you don't want to have to create a completely new table and migrate the data over? Let's hijack the existing one.
The existing models:
from django.db import models
class ModelA(models.Model):
b_models = models.ManyToManyField("app.ModelB", related_name="a_models", blank=True)
class Meta:
db_table = "app_model_a"
class ModelB(models.Model):
class Meta:
db_table = "app_model_b"
Create a model that matches the existing 'through' table exactly - and make sure to specify the existing table name using Meta.db_table
:
from django.db import models
class ModelAModelB(models.Model):
modela = models.ForeignKey("app.ModelA", on_delete=models.CASCADE)
modelb = models.ForeignKey("app.ModelB", on_delete=models.CASCADE)
class Meta:
db_table = "app_model_a_model_b"
Update the many-to-many relationship to use the new model:
class ModelA(models.Model):
b_models = models.ManyToManyField("app.ModelB", related_name="a_models", through="app.ModelAModelB", blank=True)
class Meta:
db_table = "app_model_a"
Now generate a new migration with python manage.py makemigrations
; it'll need some editing. The initial migration:
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api_app', '69420_whatever_your_last_migration_was'),
]
operations = [
migrations.CreateModel(
name='ModelAModelB',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'db_table': 'app_model_a_model_b',
},
),
migrations.AlterField(
model_name='modela',
name='b_models',
field=models.ManyToManyField(blank=True, related_name='a_models', through='app.ModelAModelB', to='app.ModelB'),
),
migrations.AddField(
model_name='modelamodelb',
name='modela',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelA'),
),
migrations.AddField(
model_name='modelamodelb',
name='modelb',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelB'),
),
]
We just need to wrap these operations in migrations.SeparateDatabaseAndState
to get the models in sync without screwing up the existing database setup. All of the changes above represent state changes:
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api_app', '69420_whatever_your_last_migration_was'),
]
state_operations = [
migrations.CreateModel(
name='ModelAModelB',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'db_table': 'app_model_a_model_b',
},
),
migrations.AlterField(
model_name='modela',
name='b_models',
field=models.ManyToManyField(blank=True, related_name='a_models', through='app.ModelAModelB', to='app.ModelB'),
),
migrations.AddField(
model_name='modelamodelb',
name='modela',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelA'),
),
migrations.AddField(
model_name='modelamodelb',
name='modelb',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelB'),
),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=state_operations,
)
]
And that should do it. That migration should successfully tie the existing 'through' table to your new model. Now you can add fields and create additional migrations as normal.
For my notes - filtering on 'through' fields
I want to make one additional note - since it wasn't immediately obvious to me. With Django 2.2, you should be able to filter on the 'through' table fields by using the lowercase name of the 'through' model.
You can add something like state
to the 'through' model:
class ModelAModelB(models.Model):
modela = models.ForeignKey("app.ModelA", on_delete=models.CASCADE)
modelb = models.ForeignKey("app.ModelB", on_delete=models.CASCADE)
state = models.CharField(
max_length=16,
null=True,
blank=True,
choices=[("good", "good"), ("ungood", "ungood")]
)
class Meta:
db_table = "app_model_a_model_b"
Then filtering on this field looks a lot like this:
# querysets
ModelA.objects.filter(modelamodelb__state="good")
ModelB.objects.filter(modelamodelb__state="ungood")
ModelAModelB.objects.filter(state="good")
# related manager on instances
a_instance = ModelA.objects.first()
a_instance.b_models.filter(modelamodelb__state="ungood")
b_instance = ModelB.objects.first()
b_instance.a_models.filter(modelamodelb__state="good")
Now hopefully I can just come back to this post next time I need to do this.
Prefer to catch my posts elsewhere?
Originally posted on my blog: https://typenil.com/hijacking-default-django-through-tables/
Congratulations @typenil! You received a personal award!
You can view your badges on your Steem Board and compare to others on the Steem Ranking
Do not miss the last post from @steemitboard:
Vote for @Steemitboard as a witness to get one more award and increased upvotes!
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit