将 Django 项目从 MySQL 迁移至 PostgreSQL

本篇文章实战演示如何把一个使用 MySQL 的经典 Django 项目的数据库迁移到更省心的 PostgreSQL 上。

起因

上周,我的 Manico 网站后台收到了一个网站崩溃的报告,这个崩溃是名叫「Yale」的用户通过「联系我」这个表单给我反馈 Manico 的问题,他在反馈中打了三个 emoji,然后点击了「发送」按钮…然后,我的网站就 500 了。

这是因为 MySQL 默认的 utf8 字符集并不支持存储 emoji 这样四个字节的字符,于是就会在数据保存的时候引发「Incorrect string value」的错误,进而导致网站 500 。

解决办法也很简单,把 MySQL 的字符集配置成 utf8mb4 就可以了,但是要去改旧数据库的表格、可能需要迁移旧数据…不太喜欢这种方式。

另外,不仅仅是 Manico 的网站用的是 Django + MySQL 的组合,本站 IMTX 也是,所以我在文章中从来就不能用 emoji(当然现在可以了😅)。所以我真的忍不了了,要换掉用了近十年的 MySQL,换成 PostgreSQL,当是练习一个经典项目的数据库迁移,也当是熟悉另一个数据库了。

于是迁移就这样开始了…

准备 PostgreSQL 环境

我的 Server 是 Ubuntu 16.04,可以安装当前最新的 PostgreSQL 9.5:

sudo apt-get install postgresql postgresql-server-dev-9.5

后面那个包是为了给 Django 编译对应的数据库支持用的。

安装好以后,首先用下面的命令,来创建一个数据库。以 postgres 这个用户的权限创建一个名为 imtx_me 的数据库:

sudo -u postgres createdb imtx_me

创建完毕后,再使用以下命令登录进 PostgreSQL 的 shell,并设置密码:

sudo -u postgres psql postgres

\password

连续输入两遍密码后,数据库就算建立成功了。

我的生产环境是搭建在 virtualenv 上的,使用一个 requirements.txt 文件来写运行环境的相关依赖,这个时候,还需要将 Psycopg2 写进这个文件里,运行 pip install -r requirements.txt。待一切运行结束后,数据库和运行环境就算搭建好了。

导出并导入 Django 数据库

迁移项目的旧数据库的原理实际上很简单,就是利用 Django 内置的 dumpdata 指令,将数据库从 MySQL dump 出一个数据库无关的 JSON 文件,再用 loaddata 指令导入到 PostgreSQL 的数据库里面去。

这里就要利用 Django 的多数据库设置的功能了,在 Django 设置的 DATABASES 里面,除了原先的 default 以外,我们再加一个 postgresql 的设置:

'postgresql': {
    'ENGINE': 'django.db.backends.postgresql_psycopg2',
    'NAME': 'dbname',
    'USER': 'dbuser',
    'PASSWORD': 'dbpass',
    'HOST': 'postgresql.example.com',
    'PORT': '',
}

在建立 PostgreSQL 的数据库基本结构前,可能会遇到无法连接的问题,这时参考这个 链接 去改一下设置,重启数据库,然后在 Django 项目根目录,运行下面这条指令先生成数据库基本结构(如果是 Django 1.8,后面的 --run-syncdb 不需要加):

python manage.py migrate --database=postgresql --run-syncdb

然后再使用 dumpdata 的功能,把 default 即 MySQL 的数据导出来:

python manage.py dumpdata --all --indent=4 -e auth.permission -e admin.logentry -e sessions -e=contenttypes > imtx.json

这里会有几个 -e 的参数,这是因为这些结构都是会自动生成的,并且有些东西我并不想继续保存了,比如大量的 sessions、log,我想趁机也清零,于是就使用这些参数。具体的要看自己的生产环境。

导出成功后,就可以用 loaddata 的指令导进 PostgreSQL 的数据库里了:

python manage.py loaddata imtx.json --database=postgresql

导出或导入的时候,不免会遇到各种各样的问题。比如:database backend does not accept 0 as a value for AutoField,这是一个典型的 MySQL 的兼容性问题。具体修复就只能自己进入 python shell 去修了了。比如如果一个 Model,有一个 parent 属性指向自己,因为各种问题在数据库存储的时候存了 0(代表没有 parent),但实际上这是不对的,应该是 None(null)才行。这个时候,自己进入 python shell,用 Models.objects.filter(parent_id=0).update(parent_id=None) 的形式去更新一次就好了。

如果一切顺利,导入成功的话,就会有类似这样的提示:

Installed 44427 object(s) from 1 fixture(s)

最后,就可以把 Django 设置中的老 default 删除掉,将 postgresql 变成 default,这样就完成了一次数据库迁移了。通过评论发一个 emoji 试试,网站再也不会挂了。

还有一点需要补充的是,我用了 Django 的 contentypes 这个框架,Comment 就是用它来与 Post 连接的,在新的数据库下,Post 的 content_type_id 可能会与旧的不一样,那么去执行一下类似这样的指令:

Comment.objects.filter(content_type_id=10).update(content_type_id=9)

Comment 和 Post 的连接也完全正常了。

后记

这次有动力迁移用了近 8 年的 MySQL 到 PostgreSQL,还得感谢这个叫 Yale 的 Manico 用户,要不是他触发这个 Bug,我可能还没动力迁呢😄

从此以后,大家也可以欢快地用 Emoji 来给我评论或在 Manico 网站上反馈问题啦~

<推广> 本站架设于 Linode 东京机房,同时使用 云梯 进行科学上网

9 Comments

  1. 一个线上项目从utf8更换成utf8mb4,几乎与重新更换一个数据库的工作量和风险一样大。真心麻烦。

  2. 据说pg比mysql耗资源(CPU、内存),是不是真的?

  3. 其实从从utf8更换成utf8mb4成本很低,目前能发现的网上的中文教程应该都在以讹传讹,实际上本质是因为mysql创建表之后每个字段都已经应用了字符集设置,所以正确的做法是将需要保存emoji的字段的字符集单独修改成utf8mb4即可,不需要改表和库的设置,更不用网上各种教程写的还要重启实例

  4. 其实只要把 MySQL 升级到 5.5 以上的版本,从 utf8 升级到 utf8mb4 非常简单,就是一句 SQL 命令。

    ```
    ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    ```

  5. 为毛我支付之后还是没收到license啊。。😐

Leave a Comment