认识Hibernate

最近工作的项目中,用的 ORM 技术是 Hibernate,学习了一下它的用法,正好 PyCon 上我有一个演讲主题是介绍 Django 的 ORM,可以拿来比较一下。这篇文章介绍了 Hibernate 的定位,基本的概念,以及用代码演示了如何使用 Hibernate。本文的内容参考了 jboss 上的一篇教程,所有的代码直接下载:hibernate-tutorials.zip

Hibernate 是什么?

Java 是一个面向对象的编程语言,数据库提供的数据结构只有 Table。所以我们在读写数据库的数据的时候不可避免的要进行结构的转换,保存数据的时候,将 Object 变成 Table 可以保存的形式,读回数据的时候要转换回来。

这样每次在读写数据的时候做转换,是非常重复的,开发成本很高。Hibernate 就是一个 对象/关系映射 的转换解决方案。在 Java 程序中,我们只写类,数据在保存的时候,Hibernate 负责将类的属性转换成 Table 的 Column。更确切的说,如果没有 Hibernate,我们就需要通过写 SQL 和用 JDBC 来跟数据库交互。

和其他 ORM 不同,Hibernate 没有完全屏蔽 SQL。并承诺你的关系型数据库的知识,在 Hibernate 下依然有价值(来源)。

一、使用 Hibernate 原生的 API 读取和保存数据

ORM 需要做的最终要的一件事情是负责 Class 和 Table 的数据结构的转换,在 Hibernate 中,定义这种转换有两种方式:通过配置文件,或者通过注解。

使用 Mapping 配置文件来映射Java class和Table的关系

Bundle/resources 下面的 hibernate.cfg.xml 是 Hibernate 的配置文件。有数据库地址,连接参数等配置。其中 dialect 定义了使用哪种SQL方言,在使用特定数据库的时候可以使用。

auto 可以指定启动的时候自动创建表结构:

这个值可选的参数如下:

  1. create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
  2. create-drop :每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
  3. update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据 model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等 应用第一次运行起来后才会。
  4. validate :每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

<mapping /> 可以告诉 Hibernate,去哪里找描述 Class 和 Table 对应的 mapping 文件。

对应路径的 mapping 文件,描述了 Java 的 Class 如何和数据库的 Table 对应:

Hibernate 会使用 java.lang.ClassLoader 去加载这些类。

有关 Entity 有两点需要注意:

  1. 这个类用标准的 JavaBean 命令方式设置 getter 和 setter 方法,private 属性也有。这是推荐做法但不是必须做法;
  2. 无参数构造函数是必须有的,Hibernate 需要用Java的反射通过这个构造函数构造对象。

Mapping 文件的详细解释

父节点的属性中,name 结合 package 定义个 FQN,对应 java 的 class;table 定义了数据库的表名字;

子节点中:

  • 需要定义 <id/> Element 来告诉 Hibernate 如何找到表中的唯一的一个 row;推荐使用 Primary key
  • <property/> 定义了属性和表字段的 mapping,column 定义了表 Column 的名字,如果不写的话 Hibernate 默认会使用 property 的 name;
  • type 是数据类型。这个数据类型既不是 SQL 的类型,也不是 Java 的类型,而是 Hibernate 的类型,负责在 Java 和 SQL 之间做转换。如果这个 type 没写的话,Hibernate 会尝试通过 Java 的反射,按照 Java 的类型找到对应的 mapping 类型;

测试代码

示例代码如下(不要复制粘贴,如果需要运行请直接复制本文开头提供的附件):

下面来分析代码。

在 setUp 中,首先构建了一个 org.hibernate.boot.registry.StandardServiceRegistry ,用来处理 hibernate.cfg.xml 等配置信息(sessionFactory 会使用 serviceRegistry 中的配置信息)。

在 setUp 中,首先构建了一个 org.hibernate.boot.registry.StandardServiceRegistry ,用来处理 hibernate.cfg.xml 等配置信息(sessionFactory 会使用 serviceRegistry 中的配置信息)。

我们使用 org.hibernate.boot.MetadataSource 先创建了 org.hibernate.boot.MetadataSources ,这个类表示了 domain Model。然后基于此创建了 SessionFactory。

最后一步就是创建 SessionFactory 了,这是一个全局单例的类,线程安全。

SessionFactory 是 Session 的工厂类,每次使用一个 Session 来完成一块任务:

以上代码中,testBasicUseage() 先创建了一个 Event 对象,然后交给 Hibernate 处理,Hibernate 的 save() 方法将其插入数据库中。

获取对象的代码如下:

我们通过写 Hibernate Query Language 向 Hibernate 表达查询,Hibernate 将其转换成 SQL 执行查询,然后将结果转换成我们需要的 class。

使用注解描述 Java class 和 Table 的映射关系

使用注解API,在配置文件中我们的 mapping 字段需要设置为:

其他的配置项和前面一样。然后,我们在定义 class 和 table 的映射的时候,不再使用 xml 文件了,而是直接在 class 上面注释:

@Entity 的注解和 xml 中的 <class /> 作用一样。这里显示地指定了表的名字,如果不指定的话,就会默认使用类名 EVENT。

字段的定义如下:

@javax.persistence.Id 定义了实体的 ID;

@javax.persistence.GeneratedValue 和 @org.hibernate.annotations.GenericGenerator 指示 Hibernate 应该用 increment 生成器自动生成这个字段。

同 xml 配置方式一样,date 也需要特殊处理一下。

实体的属性默认将会持久化,所以这个类中我们没有写 title 的 annotation,但是 title 也会被持久化。

示例代码:这部分的例子和上面一样。

二、使用 Java Persistence API(JPA)

上面我们使用的配置文件是 hibernate.cfg.xml,而 JPA 定义了自己的配置方式,叫做 persistence.xml 。启动方式是 JPA 定义的规范,Hibernate 作为持久化的提供者,需要读取并应用 META-INF/persistence.xml 里面的设置。

persistence.xml 文件范例如下:

对于每一个 persistence-unit 需要提供一个 unique name。应用在获得 javax.persistence.EntityManagerFactory 引用的时候,使用这个 unique name 来获得配置的引用。

之前的配置文件中的配置项,先在要使用 javax.persistence 前缀来区分开。对于 Herbenate 特殊肚饿配置项要用 hibernate. 前缀。

除此之外,<class /> 的内容和我们在前面看到的 Hibernate 配置文件一样。

示例代码

前面的例子中使用的是 Hibernate 的 native API,这里我们使用 JPA 的 API 。

获取 SessionFactory:

注意这里使用的 persistence unit name 是 org.hibernate.tutorial.jpa ,和上面提到的配置问题件对应。

使用 javax.persistence.EntityManager 保存实体。Hibernate 中 save 的步骤,在 JPA 中叫做 persist.

三、Envers 功能

Hibernate 有一个功能,叫做 Envers,就是可以将数据的历史版本都保存下来。数据及时更改了,也可以找到曾经的状态。

这个功能也不是无成本的,需要一些保存历史版本,保存和读取的时候,也需要额外的计算,所以我们只在需要的时候这么做。

我们通过@org.hibernate.envers.Audited 这个注解告诉Hibernate需要保留这个类的历史版本,然后,我们可以 org.hibernate.envers.AuditReader 来读取数据的历史版本。

示例代码

 

参考资料:

 

欧格玛教会与言论自由

我喜欢欧格玛教会,欧格玛教会认为知识是至高无上的,尤其是最原始的知识,最纯粹的理念。理念没有重量,但是可以移山。

欧格玛教会信奉没有任何知识是应该被销毁的,知识本身是没有问题的,可怕的是使用它的人。即使是最疯狂、最错误的想法,藏匿起来也无济于事,应该交给大众来判断。所以欧格玛教会神殿内经常会藏有被其他教会认为是禁忌的知识,比如死灵术。

欧格玛信仰如下:

Knowledge, particularly the raw knowledge of ideas, is supreme. An idea has no weight, but it can move mountains. The greatest gift of humankind, an idea outweighs anything made by mortal hands. Knowledge is power and must be used with care, but hiding it away from others is never a good thing. Stifle no new ideas, no matter how false and crazed they seem; rather, let them be heard and considered. Never slay a singer, nor stand by as others do so. Spread knowledge wherever it is prudent to do so. Curb and deny falsehoods, rumor, and deceitful tales whenever you encounter them. Write or copy lore of great value at least once a year and give it away. Sponsor and teach bards, scribes, and record keepers. Spread truth and knowledge so that all folk know more. Never deliver a message falsely or incompletely. Teach reading and writing to those who ask (if your time permits), and charge no fee for the teaching.

我在我的博客上写着:我的梦想是让网络变得更加开放、自由和快速。

对于网络,我的想法也是所有的内容不都应该被删除、藏匿。应该交由大众去判断。这是我理解的言论自由。但是最近发生了两件事,让我开始怀疑自己的想法。

第一个是 Cloudflare 停止了对 8chan 的服务。

CloudFlare 是我非常崇拜的一家公司,我一度认为互联网服务就应该是 CloudFlare 提供的那样。但是有证据证明,一起枪击事件的犯罪嫌疑人从 8chan 上受到了鼓动,从而导致了这起枪击事件。8chan 创建的本意是 4chan 依然会删除部分内容,所以 8chan 想创造一个不会删除内容的 4chan,最终却成为了仇恨的温床。

第二个是Twitter关闭了一批来自大陆的账号。

这么做的理由是 Twitter 认为,这部分账号进行了受组织协调的行动。

那么究竟,我们需要怎么样的言论自由呢?

 

IRedis 开发记录:Redis 命令语法的处理

我最近在写一个命令行应用程序,一个支持自动补全、命令校验、语法高亮的 redis-cli 。项目的地址:

https://github.com/laixintao/iredis

写这个插件是想达到 mycli/pgcli 的那种效果,让 redis-cli 用起来非常顺手,本质上,这又是一个满足我自己个人需求的项目。但是我觉得应该很多人会喜欢这个。

开发的过程中遇到了很多有意思的问题,一直想分享一下,但是最近太忙了,白天工作比较忙。晚上我基本都花在写这个项目的代码上,没有时间系统的写一下。今天这篇文章批评了我这里面写过的一段正则,所以今天借这个机会,就写一下吧。

这个正则在项目的代码库里面:代码地址。

为什么用这么大一个正则表达式呢?

完成语法校验、高亮,基于语法的补全,肯定要实现一个 lexer。这个 Lexer 是写一个状态机,还是用一个正则去匹配语法,基本上是每天都在想的问题,也每天在想用正则是不是对的,现在看来,遇到了很多问题,但是多都解决了。

对于这个 lexer 我一开始是写的 Pygments lexer,见这个commit 。但是我后来选择了用一个正则表达式来处理所有的 Redis 命令:任何能match这个正则的字符串就是一个合法的 Redis 命令。为什么呢?因为 Redis Grammar 基本不能叫做一个 Grammar,它基本没有 Grammar。要么是一个 Command 后面跟一个 key,要么是 Command 后跟一个 DB index 这种,如果你写一个 Lexer,你会发现全都是一层扁平的 if-else。

所以我放弃了用Lexer,直接用正则。

这个正则难理解吗?

我觉得不难,首先一个 command 属于一种格式,比如 command + key 是一种,command + ip + port 也是种,我是这么对这些 command 分类的:ip/port/key/fields 这种基本的token定义在最前面,并且每一种格式都会有单元测试覆盖。我觉得无论是维护还是新手理解都是可以的。

这么做的另一个好处是,我可以直接使用正则表达式里面的分组,将token拿到,lexer和 completer 都变得很简单,completer 源代码。但是你可以去看下 mycli 的 completer 。

回到本文中:

这个正则表达式你们自己都看不下去了,所以才会需要使用拼接的方式生成。

这个说的不对,我很冤枉。通过我的 commit log 可以看出,我选择用正则,是一开始就想好了要拼接的,先定义基本的 Token,然后用基本的 Token 定于语法。

费脑子,难以理解,难维护。

这不对,可能读者没有按照我的方式理解,现在这个项目开发比较快,不好写文档(我也没啥时间写),因为都在变。如果我写完了稳定你在看的话就简单多了。首先是 command_syntax.csv 文件,定义了每一个 command 的语法。然后 grammar 里面解释了每一种的语法的组成,按照字面就可以理解。比如 <command-key-field> <key> <field> ,<command-key> <key> 我觉得无论是下面的大正则,还是csv文件,都是比较好理解的。

这个组织是扁平的,其实比 Lexer 好理解、维护不少。

并且每一种语法都带测试,测试已经帮我发现了很多问题。

其实这么做也有一些坑。比如用户输入第一个字符是空格,那么按照我那个正则,空格是“可能匹配”所有的情况的。就导致用户输入第一个空格,整个进程会卡10+s。解决办法是我patch了原生的 Completer,strip() 之后的空字符串不做 completer。(PR)

compile 太慢的问题,我想过去缓存 re.compile 的结果(laike9m 的建议,我觉得这个想法太天才了。)。但是最后放弃了,原因见这里。

现在这个问题是通过应用在启动的时候新启动一个线程去编译正则,主线程正常接收用户输入来实现的。

还有一个坑是,这个正则其实不是Python本身的正则,断言无法使用,{} 风格的 repeat 无法使用,所以必须用很多 tricky 的方式绕过去。这导致有一些语法树也很难实现,比如这个。

但总体上,我觉得相比 Lexer,是比较好维护一些,但是功能弱一些。

 

下面再说一个好玩的东西,比较烧脑,我觉得可以作为一个不错的面试题目:如何实现 Bash 风格的引号处理?要注意双引号里面可以有单引号,单引号里面可以有双引号。双引号里面可以有 \ 转义的双引号,单引号也是。答案在这里。这个 PR 里面也有一个单元测试,有兴趣的同学可以试下能否写一个更优雅的函数,来通过这个单元测试。

最后,这个项目我跟朋友说了之后,得到很多不错的 Feature 建议,未来想要实现的都记录在 issues 里面了,有兴趣的可以看下。如果您有什么想法也可以跟我交流下。主要的目的是实现一个行为和 redis-cli 一致、但是更好用的 redis-cli。

 

附录:本文所谈论的原文相关的部分,这里备份一下,以防读者不知道我在说啥:

多说一句
以下内容与本次讨论的re.compile无关。

@Manjusaka给出了一个compile需要3秒钟的大型正则表达式,并以此作为例子说明re.compile的合理性。

首先这种情况下,确实需要提前re.compile。

但我所想表达的是,在这种情况下,就不应该使用正则表达式。既然要做Redis的语法校验,那么就应该使用有限状态机。这种使用很多的f表达式拼出来的正则表达式,才是真正的难以维护,难以阅读。

否则为什么里面需要用一个csv文件来存放命令呢?为什么不直接写在正则表达式里面呢?使用CSV文件每行一个命令尚且可以理解,但是SLOT/SLOTS/NODE/NEWKWY这些正则表达式,可就说不过去了。或条件连接的每一段都要加上这些东西,如果直接写进去,这个正则表达式你们自己都看不下去了,所以才会需要使用拼接的方式生成。

我在读这段代码的时候,首先看到正则表达式里面的t[xxx],会先去找t是什么东西,发现t是一个字典,字典是在commands_csv_loader.py中生成的,然后去到这个文件里面,发现它读的是一个存放Redis命令的CSV文件。然后去项目根目录读取这个csv文件的内容,知道了它的结构,于是推测出t的结构。然后再回到正则表达式里面,继续看这个超大的正则表达式。整个过程会非常费时间和脑子。

但是,我又不能直接打印REDIS_COMMANDS这个变量,因为它多且乱,不同命令长短不一,拼出来以后再打印出来根本没法看。

这个正则表达式只有两位维护者知道什么意思,如果别人想贡献新的Redis命令,那么理解这个超大正则表达式都需要花很久的时间。

如果换成有限状态机,并且t使用Python的data class来表示,而不是使用字典,那么就会简洁很多。有限状态机的一个特点是,只需要关注当前状态、转移条件和目标状态,可能一开始写起来有点麻烦,但是以后维护和新增,都是直接定位目标,直接修改,不用担心会影响不相干的其他地方。

算上维护时间,正则表达式真是一个非常糟糕的方式。

 

上海PyCon见!

2019 PyCon 中国马上要开始了,今年是 Python 诞生30周年,所以 PyCon 组织的同学们非常卖力。

PyCon 中国官网地址

举办的形式和去年一样。有一个城市是主会场,其他城市是分会场。去年主会场是北京,今年是在上海,其他城市是分会场:北京、杭州、深圳、成都、南宁。主会场和分会场的区别是:主会场城市的演讲很多,分为 web、测试、运维主题等等,但是所有的主题是同时进行的,你需要挑自己感兴趣的主题去听;分会场不分主题,所有演讲是顺序进行的。

上海一共会有2天,第一天是演讲,第二天是 Tutorials。其他城市都是一天办完。

今年的讲师阵容非常豪华。有 Armin Ronacher,Flask 社区非常活跃;Luciano Ramalho,《流畅的 Python》作者,我最喜欢的一本书。Giampaolo Rodola, CPython 的 commiter。当然还有很多老朋友。

我会参加上海场,分享的主题是 Django Migration Under the Hood , 中文标题实在不知道怎么起,英文可能更贴切一点。

这个主题我想讲 migration 的原理,内容和我最近写的一篇博客差不多。但是在演讲上,我想讲的更加浅显、容易理解一些。我选的方面特别小,本来想讲一下映射和查询的原理,但是本着多不如精的原则,就只讲 migrations 这一小点了。我会把用一些动画和图片尽量简单地展示 migration 的过程和原理,演讲内容上也会分享我用 Django 的经历,和一些想法,生动一些,减少大家理解的难度,希望能没有用过 Django 甚至没有用过 Python 的同学也能知道这里面的设计思想。

下面是我提交给 PyCon 的一个主题介绍。

主题介绍:Django强大的ORM几乎屏蔽了SQL的复杂性,让我们只要写 Python 代码,然后 python manage.py makemigrations & migrate,就可以让数据持久化起来。但是这两行命令的背后发生了什么呢?为什么有时候这个命令会执行失败呢?在部署的什么过程去执行最合适?在PyCon上我将和大家分享:

  • 我与Django的故事;
  • Django migrations的工作原理;
  • 使用Django migrations会遇到的问题,如何从原理入手去解决问题;
  • 部署 Django migrations 的最佳实践;
  • 其他一些 migrations 的思路,如果做一个 migrations 平台,如何做数据库结构版本化,DDL 回滚;

有关这个主题,大家有什么想法,或者想听什么,可以和我交流下。

另外我们 Pythonhunter 播客的4位主播也会在 PyCon 上有一个展台,四位主播会在现场和大家当面交流。我们准备了一些贴纸(有漫威、Rick and Morty,Github 等等)吸引大家,免费索取,没有扫码,没有关注,见者有份。

大会购票地址:https://www.bagevent.com/event/5293611

 

Django migration 原理

Django 自带了一套 ORM,就是将数据库的表映射成 Python 的 Class,这样我们在写程序的时候,就基本不需要写 SQL 和数据库交互了,直接操作 Python 对象就可以了,Django 的 ORM 把我们对类的操作,转换成对数据库的 SQL 操作。

那么 Class 的属性是如何对应到 Table 的 Column 的呢?Class 的属性变了,如何关联到表结构的变化?本文就介绍这种 migration 实现的原理。

 

参与 Class 和 Table 结构一致的,一共有3个角色:

  1. Models:只记录了现在的 Model 是什么样子的;
  2. Migrations:记录了 Model 是如何从最初的样子,变成现在这个样子的——即要变成现在的样子,需要走哪几步;
  3. 数据库中的 django_migrations 表:记录在 Migrations 的所有步骤中,当前的 Table 已经执行了哪几步,这样就可以只执行没有执行过的步骤,来变成最新的状态;

一次 migration 之旅

前面我们说过,Django ORM 基本屏蔽了用户需要做的 SQL 操作,包括 DDL 操作。

从一个用户的角度看,我们做一次 migration,只需要3步:

  1. 修改 models.py,比如添加字段;
  2. 执行 python manage.py makemigrations ;
  3. 执行 python manage.py migrate ;

这时候,数据库中的 Table 和我们 Django 中的 Model 就对上了,我们写业务逻辑就可以了。

但实际上,Django 在背后做了什么呢?如果不了解这些,遇到一些问题就束手无策了(文末会提到一些经典的问题)。

在整个流程中,Django 负责的是自动识别出 Model 进行了哪些变化,将这些变化应用到 Table 中,保证最终 Model 和 Table 还是对应的。本质上,就是将 py 文件的变化,转成数据库的 DDL 变化。

所以首先,Django 要知道你的 Model 做了哪些变化。这是第2步:生成 migrations。Migrations 生成的方式,颇有点现在“声明式”的意思。

它的过程是这样的(这里我们将修改后的 Model 记作 Model-2,修改前的 Model 记作 Model-1):从 migrations/ 文件夹下的 0001_xxx.py 开始 apply,到 0002_xxx.py,一直到最后一个文件,这些文件都记录了 Model 的变化,都 apply 一遍之后,就得到了修改前的的 Model-1 的状态,然后和修改过的 Model-2 来比较,得到了 diff,然后看如何消除这些 diff——即产生最新的 migrations 文件,将 Model-1 的状态转移到 Model-2.这样新的 migrations 就生成了,如果你再跑一遍的话,就发现什么 migrations 也不会出现,因为 migrations 文件记录的变化已经和最新的 Model 一样了(声明式天生的幂等性!)。

这些 migrations 记录了 Model 从 0001_init 开始的所有变化,有了 migrations 文件,你就可以从一个空的数据库,变成现在的结构了。

但是如果数据库不是空的,而是已经有一些结构呢?通常是这种情况:生产环境的数据库,表结构是 v3(执行过migrations 0001,0002,0003),你在开发的时候生成了 migrations 0004,0005。那么怎么知道这个数据库只要执行 0004,0005 就可以了呢?更复杂一点的话,假如你有一个灰度服务器,一个生产服务器,灰度服务器执行过了 0001-0004,生产服务器落后一个版本,执行了0001-0003,怎么知道这些数据库应该执行哪些 migrations 呢?

这里根本的问题是:migrate 的执行(DDL的执行)不是幂等的。所以我们就需要一个地方,记录一下哪些 migrations 已经被执行过了。Django 会自动在你的数据库中建立一张 django_migrations 表,用来记录执行过的 migrations。这样你在执行 python manage.py migrate 的时候,django 会去对比 django_migrations 表,只执行没有执行过的 migrations,无论你有多少表结构版本不同的数据库都没有关系。

下面再用一张图来表示一下整个过程。其实想明白了很简单(我也不知道为啥这张图画出来咋这么复杂,点开可以看大图)。这张图是给一个很简单的 User Model 增加了一个字段。注意一开始蓝色的 Model 和 Table 是 map 的,最终状态红色的 Model 和 Table 也是 map 的。

一些骚操作

有了上面的知识,我们处理起来一些问题就得心应手啦!

1.migrate执行到一半失败了怎么办?

这种情况是很容易发生的,因为 make migrations 的时候,Django 只知道消除 diff,并不知道表结构。这就很有可能导致生成的 migrations 实际上是违反数据库约束的,而不能执行成功。

最糟糕的情况是,一个 migrations 中有很多步,其中部分执行成功了。就造成数据库的结构处于一个“未知”的状态。

不过不要慌,只要从 log 中看下哪些 migrations 步骤已经执行了,去手动 revert,然后从 django_migrations 删除这些记录即可。参考这篇文章。

2.migrations应不应该加入到 git 仓库中?

应该!虽然是生成的代码,但是也是要用版本管理,让所有的数据库都执行一套 migrations!看看我的教训吧。

3.我什么 models 都没改,但是每次 make migrations 都会生成新的?

不要慌,我也遇到过。这时候你去看下生成的 migrations 做了啥操作,再想想 Django 为什么会这样做即可。

举个例子吧,我之前的 models 里面一个 default 值用了字典,我们知道字典 key 是没有顺序的(Python 3.7)之前,就导致每次 make migrations 的时候,顺序都有几率不一样,Django 就认为状态不一致,需要新的 migration 文件来消除 diff。

4.migrations 文件冲突了,我和同事开发的时候,我生成了一个 0003_xxx.py 我同事也生成了这个 0003.

不要慌,这是正常现象。按照 Django 的提示,执行生成一个新的 migrations 来 merge 前面的两个就好啦。python manage.py makemigrations --merge 。

5.我的migrations太多了,每次 makemigrations 都要等很久,或者我的 migrations 文件名已经快到 999_xxx.py 了!

我一生中只遇到过一个这样的项目。解决方法也很简单,因为我们只需要一个最终的状态,所以我们可以将之前的 migrations 都删掉。

步骤如下:

  1. 备份整个项目和数据库(非常重要);
  2. 删除除了 __init__.py 之外的所有 migrations 文件夹下的文件:ls */migrations/*.py | grep -v __init__ | xargs rm;
  3. 重新生成 migrations 文件,不出意外的话,所有的 app 都只有一个 migration 文件,就是 init;
  4. 删除数据库 django_migrations 表中所有的记录:delete from django_migrations;
  5. 因为我们的表结构已经是最新的了,所以新生成的 init migrations 并不需要执行。所以我们插入记录到 django_migrations 中,骗 djagno 我们已经执行过了。用这个命令:python manage.py migrate --fake;

大功告成,不出意外,makemigrations 又快了起来。

其他的一些骚操作

如何查看一个 migration 文件对应的 SQL 是啥?

如何查看这个数据库执行过哪些migrations了?

最后 n 次执行的 migrations 有问题,数据库炸了,要不要删库跑路?

不用。

 

最后,不要在部署的时候自动执行 migrations。具体推荐大家看下这篇文章:Decoupling database migrations from server startup: why and how (这里面的问题我都遇到过,哭笑)。

其他一些类似 Django ORM 的 migrations 的项目: