通常我们会将我们的代码放入到某个VCS(版本控制系统)中,进行可追溯的版本管理。一个项目除了代码,通常还会有一个数据库,这个数据库可能会随着项目的演进发生变化,甚至需要可以回滚到过去的某个状态,于是一些工具将数据库的版本化也纳入了管理。
Alembic
是Sqlalchemy
的作者实现的一个数据库版本化管理工具,它可以对基于Sqlalchemy的Model与数据库之间的历史关系进行版本化的维护。
pip install alembic
alembic init alembic
执行后会在工程目录下生成以下的目录结构:
.
├── alembic
│ ├── env.py # 配置DataBase.metadata
│ ├── README
│ ├── script.py.mako # 迁移脚本生成模版
│ └── versions # 版本更新(迁移脚本)的内容目录
└── alembic.ini # 配置数据库
# ./database.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqldb://user_name:password@localhost:3306/database_name'
db = SQLAlchemy(app)
...emmmm...
sqlalchemy.url = driver://user:pass@localhost/dbname
mysql+mysqldb://user_name:password@localhost:3306/database_name
target_metadata = None
为以下内容:import sys
from database import db
# 这个路径要改为数据库模板类所在的目录下
# dirname(dirname(abspath(__file__))) -> ./alembic
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../")
from db import db
target_metadata = db.metadata
这东西好处就是可以像Git一样升级和降级数据库。
alembic revision -m "注释"
-m
:注释跑完之后会在./alembic/versions
生成脚本。
alembic
├── env.py
├── README
├── script.py.mako
└── versions
└── 6f406f586bbb_注释.py
脚本内容如下所示:
"""注释
Revision ID: 6f406f586bbb
Revises: 8b6caa4ac725
Create Date: 2019-05-14 20:06:39.940623
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6f406f586bbb'
down_revision = '8b6caa4ac725'
branch_labels = None
depends_on = None
def upgrade():
pass
def downgrade():
pass
Operations的文档:Operation Reference
...省略...
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')
"""Add a column
Revision ID: ae1027a6acf
Revises: 1975ea83b712
Create Date: 2011-11-08 12:37:36.714947
"""
# revision identifiers, used by Alembic.
revision = 'ae1027a6acf'
down_revision = '1975ea83b712'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('account', sa.Column('last_transaction_date', sa.DateTime))
def downgrade():
op.drop_column('account', 'last_transaction_date')
总而言之,你可以用Alembic来创建和修改表的结构。
alembic revision --autogenerate -m "initdb"
没有数据库的时候,Alembic会根据模型创建,命令行执行的结果:
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table '...'
INFO [alembic.autogenerate.compare] Detected added table '...'
INFO [alembic.autogenerate.compare] Detected added table '...'
INFO [alembic.autogenerate.compare] Detected added table '...'
INFO [alembic.autogenerate.compare] Detected added table '...'
Generating /.../project_name/alembic/versions/5e2d2560b497_init_db.py ... done
看看文件内容:
"""init db
Revision ID: 5e2d2560b497
Revises:
Create Date: 2019-05-15 11:11:14.684351
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5e2d2560b497'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('...',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('...', sa.String(length=50), nullable=False),
sa.Column('...', sa.DateTime(), nullable=True),
sa.Column('...', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
......
op.create_table('...',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('...', sa.String(length=50), nullable=False),
sa.Column('...', sa.DateTime(), nullable=True),
sa.Column('...', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('...')
......
op.drop_table('...')
# ### end Alembic commands ###
执行后会在./alembic/versions
生成脚本。
alembic
├── env.py
├── README
├── script.py.mako
└── versions
└── 8b6caa4ac725_init_db.py
脚本内容如下所示:
"""init db
Revision ID: 8b6caa4ac725
Revises:
Create Date: 2019-05-14 20:02:46.802136
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8b6caa4ac725'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
...这里我省略了,都是对表修改操作...
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
...这里我省略了,都是对表修改操作...
# ### end Alembic commands ###
自动写完了代码,优秀!╮(╯▽╰)╭
Alembic是根据数据库与代码中的Model的定义进行比对,来生成对应的版本号的。(Model可以看下一张的示例)
要是创建了数据库表,而当前的Model并没有任何修改,提交之后upgrade
和downgrade
是为空的。
加入没有创建数据库表,那么就根据Model中的将创建表和删除表的操作分别在upgrade
和downgrade
中编写出来。
Model定义:
class Test(db.Model):
id = db.Column(db.Integer, primary_key=True)
test1 = db.Column(db.String(50), unique=False, nullable=False)
test2 = db.Column(db.String(120), unique=True, nullable=False)
create_at = db.Column(db.DateTime, unique=False, nullable=True, default=datetime.utcnow)
update_at = db.Column(db.DateTime, unique=False, nullable=True, default=datetime.utcnow)
alembic init alembic
alembic revision --autogenerate -m "init db"
此时数据库表没有创建INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'test'
Generating /.../project_name/alembic/versions/3b63c8e4c302_init_db.py ... done
./alembic/versions/3b63c8e4c302_init_db.py
"""init db
Revision ID: 3b63c8e4c302
Revises:
Create Date: 2019-05-15 11:48:45.805342
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3b63c8e4c302'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('test',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('test1', sa.String(length=50), nullable=False),
sa.Column('test2', sa.String(length=120), nullable=False),
sa.Column('create_at', sa.DateTime(), nullable=True),
sa.Column('update_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('test2')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('test')
# ### end Alembic commands ###
alembic history
<base> -> 3b63c8e4c302 (head), init db
alembic upgrade 3b63c8
Database changed
mysql> show tables;
+------------------+
| Tables_in_qmasdb |
+------------------+
| alembic_version |
| test |
+------------------+
2 rows in set (0.00 sec)
mysql> desc test;
+-----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| test1 | varchar(50) | NO | | NULL | |
| test2 | varchar(120) | NO | UNI | NULL | |
| create_at | datetime | YES | | NULL | |
| update_at | datetime | YES | | NULL | |
+-----------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)
hahahahahahahat
字段:class Test(db.Model):
id = db.Column(db.Integer, primary_key=True)
test1 = db.Column(db.String(50), unique=False, nullable=False)
test2 = db.Column(db.String(120), unique=True, nullable=False)
hahahahahahahat = db.Column(db.String(120), unique=True, nullable=False) # ╮(╯▽╰)╭
create_at = db.Column(db.DateTime, unique=False, nullable=True, default=datetime.utcnow)
update_at = db.Column(db.DateTime, unique=False, nullable=True, default=datetime.utcnow)
alembic revision --autogenerate -m "add hahahahahahahat."
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'test.hahahahahahahat'
INFO [alembic.autogenerate.compare] Detected added unique constraint 'None' on '['hahahahahahahat']'
Generating /.../project_name/alembic/versions/290c92587e1a_add_hahahahahahahat.py ... done
./alembic/versions/290c92587e1a_add_hahahahahahahat.py
这个文件。"""add hahahahahahahat.
Revision ID: 290c92587e1a
Revises: 3b63c8e4c302
Create Date: 2019-05-15 11:53:28.251516
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '290c92587e1a'
down_revision = '3b63c8e4c302'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('test', sa.Column('hahahahahahahat', sa.String(length=120), nullable=False))
op.create_unique_constraint(None, 'test', ['hahahahahahahat'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'test', type_='unique')
op.drop_column('test', 'hahahahahahahat')
# ### end Alembic commands ###
alembic history
3b63c8e4c302 -> 290c92587e1a (head), add hahahahahahahat.
<base> -> 3b63c8e4c302, init db
alembic upgrade 290c92
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 3b63c8e4c302 -> 290c92587e1a, add hahahahahahahat.
mysql> desc test;
+-----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| test1 | varchar(50) | NO | | NULL | |
| test2 | varchar(120) | NO | UNI | NULL | |
| create_at | datetime | YES | | NULL | |
| update_at | datetime | YES | | NULL | |
| hahahahahahahat | varchar(120) | NO | UNI | NULL | | # 参数添加进来了╮(╯▽╰)╭
+-----------------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
使用Alembic
的好处就是可以控制数据库的版本,增加字段不影响数据。
先试试版本更新的。尝试增加wtfwtfwtfwtfwtf
字段,带unique
关键字……
alembic revision --autogenerate -m "add wtfwtfwtfwtf."
alembic upgrade head
alembic downgrade -1
class Test(db.Model):
...
wtfwtfwtfwtfwtf = db.Column(db.String(120), unique=True, nullable=False)
...
TypeError: object of type 'NoneType' has no len()
unique
关键字的字段wawawawawawawawa
class Test(db.Model):
...
wawawawawawawawa = db.Column(db.String(120), nullable=False)
...
alembic revision --autogenerate -m "add wawawawawawa."
alembic upgrade head
mysql> select test.id, test.wawawawawawawawa from test;
+----+------------------+
| id | wawawawawawawawa |
+----+------------------+
| 1 | |
| 2 | wawawawawawawawa |
+----+------------------+
2 rows in set (0.00 sec)
alembic downgrade -1
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running downgrade ad3ed6b3652c -> de464ecfaa3c, add wawawawawawa.
# 数据库
mysql> select test.id, test.wawawawawawawawa from test;
ERROR 1054 (42S22): Unknown column 'test.wawawawawawawawa' in 'field list'
alembic upgrade head
mysql> select test.id, test.wawawawawawawawa from test;
+----+------------------+
| id | wawawawawawawawa |
+----+------------------+
| 1 | |
| 2 | |
+----+------------------+
2 rows in set (0.00 sec)
./alembic/versions/....py
)。alembic upgrade 版本号
alembic upgrade head
alembic upgrade +2
alembic downgrade base
alembic downgrade 版本号
alembic downgrade -2
alembic current
alembic history --verbose
alembic history -r版本号:版本号
alembic history -r-1:current
:上一个版本到当前版本alembic history -r8b6caa:6f406f
:8b6caa版本(包含)到6f406f版本(包含)alembic history -r8b6caa:
:8b6caa版本(包含)到当前版本alembic upgrade 版本号 --sql > migration.sql
alembic upgrade 版本一:版本二 --sql > migration.sql
alembic_version
,其中的version_num
关键字就是版本号。alembic_version
。alembic list_templates
Available templates:
multidb - Rudimentary multi-database configuration.
generic - Generic single-database configuration.
pylons - Configuration that reads from a Pylons project environment.
Templates are used via the 'init' command, e.g.:
alembic init --template generic ./scripts
from flask import Flask
from flask_sqlalchemy import Model, SQLAlchemy
from sqlalchemy import Column, Integer, DateTime, func, String, ForeignKey
from sqlalchemy.orm import relationship, backref
class BaseModel(Model):
id = Column(Integer, primary_key=True)
created_at = Column(DateTime(True), default=func.now(), nullable=False)
updated_at = Column(DateTime(True), default=func.now(), onupdate=func.now(), nullable=False)
@classmethod
def create(cls, **kw):
session = db.session
if 'id' in kw:
obj = session.query(cls).get(kw['id'])
if obj:
return obj
obj = cls(**kw)
session.add(obj)
session.commit()
return obj
def to_dict(self):
columns = self.__table__.columns.keys()
return {key: getattr(self, key) for key in columns}
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqldb://root:iqiyi123@localhost:3306/test'
db = SQLAlchemy(app, model_class=BaseModel)
######################################################################################################
class Parent(db.Model):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
name = Column(String(20))
class Child(db.Model):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
name = Column(String(20))
# 无法删除
parent_id = Column(Integer, ForeignKey('parent.id'))
parent = relationship("Parent")
# 父级,子级一起删除
# parent_id = Column(Integer, ForeignKey('parent.id', ondelete="CASCADE"))
# parent = relationship("Parent", backref=backref("child", passive_deletes=True))
# 父级删除,子级不删除,外键更新为null
# parent_id = Column(Integer, ForeignKey('parent.id', ondelete="CASCADE"))
# parent = relationship("Parent", backref=backref("child"))
db.create_all()
Parent.create(name='ZhangTian')
Parent.create(name='LiTian')
Child.create(name='ZhangDi', parent_id=1)
Child.create(name='LiDi', parent_id=2)
parent = db.session.query(Parent).first()
db.session.delete(parent)
db.session.commit()