前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >彻底搞懂Django中的数据迁移

彻底搞懂Django中的数据迁移

作者头像
老齐
发布2020-06-10 16:02:51
5.8K0
发布2020-06-10 16:02:51
举报
文章被收录于专栏:老齐教室老齐教室
作者:杜赛

与本文相关图书推荐:《跟老齐学Python:Django实战(第二版)》

Django是基于Python语言的网站开发框架,书中以案例的方式,介绍了Django框架的基本应用。


如果你不熟悉 Web 开发,那你可能很难理解数据迁移为什么是一个强力的功能。

对象关系映射

通俗的讲,数据库是你存放数据的地方(废话)。关系型数据库又是数据库中的一种,其中的数据以表的形式组织,表具有一定数量的列、任意数量的行,每张表又可以通过外键连接其他的表。

表中每列都有特定的数据类型,这就是 Django 里常说的字段了。每一行就是表中的一条数据。比如下面这个:

id(integer)

title(string)

created(datetime)

1

Django

2020-05-09 07:57:50

2

vs

2020-05-10 09:58:05

3

Flask

2020-05-17 17:00:13

...

...

...

关系型数据库的增删改查等操作,需要用到的是 SQL 语言。Django 为了保护程序员的头发,附带了一个对象关系映射器(简称 ORM),可以将数据库 SQL 映射到面向对象的 Python 中来,使得你可以在 Django 中像操作普通对象一样操作数据库。其直观表现就是模型 (Model)。

上面的表写成模型长这样:

代码语言:javascript
复制
class Post(models.Model):
    # id 字段不需要自己写
    title = models.TextField()
    created = models.DateTimeField()

但是定义好了模型,数据库中的表并不会神奇的出现,你还需要把模型转化为对数据库的操作,这就是迁移 Migrations。

迁移工作流

新建一个项目,并在项目中创建一个叫 migapp

然后必须在 INSTALLED_APPS 配置中添加 mig ,并且 mig 还得带有 migrations/ 目录以及目录下的 __init__.py 文件,否则 Django 不会为这个 app 创建任何迁移。

models.py 中创建如下模型:

代码语言:javascript
复制
# mig/models.py

from django.db import models
from django.utils import timezone

class Pen(models.Model):
    price = models.IntegerField()
    color = models.CharField(default='black', max_length=20)
    purchase_date = models.DateTimeField(default=timezone.now)

具有价格、颜色、购买日期的笔,很合理。

接下来在命令行执行 makemigrations 指令:

代码语言:javascript
复制
> python manage.py makemigrations
# 下面是输出
Migrations for 'mig':
  mig\migrations\0001_initial.py
    - Create model Pen
Following files were affected
 D:\...\mig\migrations\0001_initial.py

如上面的输出文字所述,指令执行完毕后会生成 mig/migrations/0001_initial.py 文件。在执行 makemigrations 指令时,Django 不会检查你的数据库,而是根据目前的模型的状态,创建一个操作列表,使项目状态与模型定义保持最新。

来看看这个文件的内容:

代码语言:javascript
复制
from django.db import migrations, models
import django.utils.timezone

class Migration(migrations.Migration):
    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name='Pen',
            fields=[
                ('id', models.AutoField(...)),
                ('price', models.IntegerField()),
                ('color', models.CharField(...)),
                ('purchase_date', models.DateTimeField(...)),
            ],
        ),
    ]

就是一个普通的 Python 文件嘛:

  • initial :初次迁移。
  • dependencies:因为是初次迁移,没有依赖项,所以这里为空。
  • operations:迁移的具体操作就在这里了。CreateModel 表示创建新表,name 即表名,fields 则是表中的字段。

注意这个时候数据库是没有变化的。直到执行了 migrate 指令:

代码语言:javascript
复制
> python manage.py migrate
# 下面是输出
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, mig, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  ...
  Applying mig.0001_initial... OK  # mig 的迁移
  ...

输出中似乎有很多不认识的迁移,不要虚,那些是 Django 自身运行所需要的表。关键是这个 Applying mig.0001_initial... OK,表示 mig 的迁移已经成功了。

打开数据库可以看到多了 mig_pen 表,并且里面的字段和模型是完全匹配的。

迁移文件

初次迁移完成后,你突然发现 price 字段不应该为整型,以便正确表示带小数的金额:

代码语言:javascript
复制
class Pen(models.Model):
    price = models.DecimalField(max_digits=7, decimal_places=2)
    ...

执行完迁移后,又多出了 mig\migrations\0002_auto_20200519_1659.py 文件:

代码语言:javascript
复制
class Migration(migrations.Migration):
    dependencies = [
        ('mig', '0001_initial'),
    ]

    operations = [
        migrations.AlterField(
            model_name='pen',
            name='price',
            field=models.DecimalField(decimal_places=2, max_digits=7),
        ),
    ]

此时 dependencies 列表不再为空了,里面是本次迁移所依赖的文件,即第一次迁移的 0001_initial.py 。由此的注意事项:

  • migrations 目录下的迁移文件非常重要并且相互依赖,一般情况下不要随意去修改(虽然 Django 允许你手动维护)。
  • 通常情况下,对数据库的操作尽可能通过迁移的方式。如果因为某些原因需要手动修改,那么你需要做好手动维护的准备。

继续回到代码。operations 列表中的 AlterField 表示这次是更改操作。Django 内部有一套机制来尽可能的判断用户对模型的操作的具体类型,但是如果你一次进行了很多复杂的改动(比如同时进行多项修改、删除、新增),那么它也会犯糊涂,不知道你想干什么。为了避免这种尴尬的事情,对数据库下手不要太重。

再修改模型试试:

代码语言:javascript
复制
class Pen(models.Model):
    price = models.DecimalField(max_digits=7, decimal_places=2)
    # 我不想要 color 字段了
    # color = models.CharField(default='black', max_length=20)
    purchase_date = models.DateTimeField(default=timezone.now)

新增的迁移文件如下:

代码语言:javascript
复制
# 0003_remove_pen_color.py

class Migration(migrations.Migration):
    dependencies = [
        ('mig', '0002_auto_20200519_1659'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='pen',
            name='color',
        ),
    ]

你可以更清楚的看出迁移文件的工作模式了,即每个迁移文件记录的仅仅是和上一次的变化,每一次对数据库的操作是高度依赖的。

你还可以通过指令查看迁移文件将实际执行的 SQL 操作:

代码语言:javascript
复制
> python manage.py sqlmigrate mig 0003
...
BEGIN;
--
-- Remove field color from pen
--
CREATE TABLE "new__mig_pen" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "price" decimal NOT NULL, "purchase_date" datetime NOT NULL);
INSERT INTO "new__mig_pen" ("id", "price", "purchase_date") SELECT "id", "price", "purchase_date" FROM "mig_pen";
DROP TABLE "mig_pen";
ALTER TABLE "new__mig_pen" RENAME TO "mig_pen";
COMMIT;

迁移记录表

很好,我们已经知道迁移文件的工作方式了。

现在我们尝试一下不修改模型,直接迁移:

代码语言:javascript
复制
> python manage.py makemigrations

No changes detected

> python manage.py migrate

Operations to perform:
  Apply all migrations: ..., mig, ...
Running migrations:
  No migrations to apply.

没有任何迁移被执行。所以 Django 是如何得知哪些操作已经执行过了、哪些操作还没执行呢?

奥秘就在于数据库中的 django_migrations 表。这是由 Django 自动管理的表,里面记录了你每一次迁移的历史回溯:

id

app

name

applied

...

...

...

...

14

mig

0001_initial

2020-05-19 ...

15

mig

0002_auto_20200519_1659

2020-05-19 ...

16

mig

0003_remove_pen_color

2020-05-19 ...

...

...

...

...

表里的每一条记录都和迁移文件是对应的,如果这个表里已经有迁移记录了,那么对应的迁移文件中的指令就不再执行了。

作死1号

接下来我们来作个死,手动将最后一个迁移文件 0003_remove_pen_color.py 删除掉,再重新执行迁移:

代码语言:javascript
复制
> python manage.py makemigrations

Migrations for 'mig':
  mig\migrations\0003_remove_pen_color.py
    - Remove field color from pen

> python manage.py migrate

Operations to perform:
  Apply all migrations: ...mig, ...
Running migrations:
  No migrations to apply.

除了 0003_remove_pen_color.py 文件被重新创建外,没有任何事情发生,因为迁移记录表中已经有对应的 0003 号记录了,数据库操作不会重复执行。

作死2号

再次手动将 0003_remove_pen_color.py 文件删除掉,并且新增一个模型字段:

代码语言:javascript
复制
class Pen(models.Model):
    price = models.DecimalField(max_digits=7, decimal_places=2)
    purchase_date = models.DateTimeField(default=timezone.now)

    # 上一次迁移时的删除更改
    # color = models.CharField(default='black', max_length=20)
    # 手动删除 0003 文件后,添加此字段
    length = models.IntegerField(default=10)

再次迁移:

代码语言:javascript
复制
> python manage.py makemigrations

Migrations for 'mig':
  mig\migrations\0003_auto_20200520_1051.py
    - Remove field color from pen
    - Add field length to pen

> python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, demo, mig, sessions
Running migrations:
  Applying mig.0003_auto_20200520_1051... OK

虽然迁移内容不同,但是由于新增字段导致 0003 号文件名称发生了变化,数据库更改还是成功执行了。

但是这里是有坑的。让我们来看看实际的 SQL 指令:

代码语言:javascript
复制
> python manage.py sqlmigrate mig 0003

BEGIN;
--
-- Remove field color from pen
--
...
--
-- Add field length to pen
--
CREATE TABLE "new__mig_pen" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "length" integer NOT NULL, "price" decimal NOT NULL, "purchase_date" datetime NOT NULL);
INSERT INTO "new__mig_pen" ("id", "price", "purchase_date", "length") SELECT "id", "price", "purchase_date", 10 FROM "mig_pen";
DROP TABLE "mig_pen";
ALTER TABLE "new__mig_pen" RENAME TO "mig_pen";
COMMIT;

由于内部迁移机制,如果你之前的 Pen 表已经有数据了,那么这些数据中的 length 字段数据将全部被替换成默认值 10。

作死3号

这次我们不搞最后一条 0003 号文件了。把 0002 号文件删了,重新迁移试试...

代码语言:javascript
复制
> python manage.py makemigrations

Traceback (most recent call last):
  File "D:\...\django_manage.py", line 43, in <module>
  ...
django...NodeNotFoundError: Migration mig.0003_auto_20200520_1115 dependencies reference nonexistent parent node ('mig', '0002_auto_20200519_1659')

报错意思是说,我现在要迁移 0003 号文件了,但是发现居然找不到 0002 号文件,所以干不下去了。意料之中。怎么办?

第一种方式:既然如此,那我把 0003 号文件的依赖改掉呢:

代码语言:javascript
复制
class Migration(migrations.Migration):
    dependencies = [
        # ('mig', '0002_auto_20200519_1659'),
        ('mig', '0001_initial'),
    ]

    operations = [
        ...
    ]

这次迁移是可以成功的,而且 Django 还补了个 0004 号文件把缺失的操作给补上了。

第二种方式:将缺失的依赖之后产生的迁移文件全部删除,也可以成功重新迁移。

作死4号

换一种更深入的作死姿势。假设现在最后一条迁移文件是 0004_a.py 。首先删掉它,然后对模型进行修改:

代码语言:javascript
复制
class Pen(models.Model):
    ...
    # 比方说,删除掉 length 字段
    # length = models.IntegerField(default=10)

现在重新 makemigrations (注意不要 migrate ):

代码语言:javascript
复制
> python manage.py makemigrations

Migrations for 'mig':
  mig\migrations\0004_b.py
    - Remove field length from pen
    ...

Django 自动生成了迁移文件 0004_b.py。精彩的来了,把这个 0004_b.py 的名称修改为 0004_a.py,然后执行 migrate

代码语言:javascript
复制
> python manage.py migrate

Operations to perform:
  Apply all migrations: ..., mig, ...
Running migrations:
  No migrations to apply.

删除 length 字段的指令没执行!这是因为数据库 django_migrations 表已经有同名记录了,Django 觉得这个文件里的操作都执行过了,就不再执行了。

这样子的结果就是 Model 和数据库字段不一致,在进行相关 ORM 操作时就会出现各种报错。

不要以为这种情况很少见,新手在不正常操作迁移的过程中是有可能发生的。

迁移伪造

如果你哪天真的手贱手动操作了与迁移相关的内容,遇到迁移表和数据库无法正常同步的问题,那么你可能会用到迁移伪造指令 --fake。这个指令根据 App 现有的迁移文件内容,伪造 dango_migrations 表中的内容,欺骗 Django 的迁移状态,从而帮助你从报错中解脱出来。

举个例子。某天你手贱将 django_migrations 表中有关于 mig App 的记录全删除了,那么就可以用:

代码语言:javascript
复制
> python manage.py migrate --fake mig

Django 会把 mig 中现有的迁移文件的记录全补到 django_migrations 。这样做能成功的前提是迁移文件本身没出问题。

又比如说因为某些骚操作,0003 号迁移文件中的 model 改动总是无法同步到数据库,那么你可以:

代码语言:javascript
复制
> python manage.py migrate --fake mig 0002

可以将 django_migrations 表退回到 0002 号迁移文件的位置,然后你可以用重新执行 0003 号文件的迁移等方法进行恢复。(或者删除 0003 号迁移文件,重新 makemigrations

又比如说你由于某些原因需要把 mig 的迁移记录全部清除,那么可以:

代码语言:javascript
复制
> python manage.py migrate --fake mig zero

执行此句后有关 migdjango_migrations 记录将全部消失,你再根据具体情况,进行后续的迁移恢复。

也就是说,migrate --fake 指令可以修改 django_migrations 表中的记录,但并不会真正的修改数据库本身。

希望你永远都用不到 --fake

迁移重建

如果经过你一顿骚操作,迁移文件、迁移记录表混乱不堪,并且无法正常迁移或者 ORM 频繁报错,有下面几种方法可以让迁移恢复正常。

方案1

项目在开发过程中,并且你不介意丢弃整个数据库。

  • 删除每个 App 下的迁移文件,__init__.py 除外。
  • 删除当前数据库,或者根目录下的 db.sqlite3 文件。
  • 重新迁移。

胜败乃兵家常事,大侠请重新来过。这是最省事的方法。

方案2

你想保留数据,但是某个 App 的迁移文件和数据库未能同步(类似上面的作死4号)。

举例如果 0003 号文件中的操作未能同步,那么执行下面的指令:

代码语言:javascript
复制
> python manage.py migrate --fake mig 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 mig 0002 指令将数据库中的 django_migrations 表回滚到 0002 号文件。

查看一下迁移状态:

代码语言:javascript
复制
> python manage.py showmigrations

...
mig
 [X] 0001_initial
 [X] 0002_xxx
 [ ] 0003_auto_xxx
...

表示 0003 号文件还未迁移。

然后重新迁移就好了:

代码语言:javascript
复制
> python manage.py migrate

Operations to perform:
  Apply all migrations: ..., mig, ...
Running migrations:
  Applying mig.0003_auto_xxx... OK

方案3

如果你的数据库是现成的,但是 Django 中没有任何迁移文件。(比如 Django 是数据库开发完成后才加入的)

首先在 models.py 中编写模型,确保模型和数据库中的表是完全一致的。

首先执行:

代码语言:javascript
复制
> python manage.py makemigrations

创建初始迁移文件 0001_initial.py

然后执行:

代码语言:javascript
复制
> python manage.py migrate --fake-initial mig

这句的意思是:伪造一份 mig App 的迁移记录表(django_migrations),让 Django 误以为迁移已经完成了。(跟 --fake 指令类似)

顺利的话就已经搞定了:

代码语言:javascript
复制
> python manage.py makemigrations

No changes detected

> python manage.py migrate

Operations to perform:
  Apply all migrations: ..., mig, ...
Running migrations:
  No migrations to apply.

除了上面三种方法外,前面还介绍了迁移伪造、修改依赖、删除错误迁移文件等方法,请量体裁衣,酌情使用。

总结

折腾这么一圈,你对 Migrations 也有一定的了解了。总结起来就是下面这张内涵丰富的图(@frostming提供):

  • 数据迁移是一个很强大的功能,让完全不了解 SQL 的人可以以面向对象的方式管理数据库,保持 model 和数据库完全同步。
  • makemigrations 生成迁移文件是完全不管你的数据表实际什么样,全部是通过 django_migrations 的记录和 migrations 文件计算出来的。
  • 迁移文件是 Django 进行迁移的重要依据且互相依赖,不要随意改动,并应该纳入版本管理。虽然它可以手动修改,但前提是你完全了解它的工作原理。
  • 在迁移遭到破坏的情况下,不要想当然的去删表删文件瞎操作,而是利用好 Django 提供的方法,小心翼翼的恢复它。

原文链接:https://www.dusaiphoto.com/article/detail/96/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 老齐教室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 对象关系映射
  • 迁移工作流
  • 迁移文件
  • 迁移记录表
  • 作死1号
  • 作死2号
  • 作死3号
  • 作死4号
  • 迁移伪造
  • 迁移重建
    • 方案1
      • 方案2
      • 方案3
      • 总结
      相关产品与服务
      数据库
      云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档