16👍
Looks like there is no way to use through option without having to do data migrations. So had to resort to data migration approach, I took some ideas from @pista329’s answer and solved the issue using following steps.
-
Create
Authorship
modelclass Authorship(models.Model): book = models.ForeignKey(Books) author = models.ForeignKey(Authors) ordering = models.PositiveIntegerField(default=1)
-
Keep original ManyToManyField relation, but add another field using above defined model as through model:
class Authors(models.Model): name = models.CharField(max_length=100) books = models.ManyToManyField(Books) published_books = models.ManyToManyField( to=Books, through=Authorship, related_name='authors_lst' # different related name is needed. )
IMPORTANT: You must use a different
related_name
to the existing ManyToManyField. If you don’t do this then Django may lose the data in the original field. -
Add data migration to copy all data from old table to new
Authorship
table.
After this, books
field on Authors
model can be removed as we have new field named published_books
.
43👍
There is a way to add "through" without data migrations.
I managed to do it based on this @MatthewWilkes’ answer.
So, to translate it to your data model:
-
Create the
Authorship
model only withbook
andauthor
fields. Specify the table name to use the same name as the auto-generated M2M table you already have. Specify theunique_together
attribute to match what the auto-generated M2M table does (source). Add the ‘through’ parameter.class Authorship(models.Model): book = models.ForeignKey(Books) author = models.ForeignKey(Authors) class Meta: db_table = 'app_name_authors_books' unique_together = ['book', 'author'] class Authors(models.Model): name = models.CharField(max_length=100) books = models.ManyToManyField(Books, through=Authorship)
-
Generate a migration, but don’t run it yet.
-
Edit the generated migration and wrap the migration operations into a
migrations. SeparateDatabaseAndState
operation with all the operations insidestate_operations
field (withdatabase_operations
left empty). You will end up with something like this:operations = [ migrations.SeparateDatabaseAndState(state_operations=[ migrations.CreateModel( name='Authorship', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('book', models.ForeignKey(to='app_name.Books')), ], options={ 'db_table': 'app_name_authors_books', }, ), migrations.AlterField( model_name='authors', name='books', field=models.ManyToManyField(through='app_name.Authorship', to='app_name.Books'), ), migrations.AddField( model_name='authorship', name='author', field=models.ForeignKey( to='app_name.Author'), ), migrations.AlterUniqueTogether( name='authorship', unique_together={('book', 'author')}, ), ]) ]
-
You can now run the migration and add the extra
ordering
field to your M2M table.
Edit:
Apparently, column names in the DB are generated slightly differently for automatic M2M tables as for models-defined tables. (I am using Django 1.9.3.)
After the described procedure, I also had to manually change the column names of a field with a 2-word name (two_words=models.ForeignKey(...)
) from twowords_id
to two_words_id
.
- [Django]-Serving Media files during deployment in django 1.8
- [Django]-How do I deploy Django on AWS?
- [Django]-SyntaxError: Generator expression must be parenthezised / python manage.py migrate
2👍
Migrations can be messy sometimes.
If you want to alter m2m field with through, I would suggest to rename altered field Authors.books
to Authors.book
. When asked by makemigrations
, if you changed name from books to book? [yN]
, choose “N
” as No. Django will delete books
and create book
field instead of altering.
class Authorship(models.Model):
book = models.ForeignKey("Books")
author = models.ForeignKey("Authors")
ordering = models.PositiveIntegerField(default=1)
class Authors(models.Model):
name = models.CharField(max_length=100)
book = models.ManyToManyField("Books", through="Authorship")
If you want to use books
anyway, change book
to books
and repeat migration process with y
as answer to makemigrations question about renaming.
- [Django]-Django package to generate random alphanumeric string
- [Django]-Django admin page Removing 'Group'
- [Django]-Custom django-user object has no attribute 'has_module_perms'