Web开发-Flask从零开始的学习(四)
5 数据库
数据库按照一定规则保存应用的数据,应用再发起查询,取回所需的数据。
这一部分内容理论会略,数据库课程学得差不多了
SQL数据库
关系型数据库把数据存储在表中,表为应用中不同的实体建模
主键 外键
关系:一对多 一对一 多对多
NoSQL数据库
所有不符合上节所述的关系模型的数据库统称为 NoSQL 数据库
Python数据库框架
Flask 并不限制你使用何种类型的数据库包,因此你可以根据自己的喜好选择使用。
还有一些数据库抽象层代码包供选择,例如 SQLAlchemy 和
MongoEngine。你可以使用这些抽象包直接处理高等级的 Python 对象,而不用处理如表、文档或查询语言之类的数据库实体。
ORM/ODM:数据库抽象层
使用Flask-SQLAlchemy管理数据库
-
Flask-SQLAlchemy 是一个 Flask 扩展,简化了在 Flask 应用中使用 SQLAlchemy 的操作。SQLAlchemy 是一个强大的关系型数据库框架,支持多种数据库后台。
-
在 Flask-SQLAlchemy 中,数据库使用 URL 指定。
-
应用使用的数据库 URL 必须保存到 Flask 配置对象的 SQLALCHEMY_DATABASE_URI 键中。Flask-SQLAlchemy 文档还建议把 SQLALCHEMY_TRACK_MODIFICATIONS 键设为 False,以便在不需要跟踪对象变化时降低内存消耗。
hello.py:配置数据库
from flask_sqlalchemy import SQLAlchemy
import pymysql
pymysql.install_as_MySQLdb() # python支持连接本地mysql
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI']='mysql://root:@localhost/demo'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
以Mysql举例
定义模型
模型这个术语表示应用使用的持久化实体。在 ORM 中,模型一般是一个 Python 类,类中的属性对应于数据库表中的列。
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role') #关系
def __repr__(self):
return '' % 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)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) # 关系
def __repr__(self):
return '' % self.username
-
类变量
__tablename__
定义在数据库中使用的表名。 -
虽然没有强制要求,但这两个模型都定义了 repr() 方法,返回一个具有可读性的字符串表示模型,供调试和测试时使用。
-
添加到 User 模型中的 role_id 列被定义为外键,就是这个外键建立起了关系。
-
db.relationship() 中的 backref 参数向 User 模型中添加一个 role 属性
实际使用起来,类的对象实例比较像表中的一行。
page68 常用的类数据类型&列选项(主键索引等)
数据库关系
-
一对多关系:多端定义外键,一端声明反向引用
users = db.relationship('User', backref='role') # ... role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))
-
一对一关系:调用 db.relationship() 时要把 uselist 设为 False,把“多”变成“一”
-
多对多关系:通常使用关联表来记录关系
page 69 常用的关系选项
数据库操作
书中给出的实例操作基于python shell
# 激活环境 设定FLASK_APP
flask shell
from hello import db
创建表
会根据python文件中建立表
db.create_all()
db.drop_all()
插入行
from hello import Role,User
admin_role = Role(name='Admin')
user_john = User(username='john', role=admin_role)
role 属性虽然用于User数据定义,但它不是真正的数据库列,是一对多关系的高级表示。
新建对象时没有明确设定 id 属性,因为在多数数据库中主键由数据库自身管理。
此时对象仍只存在于Python中,对数据库的改动通过数据库会话管理
db.session.add(admin_role)
db.session.add(user_john)
# 或者写成
> db.session.add_all([admin_role,user_john])
调用 commit() 方法提交会话
db.session.commit()
数据库会话能保证数据库的一致性。提交操作使用原子方式把会话中的对象全部写入数据库。
数据库会话也可回滚。调用 db.session.rollback() 后,添加到数据库会话中的所有对象都将还原到它们在数据库中的状态。
修改行
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role) # 重复添加的覆盖
>>> db.session.commit()
删除行
>>> db.session.delete(mod_role)
>>> db.session.commit()
查询行
Flask-SQLAlchemy 为每个模型类都提供了 query 对象。最基本的模型查询是使用 all() 方法取回对应表中的所有记录
Role.query.all()
使用过滤器可以进行精确查找
User.query.filter_by(role=admin_role).all()
将查询结果转为原生SQL查询结果,只需要转为字符串即可
str(User.query.filter_by(role=admin_role).all())
常用的SQLAlchemy查询过滤器
filter() 把过滤器添加到原查询上,返回一个新查询
filter_by() 把等值过滤器添加到原查询上,返回一个新查询
limit() 使用指定的值限制原查询返回的结果数量,返回一个新查询
offset() 偏移原查询返回的结果,返回一个新查询
order_by() 根据指定条件对原查询结果进行排序,返回一个新查询
group_by() 根据指定条件对原查询结果进行分组,返回一个新查询
最常用的SQLAlchemy查询执行方法
all() 以列表形式返回查询的所有结果
first() 返回查询的第一个结果,如果没有结果,则返回 None
first_or_404() 返回查询的第一个结果,如果没有结果,则终止请求,返回 404 错误响应
get() 返回指定主键对应的行,如果没有对应的行,则返回 None
get_or_404() 返回指定主键对应的行,如果没找到指定的主键,则终止请求,返回 404 错误响应
count() 返回查询结果的数量
paginate() 返回一个 Paginate 对象,包含指定范围内的结果
关系的查询类似
>>> users = user_role.users
>>> users
[, ]
>>> users[0].role
- 如上默认隐式的查询会调用 all() 方法,无法使用query进行更精确
- 修改了关系的设置,加入了 lazy='dynamic' 参数,从而禁止自动执行查询
>>> user_role.users.order_by(User.username).all()
[, ]
>>> user_role.users.count()
2
在视图函数中操作数据库
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)
db.session.commit()
session['known'] = False
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html', current_time=datetime.utcnow(), form=form, name=session.get('name'),
known=session.get('known',False))
{% block page_content %}
Hello, World!
The local date and time is {{ moment(current_time).format('LLL') }}.
That was {{ moment(current_time).fromNow(refresh=True) }}
Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!
{% if not known %}
Pleased to meet you!
{% else %}
Happy to see you again!
{% endif %}
{{ wtf.quick_form(form) }}
{% endblock %}
集成Python shell
避免上述情况中使用shell会话每次都要导入数据库实例和模型,让flask shell命令自动导入这些对象
若想把对象添加到导入列表中,必须使用 app.shell_context_processor 装饰器创建并注册一个 shell 上下文处理器:
# hello.py:添加一个 shell 上下文
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
使用Flask-Migrate实现数据库迁移
应对修改数据库模型的需求,方便地进行数据库迁移。
源码版本控制工具可以跟踪源码文件的变化,数据库迁移框架能跟踪数据库模式的变化,然后以增量的方式把变化应用到数据库中。
创建迁移仓库
from flask_migrate import Migrate
# ...
migrate = Migrate(app, db)
在新项目中可以使用 init 子命令添加数据库迁移支持
$ flask db init
这个命令会创建 migrations 目录,所有迁移脚本都存放在这里。如果你是通过 git checkout检出示例项目的,那么无须做这一步,因为 GitHub 仓库中已有迁移仓库。
创建迁移脚本
在 Alembic 中,数据库迁移用迁移脚本表示。脚本中有两个函数,分别是 upgrade() 和downgrade()。
upgrade() 函数把迁移中的改动应用到数据库中,downgrade() 函数则将改动删除。
可以使用 revision 命令手动创建 Alembic 迁移,也可使用 migrate 命令自动创建。
-
手动创建的迁移只是一个骨架,upgrade() 和 downgrade() 函数都是空的,开发者要使用Alembic 提供的 Operations 对象指令实现具体操作。
-
自动创建的迁移会根据模型定义和数据库当前状态之间的差异尝试生成 upgrade() 和 downgrade() 函数的内容。
自动创建的迁移不一定总是正确的,有可能会漏掉一些细节。自动生成迁移脚本后一定要进行检查,把不准确的部分手动改过来。
使用 Flask-Migrate 管理数据库模式变化的步骤如下。
(1) 对模型类做必要的修改。
(2) 执行 flask db migrate 命令,自动创建一个迁移脚本。
(3) 检查自动生成的脚本,根据对模型的实际改动进行调整。
(4) 把迁移脚本纳入版本控制。
(5) 执行 flask db upgrade 命令,把迁移应用到数据库中。
这一章后续暂时跳过。
6 电子邮件 Flask-Mail
虽然 Python 标准库中的 smtplib 包可用于在 Flask 应用中发送电子邮件,但包装了 smtplib的 Flask-Mail 扩展能更好地与 Flask 集成。
Flask-Mail 连接到简单邮件传输协议(SMTP,simple mail transfer protocol)服务器,把邮件交给这个服务器发送。如果不进行配置,则 Flask-Mail 连接 localhost 上的 25 端口,无须验证身份即可发送电子邮件。
配置Flask-Mail
配置 默认值 说明
MAIL_SERVER localhost 电子邮件服务器的主机名或 IP 地址
MAIL_PORT 25 电子邮件服务器的端口
MAIL_USE_TLS False 启用传输层安全(TLS,transport layer security)协议
MAIL_USE_SSL False 启用安全套接层(SSL,secure sockets layer)协议
MAIL_USERNAME None 邮件账户的用户名
MAIL_PASSWORD None 邮件账户的密码
在开发过程中,连接到外部 SMTP 服务器可能更方便。
Flask-mail 配置163邮箱smtp服务器
# hello.py:配置 Flask-Mail
import os
app.config['MAIL_SERVER'] = 'smtp.163.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_TLS'] = False
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
-
不要把账户和凭据写进代码中,应该从环境变量中导入敏感信息
-
个人邮箱作为smtp服务器,需要去设置中开启相关服务,获取对应的权限码
(venv) $ set MAIL_USERNAME=
(venv) $ set MAIL_PASSWORD=
- 设置环境变量
from flask_mail import Mail
mail = Mail(app)
- 初始化Flask-Mail
Python shell中发送电子邮件
(venv) $ flask shell
>>> from flask_mail import Message
>>> from hello import mail
>>> msg = Message('test email', sender='you@example.com',
... recipients=['you@example.com'])
>>> msg.body = 'This is the plain text body'
>>> msg.html = 'This is the HTML body'
>>> with app.app_context():
... mail.send(msg)
...
- Flask-Mail 的 send() 函数使用 current_app,因此要在激活的应用上下文中执行
应用中集成电子邮件服务
把应用发送电子邮件的通用部分抽象出来,定义成一个函数。该函数可以使用 Jinja2 模板渲染邮件正文,灵活性极高。
from flask_mail import Message
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin '
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail.send(msg)
-
send_email() 函数的参数分别为收件人地址、主题、渲染邮件正文的模板和关键字参数列表。
-
指定模板时不能包含扩展名,这样才能使用两个模板分别渲染纯文本正文和 HTML 正文。
-
调用者传入的关键字参数将传给 render_template() 函数,作为模板变量提供给模板使用,用于生成电子邮件正文。
app.config['FLASKY_ADMIN'] =os.environ.get('FLASKY_ADMIN')
@app.route('/', methods=['GET', 'POST'])
def index():
# ...
if app.config['FLASKY_ADMIN']:
send_email(app.config['FLASKY_ADMIN'], 'New User',
'mail/new_user', user=user)
-
两个模板文件需要自己编写
-
记得设置环境变量FLASKY_ADMIN
异步发送电子邮件
为了在处理请求过程中避免不必要的延迟,我们可以把发送电子邮件的函数移到后台线程中。
from threading import Thread
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
很多 Flask 扩展都假设已经存在激活的应用上下文和(或)请求上下文。前面说过,Flask-Mail 的 send() 函数使用 current_app,因此必须激活应用上下文。不过,上下文是与线程配套的,在不同的线程中执行 mail.send() 函数时,要使用 app.app_context() 人工创建应用上下文。app 实例作为参数传入线程,因此可以通过它来创建上下文。
应用要发送大量电子邮件时,使用专门发送电子邮件的作业要比给每封邮件都新建一个线程更合适。例如,我们可以把执行 send_async_email() 函数的操作发给 Celery 任务队列。
暂时不是很懂