Let's say we've decided that a change needs to be made to our project:
We found a mistake in the CourseTerm Model: The term_identifier is supposed to look like 20183COMSW1002
which is 14 characters (not 10) and unique. What will this involve:
# Generated by Django 2.1.3 on 2018-11-09 19:23fromdjango.dbimportmigrations,modelsclassMigration(migrations.Migration):dependencies=[('myapp','0002_auto_20181019_1821'),]operations=[migrations.AlterField(model_name='courseterm',name='term_identifier',field=models.TextField(max_length=14,unique=True),),]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(env)django-training$ ./manage.py migrateOperations to perform: Apply all migrations: admin, auth, contenttypes, myapp, oauth2_provider, sessionsRunning migrations: Applying myapp.0003_auto_20181109_1923...Traceback (most recent call last): File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/db/backends/utils.py", line 85, in _execute return self.cursor.execute(sql, params) File "/Users/alan/src/django-training/env/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 296, in execute return Database.Cursor.execute(self, query, params)sqlite3.IntegrityError: UNIQUE constraint failed: myapp_courseterm.term_identifierThe above exception was the direct cause of the following exception:Traceback (most recent call last): ... lots of stack trace here ...
This migration doesn't "just work" because our test data has a repeated non-unique field
which we have to fix to be unique. We can describe this pretty easily as "Concatentate old non-unique term_identifier
(e.g. '20181') with course.course_identifier('COMSW1002') to create new term_identifier ('20181COMSW1002')".
One approach would be to just manually "fix" the data. But a much cooler approach is to
develop some migration code, which documents what we fixed and is reproducible and reversible. See
this migration how-to
for an example.
Let's try it:
Change the migration to alter the term_identifier field length but not yet make it unique.
fromdjango.dbimportmigrations,modelsdeffix_term_id(apps,schema_editor):""" Concatentate old non-unique term_identifier (e.g. '20181') with course.course_identifier('COMSW1002') to create new term_identifier ('20181COMSW1002') """CourseTerm=apps.get_model('myapp','CourseTerm')forrowinCourseTerm.objects.all():ifrow.course:# there's a parent course relationshipcourse_id=row.course.course_identifierrow.term_identifier=row.term_identifier+course_idrow.save(update_fields=['term_identifier'])else:# there's no parent course so throw this row awayrow.delete()defundo_fix_term_id(apps,schema_editor):""" Revert fix_term_id(). """CourseTerm=apps.get_model('myapp','CourseTerm')forrowinCourseTerm.objects.all():row.term_identifier=row.term_identifier[:5]row.save(update_fields=['term_identifier'])classMigration(migrations.Migration):dependencies=[('myapp','0002_auto_20181019_1821'),]operations=[migrations.AlterField(model_name='courseterm',name='term_identifier',field=models.TextField(max_length=14),),migrations.RunPython(fix_term_id,reverse_code=undo_fix_term_id),migrations.AlterField(model_name='courseterm',name='term_identifier',field=models.TextField(unique=True),),]
Confirm the current database schema and non-unique content¶
Let's first take a look at our current database before the migration (noting that term_identifier is text NOT NULL).
And, here's the coolest part: You can reverse a migration to go back to a previous state, assuming
you included areverse_code migration script:
1
2
3
4
5
6
(env)django-training$ ./manage.py migrate myapp 0002_auto_20181019_1821Operations to perform: Target specific migration: 0002_auto_20181019_1821, from myappRunning migrations: Rendering model states... DONE Unapplying myapp.0003_unique_term_identifier... OK
Make migrations reversible allows you to revert to a prior production release of your code just in case
you discover an issue.
Finally, as an optional exercise, notice that I inadvertently made term_identifier = models.TextField
when it should have been term_identifier = models.CharField. Fix that.
Here's a hint if the data is not just the testcases:
1. Reverse the migration as above.
2. Delete the content of the course and courseterm tables:
3. Load the testcases fixture: ./manage.py loaddata myapp/fixtures/testcases.yaml
4. Migrate forward.
5. Dump the new testcases fixture: ./manage.py dumpdata --format yaml myapp >myapp/fixtures/testcases.yaml
Now do the same for the big courseterm.yaml fixture. This could take a while! Actually, with DEBUG turned off,
it went pretty quickly: