本章的主题非常重要。 对于大多数应用程序,将需要维护可以有效检索的持久性数据,而这正是数据库的用途。
我相信你已经听说过,Flask本身并不支持数据库。这是Flask故意不固执己见的许多领域之一,这很好,因为你可以选择最适合应用程序的数据库,而不必强迫自己适应它。
Python中的数据库有很多不错的选择,其中许多都带有Flask扩展,可以更好地与应用程序集成。数据库可以分为两个大组,遵循关系模型的数据库和不遵循关系模型的数据库。后者通常称为NoSQL,表示它们没有实现流行的关系查询语言SQL。虽然两组中都有出色的数据库产品,但我认为关系数据库更适合具有结构化数据(例如用户列表,博客帖子等)的应用程序,而NoSQL数据库更适合具有较少定义的结构的数据。这个应用程序和大多数其他应用程序一样,可以使用任何一种类型的数据库来实现,但是出于上述原因,我将使用关系数据库。
在第3章中,我展示了第一个Flask扩展。在本章中,我将再使用两个。第一个是Flask-SQLAlchemy,这是一个扩展,它为流行的SQLAlchemy包(对象关系映射器或ORM)提供了Flask友好的包装。 ORM允许应用程序使用高级实体(例如类,对象和方法)代替表和SQL来管理数据库。 ORM的工作是将高级操作转换为数据库命令。
关于SQLAlchemy的好处是,它不是一个ORM,适用于许多关系数据库。 SQLAlchemy支持很长的数据库引擎列表,包括流行的MySQL,PostgreSQL和SQLite。这非常强大,因为你可以使用不需要服务器的简单SQLite数据库进行开发,然后在将应用程序部署到生产服务器上时,可以选择功能更强大的MySQL或PostgreSQL服务器,而无需更改你的应用程序。
要在你的虚拟环境中安装Flask-SQLAlchemy,请确保已先将其激活,然后运行:
(venv) $ pip install flask-sqlalchemy
我见过的大多数数据库教程都介绍了数据库的创建和使用,但是并没有充分解决随着应用程序需求的变化或增长而对现有数据库进行更新的问题。 这很困难,因为关系数据库围绕结构化数据为中心,因此,当结构更改时,数据库中已经存在的数据需要迁移到修改后的结构。
我将在本章中介绍的第二个扩展是Flask-Migrate,它实际上是由你真正创建的。 此扩展是Alembic的Flask包装器,Alembic是SQLAlchemy的数据库迁移框架。 使用数据库迁移需要花费一些时间才能启动数据库,但这对于将来以健壮的方式对数据库进行更改的方式付出的代价是很小的。
Flask-Migrate的安装过程与你看到的其他扩展类似:
(venv) $ pip install flask-migrate
在开发过程中,我将使用SQLite数据库。 SQLite数据库是开发小型应用程序(有时甚至不是那么小的应用程序)的最方便选择,因为每个数据库都存储在磁盘上的单个文件中,并且无需运行MySQL和PostgreSQL之类的数据库服务器。
我们有两个新的配置项要添加到配置文件中:
#config.py: Flask-SQLAlchemy configuration
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
Flask-SQLAlchemy扩展从SQLALCHEMY_DATABASE_URI
配置变量获取应用程序数据库的位置。正如你在第3章中回顾的那样,通常最好的做法是根据环境变量设置配置,并在环境未定义变量时提供回退(默认)值。在这种情况下,我将从DATABASE_URL
环境变量中获取数据库URL,如果未定义,则要配置一个位于应用程序主目录中名为app.db
的数据库,该数据库存储在basedir
变量中。
将SQLALCHEMY_TRACK_MODIFICATIONS
配置选项设置为False
,以禁用我不需要的Flask-SQLAlchemy功能,该功能是在数据库中每次进行更改时向应用程序发出信号。
数据库将在数据库中由数据库实例表示。数据库迁移引擎还将具有一个实例。这些是需要在应用程序之后在app/__ init__.py
文件中创建的对象:
#app/__init__.py: Flask-SQLAlchemy and Flask-Migrate initialization
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
from app import routes, models
我对初始化脚本进行了三处更改。 首先,我添加了一个代表数据库的db
对象。 然后,我添加了另一个代表迁移引擎的对象。 希望你看到了如何使用Flask扩展的模式。 大多数扩展都初始化为这两个。 最后,我在底部导入了一个称为模型(models
)的新模块。 该模块将定义数据库的结构。
将存储在数据库中的数据将由通常称为数据库模型的类的集合表示。 SQLAlchemy中的ORM层将执行将这些类创建的对象映射到适当的数据库表中的行所需的转换。
让我们从创建代表用户的模型开始。 使用WWW SQL Designer工具,我制作了下图来表示我们要在users表中使用的数据:
id
字段通常在所有模型中都用作主键(primary key)。数据库中的每个用户都将被分配一个唯一的ID值,该ID值存储在此字段中。在大多数情况下,主键是由数据库自动分配的,因此我只需要提供标记为主键的id
字段即可。
username
,email
和password_hash
字段定义为字符串(或数据库术语中的VARCHAR
),并指定了它们的最大长度,以便数据库可以优化空间使用。尽管username
和email
字段是不言自明的,但password_hash
字段值得关注。我要确保正在构建的应用程序采用安全性最佳做法,因此,我不会在数据库中存储用户密码。存储密码的问题在于,如果数据库遭到破坏,攻击者将可以访问密码,这对于用户而言可能是灾难性的。我将直接编写密码哈希,而不是直接编写密码,这将大大提高安全性。这将是另一章的主题,因此暂时不必太担心。
因此,既然我知道要为用户表提供的内容,就可以将其转换为新的app/models.py
模块中的代码:
#app/models.py: User database model
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))
def __repr__(self):
return '<User {}>'.format(self.username)
上面创建的User
类继承自db.Model
,后者是Flask-SQLAlchemy中所有模型的基类。 此类将几个字段定义为类变量。 字段被创建为db.Column
类的实例,该类将字段类型作为参数,加上其他可选参数,例如,它使我可以指示哪些字段是唯一的并已建立索引,这对确保数据库搜索有效非常重要。 。
__repr__
方法告诉Python如何打印此类的对象,这将对调试很有用。 你可以在下面的Python解释器会话中看到__repr __()
方法的运行情况:
>>> from app.models import User
>>> u = User(username='susan', email='susan@example.com')
>>> u
<User susan>
在上一节中创建的模型类定义了此应用程序的初始数据库结构(或schema)。但是随着应用程序的不断增长,将需要更改该结构,非常有可能添加新的东西,但有时还要修改或删除项目。 Alembic(Flask-Migrate使用的迁移框架)将以不需要重新创建数据库的方式来进行这些模式更改。
为了完成这项看似困难的任务,Alembic维护了一个迁移存储库(migration repository),该目录是一个存储迁移脚本的目录。每次对数据库模式进行更改时,都会将带有更改详细信息的迁移脚本添加到存储库中。要将迁移应用到数据库,将按照创建脚本的顺序执行这些迁移脚本。
Flask-Migrate通过flask
命令暴露其命令。你已经看到flask run
,这是Flask的子命令。 Flask-Migrate添加了flask db
子命令,以管理与数据库迁移相关的所有内容。因此,我们通过运行flask db init
为microblog 项目创建迁移存储库:
(venv) $ flask db init
Creating directory /home/miguel/microblog/migrations ... done
Creating directory /home/miguel/microblog/migrations/versions ... done
Generating /home/miguel/microblog/migrations/alembic.ini ... done
Generating /home/miguel/microblog/migrations/env.py ... done
Generating /home/miguel/microblog/migrations/README ... done
Generating /home/miguel/microblog/migrations/script.py.mako ... done
Please edit configuration/connection/logging settings in
'/home/miguel/microblog/migrations/alembic.ini' before proceeding.
请记住,flask
命令依赖于FLASK_APP
环境变量来了解Flask应用程序所在的位置。 对于此应用程序,你想要设置FLASK_APP = microblog.py
,如第1章所述。
运行此命令后,你将找到一个新的migrations
目录,其中包含一些文件和一个versions
子目录。 从现在开始,所有这些文件都应视为项目的一部分,尤其应将其添加到源代码管理中。
有了迁移资料库之后,就该创建第一个数据库迁移了,其中将包括映射到User
数据库模型的users
表。 有两种创建数据库迁移的方法:手动或自动。 为了自动生成迁移,Alembic将数据库模型定义的数据库模式与数据库中当前使用的实际数据库模式进行比较。 然后,使用使数据库模式与应用程序模型匹配所需的更改填充迁移脚本。 在这种情况下,由于没有以前的数据库,因此自动迁移会将整个User
模型(model)添加到迁移脚本中。 flask db migration
子命令生成以下自动迁移:
(venv) $ flask db migrate -m "users table"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'user'
INFO [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']'
Generating /home/miguel/microblog/migrations/versions/e517276bb1c2_users_table.py ... done
命令的输出使你对迁移中包含的Alembic有一个了解。前两行仅供参考,通常可以忽略。然后说它找到了一个用户表和两个索引。然后,它告诉你在何处编写了迁移脚本。 e517276bb1c2
代码是为迁移自动生成的唯一代码(对你来说会有所不同)。 -m
选项提供的注释是可选的,它为迁移添加了简短的描述性文本。
现在,生成的迁移脚本是项目的一部分,需要将其合并到源代码管理中。如果你想了解脚本的外观,欢迎你检查该脚本。你会发现它具有两个函数,称为upgrade()
和downgrade()
。 upgrade()
函数将应用迁移,而downgrade()
函数将其删除。这样,Alembic可以通过使用降级路径将数据库迁移到历史记录中的任何点,甚至可以迁移到旧版本。
flask db migration
命令不会对数据库进行任何更改,而只是生成迁移脚本。要将更改应用到数据库,必须使用flask db upgrade
命令。
(venv) $ 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, users table
因为此应用程序使用SQLite,所以upgrade
命令将检测到数据库不存在并将创建该数据库(你将注意到在此命令完成后添加了名为app.db
的文件,即SQLite数据库)。 使用MySQL和PostgreSQL等数据库服务器时,必须在运行upgrade
之前在数据库服务器中创建数据库。
请注意,默认情况下,Flask-SQLAlchemy对数据库表使用“ snake case”命名约定。 对于上面的User
模型,数据库中的对应表将被命名为user
。 对于AddressAndPhone
模型类,该表将被命名为address_and_phone
。 如果你希望选择自己的表名,则可以向模型类添加名为__tablename__
的属性,并以字符串形式设置为所需的名称。
此时,该应用程序还处于起步阶段,但是讨论下一步的数据库迁移策略并不会有什么坏处。想象一下,你的应用程序已在开发计算机上,并且副本已部署到在线且正在使用的生产服务器上。
假设对于下一版应用程序,你必须对模型进行更改,例如,需要添加一个新表。如果不进行迁移,那么你将需要弄清楚如何在开发机器中然后在服务器中再次更改数据库的架构,这可能需要很多工作。
但是有了数据库迁移支持,在修改应用程序中的模型之后,你将生成一个新的迁移脚本(flask db migrate
),你可能需要对其进行检查以确保自动生成的功能正确,然后将更改应用于开发数据库(flask db upgrade
)。你将迁移脚本添加到源代码管理中并提交它。
当你准备将新版本的应用程序发布到生产服务器时,你所需要做的就是获取应用程序的更新版本,该版本将包括新的迁移脚本,然后运行flask db upgrade
。 Alembic将检测生产数据库是否未更新到架构的最新版本,并运行在先前发行版之后创建的所有新迁移脚本。
如前所述,你还有一个flask db downgrade
命令,该命令撤消上一次迁移。虽然你不太可能在生产系统上需要此选项,但是你可能会在开发过程中发现它非常有用。你可能已经生成并应用了迁移脚本,只是发现所做的更改不完全是你所需要的。在这种情况下,你可以降级数据库,删除迁移脚本,然后生成一个新的脚本来替换它。
关系数据库擅长存储数据项之间的关系。 考虑用户撰写博客文章的情况。 用户将在users
表中有一条记录,而帖子将在posts
表中有一条记录。 记录谁写给定帖子的最有效方法是链接两个相关记录。
一旦在用户和帖子之间建立了链接,数据库就可以回答有关该链接的查询。 最琐碎的是当你有一篇博客文章并且需要知道什么用户写了它。 一个更复杂的查询与此相反。 如果你有一个用户,则可能想知道该用户写的所有帖子。 Flask-SQLAlchemy将对两种类型的查询都有帮助。
让我们扩展数据库以存储博客文章,以查看实际的关系。 这是新的posts
表的架构:
posts
表将具有所需的id
,帖子的正文(body
)和时间戳(timestamp
)。 但是除了这些预期的字段之外,我还要添加一个user_id
字段,该字段将帖子链接到其作者。 你已经看到所有用户都有一个唯一的id
主键。 将博客文章链接到创建它的用户的方法是添加对用户id
的引用,而这正是user_id
字段的含义。 此user_id
字段称为外键(foreign key)。 上面的数据库图将外键显示为它引用的表的字段和id
字段之间的链接。 这种关系称为“一对多”,因为“一个”用户编写“许多”帖子。
修改后的app/models.py
如下所示:
#app/models.py: Posts database table and relationship
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
的外键,这意味着它引用了users
表中的id
值。在此参考中,用户部分是模型的数据库表的名称。不幸的是,在某些情况下(例如在db.relationship()
调用中),模型由模型类引用,该模型类通常以大写字母开头,而在其他情况下(例如,此db.ForeignKey()
声明) ,模型由数据库表名给出,SQLAlchemy会自动使用小写字符,对于多词模型名,则使用蛇形命名法(snake case)。
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
并且需要将迁移应用于数据库:
(venv) $ 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
首先创建一个新用户:
>>> u = User(username='john', email='john@example.com')
>>> db.session.add(u)
>>> db.session.commit()
对数据库的更改是在会话的上下文中完成的,可以通过db.session
进行访问。 会话中可以累积多个更改,一旦所有更改被注册,你就可以发出一个db.session.commit()
,它以原子方式写入所有更改。 如果在处理会话的任何时间出现错误,则对db.session.rollback()
的调用将中止该会话并删除其中存储的所有更改。 要记住的重要一点是,只有在调用db.session.commit()
时,更改才会写入数据库。 会话可确保数据库永远不会保持不一致状态。
让我们添加另一个用户:
>>> u = User(username='susan', email='susan@example.com')
>>> 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
,则可以按以下方式检索该用户:
>>> u = User.query.get(1)
>>> u
<User john>
现在让我们添加一个博客文章:
>>> u = User.query.get(1)
>>> p = Post(body='my first post!', author=u)
>>> db.session.add(p)
>>> db.session.commit()
我不需要为timestamp
字段设置值,因为该字段具有默认值,你可以在模型定义中看到该默认值。 那user_id
字段呢? 回想一下,我在User
类中创建的db.relationship
将posts
属性添加到用户,还将author
属性添加到post
。 我使用author
虚拟字段将作者分配给帖子,而不必处理用户ID。 在这方面,SQLAlchemy很棒,因为它提供了对关系和外键的高级抽象。
为了完成此会话,让我们看一些其他的数据库查询:
>>> # get all posts written by a user
>>> u = User.query.get(1)
>>> u
<User john>
>>> posts = u.posts.all()
>>> posts
[<Post my first post!>]
>>> # same, but with a user that has no posts
>>> u = User.query.get(2)
>>> u
<User susan>
>>> u.posts.all()
[]
>>> # print post author and body for all posts
>>> posts = Post.query.all()
>>> for p in posts:
... print(p.id, p.author.username, p.body)
...
1 john my first post!
# get all users in reverse alphabetical order
>>> User.query.order_by(User.username.desc()).all()
[<User susan>, <User john>]
Flask-SQLAlchemy文档(中文)是了解可用于查询数据库的许多选项的最佳场所。
为了完成本节,让我们删除上面创建的测试用户和帖子,以使数据库整洁并为下一章做好准备:
>>> users = User.query.all()
>>> for u in users:
... db.session.delete(u)
...
>>> posts = Post.query.all()
>>> for p in posts:
... db.session.delete(p)
...
>>> db.session.commit()
还记得在启动Python解释器之后上一节开始时所做的事情吗? 你所做的第一件事是运行一些导入:
>>> from app import db
>>> from app.models import User, Post
在处理应用程序时,你将需要非常频繁地在Python shell中进行测试,因此每次都必须重复上述导入将变得很乏味。flask shell
命令是flask
命令中另一个非常有用的工具。 shell
命令是Flask在run
后实现的第二个“核心”命令。 该命令的目的是在应用程序的上下文中启动Python解释器。 这意味着什么? 请参见以下示例:
(venv) $ python
>>> app
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'app' is not defined
>>>
(venv) $ flask shell
>>> app
<Flask 'app'>
在常规解释器会话中,除非明确导入了app
符号,否则它是未知的,但是在使用flask shell
时,该命令会预导入应用程序实例。 flask shell
的好处不是预导入app
,而是可以配置“ shell上下文”,它是要预导入的其他符号的列表。
microblog.py
中的以下函数创建一个Shell上下文,该上下文将数据库实例和模型添加到Shell会话中:
from app import app, db
from app.models import User, Post
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
app.shell_context_processor
装饰器将该函数注册为Shell上下文函数。 当flask shell
命令运行时,它将调用此函数并在shell会话中注册其返回的项。 该函数返回字典而不是列表的原因是,对于每个项目,你还必须提供一个名称,以便在外壳程序中引用该名称,该名称由字典键指定。
添加了外壳上下文处理器功能后,你可以使用数据库实体,而不必导入它们:
(venv) $ flask shell
>>> db
<SQLAlchemy engine=sqlite:////Users/migu7781/Documents/dev/flask/microblog2/app.db>
>>> User
<class 'app.models.User'>
>>> Post
<class 'app.models.Post'>
如果你尝试上述操作并在访问db
,User
和Post
时遇到NameError
异常,则说明make_shell_context()
函数未在Flask中注册。 造成这种情况的最可能原因是你尚未在环境中设置FLASK_APP = microblog.py
。 在这种情况下,请返回第1章,并回顾如何设置FLASK_APP
环境变量。 如果你在打开新的终端窗口时经常忘记设置此变量,则可以考虑将.flaskenv
文件添加到项目中。