68

I have a Django application in which I want to change a field from a ForeignKey to a ManyToManyField. I want to preserve my old data. What is the simplest/best process to follow for this? If it matters, I use sqlite3 as my database back-end.

If my summary of the problem isn't clear, here is an example. Say I have two models:

class Author(models.Model):  
    author = models.CharField(max_length=100) 

class Book(models.Model):  
    author = models.ForeignKey(Author)  
    title = models.CharField(max_length=100)

Say I have a lot of data in my database. Now, I want to change the Book model as follows:

class Book(models.Model):  
    author = models.ManyToManyField(Author)  
    title = models.CharField(max_length=100) 

I don't want to "lose" all my prior data.

What is the best/simplest way to accomplish this?

Ken

Savad KP
  • 1,625
  • 3
  • 28
  • 40
Ken H
  • 681
  • 1
  • 5
  • 3

2 Answers2

101

I realize this question is old and at the time the best option for Data Migrations was using South. Now Django has its own migrate command, and the process is slightly different.

I've added these models to an app called books -- adjust accordingly if that's not your case.

First, add the field to Book and a related_name to at least one, or both of them (or they'll clash):

class Book(models.Model):  
    author = models.ForeignKey(Author, related_name='book')
    authors = models.ManyToManyField(Author, related_name='books')
    title = models.CharField(max_length=100) 

Generate the migration:

$ ./manage.py makemigrations
Migrations for 'books':
  0002_auto_20151222_1457.py:
    - Add field authors to book
    - Alter field author on book

Now, create an empty migration to hold the migration of the data itself:

./manage.py makemigrations books --empty
    Migrations for 'books':
0003_auto_20151222_1459.py:

And add the following content to it. To understand exactly how this works, check the documentation on Data Migrations. Be careful not to overwrite the migration dependency.

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


def make_many_authors(apps, schema_editor):
    """
        Adds the Author object in Book.author to the
        many-to-many relationship in Book.authors
    """
    Book = apps.get_model('books', 'Book')

    for book in Book.objects.all():
        book.authors.add(book.author)


class Migration(migrations.Migration):

    dependencies = [
        ('books', '0002_auto_20151222_1457'),
    ]

    operations = [
        migrations.RunPython(make_many_authors),
    ]

Now remove the author field from the Model -- it should look like this:

class Book(models.Model):
    authors = models.ManyToManyField(Author, related_name='books')
    title = models.CharField(max_length=100)

Create a new migration for that, and run them all:

$ ./manage.py makemigrations
Migrations for 'books':
  0004_remove_book_author.py:
    - Remove field author from book

$ ./manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: messages, staticfiles
  Apply all migrations: admin, auth, sessions, books, contenttypes
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying books.0002_auto_20151222_1457... OK
  Applying books.0003_auto_20151222_1459... OK
  Applying books.0004_remove_book_author... OK

And that's it. The authors previously available at book.author now should be in the queryset you get from book.authors.all().

TechnoConserve
  • 140
  • 1
  • 11
Rodrigo Deodoro
  • 1,371
  • 1
  • 9
  • 17
  • 2
    Hey @JanSegre: not really, the many-to-many relationship changes are done the moment you run `.add()`. – Rodrigo Deodoro Aug 09 '16 at 03:39
  • Oh, you're right, thanks. That's what I get for glossing over the docs instead of reading them properly. – Jan Segre Aug 09 '16 at 11:35
  • just one suggestion, the query inside a for loop will be slow if run on a large table. using a single-line SQL will be much faster. e.g. `insert into books_book_authors (book_id, author_id) select id, author_id from books_book;` you can run it with `migrations.RunSQL` – kakarukeys Aug 12 '16 at 04:10
  • 1
    @kakarukeys That is true, but I wouldn't mention it as a primary way of doing it since it's not database-agnostic. – Rodrigo Deodoro Aug 12 '16 at 18:26
  • it is database-agnostic, as it is in SQL standard, and all db providers have not deviated from the standard syntax, see http://stackoverflow.com/a/25971 – kakarukeys Aug 14 '16 at 01:53
  • @RodrigoDeodoro how would I do that when I want to migrate a ManyToMany field to "through"? When I try your method, I get an error as I described here: http://stackoverflow.com/questions/40512976/django-migrate-manytomany-to-through – LukasKawerau Nov 09 '16 at 17:55
  • 2
    you can do it in one migration, in `operations` list write commands in this oreder: add_m2m_field, your_function_to_migrate_data, delete_fk_field, rename_m2m_field – alexey_efimov Feb 09 '17 at 05:21
  • 1
    just a note when applying for book in Book.objects.all(): book.authors.add(book.author) -- you may need an if statement i.e. if book.author – Brendan Metcalfe Jul 17 '18 at 19:14
  • Did something change about this in Django 1.11? I get a null id constraint error using this strategy. – rschwieb Sep 26 '18 at 02:41
  • [This tutorial](https://ponytech.net/blog/convert-foreign-key-many-to-many-using-django-migrations) uses the same technique as this answer, and also includes some extra details such as example data. – pianoJames Jul 29 '19 at 20:15
  • 2
    HI @Salvioner, `apps` is not an import, it's an argument that `RunPython` passes to `make_many_authors`. This answer is now 5 years old and I haven't run this code in recent years but in any case the solution for this wouldn't be adding an import. – Rodrigo Deodoro Sep 10 '20 at 19:20
  • @RodrigoDeodoro can you help me on this?? same issue. https://stackoverflow.com/questions/68467714/writing-sql-query-in-production-server-for-changing-foriegnkey-field-to-m2m-in-d – Reactoo Jul 21 '21 at 10:17
  • it works perfectly on my local but my aws data are gone – Reactoo Jul 22 '21 at 11:02
  • 2
    Hi @Django-Rocks, I'm really sorry your data is gone but I haven't worked with Django in years (this answer is 5 years old) but the first thing I'd tell you to look for are differences between your local and production environments. Good luck! – Rodrigo Deodoro Jul 22 '21 at 22:52
  • @Rodrigo Deodoro, thank you a lot!) I was very afraid, but it worked for me. For Many-to-Many field I have just deleted on_delete=models.SET_NULL and null=True. – Sergo Jul 16 '22 at 08:55
  • While `related_name='book'` is technically correct, since it’s different to the other `related_name=books`, it’s misleading, since in the also in the original ForeignKey field an author can have man books, not just one. – Jens-Erik Weber Mar 06 '23 at 16:58
3

Probably the best and easiest thing you should do would be:

Create the Many to many field with a different name say

authors = models.ManyToManyField(Author)

write a small function to convert foreignkey values to M2M values:

def convert():
    books = Book.objects.all()
    for book in books:
        if book.author:
            li = [book.author.id]
            book.authors.append(li)
            book.save()

Once it is run, you can delete the author field from the table and run migration again.

sprksh
  • 2,204
  • 2
  • 26
  • 43
  • in django 1.10 i get a `AttributeError: 'ManyRelatedManager' object has no attribute 'append'` i removed .id and had `book.authors = li` and that worked. I believe you can also do `book.authors.add(li)` and that should satisfy the issue. – hachacha Dec 16 '16 at 08:02
  • You can append and save in the models anyway you want. The question and the answer more specifically takes care of the migrations. – sprksh Oct 25 '17 at 16:09
  • Use `book.authors.add(*li)` if you get `AttributeError: 'ManyRelatedManager' object has no attribute 'append'` – Sudarshan Mar 02 '23 at 04:42