Wait the light to fall

Flask 入门 - 数据库

焉知非鱼

Learning Flask

文档数据库和键值对数据库这两种数据库合称 NoSQL 数据库。

SQL 数据库 #

表中有个特殊的列,称为主键,其值为表中各行的唯一标识符。表中还可以有称为外键的列,引用同一个表或不同表中某行的主键。行之间的这种联系称为关系,这是关系型数据库模型的基础。

NoSQL 数据库 #

NoSQL 数据库一般使用集合代替表,使用文档代替记录。NoSQL 数据库采用的设计方式使联结变得困难,所以大多数数据库根本不支持这种操作。

使用SQL还是NoSQL #

SQL 数据库擅于用高效且紧凑的形式存储结构化数据。这种数据库需要花费大量精力保证数据的一致性。NoSQL 数据库放宽了对这种一致性的要求,从而获得性能上的优势。

Python 数据库框架 Flask-SQLAlchemy #

Flask-SQLAlchemy 是一个 Flask 扩展,简化了在 Flask 程序中使用 SQLAlchemy 的操作。SQLAlchemy 是一个很强大的关系型数据库框架,支持多种数据库后台。SQLAlchemy 提供了高层 ORM(Object-Relational Mapper,对象关系映射),也提供了使用数据库原生 SQL 的低层功能。

(venv) $ pip install flask-sqlalchemy

在 Flask-SQLAlchemy 中, 数据库使用 URL 指定。 表 5-1 Flask-SQLAlchemy 数据库 URL

数据库引擎 URL
MySQL mysql://username:password@hostname/database
Postgres postgresql://username:password@hostname/database
SQLite(Unix) sqlite:////absolute/path/to/database
SQLite(Windows) sqlite:///c:/absolute/path/to/database

在这些 URL 中,hostname 表示 MySQL 服务所在的主机,可以是本地主机(localhost),也可以是远程服务器。数据库服务器上可以托管多个数据库,因此 database 表示要使用的数据库名。如果数据库需要进行认证,username 和 password 表示数据库用户密令。

SQLite 数据库不需要使用服务器,因此不用指定 hostname、username 和 password。URL 中的 database 是硬盘上文件的文件名。

程序使用的数据库 URL 必须保存到 Flask 配置对象的 SQLALCHEMY_DATABASE_URI 键中。配 置对象中还有一个很有用的选项,即 SQLALCHEMY_COMMIT_ON_TEARDOWN 键,将其设为 True 时,每次请求结束后都会自动提交数据库中的变动。示例 5-1 展示了如何初始化及配置一个简单的 SQLite 数据库。

示例 5-1 hello.py: 配置数据库

from flask_sqlalchemy import SQLAlchemy

basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

db = SQLAlchemy(app)

db 对象是 SQLAlchemy 类的实例, 表示程序使用的数据库。data.sqlite 是数据库的名字, 上面使用 + 号拼接了数据库的绝对路径。

定义模型 #

模型表示程序使用的持久化化的实体。在 ORM(对象关系映射)中, 模型一般是一个 Python 类, 类中的属性对应着数据库表中的

Flask-SQLAlchemy 创建的数据库实例(例如 db)为模型提供了一个基类以及一系列辅助类和辅助函数, 可以用于定义模型的结构。下面把数据库中的 roles 表和 users 表定义为 模型 Role 和模型 User。

示例 5-2 hello.py: 定义 Role 和 User 模型

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)

    def __repr__(self):
        return '<User %r>' % self.username

类变量__tablename__定义在数据库中使用的表名。如果没有定义 __tablename__ , Flask-SQLAlchemy 会使用一个默认名字。id/name/username 都是模型中的属性, 被定义为 db.Column 类的实例, 代表数据库中的

db.Column 类构造函数的第一个参数是数据库列和模型属性的类型。表 5-2 列出了一些可用的・列类型以及在模型中使用的 Python 类型。

表5-2 最常用的SQLAlchemy 列类型

类型名 Python类型 说 明
Integer int 普通整数,一般是 32 位
SmallInteger int 取值范围小的整数,一般是 16 位
BigInteger int 或 long 不限制精度的整数
Float float 浮点数
Numeric decimal.Decimal 定点数
String str 变长字符串
Text str 变长字符串,对较长或不限长度的字符串做了优化
Unicode unicode 变长 Unicode 字符串
UnicodeText unicode 变长 Unicode 字符串,对较长或不限长度的字符串做了优化
Boolean bool 布尔值
Date datetime.date 日期
Time datetime.time 时间
DateTime datetime.datetime 日期和时间
Interval datetime.timedelta 时间间隔
Enum str 一组字符串
PickleType 任何 Python 对象 自动使用 Pickle 序列化
LargeBinary str 二进制文件

db.Column 中其余的参数指定属性的配置选项。表 5-3 列出了一些可用选项。

表5-3 最常用的 SQLAlchemy 列选项

选项名 说明
primary_key 如果设为 True ,这列就是表的主键
unique 如果设为 True ,这列不允许出现重复的值
index 如果设为 True ,为这列创建索引,提升查询效率
nullable 如果设为 True ,这列允许使用空值;如果设为 False ,这列不允许使用空值
default 为这列定义默认值

Flask-SQLAlchemy 要求每个模型都要定义 主键 ,这一列经常命名为 id 。

虽然没有强制要求,但这两个模型都定义了 repr() 方法,返回一个具有可读性的字符串表示模型,可在调试和测试时使用。

关系 #

关系型数据库使用关系把不同表中的行联系起来。图 5-1 所示的关系图表示用户和角色之间的一种简单关系。这是角色到用户的一对多关系,因为一个角色可属于多个用户,而每个用户都只能有一个角色。

图 5-1 中的一对多关系在模型中的表示方法如示例 5-3 所示: 示例 5-3 hello.py: 关系

class Role(db.Model):
    # ...
    users = db.relationship('User', backref='role')
    
class User(db.Model):
    # ...
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

如图 5-1 所示, 关系使用了 users 表中的外键连接了两行。添加到 User 模型中的 role_id 列被定义为外键, 就是这个外键建立起了关系。传给 db.ForeignKey() 的参数 roles.id 表明, 这列的值是 roles 表中行的 id 值。

对于一个 Role 类的实例, 其 users 属性将返回与角色相关联的用户组成的列表。 db.relationship() 的第一个参数表明这个关系的另一端是哪个模型。关系表明有两方, 一方是 Role, 一方是 User。

db.relationship() 中的 backref 参数向 User 模型(类)中添加了一个 role 属性, 它是一个 Role 的实例, 从而定义反向关系。这一属性可替代 role_id 访问 Role 模型。 注意, 这时候 User 类中的属性有 tablename、id、username、role_id、还有 role 共 5 个属性。

大多数情况下, db.relationship() 都能自行找到关系中的外键,但有时却无法决定把哪一列作为外键。例如,如果 User 模型中有两个或以上的列定义为 Role 模型的外键,SQLAlchemy 就不知道该使用哪列。如果无法决定外键,你就要为 db.relationship() 提供 额外参数,从而确定所用外键。表 5-4 列出了定义关系时常用的配置选项。

表5-4 常用的SQLAlchemy关系选项

选项名 说明
backref 在关系的另一个模型中添加反向引用
primaryjoin 明确指定两个模型之间使用的联结条件。只在模棱两可的关系中需要指定
lazy 指定如何加载相关记录。可选值有 select (首次访问时按需加载)、 immediate (源对象加载后就加载)、 joined (加载记录,但使用联结)、 subquery (立即加载,但使用子查询),noload (永不加载)和 dynamic (不加载记录,但提供加载记录的查询)
uselist 如果设为 Fales ,不使用列表,而使用标量值
order_by 指定关系中记录的排序方式
secondary 指定多对多关系中关系表的名字
secondaryjoin SQLAlchemy 无法自行决定时,指定多对多关系中的二级联结条件

数据库操作 #

在 Python shell 中操作数据库。

创建表 #

让 Flask-SQLAlchemy 根据模型类型创建数据库。 方法是使用 db.create_all() 函数:

(venv) $ python hello.py shell
>>> from hello import db # hello 是一个包, db 是包中的变量
>>> db.create_all()

这会在程序的根目录下创建一个名为 data.sqlite 的数据库文件。这个数据库的名字是在配置中指定的。如果数据库表已经存在于数据库中, 那么 db.create_all() 不会重新创建或更新这个表。如果修改模型后要把改动应用到现有数据库中, 那更新现有数据库的粗暴方式是先删除旧表再重建新表:

>>> db.drop_all()
>>> db.create_all()

这样会把数据库中原有的数据都销毁了。本章末尾会使用数据库迁移工具来达到更好的解决方法。

插入行 #

我们先创建一些角色和用户:

# 创建三个角色
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role   = Role(name='Moderator')
>>> user_role  = Role(name='User')

# 创建三个用户
>>> user_john  = User(username='John', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)

模型类的构造函数接受的参数是使用关键字参数指定的模型属性的初始值。(实际上是初始化一个 role/user 对象)。注意 role 属性也可以使用, 虽然它不是真正的数据库列, 但却是一对多关系的高级表示。这些新建对象的 id属性并没有明确设定, 因为主键是由 Flask-SQLAlchemy 管理的。现在这些对象只存在于 Python 中, 还未写入数据库, 因此 id 尚未被赋值:

>>> print(admin_role.id)
None
>>> print(user_susan.id)
None
...

通过数据库会话(db.session)来管理对数据库所做的改动。在把对象写入数据库之前, 先要将这些对象添加到会话中:

>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)

或者简写为:

db.session.add_all([admin_role, mod_role, user_role, user_john, user_susan, user_david])

把对象添加(想想 git add)到会话中后我们还需要调用 commit() 方法提交会话(想想 git commit):

db.session.commit()

提交后就会执行数据库语句, 再次查看 id 属性,现在它们已经赋值了:

>>> print(admin_role.id) # 1
>>> print(mod_role.id)   # 2
>>> print(user_role.id)  # 3

数据库会话(session)能保证数据库的一致性。提交操作使用原子方式把会话中的对象全部写入数据库。如果在写入会话的过程中发生了错误, 那么整个会话都会失效。如果你始终把相关改动放在会话中提交, 就能避免因部分更新导致的数据库不一致性。

数据库会话也可回滚。调用 db.session.rollback() 后, 添加到数据库会话中的所有对象都会还原到它们在数据库中时的状态。

修改行 #

数据库会话上调用 add() 方法也能更新模型。我们继续在之前的 shell 会话中进行操作:

# 把 Admin 这个角色重命名为 Administrator
>>> admin_role.name = 'Administrator' # 对管理员角色进行重命名
>>> db.session.add(admin_role)
>>> db.session.commit() # 提交

删除行 #

数据库会话中的 delete() 方法用于从数据库中删除数据(想想 git rm/delete):

db.session.delete(mod_role) # 删除 mod_role
db.session.commit() # 提交

注意,删除与插入和更新一样,提交数据库会话后才会执行。

查询行 #

Flask-SQLAlchemy 为每个模型类都提供了 query 对象。最基本的模型查询是取回对应表中的所有记录:

>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>]
>>> User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]

使用过滤器可以过滤查询结果:

# 过滤列 role 为 user_role 的行
>>> User.query.filter_by(role=user_role).all()
[<User u'susan'>, <User u'david'>]

若要查看 SQLAlchemy 为查询生成的原生 SQL 查询语句,只需把 query 对象转换成字符串:

str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username,users.role_id AS users_role_id FROM users WHERE :param_1 = users.role_id'

如果你退出了 Python shell 的话, 前面例子中创建的对象就不会以 Python 对象的形式存在,而是作为各自数据库表中的行存在。如果你打开了一个新的 shell 会话, 就要从数据库中读取行, 再重新创建 Python 对象。最基本的模型查询是取回对应表中的所有记录

# 发起一个查询, 加载名为 User 的用户角色
>>> user_role = Role.query.filter_by(name='User').first()

filter_by() 等过滤器在 query 对象上调用,返回一个更精确的 query 对象。多个过滤器可以一起调用,直到获得所需结果。

表 5-5 列出了可在 query 对象上调用的常用过滤器。

表5-5 常用的SQLAlchemy查询过滤器

过滤器 说 明
filter() 把过滤器添加到原查询上,返回一个新查询
filter_by() 把等值过滤器添加到原查询上,返回一个新查询
limit() 使用指定的值限制原查询返回的结果数量,返回一个新查询
offset() 偏移原查询返回的结果,返回一个新查询
order_by() 根据指定条件对原查询结果进行排序,返回一个新查询
group_by() 根据指定条件对原查询结果进行分组,返回一个新查询

在查询上应用指定的过滤器后, 通过调用 all() 执行查询, 以「列表」的形式返回结果。除了 all() 之外,还有其它方法能触发查询的执行。表 5-6 列出了执行查询的其它方法。

表5-6 最常使用的SQLAlchemy查询执行函数

方 法 说 明
all() 以列表形式返回查询的所有结果
first() 返回查询的第一个结果,如果没有结果,则返回 None
first_or_404() 返回查询的第一个结果,如果没有结果,则终止请求,返回 404 错误响应
get() 返回指定主键对应的行,如果没有对应的行,则返回 None
get_or_404() 返回指定主键对应的行,如果没找到指定的主键,则终止请求,返回 404 错误响应
count() 返回查询结果的数量
paginate() 返回一个 Paginate 对象,它包含指定范围内的结果

关系和查询的处理方式类似。下面这个例子分别从关系的两端查询角色和用户之间的一对多关系:

>>> users = user_role.users
>>> users
[<User u'susan'>, <User u'david'>]
>>> users[0].role
<Role u'User'>

这个例子中的 user_role.users 查询有个小问题。执行 user_role.users 表达式时, 隐含的查询会调用 all() 函数并返回一个用户列表。 query 对象是隐藏的, 因此无法指定更精确的查询过滤器。就这个给定的示例而言, 返回一个按照字母顺序排列的用户列表可能会更好。在下面的示例中, 我们修改了关系的设置, 加入了 lazy = ‘dynamic’ 参数, 以禁止自动执行查询

示例 5-4 hello.py: 动态关系

class Role(db.Model):
    # ...
    users = db.relationship('User', backref='role', lazy='dynamic')
    # ...

这样配置关系之后, user_role.roles 会返回一个尚未执行的查询,因此可以在其上添加过滤器:

>>> user_role.users.order_by(User.username).all()
[<User u'david'>, <User u'susan'>]
>>> user_role.users.count()
2

在视图函数中操作数据库 #

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm() # 用户名表单
    if form.validate_on_submit(): # 提交表单后
        user = User.query.filter_by(username=form.name.data).first() # 在数据库中查询当前输入的姓名
        if user is None:
            user = User(username = form.name.data) # 如果这个名字是新人, 就在数据库中新添加一个
            db.session.add(user)
            session['known'] = False # 并在 session 中把 known 设置为假
        else:
            session['known'] = True  # 否则, 用户名已存在, known 为真
        session['name'] = form.name.data # 把用户名保存在 session 的 name 变量中
        form.name.data = '' # 提交完成后清空表单的用户名
        return redirect(url_for('index')) # 重定向到主页
    return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', False))

在这个修改后的版本中,提交表单后,程序会使用 filter_by() 查询过滤器在数据库中查找提交的名字。变量 known 被写入用户会话中,因此重定向之后,可以把数据传给模板,用来显示自定义的欢迎消息。

注意,要想让程序正常运行,你必须按照前面介绍的方法,在 Python shell 中创建数据库表。我再强调一遍如下所示:

>>> from hello import db
>>> db.create_all()
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)
>>> db.session.add_all([admin_role, mod_role, user_role,user_john, user_susan, user_david])
>>> db.session.commit()

对应的模板新版本如示例 5-6 所示。这个模板使用 known 参数在欢迎消息中加入第二行, 从而对已知用户新用户显示不同的内容。

示例 5-6 templates/index.html

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
    <h1>Hello, {% if name %}{{ name }}{% else %}Strangr{% endif %}!</h1>
    {% if not konwn %}
    <p>Pleased to meet you!</p>
    {% else %}
    <p>Happy to meet you again!</p>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}    

再说一遍,

 git clone https://github.com/miguelgrinberg/flasky.git

下载官方源码后使用 git checkout xx 就会出现该章节对应的代码, 但是没有虚拟环境, 你需要激活虚拟环境后才能运行

python hello.py runserver --host=127.0.0.1

集成 Python shell #

你在上面也看到了, 每次启动 shell 会话都要导入数据库实例(db)和模型(User, Role), 枯燥又重复。为了发挥我们的懒惰精神, 我们可以使用 Flask-Script 命令自动导入特定的对象。

我们来为 shell 命令注册一个 make_context 回调函数, 如示例 5-7 所示。这个模板所示。

示例 5-7 hello.py: 为 shell 命令添加一个上下文

from flask_script import Shell

def make_shell_context():
    return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))

make_shell_context() 函数注册了程序(app)、数据库实例(app)、模型(User, Role), 因此这些对象能直接导入 shell:

$ python hello.py shell
>>> app 
<Flask 'hello'>
>>> db
<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'>
>>> User
<class 'app.User'>

这时你启动 hello.py, 输入名字进行查询, 如果数据库中已存在该名字网页就会显示:

hello, xxx
Happy to see you again!

如果该名字是首次被查询, 则会显示:

Hello, xxxxxx!
Pleased to meet you!

同时你在 Python shell 中可以查询数据库中是否已经插入了新值:

>>> User.query.all()
[<User 'john'>, <User 'susan'>, <User 'david'>, <User 'chenyf'>, <User 'xxxxxx'>]

你也可以在这时把 Python shell 停掉, 然后提交网页, 也能正常运行。因为数据库中已经存在了相关的表了。如果先启动 hello.py 就进行查询会出现内部服务器错误, 因为数据库还没有准备好(里面什么也没有)。命令行中的 debug 信息显示 no such table。所以:

注意, 在启动 hello.py 程序之前执行 python hello.py shell 初始化数据库, 实例化 Role 和 User 时就会创建roles表和 users 表。

所以

$ python hello.py shell
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)
>>> db.session.add_all([admin_role, mod_role, user_role,user_john, user_susan, user_david])
>>> db.session.commit()
>>> User.query.all()
[<User 'john'>, <User 'susan'>, <User 'david'>]

使用 Flask-Migrate 实现数据库迁移 #

在开发程序的过程中, 你会发现有时候需要修改数据库模型(如为 User 增加 age 属性), 而且修改之后还需要更新数据库。

仅当数据库表不存在时, Flask-SQLAlchemy 才会根据模型进行创建。因此, 更新表的唯一方式就是先删除旧表, 不过这样会丢失数据库中的所有数据, 傻狍子才这样做。

更新表的更好的方法是使用数据库迁移框架。源码版本控制工具(例如 git)可以跟踪源码文件的变化, 类似地, 数据库迁移框架能跟踪数据库模式的变化, 然后增量式的把变化应用到数据库中。

SQLAlchemy 的主力开发人员编写了一个迁移框架,称为 Alembic。除了直接使用 Alembic 之外,Flask 程序还可使用 Flask-Migrate 扩展。这个扩展对 Alembic 做了轻量级包装,并集成到 Flask-Script 中,所有操作都通过 Flask-Script 命令完成。

创建迁移仓库 #

首先在虚拟环境中安装 Flask-Migrate:

$ venv\Scripts\activate
$ pip install Flask-Migrate

这个扩展的初始化方法如下所示:

from flask_migrate import Migrate, MigrateCommand
# ...
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)

为了导出数据库迁移命令, Flask-Migrate 提供了一个 MigrateCommand 类, 可附加到 Flask-Script 的 manager 对象上。在这里, MigrateCommand 类使用 db 命令附加。是不是很熟悉, 我们刚见过:

manager.add_command("shell", Shell(make_context=make_shell_context))

在维护数据库迁移之前, 要使用 init 子命令创建迁移仓库:

(venv) $ python hello.py db init
Creating directory /home/flask/flasky/migrations...done
Creating directory /home/flask/flasky/migrations/versions...done
Generating /home/flask/flasky/migrations/alembic.ini...done
Generating /home/flask/flasky/migrations/env.py...done
Generating /home/flask/flasky/migrations/env.pyc...done
Generating /home/flask/flasky/migrations/README...done
Generating /home/flask/flasky/migrations/script.py.mako...done
Please edit configuration/connection/logging settings in
'/home/flask/flasky/migrations/alembic.ini' before proceeding.

这个命令会创建一个 migrations 文件夹,所有「迁移脚本」都存放其中

数据库迁移仓库中的文件要和程序的其他文件一起纳入版本控制。

创建迁移脚本 #

在 Alembic 中, 数据库迁移用迁移脚本表示。脚本中有两个函数, 分别是 upgrade()downgrade()。 upgrade() 函数把迁移中的改动应用到数据库中, downgrade() 函数则将改动删除。Alembic 具有「添加改动」和「删除改动」的能力, 因此数据库可重设到修改历史的任意一点

我们可以使用 revision 命令手动创建 Alembic 迁移, 也可使用 migrate 命令自动创建。手动创建的迁移只是一个骨架, upgrade() 和 downgrade() 函数都是空的, 开发者要使用 Alembic 提供的 Operations 对象指令实现具体操作。自动创建的迁移会根据模型定义和数据库当前状态之间的「差异」生成 upgrade() 和 downgrade() 函数的内容。

自动创建的迁移不一定总是正确的,有可能会漏掉一些细节。自动生成迁移脚本后一定要进行检查。

migrate 子命令用来自动创建迁移脚本:

(venv) $ python hello.py db migrate -m "initial migration"
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate] Detected added table 'roles'
INFO [alembic.autogenerate] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added index
'ix_users_username' on '['username']'
Generating /home/flask/flasky/migrations/versions/1bc
594146bb5_initial_migration.py...done

更新数据库 #

检查并修正好迁移脚本之后, 我们可以使用 db upgrade 命令把迁移应用到数据库中

(venv) $ python hello.py db upgrade
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.migration] Running upgrade None -> 1bc594146bb5, initial migration

对第一个迁移来说, 其作用和调用 db.create_all() 方法一样。但在后续的迁移中, upgrade 命令能把改动应用到数据库中, 且不影响其中保存的数据。