使用Alembic遷移數據庫
Alembic 是 SQLAlchemy 作者編寫的 Python 數據庫遷移工具。我打算用它實現模型類和數據庫的同步更新,就先看了些資料,學習如何實現該功能。
1. 安裝
通過pip安裝,會自動安裝依賴包SQLAlchemy、Mako和MarkupSafe。
1
|
pip install alembic
|
SAE Python環境中只需安裝Mako,其他兩個均為內置模塊。
安裝完成后就可以使用 alembic 命令,所有 Alembic 操作均由該命令實現(感覺類似git)。
1
2
3
4
|
$ 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)。在合適的位置運行
1
2
3
4
5
6
7
8
9
|
$ 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,目錄結構如下:
1
2
3
4
5
6
7
8
9
10
11
|
/vagrant/nwpc_log/
|--alembic_models.py # 模型所在的模塊
|--...
|--alembic.ini # alembic 配置文件
|--alembic/ # alembic 檔案目錄
|--versions/ # 版本腳本存放目錄,初始化時為空
|--env.py # 數據庫環境設置
|--README
|--script.py.mako
|
3. 創建模型類
創建一個使用 SQLAlchemy 定義數據庫模型,比如下面這種
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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 中設置數據庫連接
1
|
sqlalchemy.url = driver://user:pass@localhost/dbname
|
為了使用模型類更新數據庫,需要在 env.py 中設置,將 target_metadata 賦值成數據庫的元數據(metadata)。原有配置如下
1
|
target_metadata = None
|
可修改為
1
2
3
4
5
|
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.ini所在目錄,用 alembic revision -m+注釋 創建數據庫版本。由於我提供了模型類,所以可以用–autogenerate參數自動生成遷移腳本。運行
1
2
3
4
5
6
7
8
|
$ 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
|
生成的數據庫遷移腳本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
"""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。更新到最新版
1
2
3
4
|
$ 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 腳本。
創建一個新的版本:
1
|
alembic revision -m "init data"
|
生成一個新的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
"""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 負責將這些初始數據刪除:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
# ...省略...
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官方教程 http://alembic.readthedocs.org/en/latest/tutorial.html
《Alembic 簡明教程》http://huangx.in/18/alembic-simple-tutorial
《使用alembic》 http://my.oschina.net/banxi/blog/126695
sqlalchemy-migrate 另一款適用於SQLAlchemy的數據庫遷移工具
附錄
我在SAE Python本地開發環境中使用遇到問題,代碼中使用pylibmc庫,但SAE Python本地開發環境用sae.memcache模塊代替pylibmc模塊,dev_server.py中有下面的代碼
1
2
|
import sae.memcache
sys.modules['pylibmc'] = sae.memcache
|
直接使用pylibmc在dev_server.py下沒有問題,但運行alembic程序時則會提示找不到該模塊。為了解決這個問題,我判斷是本地開發環境還是線上開發環境,分別載入不同的庫。
1
2
3
4
|
if app.config['ONLINE']:
import pylibmc
else:
import sae.memcache as pylibmc
|
問題
SAE線上環境使用的SQLAlchemy版本為0.7.10,使用Alembic時出現如下錯誤:
1
|
AttributeError: 'MetaData' object has no attribute 'schema'
|
似乎對MetaData的支持不夠,版本號過低。升級成0.8.5,就可以正常使用。
重新生成腳本
我使用Alembic時,已經使用SQLAlchemy建立初始模型的數據表,不是從零開始,所以無法使用Alembic完整遷移數據庫。所以,我使用Alembic重新生成數據庫遷移腳本。
Alembic在數據庫中僅保存當前版本號,其余信息均從文件讀取。刪除歷史記錄,只需要將數據庫表刪除,並刪除versions下的所有文件。而alembic.ini和env.py中的設置無需更改,可以再次使用。