7

Scenario:
I have a model, Customer

class Customer(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()
    company = models.CharField(max_length=100)


and now I updated the company attribute witha ForeignKey relationship as below,

class Company(models.Model):
    name = models.CharField(max_length=100)
    location = models.CharField(max_length=100)


class Customer(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()
    company = models.ForeignKey(Company)



What I need is, when the new migrations applied to the DB,corresponding Company instance must automatically generate and map to the company attribute of Customer instance.

Is that possible? How can I achieve this ?

JPG
  • 82,442
  • 19
  • 127
  • 206

3 Answers3

16

Let's start from your original model and do it step by step.

class Customer(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()
    company = models.CharField(max_length=100)

First you would have to keep the original field and create a new one, to be able to restore the old data afterwards.

class Customer(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()
    company = models.CharField(max_length=100)
    _company = models.ForeignKey(Company)

Now you can create a first migration with manage.py makemigrations. Then you will have to create a data migration. Create the migration using manage.py makemigrations yourapp --empty and update the generated file:

from django.db import migrations

def export_customer_company(apps, schema_editor):
    Customer = apps.get_model('yourapp', 'Customer')
    Company = apps.get_model('yourapp', 'Company')
    for customer in Customer.objects.all():
        customer._company = Company.objects.get_or_create(name=customer.company)[0]
        customer.save()

def revert_export_customer_company(apps, schema_editor):
    Customer = apps.get_model('yourapp', 'Customer')
    Company = apps.get_model('yourapp', 'Company')
    for customer in Customer.objects.filter(_company__isnull=False):
        customer.company = customer._company.name
        customer.save()

class Migration(migrations.Migration):

    dependencies = [
        ('yourapp', 'xxxx_previous_migration'),  # Note this is auto-generated by django
    ]

    operations = [
        migrations.RunPython(export_customer_company, revert_export_customer_company),
    ]

The above migration will populate your Company model and Customer._company field according to Customer.company.

Now you can drop the old Customer.company and rename Customer._company.

class Customer(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()
    company = models.ForeignKey(Company)

Final manage.py makemigrations and manage.py migrate.

Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
  • 1
    Almost! `customer._company = Company.objects.get_or_create(name=customer.company) ` wont work as get_or_create returns a tuple with `(object, created)` – krs Mar 11 '18 at 15:45
  • Can I have to ask you something, what's the purpose of `noop()` method? – JPG Mar 12 '18 at 07:13
  • This is a method that does nothing. Which means there is nothing to do when reversing the migration. I invite you to read the link I posted about migration. Everything I used is detailed here. :) – Antoine Pinsard Mar 12 '18 at 07:25
  • I applied the migrations as you said. Then I reverted to zero, ( `python manage.py app_name zero`) .Then I migrated again, ath this point I got some `operational error` – JPG Mar 12 '18 at 11:57
  • Did the migrations work in the first place? Any reason why you reverted to zero? What migration caused the operational error? The details of the operational error would be welcome. – Antoine Pinsard Mar 12 '18 at 12:08
  • @AntoinePinsard Migrations are migrated forward direction, ( `0010 -> 0011 -> 0012 -> etc` ). But, I'm little bit curious about `worst case scenario` :P That's why I'm thinking about revert condition too – JPG Mar 12 '18 at 16:15
  • I updated my answer to include an example of reverse code. About your migration issue, we don't have enough information to help you. I'd suggest you open another question for that one rather than solving this in comments. – Antoine Pinsard Mar 12 '18 at 16:27
  • What kind of worst case scenario are you trying to test? – Antoine Pinsard Mar 12 '18 at 16:31
3

Sure, but you have to do three migrations and the fields cant be named the same thing as both need to exist at the same time. If you already have removed the company field in your real database you are SOL and will have to fix them manually.

First, add the Company model in a normal db migration, then do a data migration and have it run after the first db migration, then do another db migration removing the company field from the Customer model.

The db migrations you can do with manage.py makemigrations as usual, just add something like below in a migration file between them, here i named the new company ForeignKey field to company_obj

def fix_companies(apps, schema_editor):
    Company = apps.get_model("myapp", "Company")
    Customer = apps.get_model("myapp", "Customer")
    for c in Customer.objects.all():
        company, _ = Company.objects.get_or_create(name=c.name)
        c.company_obj = company
        c.save()


def rev(apps, schema_editor):
    # the reverse goes here if you want to copy company names into customer again if you migrate backwards.
    pass

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', 'XXXX_migration_that_added_company_model'),
    ]

    operations = [
        migrations.RunPython(fix_companies, rev),
    ]
krs
  • 4,096
  • 19
  • 22
1

Something to note is if you are going through a cycle of renaming/ deprecating fields, using RunPython would leave you pointing to old model fields that wouldn't exist anymore after you are done with your field changes.

To avoid this, you might want to go with RunSQL instead.

# Generated by Django 3.2.3 on 2022-02-09 04:55

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ("<your_app>", "<0006_migration_name>"),
    ]

    operations = [
        migrations.RunSQL(f"""
update public.<table_name> set new_field = old_field + some_magic;
"""
        ),
    ]

Docs.

karuhanga
  • 3,010
  • 1
  • 27
  • 30