I'm building a REST API and server using Django and Django Rest Framework. We are using a postgres database.
I need to simplify a badly designed relation. We have a model (House
) that has a ManyToMany
relationship to another (City
). In reality it would be enough when this is a ForeignKey
relationship.
I googled and couldn't find any blog posts or docs how to properly migrate in this direction. I could only find the other way (FK to M2M).
I'm 98% sure that all the data on the server will be consistent with a FK relationship (meaning I'm pretty sure all houses only have one city). We need to change the relationship for several reasons and aren't able to keep the M2M.
I'm afraid to just change the model and running makemigrations
and migrate
. I was wondering, how do you properly migrate from M2M to FK? Are there any caveats I have to take into account? How can I deal with data if surprisingly there are houses with multiple city's? The data set is still quite small (less than 10k entries) if that matters.
Thank you very much.
EDIT First create a DB backup
First create a new temporary FK relationship
_city = models.ForeignKey(...)
and make a migration
python manage.py makemigration
python manage.py migrate
You then need to create a new data migration in the relevant app by creating an empty migration file:
python manage.py makemigration --empty myhouseapp
and in that file manually assign the new relationship from the M2M to the FK. It will look something like:
from django.db import migrations
def save_city_fk(apps, schema):
City = apps.get_model('myapp', 'City')
House = apps.get_model('myapp', 'House')
for house in House.objects.all():
house._city = house.cities.all().first() # Or whatever criterea you want
house._city.save()
def save_city_m2m(apps, schema):
# This should do the reverse
City = apps.get_model('myapp', 'City')
House = apps.get_model('myapp', 'House')
for house in House.objects.all():
if house._city:
house.cities.add(house._city)
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.RunPython(save_city_fk, save_city_m2m)
]
Remove the M2M field, and create another migration.
python manage.py makemigrations
Rename the FK from _city
to city
and create a final migration
python manage.py makemigrations
Then migrate:
python manage.py migrate
Based on Timmy's answer here is what I did:
I added a field like this city = models.ForeignKey(City, related_name='has_houses', blank=True, null=True)
to avoid the related_name
for reverse relations and to have the FK blank. Then I ran makemigrations
and migrate
.
Next, I ran python manage.py makemigrations --empty houses
because my app is named houses
.
I added the code for the migration (see below). Then I ran migrate
.
I deleted the M2M field and the related_name
, null
and blank
constraints and ran makemigrations
and migrate
one last time.
Code for the migration:
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2019-02-15 09:09
from __future__ import unicode_literals
from django.db import migrations
def save_city_fk(apps, schema):
City = apps.get_model('cities', 'City')
House = apps.get_model('houses', 'House')
for house in House.objects.all():
house.city = house.cities.all().first()
house.save()
class Migration(migrations.Migration):
dependencies = [
('houses', '0003_auto_20190215_0949'),
]
operations = [
migrations.RunPython(save_city_fk),
]
First obviously you need to make sure (by making the appropriate query) that you really only have one city per house. If there are houses which have more than one city, you need to resolve the conflict by deleting cities from the relationship, splitting houses etc.
After that, you can do it in steps:
House
, migrate and populate it with the one city id from the old m2m relationshipIf you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With