最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。
这是Flask Mega-Tutorial系列的第四章,其中我将告诉您如何使用数据库。
供您参考,以下是本系列文章的列表。
本章的主题非常重要。对于大多数应用程序,需要维护能够有效检索的持久数据,这正是数据库的用途。
本章的GitHub链接是:Browse,Zip,Diff。
我相信你已经听说过,Flask本身并不支持数据库。但是Flask数据库扩展有很多可以选择,这样您可以自由选择最适合您应用程序的数据库,而不必被迫再适应您的应用程序。
Python中的数据库有很多选择,其中许多都带有Flask扩展,可以更好地与应用程序集成。数据库可以分为两大类,一组遵循关系模型,以及另一组不遵循关系模型。后一组通常称为NoSQL,表明它们没有实现流行的关系查询语言SQL。虽然两个组都有很棒的数据库产品,但我认为关系数据库更适合具有结构化数据的应用程序,例如用户列表,博客帖子等,而NoSQL数据库往往更适合具有结构不太明确。与大多数其他应用程序一样,此应用程序可以使用任一类型的数据库实现,但出于上述原因,我将使用关系数据库。
在第3章中,我向您展示了第一个Flask扩展。在本章中,我将再使用两个。第一个是Flask-SQLAlchemy,它为流行的SQLAlchemy包提供了一个Flask友好的封装,它是一个对象关系映射器或ORM。ORMs允许应用程序使用高级实体(如类,对象和方法)而不是表和SQL来管理数据库。执行在对象的操作会被ORMs翻译成数据库命令,这意味着我们不需要学习SQL语言。
关于SQLAlchemy的好处是它不仅仅是一个ORM,而且可以操作许多种关系数据库。SQLAlchemy支持一长串数据库引擎,包括流行的MySQL,PostgreSQL和SQLite。这非常强大,因为您可以使用简单SQLite数据库进行开发,然后在生产服务器上部署应用程序时,再选择更强大的MySQL或PostgreSQL服务器,而无需改变你的应用。
要在虚拟环境中安装Flask-SQLAlchemy,请确保先激活虚拟环境,然后运行:
(venv) $ pip install flask-sqlalchemy
我见过的大多数数据库教程都涵盖了数据库的创建和使用,但没有充分解决在应用程序需要更改或增长时,对现有数据库进行更新的问题。这是个难题,因为关系数据库以结构化数据为中心,当结构发生更改时,数据库中已有的数据需要迁移到修改后的结构中。
我介绍的第二个扩展是Flask-Migrate。此扩展是Alembic的Flask封装,它是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的自动写进。这个功能将会追踪对象的修改并发送信号,需要额外的内存,我并不需要。
最后,当我们初始化应用程序的时候,我们也必须初始化SQLAlchemy和Migrate:
# 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
表示数据库的实例对象。然后我添加了另一个migrate代表迁移引擎的实例对象。希望您可以从这两个扩展初始化模式,看到如何使用其它Flask扩展。最后,我导入一个名为models
的新模块。该模块将定义数据库的结构。接下来我们将编写这个模块。
我们存储在数据库中数据将会以类的集合来表示,通常称为数据库模型。ORMs需要做的翻译就是将从这些类创建的对象映射到适合的数据库表的行。
让我们创建一个表示用户的模型。使用WWW SQL Designer工具,我制作了下图来表示我们要在users表中使用的数据表:
id
字段通常在所有模型中,并用用于作主键。将为数据库中的每个用户分配一个唯一的id值,该id值存储在此字段中。在大多数情况下,主键由数据库自动分配,因此我们只需要将id这个
字段标记为主键。
username
,email和
password_hash
字段被定义为string(或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 ''.format(self.username)
新建的User类
继承Flask-SQLAlchemy基类db.Model
。User类
定义几个字段为类变量。字段被创建为db.Column
类的实例,db.Column
类将字段类型作为参数,以及还有其他可选的参数,例如,指定哪些字段是唯一的和索引的,这对于数据库搜索是有效的非常重要。
__repr__
方法告诉Python如何打印这个类的对象,我们将用它来调试。
您可以在Python解释器中看到__repr__()
的实际应用:
>>> from app.models import User
>>> u = User(username='susan', email='[email protected]')
>>> u
上一节中创建的User模型类定义了此应用程序的初始数据库结构。但随着应用程序的不断增长,需要更改结构,很可能会添加新内容,也可能修改或删除内容。Alembic(Flask-Migrate使用的迁移框架)将以一种很容易的操作方式来升级或者降级数据库,让我们更便利地更改数据库。
为了完成这项看似困难的任务,Alembic维护了一个迁移存储库,存储库是一个存储其迁移脚本的目录。每次对数据库模式进行更改时,都会将迁移脚本添加到存储库,其中包含数据库更改的详细信息。迁移数据库时,这些迁移脚本将按创建顺序执行。
Flask-Migrate通过flask
命令行查看。您已经看过flask run
,这是Flask原生的子命令。flask db
由Flask-Migrate加入到flask命令行,来管理有关数据库迁移的操作。使用flask db可以查看。因此,让我们运行flask db init
为microblog创建迁移存储库:
(venv) $ flask db
Usage: flask db [OPTIONS] COMMAND [ARGS]...
Perform database migrations.
Options:
--help Show this message and exit.
Commands:
branches Show current branch points
current Display the current revision for each...
downgrade Revert to a previous version
edit Edit a revision file
heads Show current available heads in the script...
history List changeset scripts in chronological...
init Creates a new migration repository.
merge Merge two revisions together, creating a new...
migrate Autogenerate a new revision file (Alias for...
revision Create a new revision file.
show Show the revision denoted by the given...
stamp 'stamp' the revision table with the given...
upgrade Upgrade to a later version
(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将比较数据库(本例中从app.db中获取)与我们模型的结构(app/models.py中获取)。两者之间的不同将会记录成一个迁移脚本存放在迁移仓库中,迁移脚本知道如何去迁移或撤销它,用来升级或者降级一个数据库,并保证数据库和本地模型的一致性。目前为止,由于之前没有数据库,自动迁移会将整个User
模型添加到迁移脚本中。使用flask db migrate
生成这些自动迁移:
(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
选项是可选的,它为迁移添加了一个简短的描述性文本注释。
生成的迁移脚本现在是项目的一部分,需要合并到源代码管理中。如果您想要了解它的内容,您可以查看versions的脚本。你会发现它有两个叫做upgrade()
和downgrade()
的函数。upgrade()
函数用于迁移升级,downgrade()
函数用于迁移降级(删除升级的更改)。这允许Alembic使用降级命令将数据库迁移到历史记录中的任何位置,甚至迁移到旧版本。
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 -> e517276bb1c2, users table
因为我们使用SQLite,所以
upgrade
将检测到数据库不存在并将创建它(您将注意到在此命令完成后创建了app.db文件,即SQLite数据库)。使用MySQL和PostgreSQL等数据库服务器时,必须先在数据库服务器中创建数据库,再在运行upgrade
。
注意:默认情况下,Flask-SQLAlchemy对数据库表使用“snake case”命名约定。对于上面的
User
模型,将命名数据库中的相应表名user
。对于AddressAndPhone
模型类,将命名表名address_and_phone
。如果您更喜欢自己选择表名,可以添加一个名为__tablename__
模型类的属性,并将其设置为你喜欢的表名(类型string)。
虽然应用程序还处于起步阶段,但讨论数据库迁移策略也没什么坏处。想象一下,您的应用程序在您的开发机器上,并且还有一个副本部署到线上或者生产服务器上。
假设您应用程序的下一个版本需要对模型进行更改,例如需要添加新表。如果没有迁移,您需要弄清楚如何同时在开发机器和服务器上更改数据库模型,这可能需要做很多工作。
但是,通过数据库迁移支持,在您修改应用程序中的模型后,您将生成一个新的迁移脚本(flask db migrate
),您可能会检查以确保它自动生成做正确的命令,然后将更改应用于您的开发数据库(flask db upgrade
)。将迁移脚本添加到源代码管理并提交它。
当您准备将新版本的应用程序发布到生产服务器时,您仅仅需要获取应用程序的更新版本(包括新的迁移脚本)并运行flask db upgrade
。Alembic将检测到生产数据库未更新到最新版本的模型,并运行在当前版本之后创建的所有新迁移脚本。
正如我之前提到的,您还有一个flask db downgrade
命令,可以撤消上次迁移。虽然您不会在生产环境上使用此选项,但您会发现它在开发过程中非常有用。当您已生成迁移脚本并将其应用,但发现所做的更改并非您所需的更改。在这种情况下,您可以降级数据库,删除迁移脚本,然后重新生成一个新的替换它。
关系数据库可以很好的处理存储数据项之间的关系。考虑用户撰写blog时,用户将在users
表中有一条记录,帖子将在posts
表格中有一条记录。记录谁撰写blog最有效的方法是关联这两个相关的数据项。
一旦建立了用户和帖子posts之间的关联,数据库就可以进行此关联的查询。最简单的一个例子是你通过blog知道它的作者。更复杂的查询,就是一个用户编写的所有blog。Flask-SQLAlchemy将帮助处理这两种类型的查询。
让我们扩展数据库以便存储blog,并看看他们之间如何关联。以下是新posts
表的结构:
posts
表有不可少的id字段
,以及blog在body
字段和一个timestamp
。但除了这些字段之外,我还添加了一个user_id
字段,将帖子关联到作者。您已经知道所有用户都有一个唯一的id
主键。将blog关联到作者的方法是添加对用户的引用id
,这正是user_id
字段的内容。user_id
字段称为外键。上面的数据库图显示,将外键id
字段与其引用表的字段之间的关联。这种关系被称为一对多,因为“一个”用户写了“很多”blog。
修改后的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 ''.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 ''.format(self.body)
Post
类将代表用户撰写的blog。timestamp
字段将被编入索引,当您想按时间顺序检索帖子,这将非常有用。我还添加了一个default
参数,并传递了datetime.utcnow
函数。当您将函数作为默认函数传递时,SQLAlchemy会将该字段设置为调用该函数的值(请注意,我没有在utcnow后面
包含() ,我要传递函数本身,而不是调用它的结果)。通常,您需要在服务器应用程序中设置UTC时区。这可确保无论用户位于何处,您使用统一的时间戳。这些时间戳将在显示时转换为用户的本地时间。
user_id
字段已初始化为外键user.id
,这意味着它引用users表中的id
值。user.id中,user
是User模型的数据库的表名。这是一个不幸的不一致,在某些情况下,例如在db.relationship()
调用中,模型由模型类(Post)引用,模型类通常以大写字符开头,而在其他情况下,例如此db.ForeignKey()
声明,模型由其数据库的表名(user)给出,SQLAlchemy自动使用小写字符,对于多单词模型名称,使用“snake case”命名约定。
User类
有一个新的posts
字段,并被初始化db.relationship
。这并不是一个实际的数据库字段,创建一个虚拟的列,该列会与 Post.user_id
(db.ForeignKey) 建立联系。这一切都交由 SQLAlchemy 自身管理,因此它不在上面的数据库图中。
对于一对多关系,db.relationship
字段通常在“一”侧定义,并且用作访问“许多”的便捷方式。加入我有一个用户名叫u
,u.posts
将运行一个数据库查询,返回该用户写的所有帖子。
db.relationship:
第一个参数是表示关联“多”侧的模型类。如果模型在models中定义,则此参数可以提供一个带有类名的字符串。
第二个参数
backref
用于指定表之间的双向关系,如果在一对多的关系中建立双向的关系,这样的话在对方看来这就是一个多对一的关系。post.author
,将返回给定blog的作者。第三个参数
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提示后,让我们导入数据库实例和模型:
>>> from app import db
>>> from app.models import User, Post
首先创建一个新用户:
>>> u = User(username='john', email='[email protected]')
>>> db.session.add(u)
>>> db.session.commit()
对数据库的更改必须session的上下文中完成,也可以通过db.session完成
。您可以在session中累积多个更改,一旦想注册了所有更改,您就可以使用db.session.commit()
,它以自动写入所有更改。如果在session期间的任何时间出现错误,调用db.session.rollback()
将中止session并删除存储在其中的任何更改。
记住重要的一点,更改只会在
db.session.commit()
调用时写入数据库。session保证数据库永远不会处于不一致状态。
让我们添加另一个用户:
>>> u = User(username='susan', email='[email protected]')
>>> db.session.add(u)
>>> db.session.commit()
数据库可以返回所有用户的查询:
>>> users = User.query.all()
>>> users
[, ]
>>> 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
现在让我们添加一篇blog:
>>> u = User.query.get(1)
>>> p = Post(body='my first post!', author=u)
>>> db.session.add(p)
>>> db.session.commit()
我不需要为
timestamp
字段设置值,因为该字段具有默认值,您可以在模型定义中看到该值。那user_id字段
呢?回想一下,我在User
类中创建的posts
属性为用户添加了db.relationship
属性,并author
为blog添加了属性。我使用author
虚拟字段将作者分配到blog,而不必处理用户ID。SQLAlchemy在这方面非常出色,因为它提供了对关联和外键的高级抽象。
要完成此session,我们来看几个数据库查询:
>>> # get all posts written by a user
>>> u = User.query.get(1)
>>> u
>>> posts = u.posts.all()
>>> posts
[]
>>> # same, but with a user that has no posts
>>> u = User.query.get(2)
>>> u
>>> 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()
[, ]
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解释器之后,你在上一节首先做了些什么吗?你做的第一件事是import:
>>> 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 "", line 1, in
NameError: name 'app' is not defined
>>>
(venv) $ flask shell
>>> app
使用常规python解释器时,
app
除非定义此符号,否则该符号是不会定义。但在使用时flask shell
,该命令会预先导入应用程序实例。flask shell的
好处不是它预先导入app
,而是你可以配置一个“shell上下文”,它是一个可以预先导入的其他符号的列表。
在microblog.py中用以下函数创建一个shell上下文,将数据库实例和模型添加到shell会话中:
# microblog.py
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会话中注册它返回的项。函数返回字典而不是列表的原因是,对于每个项目,在shell中引用时,您还必须提供一个名称,该名称由字典keys给出。
添加shell上下文处理器功能后,您可以使用数据库实例而无需导入它们:
(venv) $ flask shell
>>> db
>>> User
>>> Post
如果你尝试上面的操作,当你访问
db
,User
以及Post,出现异常并
得到NameError错误信息时
,那么make_shell_context()
函数可能没有被flask注册。最可能的原因是您尚未设置环境变量FLASK_APP=microblog.py
。在这种情况下,请返回第1章并查看如何设置FLASK_APP
环境变量。如果您在打开新的终端窗口时经常忘记设置此变量,则可以考虑将.flaskenv文件添加到项目中,如第一章末尾所述。
原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-iv-database