误踩 Django makemigrations 与 migrate 小坑

531 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

1. 背景

最近部署项目在测试环境的时候,出现了一个小bug,本着不可能有bug的心态下,按照以下的调试方式去查问题。

1. 本地环境和测试环境代码一毛一样,多一毛都不行,测试发现,哎呀本地环境正常啊,那肯定不是代码的问题(不是我的问题!!!)
2. 肯定是浏览器的问题,无痕+清缓存+重启浏览器,居然还是有问题,不可能啊
3. 老老实实的一步一步分析,最后发现是数据库的问题

仔细想了一下,毕竟这代码我前两天都已经部署上去了,如果数据库当时有问题,早就会报错了,为啥搁到今天?

后面仔细想了一下,好像这两天有同事在上面切换分支部署过代码,难道是同事搞的?(妥妥的甩锅!!!)

但是本着不能随意丢锅,然后问了一下同事,同事说这情况,他部署他分支的时候也发现了同样的问题,然后他只是重新把他的字段migrate了。

看到这里,其实广大机智聪明睿智的网友应该可以知道,很大问题出现了在数据migrate 的时候。

2. 分析模拟

后面我本地模拟分析了这种情况,开了两个分支,test-0.0.1分支, test-0.0.2分支 在test-0.0.1分支上,django model层的代码是这样子

class DingUser(models.Model):
    """
    保存用户信息, name + userid
    """
    objects = models.Manager()
    name = models.CharField(max_length=64, db_index=True, null=False, blank=False, verbose_name='用户名称')
    user_id = models.CharField(max_length=64, db_index=True, null=False, blank=False, verbose_name='钉钉里面的userid')

然后执行一下数据migrate的执行代码

python manage.py makemigrations
python manage.py migrate

先查看了一下migrations文件夹下的文件,正常生成了对应映射0001_initial.py文件 perfect, 很完美,再检查数据库,对应的数据表和字段也都生成了,没问题。

然后在test-0.0.2分支上, django model层的代码增加了几个字段,变成了这样子

class DingUser(models.Model):
    """
    保存钉钉用户信息, name + userid
    """
    objects = models.Manager()
    name = models.CharField(max_length=64, db_index=True, null=False, blank=False, verbose_name='用户名称')
    user_id = models.CharField(max_length=64, db_index=True, null=False, blank=False, unique=True, verbose_name='钉钉里面的userid')
    email = models.CharField(max_length=64, db_index=True, null=True, blank=True, verbose_name='钉钉里面的email')
    org_email = models.CharField(max_length=64, db_index=True, null=True, blank=True, verbose_name='钉钉里面的org_email')
    union_id = models.CharField(max_length=64, db_index=True, null=True, blank=True, verbose_name='钉钉里面的unionid')

执行上面一样的步骤

python manage.py makemigrations
python manage.py migrate

控制台输出了下面的信息

1653621179137_AAADF446-3C37-4c7d-8A76-AC0ECAA6D673.png migrations文件夹下的文件,生成了对应映射0002_auto_20220527_1048.py文件,这个文件是基于0001_initial.py,在里面新增了test-0.0.2分支的字段。

数据库表里面对应新增的字段也都存在,没有任何的问题,妥妥的!

按道理到了这块是不是都没有啥问题,问题在后面!!!

重新切换到test-0.0.1分支,然后model并没有和test-0.0.2分支代码合并,model层代码是不一样的。 然后执行

python manage.py makemigrations
python manage.py migrate

控制台输出了这些信息

1653621422905_19B9F3F9-0F1A-4b25-98E5-1B9A4979A518.png migrations文件夹下的文件,生成了对应映射0003_auto_20220527_1116.py文件, 这个文件是基于0002_auto_20220527_1048.py。

从图上可以看出来,没错,这些存在test-0.0.2分支的字段被remove删除了,导致了数据库为啥会没了这些字段,并且对应的相关数据也没有了。 那究竟为什么出现这种情况呢?

其实 migrate 的作用是生成迁移和撤销迁移。 当执行 makemigrations 时,模型将被扫描并与当前包含在你的迁移文件中的版本进行比较,然后将写出一组新的迁移。 将模型生成迁移脚本,负责将模型修改打包进独立的迁移文件中。

也就是说第三次执行迁移的时候,会和第二次迁移做对比,对相应的内容进行检查,然后进行迁移生成或者迁移撤销。

3. 问题解决

既然问题找到了,那么怎么去避免呢? 大概有这几种解决方案:

  1. 代码合并,把model层都统一再执行数据迁移,这样就没啥问题了
  2. 如果代码没有合并,在部署自己代码的时候并不想影响到其他人的数据,那么可以使用这些命令
python manage.py makemigrations
python manage.py  migrate --fake

可以看到控制台输出是这些

1653621422905_19B9F3F9-0F1A-4b25-98E5-1B9A4979A518.png migrations文件夹下的文件生成的文件是这样子的。

1653621846035_C19BE2C5-EDD1-4ef3-9709-AFE533A6A5B1.png

但是到数据库看,原本的这些字段还是存在的,并没有跟着删除,那么这样子也达到了想要的效果。

当然也有一个问题,如果你真的想要删除某个数据字段,使用这个--fake 则会达不到效果,这种还是应该具体场景具体使用。

4. 其他

最后,放一些常见的命令吧。

# 全局迁移数据文件
python manage.py makemigrations
python manage.py migrate

# 迁移某个app_name的数据文件
python manage.py makemigrations  app_name
python manage.py migrate app_name

# 合并冲突
python  manage.py makemigrations --merge

# 将目标的迁移操作标记为已应用,不实际运行SQL来更改数据库结构
python manage.py  migrate --fake

# 数据库表存在,则跳过初始迁移
python manage.py  migrate --fake-initial