Contents

Django migration原理简述

前阵子面试的时候突然被问到Django的migration原理,还让我有多仔细讲多仔细,面试官明显吃透了这个机制要考考我,当时就跪了。。。抛开半年没有碰django项目不谈,本身对migration背后的原理确实了解不深。当时还有点嗤之以鼻,觉得用的时候或者碰到问题的时候再查就好了,然而抛开不懂知识点面试就得跪不说,在认真了解一下django migration原理后,我发现它实际上是一个十分值得学习的数据库表同步的实现,今天在这里做下记录。

什么是migration

我们知道Django使用了Active Record的架构模式:一个模型对应数据库的一个表。Migration就是一个工具,让用户在对模型进行修改后(如增删改字段),将变化同步应用到数据库对应的表中。本质上就是migration分析models的变化后生成对应的ddl命令,然后应用到数据库中。

migration流程

假设我们创建一个project neko和app nekocollect:

1
2
django-admin startproject neko
python manage.py startapp nekocollect

然后在nekocollect的models.py 中创建一个品种模型breed:

1
2
3
4
5
from django.db import models

# Create your models here.
class Breed(models.Model):
    name = models.CharField(max_length=200)

此时我们要将这个模型同步到mysql中,生成对应的表。只需在neko项目文件夹中运行以下命令:

1
2
python manage.py makemigrations
python manage.py migrate

此时我们会发现nekocollect文件夹中的migrations文件有新的文件生成:

1
2
3
migrations/
├── 0001_initial.py
├── __init__.py

里面的内容长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Generated by Django 4.1.5 on 2023-01-09 10:11
from django.db import migrations, models

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Breed',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=200)),
            ],
        ),
    ]

这个文件主要记录两个信息:dependencies和operations。前者记录本次migration所依赖的migration,由于这是第一次migration,所以为空;后者则记录了本次migration的具体改动,比如这一次就是新增模型Breed和字段name。

接下来我们尝试再添加一个字段,并为name加一个默认值:

1
2
3
4
5
6
from django.db import models

# Create your models here.
class Breed(models.Model):
    name = models.CharField(max_length=200, default="Unknown")
    color = models.CharField(max_length=200, null=True)

此时我们会发现新的migration文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Generated by Django 4.1.5 on 2023-01-09 14:16
# 0002_breed_color_alter_breed_name.py
'''
migrations/
├── 0001_initial.py
├── 0002_breed_color_alter_breed_name.py
├── __init__.py
'''
from django.db import migrations, models

class Migration(migrations.Migration):

    dependencies = [
        ('nekocollect', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='breed',
            name='color',
            field=models.CharField(default='black', max_length=200),
            preserve_default=False,
        ),
        migrations.AlterField(
            model_name='breed',
            name='name',
            field=models.CharField(default='Unknown', max_length=200),
        ),
    ]

Behind the scenes

在第二次migration中,django实际上进行了以下操作,这便是所有migration背后的具体流程:

  1. 在内存中创建一个breed模型的虚拟对象,根据nekocollect的migrations文件夹从0001文件开始,按照文件内的operations对对象进行改动,一直应用到最后的文件,得到当前模型改动前的最新版本
  2. 对比旧的最新版本与当前修改后的models.py,计算出本次要应用的改动,生成新的migration文件,这就是makemigrations命令做的事情
  3. migrate命令按照migrations文件生成ddl命令,并应用到数据库中。

然而第三步有个问题:应该从第几个migration文件开始应用?假如数据库是新的,那么从第一个文件开始应用即可,但假如是已经执行过0001文件的数据库呢?进一步考虑,假如在更新数据库之前我们又新增了若干个migrations,怎么让django知道从哪里开始应用?要知道ddl命令不是幂等的,同一个命令执行两遍很有可能会出错。

这里的核心问题是:整套migrations是幂等的,而数据库是有状态的,我们需要知道数据库已经执行了哪些migration,这样才能从未执行的开始。

migrations数据表

为了解决上面这个问题,django会在数据库中创建一个表django_migrations,里面记录这个数据库应用了哪些migration:

有了这个表,django在migrate的时候便能知道数据库已经执行了哪些migration,接着从未执行的开始应用,这样就不会因重复执行ddl而出错了。

总结

总的来说,django在执行migrate的时候,会根据migrations文件计算出当前未修改的最新模型,然后与修改后的模型进行对比,生成新的migration文件,然后到数据库中,按照django_migrations表的记录,定位最旧的未应用migration,然后从该文件开始生成并执行ddl语句,当所有migration都应用完,更新django_migrations表,自此,假如中途没有出错,则数据库表与models.py同步完成。

Reference

Django

Django migration 原理 | 卡瓦邦噶!