16

I have two classes, one of which is descended from the other, and I would like to make them both sibling classes descended from the same base class.

Before:

from django.db import models

class A(models.Model):
    name = models.CharField(max_length=10)

class B(models.Model):
    title = models.CharField(max_length=10)

After:

from django.db import models

class Base(models.Model):
    name = models.CharField(max_length=10)

class A(Base):
    pass

class B(Base):
    title = models.CharField(max_length=10)

When I generate a schema migration, this is the output, including my answers to the questions:

+ Added model basetest.Base
? The field 'B.a_ptr' does not have a default specified, yet is NOT NULL.
? Since you are removing this field, you MUST specify a default
? value to use for existing rows. Would you like to:
?  1. Quit now, and add a default to the field in models.py
?  2. Specify a one-off value to use for existing columns now
?  3. Disable the backwards migration by raising an exception.
? Please select a choice: 3
- Deleted field a_ptr on basetest.B
? The field 'B.base_ptr' does not have a default specified, yet is NOT NULL.
? Since you are adding this field, you MUST specify a default
? value to use for existing rows. Would you like to:
?  1. Quit now, and add a default to the field in models.py
?  2. Specify a one-off value to use for existing columns now
? Please select a choice: 2
? Please enter Python code for your one-off default value.
? The datetime module is available, so you can do e.g. datetime.date.today()
>>> 37
+ Added field base_ptr on basetest.B
? The field 'A.id' does not have a default specified, yet is NOT NULL.
? Since you are removing this field, you MUST specify a default
? value to use for existing rows. Would you like to:
?  1. Quit now, and add a default to the field in models.py
?  2. Specify a one-off value to use for existing columns now
?  3. Disable the backwards migration by raising an exception.
? Please select a choice: 3
- Deleted field id on basetest.A
? The field 'A.name' does not have a default specified, yet is NOT NULL.
? Since you are removing this field, you MUST specify a default
? value to use for existing rows. Would you like to:
?  1. Quit now, and add a default to the field in models.py
?  2. Specify a one-off value to use for existing columns now
?  3. Disable the backwards migration by raising an exception.
? Please select a choice: 3
- Deleted field name on basetest.A
? The field 'A.base_ptr' does not have a default specified, yet is NOT NULL.
? Since you are adding this field, you MUST specify a default
? value to use for existing rows. Would you like to:
?  1. Quit now, and add a default to the field in models.py
?  2. Specify a one-off value to use for existing columns now
? Please select a choice: 2
? Please enter Python code for your one-off default value.
? The datetime module is available, so you can do e.g. datetime.date.today()
>>> 73
+ Added field base_ptr on basetest.A
Created 0002_auto__add_base__del_field_b_a_ptr__add_field_b_base_ptr__del_field_a_i.py. You can now apply this migration with: ./manage.py migrate basetest

I do not know how to answer the questions about default values for B.base_ptr and A.base_ptr. Any constant I give causes the migration to fail when it is run, with this output:

FATAL ERROR - The following SQL query failed: CREATE TABLE "_south_new_basetest_a" ()
The error was: near ")": syntax error
RuntimeError: Cannot reverse this migration. 'B.a_ptr' and its values cannot be restored.

This is the result when I use sqlite3, by the way. Using Postgres gives something like this:

FATAL ERROR - The following SQL query failed: ALTER TABLE "basetest_a" ADD COLUMN "base_ptr_id" integer NOT NULL PRIMARY KEY DEFAULT 73;
The error was: could not create unique index "basetest_a_pkey"
DETAIL:  Key (base_ptr_id)=(73) is duplicated.

Error in migration: basetest:0002_auto__add_base__del_field_b_a_ptr__add_field_b_base_ptr__del_field_a_i
IntegrityError: could not create unique index "basetest_a_pkey"
DETAIL:  Key (base_ptr_id)=(73) is duplicated.

What values should I use for base_ptr to make this migration work? Thanks!

Jack Twilley
  • 346
  • 2
  • 10
  • I do think that most people coming to this thread are looking for my answer (+13 vs +10) perhaps you could accept it @JackTwilley – Oleg Belousov Feb 13 '20 at 04:18

2 Answers2

16

If base will not be instantiated on its own, you can easily solve the problem using abstract = True prop to class Meta.

Example code:

from django.db import models

class Base(models.Model):
    name = models.CharField(max_length=10)
    class Meta:
        abstract = True

class A(Base):
    pass

class B(Base):
    title = models.CharField(max_length=10)
Brian Burns
  • 20,575
  • 8
  • 83
  • 77
Oleg Belousov
  • 9,981
  • 14
  • 72
  • 127
  • 2
    Excellent, this is a really significant detail/difference to the first answer and necessary to have as a reference here. – jhrr Oct 04 '17 at 19:53
12

You do this in separate phases.

Phase 1: Create your "Base" model in the code. On the A and B models, add base_ptr as a nullable FK to Base (the name base_ptr is made by lowercasing the class-name Base, adapt your names accordingly). Specify db_column='base_ptr' on the new column, so you don't get an _id suffix added. Don't change parenthood yet: Keep B as a child of A and A as it was before (Base has no child classes yet). Add a migration to make the respective database changes, and run it.

Phase 2: Create a data migration, copying relevant data around. You should probably copy all A data into Base, remove redundant A records (those that served B instances), and in remaining records (of both A and B) copy the id into base_ptr. Note that the child class B uses two tables -- its id field comes from A's table, and on its own table there is a field a_ptr which is a FK to A -- so your update operation will be more efficient if you copy values from a_ptr to base_ptr. Make sure the copying into base_ptr occurs after the copying into the Base table, so you don't violate the FK constraints.

Phase 3: Now change the models again -- remove the explicit base_ptr FK and change parents to the way you like, and create a third migration (automatic schema migration). Note that setting the parent to Base implicitly defines a non-nullable base_ptr field, so with respect to the base_ptr fields, you are only changing a nullable field into non-nullable, and no default is needed.

You should still be asked for a default value for a_ptr -- the implicit FK from B to A that is removed when the parent is changed from A to Base; the default is needed for the migration in the backward direction. You can either do something that will fail the backward migration, or, if you do want to support it, add an explicit nullable a_ptr to B, like the base_ptr columns you used before. This nullable column can then be removed in a fourth migration.

Shai Berger
  • 2,963
  • 1
  • 20
  • 14
  • 1
    Worth watching out for generic relations that point to A or B, their relationships will be lost if you move to phase 3 (changing the parents) too early as the original id columns are dropped. I thought I could backfill data after the fact (once I had the 1:1 relationship set up) but could no longer establish where the generic foreign keys were supposed to point to. Thanks for this thorough walkthrough. I had to read it a few times to fully understand but it works great. – owenfi Aug 21 '14 at 06:29
  • 1
    @owenfi I'm happy you found this helpful; can you maybe say what the hard points were, so we can improve the answer? – Shai Berger Aug 22 '14 at 10:53
  • 1
    I read past or didn't comprehend "don't change parenthood" in part 1 for a while (until struggling with South's default-value prompts). I didn't know the original model's id field would be lost and replaced with the parent class's (This seems non-intuitive, as it could keep/rename the id field, and just have both -- as it is, the subclasses no longer have their own primary key, they just point to the parent's). Every time I thought I caught something missing from the answer I went back and re-read it and it was there (such as don't change parenthood) so really it's just info dense – owenfi Aug 22 '14 at 21:49
  • 1
    I tried to improve a little, would appreciate your feedback. – Shai Berger Aug 30 '14 at 13:01
  • 1
    One thing your example accounts for is Jack mentioning "one of [the models] which is descended from the other" (B from A?) But his Before code actually doesn't show that (both descend from the standard base class). In that case (my models were that way) it simplifies phase 1 & 2 somewhat, but your description should work for both. The clarification seems helpful, I think it makes it more clear and easy to comprehend in one pass, but perhaps will leave the true judgment up to the next unfamiliarized passerby. – owenfi Sep 01 '14 at 22:05
  • It seems like this way I can only run the migrations once. And then I have to be careful to run step 2 and 3 in model "snapshots", as in: running step 2, modify the model code, create and run the migrations for step 3. Because if I restore a database dump and try to run everything at once it asks for a 'base_ptr_id' field again. – tutuca Aug 04 '19 at 21:11
  • @tutuca You should only be asked this sort of question when generating migrations, not when running them. Also, specifically, being asked for `base_ptr_id` indicates that you left out the `db_column='base_ptr' ` part. Also also, some details may have changed between South and Django Migrations (I do hope you're not using a pre-1.7 version of Django). – Shai Berger Aug 05 '19 at 22:37
  • I did not. The migration corresponding to the first step has the right configuration and sets the `db_column` correctly. It's after i create the "third" migration, that it complains. I believe it's because I do the data migration (step 2) using a `RunPython` operation. It's really weird because if I run the step 1 and 2 isolated, without the migration for step 3 created, the data migration runs ok. If I remove the ForeignKey, reparent all derived clases and create the third migration that it turns unreproducible. I'll try to create a minimal example. – tutuca Aug 06 '19 at 14:53