本篇文章实战演示如何把一个使用 MySQL 的经典 Django 项目的数据库迁移到更省心的 PostgreSQL 上。
上周,我的 Manico 网站后台收到了一个网站崩溃的报告,这个崩溃是名叫「Yale」的用户通过「联系我」这个表单给我反馈 Manico 的问题,他在反馈中打了三个 emoji,然后点击了「发送」按钮…然后,我的网站就 500 了。
这是因为 MySQL 默认的 utf8 字符集并不支持存储 emoji 这样四个字节的字符,于是就会在数据保存的时候引发「Incorrect string value」的错误,进而导致网站 500 。
解决办法也很简单,把 MySQL 的字符集配置成 utf8mb4 就可以了,但是要去改旧数据库的表格、可能需要迁移旧数据…不太喜欢这种方式。
另外,不仅仅是 Manico 的网站用的是 Django + MySQL 的组合,本站 IMTX 也是,所以我在文章中从来就不能用 emoji(当然现在可以了:sweat_smile:)。所以我真的忍不了了,要换掉用了近十年的 MySQL,换成 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 内置的 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,我可能还没动力迁呢:smile:
从此以后,大家也可以欢快地用 Emoji 来给我评论或在 Manico 网站上反馈问题啦~