关系型数据库的增删改查等操作,需要用到的是 SQL 语言。Django 为了保护程序员的头发,附带了一个对象关系映射器(简称 ORM),可以将数据库 SQL 映射到面向对象的 Python 中来,使得你可以在 Django 中像操作普通对象一样操作数据库。其直观表现就是模型 (Model)。
迁移工作流
company
INSTALLED_APPScompanycompanymigrations/__init__.py
models.py
class Company(models.Model):
name = models.CharField(max_length=20, verbose_name='Company name')
class Person(models.Model):
name = models.CharField(max_length=20, verbose_name='name')
company = models.CharField(max_length=50, verbose_name='company')
address = models.CharField(max_length=50, default='', verbose_name='address')
makemigrations
danlan@bogon ~/Documents/code/mysite python3 manage.py makemigrations company
Migrations for 'company':
company/migrations/0001_initial.py
- Create model Company
- Create model Person
company/migrations/0001_initial.pymakemigrations
文件内容:
# Generated by Django 2.0.3 on 2020-12-02 14:28
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Company',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, verbose_name='Company name')),
],
),
migrations.CreateModel(
name='Person',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, verbose_name='name')),
('company', models.CharField(max_length=50, verbose_name='company')),
('address', models.CharField(default='', max_length=50, verbose_name='address')),
],
),
]
就是一个普通的 Python 文件嘛:
initialdependenciesoperationsCreateModelnamefields
migrate
danlan@bogon ~/Documents/code/mysite python3 manage.py migrate company
Operations to perform:
Apply all migrations: company
Running migrations:
Applying company.0001_initial... OK # compangy的迁移
迁移文件
初次迁移完成后,我们要把Person模型中的company字段变为一个ForeignKey,因为一个公司可能有许多人
class Company(models.Model):
name = models.CharField(max_length=20, verbose_name='Company name')
class Person(models.Model):
name = models.CharField(max_length=20, verbose_name='name')
company = models.ForeignKey(Company, on_delete=models.CASCADE, verbose_name='company')
address = models.CharField(max_length=50, default='', verbose_name='address')
执行完迁移后,又多出了company/migrations/0002_auto_20201202_1445.py
class Migration(migrations.Migration):
dependencies = [
('company', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='person',
name='company',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='company.Company', verbose_name='company'),
),
]
注意:
当您对一个模型进行增减一个字段或改变一个字段的数据类型时,你一定要考虑数据表已经存在的数据与新数据类型不匹配的问题,之前的company对应数据是字符串, 而新对应的字段是整数(company_id),你把字符串存在整数格式的字段里能不出问题吗?当已存在的数据不符合新的数据类型要求时,我们必需要把那些不符合要求的数据先删除掉或替换掉,才能更改数据表。
对于这个问题,有两种解决方案。
做法1 暴力做法
使用python manage.py flush命令清空所有数据表中的数据,然后再删除migrations文件夹里所有py文件(除了__init__.py)。我们的模型一下回到原点,变成全新模型了,自然不会有问题啦。缺点是所有已有数据都丢失了。
做法2 推荐做法
先删除migrations文件夹里所有py文件(除了__init__.py),再运行运行python manage.py migrate。这时仅不符合新数据要求的company字段数据(比如Baidu)丢失,John和Max对象还在。如果数据表中已存在的数据还有价值,建议先对数据库被备份再执行migrate命令迁移。
dependencies0001_initial.py
migrations
operationsAlterField
再修改模型:
class Company(models.Model):
name = models.CharField(max_length=20, verbose_name='Company name')
class Person(models.Model):
name = models.CharField(max_length=20, verbose_name='name')
company = models.ForeignKey(Company, on_delete=models.CASCADE, verbose_name='company')
# address我不想要了
# address = models.CharField(max_length=50, default='', verbose_name='address')
新增的迁移文件company/migrations/0003_remove_person_address.py如下:
class Migration(migrations.Migration):
dependencies = [
('company', '0002_auto_20201202_1445'),
]
operations = [
migrations.RemoveField(
model_name='person',
name='address',
),
]
你可以更清楚的看出迁移文件的工作模式了,即每个迁移文件记录的仅仅是和上一次的变化,每一次对数据库的操作是高度依赖的。
你还可以通过指令查看迁移文件将实际执行的 SQL 操作:
danlan@bogon ~/Documents/code/mysite python3 manage.py sqlmigrate company 0003
BEGIN;
--
-- Remove field address from person
--
ALTER TABLE "company_person" RENAME TO "company_person__old";
CREATE TABLE "company_person" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(20) NOT NULL, "company_id" integer NOT NULL REFERENCES "company_company" ("id") DEFERRABLE INITIALLY DEFERRED);
INSERT INTO "company_person" ("id", "name", "company_id") SELECT "id", "name", "company_id" FROM "company_person__old";
DROP TABLE "company_person__old";
CREATE INDEX "company_person_company_id_a3260f3a" ON "company_person" ("company_id");
COMMIT;
迁移记录表
django_migrations
表里的每一条记录都和迁移文件是对应的,如果这个表里已经有迁移记录了,那么对应的迁移文件中的指令就不再执行了。
实验一
手动将最后一个迁移文件0003_remove_person_address.py删除掉,再执行迁移
danlan@bogon ~/Documents/code/mysite python3 manage.py makemigrations company
Migrations for 'company':
company/migrations/0003_remove_person_address.py
- Remove field address from person
danlan@bogon ~/Documents/code/mysite python3 manage.py migrate company
Operations to perform:
Apply all migrations: company
Running migrations:
No migrations to apply.
Your models have changes that are not yet reflected in a migration, and so won't be applied.
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
除了0003_remove_person_address.py文件被创建外,没有任何事情发生,因为迁移记录表中已经有对应的 0003 号记录了,数据库操作不会重复执行
实验二
再次将0003_remove_person_address.py文件删除掉,并且新增一个字段
class Company(models.Model):
name = models.CharField(max_length=20, verbose_name='Company name')
class Person(models.Model):
name = models.CharField(max_length=20, verbose_name='name')
company = models.ForeignKey(Company, on_delete=models.CASCADE, verbose_name='company')
# address我不想要了
# address = models.CharField(max_length=50, default='', verbose_name='address')
phone = models.CharField(max_length=20, default='', verbose_name='phone')
再次迁移
danlan@bogon ~/Documents/code/mysite python3 manage.py makemigrations company
Migrations for 'company':
company/migrations/0003_auto_20201202_1513.py
- Remove field address from person
- Add field phone to person
danlan@bogon ~/Documents/code/mysite python3 manage.py migrate company
Operations to perform:
Apply all migrations: company
Running migrations:
Applying company.0003_auto_20201202_1513... OK
虽然迁移内容不同,但是由于新增字段导致 0003 号文件名称发生了变化,数据库更改还是成功执行了
实验三
删除0002号文件,重新迁移
报错意思是说,我现在要迁移 0003 号文件了,但是发现居然找不到 0002 号文件,所以干不下去了。
第一种方式:既然如此,那我把 0003 号文件的依赖改掉呢:
执行迁移
danlan@bogon ~/Documents/code/mysite python3 manage.py makemigrations company
Migrations for 'company':
company/migrations/0004_auto_20201202_1536.py
- Alter field company on person
danlan@bogon ~/Documents/code/mysite python3 manage.py migrate company
Operations to perform:
Apply all migrations: company
Running migrations:
Applying company.0004_auto_20201202_1536... OK
迁移是可以成功的,成功后生成了 0004号文件
第二种方式:将缺失的依赖之后产生的迁移文件全部删除,也可以成功重新迁移
迁移伪造
--fakedango_migrations
django_migrationsmig
> python manage.py migrate --fake company
django_migrations
又比如说因为某些骚操作,0003 号迁移文件中的 model 改动总是无法同步到数据库,那么你可以:
> python manage.py migrate --fake company 0002
django_migrationsmakemigrations
又比如说你由于某些原因需要把 的迁移记录全部清除,那么可以:
> python manage.py migrate --fake company zero
django_migrations
migrate --fakedjango_migrations
--fake
迁移重建
如果经过你一顿骚操作,迁移文件、迁移记录表混乱不堪,并且无法正常迁移或者 ORM 频繁报错,有下面几种方法可以让迁移恢复正常。
方案1
项目在开发过程中,并且你不介意丢弃整个数据库。
__init__.pydb.sqlite3
胜败乃兵家常事,大侠请重新来过。这是最省事的方法。
方案2
你想保留数据,但是某个 App 的迁移文件和数据库未能同步(类似上面的作死4号)。
举例如果 0003 号文件中的操作未能同步,那么执行下面的指令:
> python manage.py migrate --fake company 0002 Operations to perform: Target specific migration: 0002_xxx, from mig Running migrations: Rendering model states... DONE Unapplying mig.0003_auto_xxx... FAKED
migrate --fake company 0002django_migrations
查看一下迁移状态:
> python manage.py showmigrations ... mig [X] 0001_initial [X] 0002_xxx [ ] 0003_auto_xxx ...
表示 0003 号文件还未迁移。
然后重新迁移就好了:
> python manage.py migrate Operations to perform: Apply all migrations: ..., company, ... Running migrations: Applying mig.0003_auto_xxx... OK
方案3
如果你的数据库是现成的,但是 Django 中没有任何迁移文件。(比如 Django 是数据库开发完成后才加入的)
models.py
首先执行:
> python manage.py makemigrations
0001_initial.py
然后执行:
> python manage.py migrate --fake-initial company
django_migrations--fake
顺利的话就已经搞定了:
> python manage.py makemigrations No changes detected > python manage.py migrate Operations to perform: Apply all migrations: ..., company, ... Running migrations: No migrations to apply.
除了上面三种方法外,前面还介绍了迁移伪造、修改依赖、删除错误迁移文件等方法,请量体裁衣,酌情使用。
Migration 冲突
假设我们有一个 Django 的项目,一份初始化 migration,同时也使用 GIT。我们使用 GIT,所以我们使用分支。我们使用分支,所以在 并分支的时候可能有 migrations 的冲突。当然,在团队中应该知道每个人在做什么,避免修改相同的 models。团队合作是重要的,但是一个人不可能总是避免 migrations 的冲突。因此千万别吓坏了,然后删除数据库,我就是这样干了很多次。它们是可以被很简单的修复,为了做到这一点,我将会解释一些简单的例子,但是它们是很有用的方法。你可以在你需求上选择一个最好的。
我们需要一份数据库的初始化 migration(Django 将会比较任何一个 model 去初始化 migration,而不是当前的数据库状态),我们不需要手动的修改数据库。
假设,我们有一个 UserProfile 的 model 同时还有两个分支:
- 分支1,一个开发者添加一个 “address” 的字段到 UserProfile Model 中,这样就有了 “migration 0003_userprofile_address.py”
- 分支2,我想添加一个 “age” 字段到 UserProfile Model 中,这样就有了 “0003_userprofile_age.py”
我的分支是分支2,我想合并分支1到分支2,在执行 GIT merge 后,在分支2中不但有我的 migrations 还有从分支1中来的 “0003_userprofile_address.py”。那么问题来了,这两个 migration 文件都试图修改相同的 model,而且都是以 “0003_” 命名的。
这里有三种可用的解决方法。前面两种是我推荐的,但是劝告你最好避免是使用第三种方法。
方法1:使用 -merge
无论何时,在你使用这个方法之前,这都是非常容易的,因为 Django 会自动合并。因此,如果你是有经验丰富的开发者,你事先会知道这种方法是会失败的。考虑到这个选项只对非常简单的 model 的变化非常有用。
那么,为了让 Django 合并你的 migrations,你应该遵循以下步骤:
- 尝试执行 python manage.py migrate (在这个时候,Django 将会查看这些冲突,然后告诉你执行 python manage.py makemigrations –merge
- 执行 python manage.py makemigrations –merge,migrations 将会自动被合并;你将会创建一个新的 migration 叫 0004_merge.py 被放在migrations 文件夹中。
- 执行 python manage.py migrate
$ python manage.py migrate
CommandError: Conflicting migrations detected (0003_userprofile_age, 0003_userprofile_address in myapp).
To fix them run 'python manage.py makemigrations --merge'
$ python manage.py makemigrations --merge
Merging berguiapp
Branch 0003_userprofile_age
- Add field age to userprofile
Branch 0003_userprofile_address
- Add field address to userprofile
Merging will only work if the operations printed above do not conflict
with each other (working on different fields or models)
Do you want to merge these migration branches? [y/N] y
Created new merge migration .../migrations/0004_merge.py
$ python manage.py migrate
Operations to perform:
Synchronize unmigrated apps: ...
Apply all migrations: ... myapp, ...
Synchronizing apps without migrations:
Creating tables...
Installing custom SQL...
Installing indexes...
Running migrations:
Applying myapp.0003_userprofile_age... OK
Applying myapp.0003_userprofile_address... OK
Applying myapp.0004_merge... OK
$
请记住这条信息 “Merging will only work if the operations printed above do not conflict with each other (working on different fields or models)”。如果有复杂的修改,那么Django的可能不会正确合并 migrations,你将需要使用另一种方法。
方法2:回滚然后再次合并
若果是第一次失败,你应该选择这个方法,或者你不认同有这么多的 migration 文件在你应用程序中。(尽管 Django 允许多个 migration 文件在你的项目中)。
python manage.py migrate myapp my_most_recent_common_migrationpython manage.py migratepython manage.py migratepython manage.py makemigrationspython manage.py migratepython manage.py migrate
执行下面这个 snippet 之前,我移除两个 migrations (考虑到这两个分支都是我创建并且没有别人使用,因此我也使用第二种方法解释以上的东西)
$ python manage.py migrate myapp 0002_auto_20160316_0909
Operations to perform:
Target specific migration: 0002_auto_20160316_0909, from myapp
Running migrations:
Unapplying myapp.0003_userprofile_age... OK
$ python manage.py makemigrations
Migrations for 'myapp':
0003_auto_20160318_1345.py:
- Add field address to userprofile
- Add field age to userprofile
$ python manage.py migrate
Operations to perform:
Synchronize unmigrated apps: ...
Apply all migrations: ... myapp, ...
Synchronizing apps without migrations:
Creating tables...
Installing custom SQL...
Installing indexes...
Running migrations:
Applying myapp.0003_auto_20160318_1345... OK
$
如果有来自分支1的 migration 在相同的地方做和修改,你应该要小心地决定哪些是需要保留的,然后重新 migration。
方法3: 手动修改 migrations
这种情况是极少发生的,但是如果你真的到了无法选择的地步,阅读 Django 中 writing Django migrations 的文档(英文 中文),可以让你快速熟悉 migration 的主要部分。
总结
Migrations
makemigrationsdjango_migrationsmigrations