Back to home

Alembic 简明教程

Alembic 简明教程

本文记录了Alembic的主要使用过程。

数据库版本化

通常我们会将我们的代码放入到某个VCS(版本控制系统)中,进行可追溯的版本管理。一个项目除了代码,通常还会有一个数据库,这个数据库可能会随着项目的演进发生变化,甚至需要可以回滚到过去的某个状态,于是一些工具将数据库的版本化也纳入了管理。

Alembic 是 Sqlalchemy 的作者实现的一个数据库版本化管理工具,它可以对基于Sqlalchemy的Model与数据库之间的历史关系进行版本化的维护。

Alembic

你可以通过 pip install alembic 直接安装,它需要三个依赖包,PIP会自动处理。

  • SQLAlchemy 同作者的ORM工具
  • Mako 同作者的模版工具
  • MarkupSafe 转换Markup到HTML的组件

初始化

在你的项目根目录运行

alembic init YOUR_ALEMBIC_DIR

随后你的项目目录应该会新增一个alembic.ini文件以及一个YOUR_ALEMBIC_DIR目录,最好指定一个符合自己项目风格的命名。

接下来的操作都是围绕这个目录。

yourproject/
    alembic.ini
    YOUR_ALEMBIC_DIR/
        env.py
        README
        script.py.mako
        versions/
            3512b954651e_add_account.py
            2b1ae634e5cd_add_order_id.py
            3adcc9a56557_rename_username_field.py
  • alembic.ini 提供了一些基本的配置
  • env.py 每次执行Alembic都会加载这个模块,主要提供项目Sqlalchemy Model 的连接
  • script.py.mako 迁移脚本生成模版
  • versions 存放生成的迁移脚本目录

除了基本的Alembic项目之外,你还可以指定几个特殊的项目模版。

$ alembic list_templates
Available templates:


generic - Generic single-database configuration.
multidb - Rudimentary multi-database configuration.
pylons - Configuration that reads from a Pylons project environment.


Templates are used via the 'init' command, e.g.:


  alembic init --template pylons ./scripts

你需要编辑alembic.ini文件去指定Alembic的数据库连接。

# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s


# set to 'true' to run the environment during
# the 'revision' command, regardless of auto generate
# revision_environment = false


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


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

版本

首先创建一个基本数据库版本,你当然可以从已有的数据库出发。

$ alembic revision -m "create account table"
Generating /path/to/yourproject/YOUR_ALEMBIC_DIR/versions/1975ea83b712_create_accoun
t_table.py...done

生成的版本文件类似于:

"""create account table


Revision ID: 1975ea83b712
Revises: None
Create Date: 2011-11-08 11:40:27.089406


"""


# revision identifiers, used by Alembic.
revision = '1975ea83b712'
down_revision = None


from alembic import op
import sqlalchemy as sa


def upgrade():
    pass


def downgrade():
    pass

其中 revision = '1975ea83b712'down_revision = None指定了这个reversion的当前版本号,以及父版本号,就是通过这个进行追溯。

然后我们修改upgradedowngrade进行实际的升降级操作。通过易用的API,我们只需要对opsa对象进行操作即可。

def upgrade():
    op.create_table(
        'account',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('name', sa.String(50), nullable=False),
        sa.Column('description', sa.Unicode(200)),
    )


def downgrade():
    op.drop_table('account')

具体op所支持的操作请看操作API引用

升级,降级

然后我们更新好最新的版本。

$ alembic upgrade head
INFO [alembic.context] Context class PostgresqlContext.
INFO [alembic.context] Will assume transactional DDL.
INFO [alembic.context] Running upgrade None -> 1975ea83b712

一般我们需要指定版本号进行升级,但是对于最新以及最初版本有两个额外的别名,head指最新版本,base指最初的版本。

降级也很简单,只要upgrade以及downgrade实现足够鲁棒。

$ alembic downgrade base
INFO [alembic.context] Context class PostgresqlContext.
INFO [alembic.context] Will assume transactional DDL.
INFO [alembic.context] Running downgrade 1975ea83b712 -> None

自动生成迁移脚本

Alembic 不仅仅能够维护数据库历史版本,而且带来这个新奇的特性,自动生成迁移脚本。

通常我们进行编码时候,在确定需求后,通常需要对数据模型进行变化。
不要担心,只要不是出于技术实现问题,诸如长度过少数据类型应用错等。而是因为业务的变更而导致的数据模型变更完全可以理解。"程序=算法+数据结构“,业务逻辑发生了变化,实现算法、数据结构必然发生变化,不要贪图抽象隔离设计之类云云,这类除了使得程序复杂度增加之外,很难保证未来预期是否如之前所设计的工作量完全可以避免。

口头商量,确定方案,然后开始打算修改,除了设计稿之外,往往最先实现的是ORM中我们的实体类,因为他们简单易懂。
在此例中我因为需要给用户添加微博OAuth2.0的绑定,所以新增了两个字段:

class User(Base):


    # …. origin others setting


    weibo_token = Column(String(64), nullable=True)
    weibo_expires = Column(DateTime, nullable=True)

这个时候直接运行程序必然会失败,因为映射关系已经被打乱了,我们需要重建这个关系。

配置YOUR_ALEMBIC_DIR/env.py文件,修改target_metadata = None为你的元信息对象。
这里我就直接导入了,但是在导入的过程中由于目录问题,所以一个额外的Hack。

# Hack for model import 
import os
import sys
sys.path.insert(0, os.path.realpath("."))


# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import my model
# target_metadata = None
from model import Base
target_metadata = Base.metadata

见证奇迹的时刻:

$ alembic revision --autogenerate -m "add weibo token fields for user"
INFO [alembic.migration] Context impl MySQLImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate] Detected added column 'user.weibo_expires'
INFO [alembic.autogenerate] Detected added column 'user.weibo_token'
  Generating migrate/versions/3117ba3f1f1f_add_weibo_token_expires_for_user.py...done

剩下的就可以按照正常的Alembic操作进行升降级了,是不是把烦人的数据库结构变更问题解决了?当然,这些问题可能在其他生态圈中根本不是问题(Java/Hibernate,Ruby/RoR)。 别忘了将这些变更提交到VCS中,他们可是非常值得你去维护。

自动迁移脚本生成功能实现

你可以在Alembic的源码的autogenerate.py模块中找到,一共近七百行。
程序流程如下: d.png

写得挺流水的,没有抽象,至少_compare_*都只是对于两个集合差集的提取,以及交集的迭代,这部分是可以很容易抽象出来的。
得到差异后,再对差异数据进行“渲染”,既有原地执行,也有渲染输出为Sql,还能对升降级进行不同的输出。

Sqlalchemy-migrate

Sqlalchemy-migrate 借鉴RoR的思路,对数据库进行版本化管理。但是只是提供了基本的功能,对于多个数据库引擎的差异暴露给了开发者,不易用。
命令行的控制细节也挺多了,和Sqlalchemy似乎不是一个思路。 这里就有一个StackOverflow的用户在抱怨难以上手。

Reference