首页 > Python基础教程 >
-
带你认识 flask 中的数据库(2)
flask db migrate
),你可能会审查它以确保自动生成的正确性,然后将更改应用到你的开发数据库(flask db upgrade
)。测试无误后,将迁移脚本添加到源代码管理并提交。
当准备将新版本的应用发布到生产服务器时,你只需要获取包含新增迁移脚本的更新版本的应用,然后运行flask db upgrade
即可。Alembic将检测到生产数据库未更新到最新版本,并运行在上一版本之后创建的所有新增迁移脚本。
正如我前面提到的,flask db downgrade
命令可以回滚上次的迁移。虽然在生产系统上不太可能需要此选项,但在开发过程中可能会发现它非常有用。你可能已经生成了一个迁移脚本并将其应用,只是发现所做的更改并不完全是你所需要的。在这种情况下,可以降级数据库,删除迁移脚本,然后生成一个新的来替换它。
数据库关系
关系数据库擅长存储数据项之间的关系。考虑用户发表动态的情况, 用户将在user
表中有一个记录,并且这条用户动态将在post
表中有一个记录。标记谁写了一个给定的动态的最有效的方法是链接两个相关的记录。
一旦建立了用户和动态之间的关系,数据库就可以在查询中展示它。最小的例子就是当你看一条用户动态的时候需要知道是谁写的。一个更复杂的查询是, 如果你好奇一个用户时,你可能想知道这个用户写的所有动态。Flask-SQLAlchemy有助于实现这两种查询。
让我们扩展数据库来存储用户动态,以查看实际中的关系。这是一个新表post
的设计(译者注:实际表名分别为user和post):
post
表将具有必须的id
、用户动态的body
和timestamp
字段。除了这些预期的字段之外,我还添加了一个user_id
字段,将该用户动态链接到其作者。你已经看到所有用户都有一个唯一的id
主键, 将用户动态链接到其作者的方法是添加对用户id
的引用,这正是user_id
字段所在的位置。这个user_id
字段被称为外键。上面的数据库图显示了外键作为该字段和它引用的表的id
字段之间的链接。这种关系被称为一对多,因为“一个”用户写了“多”条动态。
修改后的app/models.py如下:
from datetime import datetime
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return '<Post {}>'.format(self.body)
新的“Post”类表示用户发表的动态。 timestamp
字段将被编入索引,如果你想按时间顺序检索用户动态,这将非常有用。我还为其添加了一个default
参数,并传入了datetime.utcnow
函数。当你将一个函数作为默认值传入后,SQLAlchemy会将该字段设置为调用该函数的值(请注意,在utcnow
之后我没有包含()
,所以我传递函数本身,而不是调用它的结果)。通常,在服务应用中使用UTC日期和时间是推荐做法。这可以确保你使用统一的时间戳,无论用户位于何处,这些时间戳会在显示时转换为用户的当地时间。
user_id
字段被初始化为user.id
的外键,这意味着它引用了来自用户表的id
值。本处的user
是数据库表的名称,Flask-SQLAlchemy自动设置类名为小写来作为对应表的名称。 User
类有一个新的posts
字段,用db.relationship
初始化。这不是实际的数据库字段,而是用户和其动态之间关系的高级视图,因此它不在数据库图表中。对于一对多关系,db.relationship
字段通常在“一”的这边定义,并用作访问“多”的便捷方式。因此,如果我有一个用户实例u
,表达式u.posts
将运行一个数据库查询,返回该用户发表过的所有动态。 db.relationship
的第一个参数表示代表关系“多”的类。 backref
参数定义了代表“多”的类的实例反向调用“一”的时候的属性名称。这将会为用户动态添加一个属性post.author
,调用它将返回给该用户动态的用户实例。 lazy
参数定义了这种关系调用的数据库查询是如何执行的,这个我会在后面讨论。不要觉得这些细节没什么意思,本章的结尾将会给出对应的例子。
一旦我变更了应用模型,就需要生成一个新的数据库迁移:
(venv) $ flask db migrate -m "posts table"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'post'
INFO [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']'
Generating /home/miguel/microblog/migrations/versions/780739b227a7_posts_table.py ... done
并将这个迁移应用到数据库:
$ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade e517276bb1c2 -> 780739b227a7, posts table
如果你对项目使用了版本控制,记得将新的迁移脚本添加进去并提交。
表演时刻
经历了一个漫长的过程来定义数据库,我却还没向你展示它们如何使用。由于应用还没有任何数据库逻辑,所以让我们在Python解释器中来使用以便熟悉它。立即运行python
命令来启动Python(在启动解释器之前,确保您的虚拟环境已被激活)。
进入Python交互式环境后,导入数据库实例和模型:
from app import db
from app.models import User, Post
开始阶段,创建一个新用户:
'john', email='john@example.com') > u = User(username=
> db.session.add(u)
> db.session.commit()
对数据库的更改是在会话的上下文中完成的,你可以通过db.session
进行访问验证。允许在会话中累积多个更改,一旦所有更改都被注册,你可以发出一个指令db.session.commit()
来以原子方式写入所有更改。如果在会话执行的任何时候出现错误,调用db.session.rollback()
会中止会话并删除存储在其中的所有更改。要记住的重要一点是,只有在调用db.session.commit()
时才会将更改写入数据库。会话可以保证数据库永远不会处于不一致的状态。
添加另一个用户:
'susan', email='susan@example.com') > u = User(username=
> db.session.add(u)
> db.session.commit()
数据库执行返回所有用户的查询:
users = User.query.all()
users
[<User john>, <User susan>]
for u in users:
print(u.id, u.username)
...
1 john
2 susan
所有模型都有一个query
属性,它是运行数据库查询的入口。最基本的查询就是返回该类的所有元素,它被适当地命名为all()
。请注意,添加这些用户时,它们的id
字段依次自动设置为1和2。
另外一种查询方式是,如果你知道用户的id
,可以用以下方式直接获取用户实例:
1) > u = User.query.get(
> u
<User john>
现在添加一条用户动态:
1) > u = User.query.get(
'my first post!', author=u) > p = Post(body=
> db.session.add(p)
> db.session.commit()
我不需要为timestamp
字段设置一个值,因为这个字段有一个默认值,你可以在模型定义中看到。那么user_id
字段呢?回想一下,我在User
类中创建的db.relationship
为用户添加了posts
属性,并为用户动态添加了author
属性。我使用author
虚拟字段来调用其作者,而不必通过用户ID来处理。SQLAlchemy在这方面非常出色,因为它提供了对关系和外键的高级抽象。
为了完成演示,让我们看看另外的数据库查询案例: