本章的主题是重中之重!因为一个网站的大多数应用都需要持久化存储数据,并高效地执行的增删查改的操作,数据库为此而生。
Flask 中的数据库
Flask 本身并不自带数据库,你可以自由选择数据库插件来实现数据库的功能,这是 Flask 有意为之,为了让开发者让人拥有更大的主动权。
绝大多数的数据库都提供了 Python 客户端包,它们之中的大部分都被封装成 Flask 插件以便更好地和 Flask 应用结合。
数据库被划分为两大类,遵循关系模型的一类是关系数据库,另外的则是非关系数据库,简称 NoSQL
。虽然两类数据库都是伟大的产品,但我认为关系数据库更适合具有结构化数据的应用程序,例如用户列表,用户动态等,而 NoSQL
数据库往往更适合非结构化数据。
本应用可以像大多数其他应用一样,使用任何一种类型的数据库来实现,但是出于上述原因,我将使用关系数据库。
在本章节,将使用到 Flask-SQLAlchemy
这个扩展,这个插件把 SQLAlchemy
包做了一层封装以便在 Flask 中调用更方便,类似 SQLAlchemy
这样的包叫做 Object Relational Mapper,简称 ORM。 ORM 允许应用程序使用高级实体(如类,对象和方法)而不是表和 SQL 来管理数据库。
SQLAlchemy
不只是某一款数据库软件的 ORM,而是支持包含 MySQL、PostgreSQL 和 SQLite 在内的很多数据库软件。在开发的时候使用简单易用且无需另起服务的 SQLite,需要部署应用到生产服务器上时,则选用更健壮的 MySQL 或 PostgreSQL 服务,并且不需要修改应用代码,只需修改应用配置就可以了。
现在我们先完成插件的安装,激活虚拟环境之后,利用如下命令安装:
(venv) $ pip install flask-sqlalchemy
数据库迁移
回顾我们学习数据库的流程,都是定义一个数据库,再创建数据库然后使用它,如果在数据库创建后,我们要对数据库的结构进行修改,或者修改完成后又想到退回修改前的版本,那么就需要数据库迁移了。
数据库迁移的原理就是用一个文件来记录数据库各个版本的结构,根据记录文件,可以创建或退回某个版本的数据库。
插件 Flask-Migrate
。 这个插件是 Alembic
的一个 Flask 封装,是 SQLAlchemy
的一个数据库迁移框架。
现在我们先去安装该插件:
(venv) $ pip install flask-migrate
后面我们再会用到这个插件。
Flask-SQLAlchemy 配置
在配置文件 config.py
添加两个新的配置项:
# config.py
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
SQLALCHEMY_DATABASE_URI
定义了 Flask-SQLAlchemy
获取应用的数据库的路径。这里使用了 or
语法,先从环境变量中获得,如果不存在则使用默认值。默认值被设置为 basedir
变量表示的顶级目录下的一个名为 app.db
的文件路径。
SQLALCHEMY_TRACK_MODIFICATIONS
配置项用于定义数据发生变更之后是否发送信号给应用,现在不需要这项功能,因此将其设置为 False
。
要使用 Flask-SQLAlchemy
和 Flask-Migrate
先要将应用实例化之后进行实例化和注册操作。
在 app/__init__.py
文件中修改工厂函数:
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config
db = SQLAlchemy()
migrate = Migrate()
def create_app():
app = Flask(__name__)
# 加载配置
app.config.from_object(Config)
# 初始化各种扩展库
db.init_app(app)
migrate.init_app(app, db)
# 引入蓝图并注册
from app.routes import main_routes
app.register_blueprint(main_routes)
return app
from app import models
最后在底部导入 models
模块,这个模块将会用来定义数据库结构,之所以要在最底部导入是为了避免循环引用。
数据库模型
定义数据库中一张表及其字段的类,通常叫做数据模型。ORM(SQLAlchemy) 会将类的实例关联到数据库表中的数据行。
首先从用户模型开始,利用 SQL Designer 工具,画一张图来设计用户表的各个字段:
数据库示意图id
字段通常存在于所有模型并用作主键。每个用户都会被数据库分配一个 id
值,并存储到这个字段中。大多数情况下,主键都是数据库自动赋值的,我只需要提供 id
字段作为主键即可。
username
,email
和 password_hash
字段被定义为字符串(数据库术语中的 VARCHAR
),并指定其最大长度,以便数据库可以优化空间使用率。 username
和 email
字段的用途不言而喻。
password_hash
字段值得提一下,为想确保网站采用安全最佳实践,不应该将用户密码明文存储在数据库中。 明文存储密码的问题是,如果数据库被攻破,攻击者就会获得密码,这对用户隐私来说可能是毁灭性的。 如果使用哈希密码,这就大大提高了安全性。
用户表构思完毕之后,我将其用代码实现,并存储到新建的模块 app/models.py
中,代码如下:
# app/models.py
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
类的实例,它传入字段类型以及其他可选参数
创建数据库迁移存储库
现在我们用模型类定义了初始数据库结构,下一步我们要根据这个模型类来创建数据库。而随着开发的不断深入,我们会不断新增、修改或删除数据库结构,Alembic(Flask-Migrate
使用的迁移框架)将以一种不需要重新创建数据库的方式进行数据库结构的变更。
为了实现它,Alembic 维护一个数据库迁移存储库,它是一个存储迁移脚本的目录。每当对数据库结构进行更改后,都需要向存储库中添加一个包含更改的详细信息的迁移脚本。当应用这些迁移脚本到数据库时,它们将按照创建的顺序执行。
Flask-Migrate
通过 flask
命令运行它的子命令。比如之前已经运行过的 flask run
,这是一个 Flask 本身的子命令。 Flask-Migrate
添加了 flask db
子命令来管理与数据库迁移相关的所有事情。 那么让我们通过运行 flask db init
来创建 microblog 的迁移存储库:
(venv) $ flask db init
Creating directory ...\flask demo\migrations ... done
Creating directory ...\flask demo\migrations\versions ... done
Generating ...\flask demo\migrations\alembic.ini ... done
Generating ...\flask demo\migrations\env.py ... done
Generating ...\flask demo\migrations\README ... done
Generating ...\flask demo\migrations\script.py.mako ... done
Please edit configuration/connection/logging settings in
'... \\flask demo\\migrations\\alembic.ini' before proceeding.
请记住,flask
命令依赖于 FLASK_APP
环境变量来知道 Flask 应用入口在哪里。要运行以上命令请先确认设置了 FLASK_APP = microblog.py
。
执行完毕后,应用的顶级目录就创建了一个 migrations
文件夹,数据库迁移的相关文件都会在里面。 该文件夹中包含一个名为 versions
的子目录以及若干文件,记录了数据库版本的相关信息。从现在起,这些文件就是你项目的一部分了,应该添加到代码版本管理中去。
第一次数据库迁移
User
数据库模型的迁移存储库生成后,是时候创建第一次数据库迁移了。有两种方法来创建数据库迁移:手动或自动。 要自动生成迁移,Alembic 会将数据库模型定义的数据库模式与数据库中当前使用的实际数据库模式进行比较。然后,使用对需要更改的部分进行填充迁移脚本。当前情况是,由于之前没有数据库,自动迁移将把整个 User
模型添加到迁移脚本中。
使用 flask db migrate
命令完成第一次数据库迁移:
(venv) $ flask db migrate
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 C:\Users\Administrator\Desktop\flask demo\migrations\versions\8f87e577f3a1_.py ... done
通过命令输出,你可以了解到 Alembic 在创建迁移的过程中执行了哪些逻辑。前两行是常规信息,通常可以忽略。 之后的输出表明检测到了一个 user 表和两个索引。 然后它会告诉你迁移脚本的输出路径。 8f87e577f3a1
是自动生成的一个用于迁移的唯一标识(你运行的结果会有所不同)。
现在路径 migrations\versions
之下生成了一个 8f87e577f3a1.py
文件,记录的就是当前版本的数据库结构。如果查看它的代码,就会发现它有两个函数叫 upgrade()
和 downgrade()
。 upgrade()
函数用于应用迁移,downgrade()
函数用于回滚迁移。
flask db migrate
命令不会对数据库进行任何更改,只会生成迁移脚本。 要将更改应用到数据库,必须使用 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 -> 8f87e577f3a1, empty message
因为本应用使用 SQLite,所以 upgrade
命令检测到数据库不存在时,会创建它(在这个命令完成之后,你会注意到一个名为 app.db
的文件,即 SQLite 数据库)。 在使用类似 MySQL 和 PostgreSQL 的数据库服务时,必须在运行 upgrade
之前在数据库服务器上创建数据库。
数据库升级和降级流程
假设你的开发计算机上存有应用的源代码,并且还将其部署到生产服务器上,运行应用并上线提供服务。而应用在下一个版本必须对模型进行更改,例如需要添加一个新表。 如果没有迁移机制,这将需要做许多工作。
通过数据库迁移机制的支持,在你修改应用中的模型之后,将生成一个新的迁移脚本(flask db migrate
),测试无误后,将迁移脚本添加到源代码管理并提交到服务器。在服务器运行 flask db upgrade
就很容易完成数据库的更新建构。
正如我前面提到的,flask db downgrade
命令可以回滚上次的迁移。 虽然在生产系统上不太可能需要此选项,但在开发过程中可能会发现它非常有用。你可能已经生成了一个迁移脚本并将其应用,只是发现所做的更改并不完全是你所需要的。在这种情况下,可以降级数据库,删除迁移脚本,然后生成一个新的来替换它。
数据库关系
关系数据库擅长存储数据项之间的关系。思考下用户发表动态的情况,用户的资料在 user
表有一条记录,这条用户动态在 post
表中有一条记录。我们要考虑的是标记这一条 post
记录属于哪一位 user
。
现在扩展数据库来存储用户动态,以查看两个表中各条记录的关系。这是一个新表 post
的设计
post
表将具有必须的 id
、用户动态的 body
和时间戳 timestamp
字段。 除了这些预期的字段之外,我还添加了一个 user_id
字段,它指向user
的 id
,也就是记录了这条 post
属于哪一位 user
。这个 user_id
字段被称为外键。
修改后的 app/models.py
如下:
# 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
函数。在服务应用中使用 UTC 日期和时间是推荐做法。 这可以确保你使用统一的时间戳,无论用户位于何处,这些时间戳会在显示时转换为用户的当地时间。
Post
类的 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 ...\migrations\versions\290b8a59b3d3_posts_table.py ... done
-m
为可选参数,只是为迁移添加了一个简短的注释。
最后更新数据库:
(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 8f87e577f3a1 -> 290b8a59b3d3, posts table
如果你对项目使用了版本控制,记得将新的迁移脚本添加进去并提交。
数据库关系模型演示
定义好数据库关系模型,并创建数据库后,我们来看看怎么使用它。由于应用还没有任何数据库逻辑,所以让我们在 Python Shell 中来测试。
首先我们要在 microblog.py
中实现一个函数,它通过添加数据库实例和模型来创建了一个 shell
上下文环境:
# microblog.py
from app import create_app, db
from app.models import User, Post
app = create_app()
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
app.shell_context_processor
装饰器将该函数注册为一个 shell
上下文函数。 当 flask shell
命令运行时,它会调用这个函数并在 shell
会话中注册它返回的项目。
在添加 shell
上下文处理器函数后就可以开始我们的演示:
(venv) $ flask shell
>>> db
<SQLAlchemy engine=sqlite:///...\flask demo\app.db>
>>> User
<class 'app.models.User'>
>>> Post
<class 'app.models.Post'>
现在创建一个新用户:
>>> u = User(username='john', email='john@example.com')
>>> db.session.add(u)
>>> db.session.commit()
对数据库的更改是在会话的上下文中完成的,你可以通过 db.session
进行访问验证。 允许在会话中累积多个更改,一旦所有更改都被注册,你可以发出一个指令 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
类中使用 db.relationship
为用户添加了 posts
属性,在 Post
类中为用户动态添加了 author
属性。所以现在可以直接使用 author
虚拟字段来调用其作者,而不必通过用户 ID 来处理。SQLAlchemy 在这方面非常出色,因为它提供了对关系和外键的高级抽象。
现在 Post
表的记录如下:
id | body | timestamp | user_id |
---|---|---|---|
1 | my first post! | 2021-06-30 02:46:09.341436 | 1 |
user_id
字段为 1,表示这条动态是属于 id
为 1 的用户的。
下面演示关系数据库多表联查。
通过用户查询动态:
>>> u = User.query.get(1)
>>> u
<User john>
>>> posts = u.posts.all()
>>> posts
[<Post my first post!>]
>>> u = User.query.get(2)
>>> u
<User susan>
>>> u.posts.all()
[]
通过动态查询作者:
>>> p = Post.query.get(1)
>>> p
<Post my first post!>
>>> p.author
<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()
本文源码:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-04
网友评论