前阵子面试的时候突然被问到Django的migration原理,还让我有多仔细讲多仔细,面试官明显吃透了这个机制要考考我,当时就跪了。。。抛开半年没有碰django项目不谈,本身对migration背后的原理确实了解不深。当时还有点嗤之以鼻,觉得用的时候或者碰到问题的时候再查就好了,然而抛开不懂知识点面试就得跪不说,在认真了解一下django migration原理后,我发现它实际上是一个十分值得学习的数据库表同步的实现,今天在这里做下记录。
什么是migration
我们知道Django使用了Active Record的架构模式:一个模型对应数据库的一个表。Migration就是一个工具,让用户在对模型进行修改后(如增删改字段),将变化同步应用到数据库对应的表中。本质上就是migration分析models的变化后生成对应的ddl命令,然后应用到数据库中。
migration流程
假设我们创建一个project neko和app nekocollect:
django-admin startproject neko
python manage.py startapp nekocollect
然后在nekocollect的models.py 中创建一个品种模型breed:
from django.db import models
# Create your models here.
class Breed(models.Model):
name = models.CharField(max_length=200)
此时我们要将这个模型同步到mysql中,生成对应的表。只需在neko项目文件夹中运行以下命令:
python manage.py makemigrations
python manage.py migrate
此时我们会发现nekocollect文件夹中的migrations文件有新的文件生成:
migrations/
├── 0001_initial.py
├── __init__.py
里面的内容长这样:
# 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加一个默认值:
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文件:
# 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背后的具体流程:
- 在内存中创建一个breed模型的虚拟对象,根据nekocollect的migrations文件夹从0001文件开始,按照文件内的operations对对象进行改动,一直应用到最后的文件,得到当前模型改动前的最新版本
- 对比旧的最新版本与当前修改后的models.py,计算出本次要应用的改动,生成新的migration文件,这就是makemigrations命令做的事情
- 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
comments powered by Disqus