使用Alembic迁移数据库

目录

Alembic 是 SQLAlchemy 作者编写的 Python 数据库迁移工具。我打算用它实现模型类和数据库的同步更新,就先看了些资料,学习如何实现该功能。

1. 安装

通过pip安装,会自动安装依赖包SQLAlchemy、Mako和MarkupSafe。

pip install alembic

SAE Python环境中只需安装Mako,其他两个均为内置模块。

安装完成后就可以使用 alembic 命令,所有 Alembic 操作均由该命令实现(感觉类似git)。

$ alembic
usage: alembic [-h] [-c CONFIG] [-n NAME] [-x X] [--raiseerr]
               {branches,current,downgrade,edit,heads,history,init,list_templates,merge,revision,show,stamp,upgrade}
               ...

2. 初始化

与 Git 类似,使用 Alembic 前需要通过 alembic init 命令创建一个 alembic 项目,该命令创建一个 alembic.ini 配置文件和一个 alembic 档案目录(YOUR_ALEMBIC_DIR)。在合适的位置运行

$ alembic init alembic
  Creating directory /vagrant/nwpc_log/alembic ... done
  Creating directory /vagrant/nwpc_log/alembic/versions ... done
  Generating /vagrant/nwpc_log/alembic/README ... done
  Generating /vagrant/nwpc_log/alembic.ini ... done
  Generating /vagrant/nwpc_log/alembic/env.py ... done
  Generating /vagrant/nwpc_log/alembic/script.py.mako ... done
  Generating /vagrant/nwpc_log/alembic/env.pyc ... done
  Please edit configuration/connection/logging settings in '/vagrant/nwpc_log/alembic.ini' before proceeding.

我在模块包目录下创建目录alembic,目录结构如下:

/vagrant/nwpc_log/
|--alembic_models.py      # 模型所在的模块
|--...
|--alembic.ini            # alembic 配置文件
|--alembic/               # alembic 档案目录
   |--versions/           # 版本脚本存放目录,初始化时为空
   |--env.py              # 数据库环境设置
   |--README
   |--script.py.mako

3. 创建模型类

创建一个使用 SQLAlchemy 定义数据库模型,比如下面这种

from sqlalchemy import Column, Integer, String, Text, Date, Time
from sqlalchemy.ext.declarative import declarative_base
Model = declarative_base()
class User(Model):
    __tablename__ = "user"
    user_id = Column(Integer(), primary_key=True)
    user_name = Column(String(45))
    def __init__(self):
        pass
    def columns(self):
        return [c.name for c in self.__table__.columns]
    def to_dict(self):
        return dict([(c, getattr(self, c)) for c in self.columns()])
class Repo(Model):
    __tablename__ = "repo"
    repo_id = Column(Integer(), primary_key=True)
    user_id = Column(Integer())
    repo_name = Column(String(45))
    repo_type = Column(String(45))
    def __init__(self):
        pass
    def columns(self):
        return [c.name for c in self.__table__.columns]
    def to_dict(self):
        return dict([(c, getattr(self, c)) for c in self.columns()])
# ...省略...

上面例子在 Model 中定义两个表 user 和 repo。

4. 修改配置文件

在 alembic.ini 中设置数据库连接

sqlalchemy.url = driver://user:pass@localhost/dbname

为了使用模型类更新数据库,需要在 env.py 中设置,将 target_metadata 赋值成数据库的元数据(metadata)。原有配置如下

target_metadata = None

可修改为

import os
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../")
from alembic_models import Model
target_metadata = Model.metadata

自此,alembic 将可以获取模型模块中定义的信息。

5. 自动创建版本

用 alembic revision -m+注释 创建数据库版本。由于我提供了模型类,所以可以用–autogenerate参数自动生成迁移脚本。运行

$ alembic revision --autogenerate -m "create tables"
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
...省略...
INFO  [alembic.autogenerate.compare] Detected added table 'repo'
...省略...
INFO  [alembic.autogenerate.compare] Detected added table 'user'
  Generating /vagrant/nwpc_log/alembic/versions/5d2e2a345bed_create_tables.py ... done

生成的数据库迁移脚本如下:

"""create tables
Revision ID: 5d2e2a345bed
Revises:
Create Date: 2016-09-20 03:24:59.799784
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5d2e2a345bed'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    # ...省略...
    op.create_table('repo',
    sa.Column('repo_id', sa.Integer(), nullable=False),
    sa.Column('user_id', sa.Integer(), nullable=True),
    sa.Column('repo_name', sa.String(length=45), nullable=True),
    sa.Column('repo_type', sa.String(length=45), nullable=True),
    sa.PrimaryKeyConstraint('repo_id')
    )
    # ...省略...
    op.create_table('user',
    sa.Column('user_id', sa.Integer(), nullable=False),
    sa.Column('user_name', sa.String(length=45), nullable=True),
    sa.PrimaryKeyConstraint('user_id')
    )
    ### end Alembic commands ###
def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('user')
    # ...省略...
    op.drop_table('repo')
    # ...省略...
    ### end Alembic commands ###

两个函数用于数据库的升级和降级。

6. 更新数据库

升级数据库使用 alembic upgrade,降级使用 alembic downgrade。更新到最新版

$ alembic upgrade head
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 5d2e2a345bed, create tables

查看数据库,发现已经创建 user, repo 表。还有一个表叫做alembic_version,只有一个字段和一个值version_num,记录当前的数据库版本。

7. 手动创建脚本

自动创建脚本有限制,另外诸如插入初始数据条目的任务无法自动创建,所以使用 Alembic 更常见的方法就是自己编写 upgrade 和 downgrade 脚本。
创建一个新的版本:

alembic revision -m "init data"

生成一个新的版本

"""init data
Revision ID: 8f0a4c7f4794
Revises: 9cc5f9104d2e
Create Date: 2016-09-20 15:52:46.126675
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8f0a4c7f4794'
down_revision = '9cc5f9104d2e'
branch_labels = None
depends_on = None
def upgrade():
    pass
def downgrade():
    pass

修改上面的 upgrade 和 downgrade 函数就可以定制数据库迁移规则。例如下面的例子中,upgrade 负责插入初始数据,而 download 负责将这些初始数据删除:

# ...省略...
from sqlalchemy.orm import sessionmaker
Session = sessionmaker()
import os
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../../../")
from install.nwpc_monitor.model import init_data
def upgrade():
    bind = op.get_bind()
    session = Session(bind=bind)
    init_data.initial_owners(session)
    init_data.initial_orgs(session)
    init_data.initial_users(session)
    init_data.initial_org_user(session)
    init_data.init_repos(session)
    init_data.init_dingtalk_users(session)
def downgrade():
    bind = op.get_bind()
    session = Session(bind=bind)
    init_data.remove_dingtalk_users(session)
    init_data.remove_repos(session)
    init_data.remove_org_user(session)
    init_data.remove_users(session)
    init_data.remove_orgs(session)
    init_data.remove_owners(session)

上述的例子中,downgrade有bug,无法再有其他数据的情况下实现降级,不过该降级没有实际意义,不如将数据库整个清空。

参考

alembic官方教程 //alembic.readthedocs.org/en/latest/tutorial.html
《Alembic 简明教程》<//huangx.in/18/alembic-simple-tutorial>
《使用alembic》 <//my.oschina.net/banxi/blog/126695>
sqlalchemy-migrate 另一款适用于SQLAlchemy的数据库迁移工具

附录

我在SAE Python本地开发环境中使用遇到问题,代码中使用pylibmc库,但SAE Python本地开发环境用sae.memcache模块代替pylibmc模块,dev_server.py中有下面的代码

import sae.memcache
sys.modules['pylibmc'] = sae.memcache

直接使用pylibmc在dev_server.py下没有问题,但运行alembic程序时则会提示找不到该模块。为了解决这个问题,我判断是本地开发环境还是线上开发环境,分别载入不同的库。

if app.config['ONLINE']:
import pylibmc
else:
import sae.memcache as pylibmc

问题

SAE线上环境使用的SQLAlchemy版本为0.7.10,使用Alembic时出现如下错误:

AttributeError: 'MetaData' object has no attribute 'schema'

似乎对MetaData的支持不够,版本号过低。升级成0.8.5,就可以正常使用。

重新生成脚本

我使用Alembic时,已经使用SQLAlchemy建立初始模型的数据表,不是从零开始,所以无法使用Alembic完整迁移数据库。所以,我使用Alembic重新生成数据库迁移脚本。
Alembic在数据库中仅保存当前版本号,其余信息均从文件读取。删除历史记录,只需要将数据库表删除,并删除versions下的所有文件。而alembic.ini和env.py中的设置无需更改,可以再次使用。

更新记录

2014.03.06 在 SAE 的项目中使用 Alembic
2016.09.21 在 Vagrant 中使用 Alembic