杨永刚老师所著的《Flask2+Vue.js实战派》结构清晰、内容由浅入深,是市面上为数不多的、全面介绍Flask框架的好教程。不过,由于纸版书籍篇幅的限制,加之每一个学习者的知识储备不同,这本书对一些技术细节的介绍不可能做到全全俱到。
为此,千哥将陆续将本书的一些学习心得通过笔记的形式呈现出来,既为开发Flask程序深入掌握这些知识,也为共同学习的读者留下更多的参考资料。
本书的源代码下载地址:https://pan.baidu.com/s/1WSvlR4qT0fcxVzksto6mtg
提取码: aabb
本书实例的开发环境为:Windows10 / Linux 、Python 3.9.13、Flask2.32。在安装完Python之后,可以通过书中的requirements.txt文件,一次性用pip install -r requirements.txt命令,将相关模块安装完。
《Flask2+Vue.js实战派》第8章是全书中重要的、承上启下的章节,通过“使用Flask+Bootstrap框架开发图书管理系统后台”,将前面7章的知识进行融汇贯通,并为后续的学习打好坚实的基础。第8章程序运行后的界面如下:

本章所涉及的知识内容如下图所示:

既然是读书笔记,在这里我只记录我认为重要的,或者书中没有的内容,而且内容结构不一定按照《Flask2+Vue.js实战派》的编排方式,感兴趣的朋友也可以根据自己的需要对本笔记进行收藏和补充。
一、代码组织管理
本章的“图书管理系统后台”,不再采用简单的“app.py(文件) + templates(目录)”形式,即:由app.py文件编写配置信息、路由和视图、各类模型、模板配置等,templates目录存放模板文件、静态文件等。这种代码组织的方式在第8章以前普遍采用,本地调试的时候可以用两种方式来执行程序:
python app.y
或者:
flask run
对于第二种方式,fask会自动寻找名为app.py的文件并且运行,还可以加入--debug选项,开启debug模式:
flask run --debug
如下图:

还可以用flask routes 命令来查看视图:

而第8章则采取了包的方式来组织管理代码,将配置信息、路由和视图、各类模型放在不同的python文件中,再分别导入和使用。
在mybook文件夹下,有三个重要的文件和文件夹:config.py、manage.py和apps:
其中config.py是基本的Flask配置文件,内容大致如下,部分代码省略:
import os
base_dir = os.path.abspath(os.path.dirname(__file__))
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(base_dir, 'data1.sqlite')
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_COMMIT_TEARDOWN = True
#app.secret_key="1432rsaasfdasf"
SECRET_KEY = "12345678!@#$%^&*"
class BaseConfig(object):
PORT = 5000
APP_NAME = "flask-app"
JSON_AS_ASCII = False # 禁止中文转义
# 因为flask的session是通过加密之后放到了cookie中。所以有加密就有密钥用于解密,所以,只要用到了flask的session模块就一定要配置“SECRET_KEY”这个全局宏。一般设置为24位的字符
SECRET_KEY = os.urandom(24)
# 权限白名单列表
WHITE_LIST = ["/sys/user/login", "/callback/gitee"]
# 环境变量中获取DB_NAME
DB_NAME = os.getenv("DB_NAME")
if DB_NAME is None:
DB_NAME = "flaskrbac"
# 环境变量中获取数据库DB_HOST
DB_HOST = os.getenv("DB_HOST", "172.16.31.160")
# 环境变量中获取数据库DB_PORT
DB_PORT = os.getenv("DB_PORT", "3306")
# 环境变量中获取数据库DB_USER
DB_USER = os.getenv("DB_USER", "root")
# 环境变量中获取数据库DB_PASSWORD
DB_PASSWORD = os.getenv("DB_PASSWORD", "8Eli#gr#AUk")
# 环境变量中获取数据库REDIS_HOST
REDIS_HOST = os.getenv("REDIS_HOST", "172.16.31.160")
# 环境变量中获取数据库REDIS_PORT
REDIS_PORT = os.getenv("REDIS_PORT", "6379")
# 环境变量中获取数据库REDIS_PASSWORD
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "gEwjEHLM")
# mysql+pymysql://root:xxxxx@172.16.31.160/flaskrbac?charset=utf8mb4
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{parse.quote_plus(DB_PASSWORD)}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4'
PERMANENT_SESSION_LIFETIME = datetime.timedelta(days=90)
SQLALCHEMY_TRACK_MODIFICATIONS = True
SQLALCHEMY_RECORD_QUERIES = True
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 5,
'pool_timeout': 90,
'pool_recycle': 7200,
'max_overflow': 1024
}
# REDIS_URL = "redis://:password@localhost:6379/0"
REDIS_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/0'
# 回调配置
# 回调密码
GITEE_CALLBACK_PASSWORD = os.getenv("GITEE_CALLBACK_PASSWORD", "666")
# 回调执行的脚本
GITEE_CALLBACK_SHELL = os.getenv("GITEE_CALLBACK_SHELL",
"/home/admin/app/restart.sh")
#以下代码省略
#………
config = {
"dev": Development,
"development": Development,
"test": Test,
"prod": Production,
"production": Production
}
代码中,包括数据库配置方式,用于加密会话数据、保护 CSRF 令牌的SECRET_KEY的设置等。
manage.py是启动文件,采取:python manage.py 运行程序。当然,也可以按照下面的方法,采取:flask run 运行:
export FLASK_APP=manage.py # Linux/macOS
set FLASK_APP=manage.py # Windows
flask run
manage.py里的代码很简单,如下:
from apps import app
#db.drop_all()
#db.create_all()
if __name__ == "__main__":
app.run(debug=True, port=9000)
这里需要注意的是:
from apps import app
这条语句是从apps这个目录中导入一个名为app的Flask类的对象实例,由这个实例的app.run方法运行程序。
在apps目录下,有一个__init__.py文件,__init__.py中的代码如下:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
import config
base_dir=os.path.abspath(os.path.dirname(__file__))
print(base_dir)
app=Flask(__name__)
app.config.from_object(config)
db=SQLAlchemy(app)
from apps import views
在manage.py中,名为app的Flask类的对象实例,由__init__.py中的语句:app=Flask(__name__) 生成。再由__init__.py中的语句:app.config.from_object(config)导入config.py文件的配置信息。
这里需注意的是:
1、app.config.from_object(config)这条语句:from_object(config)中的config指的是config.py模块,加载模块级变量,因此config.py中的BaseConfig类和其他类,以及包含有"dev"、 "development"、"test"、"prod"、"production"四种模式的字典变量config都没有使用。也就是config.py文件中,有效代码实际上只有这部分:
import os
base_dir = os.path.abspath(os.path.dirname(__file__))
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(base_dir, 'data1.sqlite')
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_COMMIT_TEARDOWN = True
#app.secret_key="1432rsaasfdasf"
SECRET_KEY = "12345678!@#$%^&*"
而如果要使用config.py中的BaseConfig类和其他类,以及字典变量config,需在__init__.py中使用如下语句:
# 根据环境变量选择配置类,例如 FLASK_ENV=dev
env = os.environ.get("FLASK_ENV", "dev")
app.config.from_object(config[env]) # 加载 Development 类
或者:
# 加载 Development 类
app.config.from_object(config["dev"])
并且还要防止模块级变量覆盖BaseConfig类和其他类的同名变量。例如config.py文件中的模块级变量SQLALCHEMY_DATABASE_URI就和BaseConfig类中的变量SQLALCHEMY_DATABASE_URI同名。
因此,在app.config.from_object这条语句的时候,必须选择其中的一种模式:app.config.from_object(config)或app.config.from_object(config["dev"])。前者是使用所有的模块级变量,后者是使用config字典中的"dev"模式(也可以选择其他模式)。
2、用于加密会话数据、保护 CSRF 令牌的SECRET_KEY,可以在执行app.config.from_object语后,用print在后台打印出来检查是否有效:
app.config.from_object(config)
print("SECRET_KEY:", app.config["SECRET_KEY"])
3、__init__.py文件的作用,主要便于用包的方式来组织管理代码。如果一个目录包含__init__.py,python会将这个目录视为一个包,允许通过包的方式导入其内部的模块、类、函数或者对象。如果没有__init__.py这个文件,这个目录下的文件会当成普通文件,python无法直接导入相关的模块。
在这个例子中,正因为apps目录下,包含了__init__.py文件,所以python就能导入apps目录下的相关模块。apps的目录结构如下:
.
└── apps
├── __init__.py
├── forms.py
├── models.py
└── views.py
└── static
└── templates
其中 views.py编写的是路由和视图函数,models.py编写的是数据模型,forms.py编写的是Flask的表单类,templates和static分别是模板文件夹和静态文件夹。
__init__.py文件的最后一条语句:
from apps import views
就导入了views模块,从而可以使用 views.py中的路由和视图函数。views.py的部分代码如下:
from apps import app, db
from flask import request, render_template, redirect, url_for, flash, jsonify
from sqlalchemy.exc import SQLAlchemyError
from apps.models import Publish, Author, Book
from apps.forms import PublishForm, AuthorForm, BookForm
from werkzeug.datastructures import CombinedMultiDict
import os
base_dir = os.path.abspath(os.path.dirname(__file__))
@app.route("/pub_list", methods=["GET", "POST"])
def pub_list():
if request.method == "GET":
pubs = Publish.query.all()
return render_template("publish/index.html", pubs=pubs)
if request.method == "POST":
name = request.form.get("name")
if name:
pubs = Publish.query.filter(Publish.name.ilike(f'%{name}%'))
else:
pubs = Publish.query.all()
return render_template("publish/index.html", pubs=pubs)
#以下代码省略
#………
需注意的是这两行代码:
from apps.models import Publish, Author, Book
from apps.forms import PublishForm, AuthorForm, BookForm
通过这两行代码,就导入了models.py中的Flask的数据库模型类,forms.py中的Flask的表单类。当然,也可以用点号.进行相对导入:
from .models import Publish, Author, Book
from .forms import PublishForm, AuthorForm, BookForm
对于上述代码组织关系,如下图所示:

至于其他的模块的组织和导入方式,基本都是常规的方式,不再赘述。
二、后端需重点关注的代码
对于后端的Flask代码,需重点关注views.py文件中的author_list()、book_add()、book_edit() 、index() 四个视图函数。下面,结合models.py文件(数据模型类文件)、froms.py(表单类文件)进行分析。
(一)视图函数author_list()
代码如下:
# 作者相关
@app.route("/author_list", methods=["GET", "POST"])
def author_list():
if request.method == "GET":
name = request.args.get("name", "")
mobile = request.args.get("mobile", "")
search = dict()
if name:
search["name"] = name
if mobile:
search["mobile"] = mobile
page = request.args.get("page", 1, type=int)
pagination = Author.query.filter_by(**search).paginate(page,
per_page=2,
error_out=False)
authors = pagination.items
return render_template("author/index.html",
authors=authors,
pagination=pagination,
name=name,
mobile=mobile)
author_list() 的作用:
1、如果前端没有通过表单(name和mobile)提交查询数据,那么就通过pagination = Author.query.filter_by语句返回所有数据并进行分页。
2、如果有查询数据,那么就通过pagination = Author.query.filter_by语句,调用search字典返回查询结果并进行分页。
这里需要注意的是:
1、request.args.get()
name = request.args.get("name", "")
mobile = request.args.get("mobile", "")
- 作用:从 URL 的查询字符串中获取名为 name 和mobile的参数值。
例如:访问
/author_list?name=张三&mobile=13999888888 时:
- request.args.get("name", ""),会返回 "张三"
- request.args.get("mobile", ""),会返回 "13999888888"
- 参数:
- 第一个参数:要获取的键名(name)。
- 第二个参数:默认值(如果未找到name 参数,则返回空字符串 "")。
- 返回值:字符串类型(即使参数不存在或未传值,也能避免 KeyError)
- request 对象的使用场景:
① GET 请求参数(URL 参数)
- 通过request.args 字典获取 URL 中的查询参数:
page = request.args.get("page", 1, type=int) # 获取 page 参数,默认值 1,类型转为 int
② POST 请求的表单数据
- 通过request.form 字典获取表单提交的数据:
username = request.form.get("username")
password = request.form.get("password")
③ JSON 数据
- 通过request.json 直接获取请求体中的 JSON 数据:
data = request.json
④ 上传文件
- 通过 request.files 字典获取上传的文件:
file = request.files.get("file")
file.save("path/to/save")
⑤ 请求头信息
- 通过request.headers 获取请求头:
user_agent = request.headers.get("User-Agent")
本例中,name和mobile在通过request.args.get获取时,需在前端的表单中,将form的 method设置为get:
<form id="search_form" method="get">
2、search = dict()
search变量是一个字典变量,用于接收name和mobile参数的数据:
if name:
search["name"] = name
if mobile:
search["mobile"] = mobile
如果name和mobile参数没有数据,search则是一个空字典。
3、pagination = Author.query.filter_by(**search).paginate(page, per_page=2,error_out=False)
其中,**search将字典数据传递到Author.query.filter_by中,如果字典为空,filter_by(**search)等价于无过滤条件Author.query.all(),最终查询的是Author表中的全部数据。
这里还涉及到Flask分页的知识,pagination对象分页后的结果包含以下关键属性:
- items: 当前页的数据列表(如 [Author1, Author2])。
- page: 当前页码。
- per_page: 每页显示的条目数。
- total: 总条目数。
- pages: 总页数。
- has_prev: 是否有上一页。
- has_next: 是否有下一页。
- prev_num: 上一页的页码。
- next_num: 下一页的页码。
对于接下来的代码来说:
authors = pagination.items
return render_template("author/index.html",
authors=authors,
pagination=pagination,
name=name,
mobile=mobile)
pagination.items 是分页查询的核心,直接提供当前页的数据列表。pagination.items 是一个包含当前页数据的 Python 列表,列表中的每个元素对应数据库查询结果的一行记录(例如 Author 模型对象)。
例如:
- 如果每页显示 2 条数据(per_page=2),当用户访问第 1 页时,items 会返回前 2 条数据。
- 如果用户访问第 3 页,items 会返回第 5~6 条数据(假设总共有 6 条数据)
至此,视图函数author_list()分析完毕。
(二)视图函数book_add()
代码如下:
@app.route("/book_add", methods=["GET", "POST"])
def book_add():
if request.method == "GET":
f = BookForm()
return render_template("book/add.html", form=f)
elif request.method == "POST":
f = BookForm(CombinedMultiDict([request.form, request.files]))
if f.validate_on_submit():
filepath = os.path.abspath(os.path.dirname(__file__))
book = Book()
book.name = f.name.data
book.isbn = f.isbn.data
file = f.photo.data
if file:
filename = os.path.join(filepath, 'static/media/',
file.filename)
file.save(filename)
book.photo = file.filename
book.price = f.price.data
book.pub_id = f.publish.data
db.session.add(book)
db.session.flush() #保证可以返回b_id
#返回自增id
b_id = book.b_id
book_obj=Book.query.filter(b_id==b_id).first()
#获取前台多选框内的作者信息
select_authors = request.form.getlist("author")
for i in select_authors:
author = Author.query.get(i)
book_obj.back_author.append(author)
db.session.commit()
return redirect(url_for("book_list"))
else:
return render_template("book/add.html", form=f)
book_add() 的作用:
1、通过BookForm()类获取POST数据,将相关字段的数据保存到数据库中
2、通过request.files,将上传的图片数据保存到/mybook/apps/static/media中。
这里需要注意的是:
1、表单数据的获取
f = BookForm(CombinedMultiDict([request.form, request.files]))
在这条语句中,CombinedMultiDict 是 Werkzeug(Flask 的底层库)提供的一个工具类,用于合并多个 MultiDict 对象,常用于同时处理表单数据和文件上传的场景。
在 HTTP 请求中:
- 普通表单数据(如文本、数字)存储在 request.form(MultiDict 类型)。
- 文件上传数据存储在 request.files(同样是 MultiDict 类型)。
当表单同时包含普通字段和文件字段(例如 enctype="multipart/form-data"),Flask 会分别将数据解析到 request.form 和 request.files 中。若要将这些数据传递给表单验证工具(如 WTForms),需要将它们合并为一个对象,否则表单可能无法正确读取所有字段。
语法:
from werkzeug.datastructures import CombinedMultiDict
combined = CombinedMultiDict([dict1, dict2, ...])
- 参数:接受一个包含多个 MultiDict 的列表(如 [request.form, request.files])。
- 行为:按顺序从传入的 MultiDict 中查找键,返回第一个匹配的值。
示例代码:
from flask import request
from werkzeug.datastructures import CombinedMultiDict
# 合并表单数据和文件数据
combined_data = CombinedMultiDict([request.form, request.files])
# 将合并后的数据传递给表单
form = BookForm(combined_data)
在本例中,CombinedMultiDict的作用:
f = BookForm(CombinedMultiDict([request.form, request.files]))
- 目的:将 request.form(普通字段)和 request.files(文件字段)合并,确保表单 BookForm 能同时验证普通字段(如 name、isbn)和文件字段(如 photo)。
- 必要性:如果不合并,表单可能无法获取文件字段的数据,导致验证失败。
注意事项:
- 键冲突:如果多个 MultiDict 中存在同名键,CombinedMultiDict 会返回第一个匹配的值。例如,若 request.form 和 request.files 都有键 photo,将优先返回 request.form 中的值(但实际场景中二者通常不会冲突)。
- 文件字段处理:确保表单类(如 BookForm)中正确声明了文件字段(例如使用 FileField)。
替代方案:
如果使用 Flask-WTF 扩展,可以直接通过 form = BookForm() 并单独处理文件字段,但合并方式更简洁且符合 Flask 的常见实践。
在一般情况下,如果仅有表单普通字段(如 name、isbn),没有文件字段(如 photo),可以使用 form = BookForm(request.form)。
但如果有文件字段(如 photo),必须用 form = BookForm() 或者 form = BookForm(CombinedMultiDict([request.form, request.files]))。当然,form = BookForm()会更简洁。
对比本例中,f = BookForm(request.form)和f = BookForm() 两种方式的差异:
| 行为 | f = BookForm(request.form) | f = BookForm() (自动合并) |
| 数据来源 | 仅request.form | request.form+request.files |
| 文件字段值 | form.photo.data为None | form.photo.data包含文件对象 |
| 代码简洁性 | 如果要获得文件对象,需要手动处理文件字段,比如使用f = BookForm(CombinedMultiDict([request.form, request.files])) | 自动处理所有字段 |
| 适用场景 | 特殊需求(如仅处理部分字段) | 标准表单处理(推荐) |
2、文件上传的安全性
在本例的源代码中,book_add() 还存在一定的安全隐患,体现在下面的语句中:
file = f.photo.data
if file:
filename = os.path.join(filepath, 'static/media/',file.filename)
file.save(filename)
book.photo = file.filename
在这里,通过f.photo.data获得文件数据之后,没有通过werkzeug.utils的secure_filename函数对文件据进行安全处理,容易造成以下漏洞:
(1) 路径穿越攻击(Directory Traversal)
- 恶意文件名示例:../../../etc/passwd
- 后果:攻击者可能覆盖或读取服务器敏感文件。
- 原理:直接保存文件时,路径中的 ../ 会跳转到上级目录。
(2) 文件名冲突或覆盖
- 示例:两个用户上传同名文件 image.jpg,后上传的会覆盖前者。
- 后果:数据丢失或服务异常。
(3) 特殊字符导致异常
- 示例:文件名包含空格(my file.txt)、中文(我的文件.jpg)、#、$ 等。
- 后果:某些操作系统或代码逻辑无法处理这些字符,导致文件保存失败或路径解析错误。
(4) 修复方案
- 在views.py中导入secure_filename函数:
from werkzeug.utils import secure_filename # 新增导入
- 将文件上传语句改为:
file = f.photo.data
if file:
# 新增文件名安全处理
safe_filename = secure_filename(file.filename) # 关键修改
filename = os.path.join(filepath, 'static/media/',safe_filename) # 使用安全文件名
file.save(filename)
book.photo = safe_filename # 存储清理后的文件名到数据库
3、获取前台多选框内的作者信息
select_authors = request.form.getlist("author")
这条语句是为了获取前台多选框内的作者信息,可以用下面的语句代替:
select_authors = f.author.data
在这里 request.form.getlist("author") 和f.author.data在这里是等效的,区别在于:
- request.form.getlist直接操作原始请求数据
- f.author.data通过表单对象提供了类型转换、验证集成等额外功能。使用 f.author.data 能确保数据经过表单验证(例如是否在允许的选项范围内)。直接操作 request.form.getlist会绕过表单验证逻辑,可能导致非法数据进入业务逻辑。此外,这种方式代码更简洁,避免手动操作原始请求数据,代码更符合 Flask-WTF 的设计哲学。
4、多选框内的作者信息入库
book_obj=Book.query.filter(b_id==b_id).first()
#获取前台多选框内的作者信息
select_authors = request.form.getlist("author")
for i in select_authors:
author = Author.query.get(i)
book_obj.back_author.append(author)
db.session.commit()
在这段代码中,存在一个语法错误,即这条语句:
book_obj=Book.query.filter(b_id==b_id).first()
- 当使用query.filter时,其语法应为:query.filter(模型名.字段名 == 值)
- 而使用query.filter_by,方式是query.filter_by(字段名=值)
- 注意query.filter采用的是两个等号(==),query.filter_by采用的是一个等号(=)。
因此,book_obj=Book.query.filter(b_id==b_id).first()应更改为:
book_obj = Book.query.filter(Book.b_id == b_id).first()
在 SQLAlchemy 中,filter 和 filter_by 是两种常用的查询过滤方法,但它们在语法、灵活性和使用场景上有显著区别。以下是两者的详细对比:
语法差异:
①filter
- 语法:query.filter(模型字段表达式)。
- 字段引用:必须明确指定模型类字段(如 Model.column)。
- 操作符:使用 Python 的比较操作符(==、!=、> 等)或 SQLAlchemy 的查询构造器(如 and_、or_)。
- 示例:
# 查询用户名为 "Alice" 的用户
User.query.filter(User.name == "Alice").first()
# 多条件查询
from sqlalchemy import and_
User.query.filter(and_(User.age > 18, User.country == "USA")).all()
② filter_by
- 语法:query.filter_by(字段名=值)。
- 字段引用:直接使用字段名(无需模型类前缀)。
- 操作符:仅支持等值比较(=),隐式使用 ==。
- 示例:
# 查询用户名为 "Alice" 的用户
User.query.filter_by(name="Alice").first()
# 多条件查询(隐式 AND)
User.query.filter_by(age=25, country="USA").all()
参数类型对比:
| 特性 | filter | filter_by |
| 参数类型 | SQL 表达式(如User.name == "Alice") | 关键字参数(如name="Alice") |
| 多表支持 | ✅ 支持跨表关联查询(需显式join) | ❌ 仅限当前模型的字段 |
| 复杂条件 | ✅ 支持and_、or_、in_等操作符 | ❌ 仅支持等值比较 |
灵活性对比:
①filter 的灵活性
- 多表关联查询:
# 查询作者为 "J.K. Rowling" 的书籍
Book.query.join(Author).filter(Author.name == "J.K. Rowling").all()
- 复杂条件组合:
from sqlalchemy import or_
User.query.filter(or_(User.age < 18, User.age > 65)).all()
- 非等值操作:
Product.query.filter(Product.price > 100).all()
②filter_by 的局限性
- 仅限单表等值查询:
# 无法直接处理多表关联或非等值条件
User.query.filter_by(age=25, country="USA").all()
链式调用的区别:
① filter 的链式调用
- 可以自由混合不同模型的字段(需关联表):
session.query(User, Order)
.join(Order)
.filter(User.id == 1)
.filter(Order.total > 100)
.all()
②filter_by 的链式调用
- 仅限当前模型的字段:
User.query.filter_by(country="USA").filter_by(age=25).all()
# 等价于
User.query.filter_by(country="USA", age=25).all()
适用场景:
| 场景 | 推荐方法 | 理由 |
| 单表简单等值查询 | filter_by | 语法简洁,无需重复写模型类名。 |
| 多条件、非等值或跨表复杂查询 | filter | 支持复杂操作符和跨表关联。 |
| 动态构建查询条件 | filter | 可以通过条件表达式灵活拼接。 |
| 快速原型开发或简单 CRUD | filter_by | 减少代码量,提高可读性。 |
示例对比:
①查询用户名为 "Alice" 且年龄为 25
- filter_by:
User.query.filter_by(name="Alice", age=25).first()
- filter:
User.query.filter(User.name == "Alice", User.age == 25).first()
② 查询价格大于 100 且库存小于 10 的商品
- 仅能用filter:
Product.query.filter(Product.price > 100, Product.stock < 10).all()
总结:
- filter_by:适用于单表简单等值查询,语法简洁。
- filter:适用于复杂条件、多表关联或非等值查询,灵活性更高。
- 选择原则:
- 如果查询条件简单且均为等值比较,优先使用filter_by。
- 需要复杂逻辑或跨表查询时,必须使用filter。
最后,数据提交这条语句:
db.session.commit()
在实际的程序运行中,为什么db.session.commit()前面,不加入db.session.add(book_obj)这条句语,也可以提交成功?
而在《Flask2+Vue.js实战派》第88页,多对多关系,增加数据的时候,是有db.session.add这一条句的。
在查阅了相关资料得知:在 SQLAlchemy 中,book_obj 对象已经处于会话的跟踪状态(即 "persistent" 状态),因此对其关联关系(如 back_author)的修改会自动被会话记录,无需显式调用 db.session.add(book_obj)。以下是详细解释:
对象状态分析:
①book_obj 的来源
book_obj = Book.query.filter(Book.b_id == b_id).first()
- 通过Book.query 查询得到的 book_obj 已自动被 SQLAlchemy 会话(Session)跟踪。
- 此时book_obj 处于 persistent(持久化) 状态,会话会监控其所有属性变化。
②关联关系修改
book_obj.back_author.append(author)
当向book_obj.back_author(多对多关系)添加 author 对象时,SQLAlchemy 会自动记录这一变更,无需手动操作中间表或调用 add()。
SQLAlchemy 的自动跟踪机制:
① Persistent 状态的对象
- 通过查询(如query.get()、query.first())获取的对象默认被会话跟踪。
- 对这些对象的属性修改或关联关系变更会被会话自动捕获,并在 commit() 时生成对应的 SQL(如插入中间表记录)。
②关联关系与中间表
- 如果back_author 是使用 relationship 定义的多对多关系,例如在下面的代码中:
class Book(Base):
__tablename__ = "books"
b_id = Column(Integer, primary_key=True)
authors = relationship("Author", secondary="book_author", backref="books")
class Author(Base):
__tablename__ = "authors"
a_id = Column(Integer, primary_key=True)
# 中间表
book_author = Table(
"book_author",
Base.metadata,
Column("book_id", Integer, ForeignKey("books.b_id")),
Column("author_id", Integer, ForeignKey("authors.a_id")),
)
向 book_obj.authors 添加 author 对象时,SQLAlchemy 会自动向中间表 book_author 插入记录。
这就解释了为什么不需要 db.session.add(book_obj):
①对象已在会话中
- book_obj是查询得到的,已存在于会话的 Identity Map 中。
- 会话会跟踪其所有变更(包括关联关系)。
②关联对象的处理
- author对象通过 Author.query.get(i) 获取,同样处于会话跟踪状态。
- 向book_obj.back_author 添加 author 时,SQLAlchemy 会:检测到关联关系的变更,并自动生成对中间表的INSERT 语句。
③提交时的操作
- db.session.commit()
会将会话中所有变更(包括关联关系的更新)一次性提交到数据库。
对比显式 add() 的场景:
①需要显式 add() 的情况
- 新增对象:例如创建一个新 Book 对象并保存:
new_book = Book(name="New Book")
db.session.add(new_book) # 必须显式添加
db.session.commit()
- 游离(Detached)对象:如果对象未与会话关联(如从其他会话加载后未合并),需重新添加。
②不需要显式 add() 的情况
- 查询得到的对象:已自动被会话跟踪。
- 通过关联关系操作中间表:SQLAlchemy 自动处理。
结合前面的知识点,最终修改后的book_add()视图函数的代码:
@app.route("/book_add", methods=["GET", "POST"])
def book_add():
if request.method == "GET":
f = BookForm()
return render_template("book/add.html", form=f)
elif request.method == "POST":
#f = BookForm(CombinedMultiDict([request.form, request.files]))
f = BookForm(
) #使用 Flask-WTF 扩展,可以直接通过 form = BookForm() 并单独处理文件字段,但合并方式更简洁
if f.validate_on_submit():
filepath = os.path.abspath(os.path.dirname(__file__))
book = Book()
book.name = f.name.data
book.isbn = f.isbn.data
file = f.photo.data
"""存在安全隐患
if file:
filename = os.path.join(filepath, 'static/media/',
file.filename)
file.save(filename)
book.photo = file.filename
"""
if file:
# 新增文件名安全处理
safe_filename = secure_filename(file.filename) # 关键修改
filename = os.path.join(filepath, 'static/media/',
safe_filename) # 使用安全文件名
file.save(filename)
book.photo = safe_filename # 存储清理后的文件名到数据库
book.price = f.price.data
book.pub_id = f.publish.data
db.session.add(book)
db.session.flush() #保证可以返回b_id
#返回自增id
b_id = book.b_id
book_obj = Book.query.filter(Book.b_id == b_id).first()
#获取前台多选框内的作者信息
#select_authors = request.form.getlist("author") #采用原生的request.form.getlist也可以
select_authors = f.author.data # 采用form对象的data属性也可以
for i in select_authors:
author = Author.query.get(i)
book_obj.back_author.append(author)
db.session.add(book_obj)
db.session.commit()
return redirect(url_for("book_list"))
else:
return render_template("book/add.html", form=f)
至此,视图函数book_add()分析完毕。
(三)视图函数book_edit()
代码如下:
app.route("/book_edit/<int:bid>", methods=["GET", "POST"])
def book_edit(bid):
if request.method == "GET":
# 根据bid找到具体的图书
book_obj = Book.query.get(bid)
#反向查询找到作者
authors=book_obj.back_author
#构造列表
author_ids=[i.a_id for i in authors]
f=BookForm(request.form)
f.name.data=book_obj.name
f.isbn.data=book_obj.isbn
f.photo.data=book_obj.photo
# 反向查询找到出版社
publish_obj = book_obj.back_publish
f.publish.data=publish_obj.p_id
f.author.data=author_ids
return render_template("book/edit.html", form= f, book=book_obj)
elif request.method == "POST":
f = BookForm(CombinedMultiDict([request.form, request.files]))
book_obj = Book.query.get(bid)
filepath=os.path.abspath(os.path.dirname(__file__))
if f.validate_on_submit():
book_obj.name = f.name.data
book_obj.isbn = f.isbn.data
file = f.photo.data # 原文件名
if file:
filename=os.path.join(filepath,'static/media/',file.filename)
file.save(filename)
book_obj.photo = file.filename
book_obj.price = f.price.data
book_obj.pub_id = f.publish.data
# 获取前台多选框内的作者信息
select_authors = request.form.getlist("author")
#将已经存在的关系删除
for obj in book_obj.back_author:
author = Author.query.get(obj.a_id)
book_obj.back_author.remove(author)
#再插入
for i in select_authors:
author=Author.query.get(i)
book_obj.back_author.append(author)
db.session.commit()
return redirect(url_for("book_list"))
else:
return render_template("book/edit.html", form= f, book= book_obj)
book_edit() 的作用:
1、如果以GET方式访问,通过获得的bid进行查询,将查询到的相关数据传递到模板edit.html中,便于在前台进行数据的呈现。
2、如果以POST方式访问,就将前台提交的修改信息入库。
这里需要注意的是:
1、根据bid找到具体的图书
book_obj = Book.query.get(bid)
query.get 是 SQLAlchemy 中用于根据主键值查找数据库记录的方法。它接受一个主键值作为参数,在 Book 表中查找具有该主键值的记录,并返回对应的模型对象。如果找不到对应的记录,它将返回 None。
此外,可以用 Book.query.filter_by 代替 Book.query.get 来实现相同的功能 (这里数据库的book的ID字段为b_id):
# 原代码
book_obj = Book.query.get(bid)
# 替代代码
book_obj = Book.query.filter_by(b_id=bid).first()
query.get 和query.filter_by的区别在于:
参数类型
- query.get :只能接受主键值作为参数。它会直接根据主键去数据库中查找对应的记录,性能相对较高,因为主键通常有索引优化。
- query.filter_by :可以接受任意字段和对应的值作为参数,用于指定过滤条件。它可以根据多个字段进行组合查询,灵活性更高。例如:
Python复制# 根据 name 和 isbn 进行过滤查询
books = Book.query.filter_by(name='Python 编程', isbn='1234567890').all()
查询逻辑
- query.get :优先从会话的缓存中查找记录,如果缓存中不存在,再去数据库中查找。这种机制可以避免不必要的数据库查询,提高性能。
- query.filter_by :总是会发起数据库查询,不会从会话缓存中查找。因此,在某些情况下,使用 Book.query.filter_by 可能会比 Book.query.get 稍微慢一些。
返回结果
- Book.query.get :如果找不到对应的记录,会直接返回 None。
- Book.query.filter_by :如果没有找到符合条件的记录,first() 方法会返回 None,而 all() 方法会返回一个空列表。
综上所述,当你只想根据主键值查找记录时,使用 Book.query.get 更为简洁高效;当需要根据多个字段进行组合查询时,应该使用 Book.query.filter_by 。
2、反向查询找到作者和出版社信息
对于前面的Book这个模型来说,其代码在models.py文件中:
from apps import db
class Publish(db.Model):
__tablename__ = "tb_publish"
p_id = db.Column(db.Integer,comment='ID', primary_key=True,autoincrement=True) # 主键
name = db.Column(db.String(32),comment='出版社名称')
address = db.Column(db.String(64),comment='出版社地址')
intro = db.Column(db.String(500),comment='出版社简介', nullable=True)
books = db.relationship("Book", backref="back_publish", lazy=True, passive_deletes=True)
#中间表
a_b=db.Table(
"author_book",
db.Column("a_id",db.Integer,db.ForeignKey("tb_author.a_id"),primary_key=True),
db.Column("b_id", db.Integer, db.ForeignKey("tb_book.b_id"), primary_key=True),
)
class Author(db.Model):
__tablename__="tb_author"
a_id = db.Column(db.Integer,comment='ID', primary_key=True,autoincrement=True)
name = db.Column(db.String(20),comment='作者姓名')
age = db.Column(db.Integer,comment='作者年龄', default=1)
mobile = db.Column(db.String(11),comment='电话')
address = db.Column(db.String(20),comment='地址')
intro = db.Column(db.Text,comment='简介', nullable=True)
books=db.relationship("Book",secondary="author_book", backref="back_author")
def __repr__(self):
return f"{self.a_id}"
class Book(db.Model):
__tablename__="tb_book"
b_id = db.Column(db.Integer,comment='ID', primary_key=True,autoincrement=True)
name = db.Column(db.String(50),comment='书籍名称')
isbn = db.Column(db.String(50),comment='ISBN')
photo = db.Column(db.String(50),comment='书籍封面')
price = db.Column(db.Numeric(5,2),comment='价格')
pub_id=db.Column(db.Integer,db.ForeignKey("tb_publish.p_id"))
| 主模型 | 关联模型 | 关系类型 | 正向属性 | 反向属性 | 中间表(如有) |
| Publish | Book | 一对多 | books | back_publish | 无 |
| Author | Book | 多对多 | books | back_author | a_b(author_book) |
- Publish和Book是一对多关系,Publish处于“一方”,Book处于“多方”。 Book的pub_id字段设置了外键db.ForeignKey("tb_publish.p_id"),关联到Publish的tb_publish表的p_id字段;Publish的books字段通过db.relationship的backref="back_publish"与Book建立了反向查询。
- Book和Author是多对多关系,二者都是“多方”,也就是说,一本书可以对应多个作者,一个作者也可以对应多本书。二者通过中间表a_b=db.Table建立了联系,并为双方建立了外键db.ForeignKey("tb_author.a_id")和db.ForeignKey("tb_book.b_id")。在Author中,books字段通过db.relationship的backref="back_author"与Book建立了反向查询。而db.relationship的secondary="author_book",author_book是一个中间表,用来连接作者(Author)和书籍(Book)之间的多对多关系。
- SQLAlchemy 中,多对多关系只需在一侧模型(通常是主导关系的模型)中定义 secondary 参数,另一侧通过反向引用访问。例如,若 Author 是主导模型(如“作者可以写多本书”),则 Author 模型中定义 secondary,Book 模型通过 back_author 反向查询关联的作者
- 多对多关系建立起来之后,Author 和 Book 可以互相查询。
从 Author 查询关联的 Book:
author = Author.query.get(1)
books = author.books # 返回所有关联的 Book 对象
从 Book 查询关联的 Author:
book = Book.query.get(1)
authors = book.back_author #反向查询,返回所有关联的 Author 对象
- 一对一、多对多的区别:
| 关系类型 | 实现方式 | 查询方向 | 适用场景 |
| 一对多 | 一方定义relationship | 单向或双向 | 如出版社与图书 |
| 多对多 | 需中间表 + 双方relationship | 双向互查 | 如作者与图书 |
关于例子中的数据库模型中的关系与反向查询,可以用以下思维导图表示:
数据库模型中的关系与反向查询
├── One-to-Many (一对多)
│ └── Example: Publisher to Book
│ ├── 一个Publisher对应多本Books
│ ├── 关系属性:
│ │ - Publisher.books(关联到Book)
│ │ - Book.back_publish(反向查询到Publisher)
│ └── 实现方式:通过外键pub_id在Book表中引用Publisher的p_id
├── Many-to-Many (多对多)
│ └── Example: Author to Book
│ ├── 一个Author对应多个Books,一本书由多个Authors编写
│ ├── 关系属性:
│ │ - Author.books(关联到Book)
│ │ - Book.back_author(反向查询到Author)
│ └── 实现方式:使用中间表author_book存储关系
└── Reverse Queries (反向查询)
└── 示例:
├── 书籍如何访问其出版社和作者?
│ - Book对象通过back_publish访问Publisher
│ - Book对象通过back_author访问Author列表
└── 实现方式:通过ORM框架(如SQLAlchemy)自动管理关系属性
基于于上述模型的关联关系,继续分析这段代码:
# 根据bid找到具体的图书
book_obj = Book.query.get(bid)
#反向查询找到作者
authors=book_obj.back_author
#构造列表
author_ids=[i.a_id for i in authors]
f=BookForm(request.form)
f.name.data=book_obj.name
f.isbn.data=book_obj.isbn
f.photo.data=book_obj.photo
# 反向查询找到出版社
publish_obj = book_obj.back_publish
f.publish.data=publish_obj.p_id
f.author.data=author_ids
return render_template("book/edit.html", form=f, book=book_obj)
当执行book_obj = Book.query.get(bid)语句,获得图书信息的对象book_obj之后,能够通过authors=book_obj.back_author查询到一本书的多个作者信息。对于这条语句来说:
authors=book_obj.back_author
查询到的是多个作者的信息,这些信息的字段,就位于Author模型之中,包括a_id、name、age、mobile、address、intro等字段。
这里需注意的是,在Author模型有这样一条语句:
def __repr__(self):
return f"{self.a_id}"
在 Python 中,__repr__ () 是一个特殊方法(Magic Method),用于定义对象的无歧义字符串表示形式。其主要目的是为开发者提供明确的调试信息或日志记录内容。
而return f"{self.a_id}",会自动返回a_id这个字段的值。如果没有 __repr__这个方法,在authors=book_obj.back_author后面,加入一条语句print(authors):
authors=book_obj.back_author
print(authors)
调试信息打印出的是Author模型的数据集列表,比如一本书本的作者的a_id分别是:3、4、1、2,对应的Author模型的数据集列表就是:
[<Author 3>, <Author 4>, <Author 1>, <Author 2>]
而有这个__repr__这个方法,在authors=book_obj.back_author后面,加入一条语句print(authors):
authors=book_obj.back_author
print(authors)
调试信息打印出的是Author模型的a_id列表,比如一本书本的作者的a_id分别是:3、4、1、2,对应的Author模型的a_id列表就是:
[3, 4, 1, 2]
这个细微的差别,就解释了为什么在本书第四章第65页,为什么要使用__repr__ ()方法。也就是通过__repr__ (),可以让模型返回一些有用的信息。
接下来,book_edit()的这条语句:
author_ids=[i.a_id for i in authors]
当查询到多个作者的信息之后,通过列表推导式[i.a_id for i in authors]返回多个作者的a_id列表。而在Author模型中,使不使用__repr__ ()方法,其结果都是一样的。
接下来,通过
publish_obj = book_obj.back_publish
这条语句,反向查询到Publish的数据。最后,将相关表单字段进行赋值:
f=BookForm(request.form)
f.name.data=book_obj.name
f.isbn.data=book_obj.isbn
f.photo.data=book_obj.photo
# 反向查询找到出版社
publish_obj = book_obj.back_publish
f.publish.data=publish_obj.p_id
f.author.data=author_ids
return render_template("book/edit.html", form=f, book=book_obj)
从而在进入编辑界面之后呈现原先已保存的数据:

这里需注意的是,在对相关表单字段进行赋值的时候,这两条语句:
f.publish.data=publish_obj.p_id
f.author.data=author_ids
出版社的信息f.publish.data(单一值)和作者的信息f.author.data(多个值)都是相关的ID值。例如,“电子工作出版社”对应值是1,作者“张三、李四、王五、王玲”对应的列表值是[3, 4, 1, 2]。
那么,在进入编辑状态(比如: /book_edit/1)时,Flask是如何实现自动实现相关信息的选中状态的呢?
我们先来看看form.py中,对BookForm这个类对相关字段的定义:
class BookForm(FlaskForm):
name = StringField(label="图书名称",
validators=[
DataRequired(message="图书名称不能为空"),
Length(4, 32, message="长度最少4位")
],
render_kw={
'class': 'form-control',
'placeholder': "请输入图书名称"
})
isbn = StringField(label="ISBN",
validators=[
DataRequired(message="ISBN不能为空"),
Length(1, 20, message="ISBN在1-20位之间")
],
render_kw={
'class': 'form-control',
'placeholder': "请输入ISBN"
})
photo = StringField(label="图书封面",
widget=widgets.FileInput(),
render_kw={'class': 'custom-file-input'})
price = DecimalField(label="价格",
places=2,
validators=[DataRequired(message="价格不能为空")],
render_kw={
'class': 'form-control',
'placeholder': "请输入价格"
})
publish = SelectField(label="出版社",
coerce=int,
validators=[DataRequired(message="出版社不能为空")],
choices=[('0', '请选择出版社...')] +
[(v.p_id, v.name) for v in Publish.query.all()],
render_kw={
'class': 'form-control custom-select',
'placeholder': "请选择出版社"
})
author = SelectMultipleField(label="作者",
coerce=int,
validators=[DataRequired(message="作者不能为空")],
choices=[(v.a_id, v.name)
for v in Author.query.all()],
render_kw={
'class': 'form-control custom-select',
'placeholder': "请选择作者"
})
以publish这个字段为例,其中有这样一条语句:
choices=[('0', '请选择出版社...')] +[(v.p_id, v.name) for v in Publish.query.all()],
这行代码会生成类似以下结构的选项列表:
[('0', '请选择出版社...'), (1, '电子工业出版社'), (2, '人民邮电出版社'), # 其他出版社...]
每个选项由元组(值(p_id), 显示文本(name))构成。
在book_edit()视图函数中,通过f.publish.data = publish_obj.p_id将当前图书的出版社ID赋值给表单字段。例如,假设当前图书的出版社p_id是1,则f.publish.data的值为1。
- 在模板渲染时,WTForms会根据data属性的值(即p_id)自动匹配choices中的选项:
- 遍历choices列表:表单内部会遍历所有选项,检查每个选项的值(p_id)是否等于的data值。
- 标记匹配项为选中状态:当找到匹配项(如(1, '电子工业出版社')),WTForms会在生成的HTML中为该选项添加selected属性,例如:
<option value="1" selected>电子工业出版社</option>
最终渲染的下拉菜单会显示所有出版社名称,但只有与p_id匹配的选项会被默认选中。例如:
<select class="form-control" id="publish" name="publish">
<option value="0">请选择出版社...</option>
<option value="1" selected>电子工业出版社</option>
<option value="2">人民邮电出版社</option>
<!-- 其他选项 -->
</select>
浏览器会根据selected属性自动高亮显示匹配的选项。类似的,作者信息也通过这种方式实现了html渲染字段的关联。
换句话说,只要执行了:
f.publish.data=publish_obj.p_id
f.author.data=author_ids
Flask会自动将出版社的信息和作者的信息实现自动填充和选中。
3、图片上传的安全性问题
在BookForm这个类中,有一段关于photo字段的定义:
photo = StringField(label="图书封面",widget=widgets.FileInput(),render_kw={'class': 'custom-file-input'})
这里比较奇怪的是:photo这个用于文件上传的字段,用的是StringField。而在Flask-WTF中,FileField 才是专门用于处理文件上传的字段类型。当然,这里用的是widget=widgets.FileInput()这个属性,也可以同样达到FileField的效果。
- Widget 可以扩展表单的功能。例如:
- FileInput:用于文件上传。
- CheckboxInput:用于复选框。
- Select:用于下拉选择框。
- 开发者还可以创建自定义 widget,以实现更复杂的表单元素(如富文本编辑器、日期选择器等)。
但是,这段代码由于没有对图片格式进行限制,很容易被上传其他文件,可以作如下修改:
from flask_wtf.file import FileField, FileAllowed, FileRequired
photo = FileField(
label="图书封面",
validators=[
FileRequired(message="请选择封面文件"), # 确保文件已上传
FileAllowed(['jpg', 'png','gif'], message="仅支持 JPG/PNG/gif 格式") # 限制文件类型
],
render_kw={'class': 'custom-file-input'})
至此,视图函数book_edit()分析完毕。
(四)视图函数index()
代码如下:
@app.route("/index", methods=["GET"])
def index():
# 获取统计信息
pub_count = db.session.query(func.count(Publish.p_id)).scalar()
author_count = db.session.query(func.count(Author.a_id)).scalar()
book_count = db.session.query(func.count(Book.b_id)).scalar()
# 图表的label和data组装
#select count(b.name) from book a,publish b where a.pub_id=b.p_id group by b.name
infos = Book.query.join(
Publish, Book.pub_id == Publish.p_id).with_entities(
Publish.name,
func.Count(Publish.name)).group_by(Publish.name).all()
#print(
# Book.query.join(Publish, Book.pub_id == Publish.p_id).with_entities(
# Publish.name, func.Count(Publish.name)).group_by(Publish.name))
labels = [info[0] for info in infos]
datas = [info[1] for info in infos]
return render_template('index.html',
pub_count=pub_count,
author_count=author_count,
book_count=book_count,
labels=labels,
datas=datas)
index()的作用是:
1、通过聚合查询,获得相关统计信息
2、将相关统计信息传入index.html模板,实现下面的效果:

这里,重点讲解一下聚合查询。对于下面的语句:
pub_count = db.session.query(func.count(Publish.p_id)).scalar()
在 SQLAlchemy 中,scalar() 是一个查询方法,用于执行查询并返回结果的第一行第一列的值(即标量值)。它特别适用于聚合查询(如 COUNT(), SUM(), MAX() 等),这些查询通常只返回单个值。
db.session.query(func.count(Publish.p_id))会生成一个 SQL 查询,类似sql语句:
SELECT COUNT(publish.p_id) AS count_1 FROM publish
scalar()的作用是:
- 执行这个查询。
- 获取结果的第一行。
- 提取该行的第一列的值(即COUNT() 的结果)。
- 最终返回一个单一的整数(例如42),而不是返回一个包含元组的列表(如 [(42,)])。
对比其他方法:
- .first():返回结果的第一行(一个元组),例如 (42,)。
- .one():严格确保结果只有一行,否则抛出异常。
- .scalar():直接提取第一行第一列的值,适合获取单个标量结果。
如果没有使用 scalar():
result = db.session.query(func.count(Publish.p_id)).first()
# result 是一个元组,如 (42,)
pub_count = result[0] # 需要手动提取第一个元素
总之,使用 scalar() 可以简化代码,直接得到标量值。
而对于这条语句:
infos = Book.query.join(
Publish, Book.pub_id == Publish.p_id).with_entities(
Publish.name,
func.Count(Publish.name)).group_by(Publish.name).all()
代码生成的 SQL 等价于:
SELECT publish.name, COUNT(publish.name) FROM book JOIN publish ON book.pub_id = publish.p_id GROUP BY publish.name;
目的是查询 每个出版社的名字及其出版的书籍数量。
①Book.query.join(Publish, Book.pub_id == Publish.p_id)
- 作用:将 Book 表和 Publish 表通过外键关联(Book.pub_id = Publish.p_id)进行连接。
- 等价 SQL:
FROM book JOIN publish ON book.pub_id = publish.p_id
② .with_entities(Publish.name, func.Count(Publish.name))
- 作用:指定查询返回的字段:
- Publish.name:出版社名称。
- func.Count(Publish.name):统计每个出版社的书籍数量。
- 等价 SQL:
SELECT publish.name, COUNT(publish.name)
③ .group_by(Publish.name)
- 作用:按出版社名称分组,确保 COUNT 统计的是每个出版社的书籍数量。
- 等价 SQL:
GROUP BY publish.name
④ .all()
- 作用:执行查询并返回所有结果,结果是一个列表,每个元素是一个元组,格式为 (出版社名称, 书籍数量),例如:
[('电子工业出版社', 5), ('人民邮电出版社', 3), ('机械工业出版社', 8)]
返回结果的用途
- labels = [info["name"] for info in infos]:提取所有出版社名称(用于图表横坐标)。
- datas = [info[1] for info in infos]:提取每个出版社的书籍数量(用于图表纵坐标)。
最终返回的数据可能是这样的:
labels = ['电子工业出版社', '人民邮电出版社', '机械工业出版社']
datas = [5, 3, 8]
至此,视图函数index()分析完毕。
三、前端需重点学习的代码
(一)base.html模板文件
这个模板文件,位于mybook\templates\ 下,是所有模板文件的公共模板文件。需要重点关注的代码有:
1、HTML5的<aside>元素
对于base.html中下面的代码:
<aside class="main-sidebar sidebar-dark-primary elevation-4">
<a href="{{ url_for("index")}}" class="brand-link">
<img src="{{ url_for('static',filename='img/AdminLTELogo.png' ) }}" alt="AdminLTE Logo"
class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-light">图书管理系统</span>
</a>
<div class="sidebar">
<div class="user-panel mt-3 pb-3 mb-3 d-flex">
<div class="image">
<img src="{{ url_for('static',filename='img/user2-160x160.jpg' )}}" class="img-circle elevation-2" alt="User Image">
</div>
<div class="info"><p>
<a href="{{ url_for('logout')}}">退出</a></p>
</div>
</div>
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu"
data-accordion="false">
<li class="nav-header">书籍管理</li>
<li class="nav-item">
<a href="{{ url_for('pub_list') }}" class="nav-link">
<i class="nav-icon far fa-image"></i>
<p>
出版社管理
</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('author_list')}}" class="nav-link">
<i class="nav-icon far fa-image"></i>
<p>
作者管理
</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('book_list') }}" class="nav-link">
<i class="nav-icon far fa-image"></i>
<p>
图书管理
</p>
</a>
</li>
<li class="nav-header">系统管理</li>
<li class="nav-item">
<a href="{{ url_for('user_list')}}" class="nav-link">
<i class="nav-icon far fa-circle text-danger"></i>
<p class="text">用户管理</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
对应的界面是管理界面左边的侧边栏(打红框部分):

在早期的HTML语法中,很多时候,是用frameset、frame、iframe和noframes等传统框架元素来实现侧边栏以及其他框架效果。
而从HTML5标准开始,就明确移除了frameset、frame和noframes元素。只是iframe仍适用于部分场景,例如嵌入第三方内容(地图、广告)、实现沙箱隔离(如代码编辑器预览)、或需完全独立上下文的子页面(如支付网关)。
HTML5引入了<aside>元素 ,其作用是:
- 全局侧边栏
- 用途:作为整个页面的侧边栏,包含与主内容松散相关的信息,如导航菜单、友情链接、广告等
- 示例:
<body>
<header>网站标题</header>
<main>主内容区</main>
<aside>
<h3>相关链接</h3>
<nav>
<ul>
<li><a href="#">友情链接1</a></li>
<li><a href="#">友情链接2</a></li>
</ul>
</nav>
<div class="ad">广告位</div>
</aside>
</body>
- 文章内附属信息
- 用途:嵌入在<article>内部,展示与当前文章相关的补充内容,如名词解释、参考资料等
- 示例:
<article>
<h2>F#编程入门</h2>
<p>Lambda表达式可以创建词法闭包...</p>
<aside>
<h4>名词解释</h4>
<p>词法闭包:指将创建Lambda表达式时的环境保存起来的机制。</p>
</aside>
</article>
在本书的源代码中,由于使用了Bootstrap前端框架的分支框架AdminLTE,故可以通过特定的CSS来进行布局。
而在其他模板文件中,例如mybook\templates\index.html ,通过模板标签{% block content %}和{% endblock %}中的内容,替换base.html中的{% block content %}{% endblock %},实现整体内容的展示。
只要点击由<aside>元素包裹的左侧侧边栏的链节,就可以在右侧显示相应的内容:

换句话说,通过Bootstrap的CSS,就可以实现过去包括frame等传统框架元素的布局 ,从而避免过去使用传统框架元素导致的页面结构复杂化、难以维护,以及在多设备适配时存在兼容性问题,甚至可能导致的安全风险,例如跨站脚本攻击(XSS)。
2、Bootstrap的data-widget="fullscreen"和data-widget="control-sidebar"
base.html中有这样一段代码:
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" data-widget="fullscreen" href="#" role="button">
<i class="fas fa-expand-arrows-alt"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-widget="control-sidebar" data-slide="true" href="#" role="button">
<i class="fas fa-th-large"></i>
</a>
</li>
</ul>
含义及作用
① data-widget="fullscreen"
- 含义:data-widget 是一个自定义的 HTML5 数据属性,data-widget="fullscreen" 用于标识这个元素与全屏功能相关。在 Bootstrap 中,它本身并不是标准的 Bootstrap 属性,而是开发者自定义用来触发全屏功能的标识。
- 作用:当用户点击带有这个属性的元素时,通常会触发页面进入全屏模式。这种方式可以将页面内容以全屏的形式展示,提供更沉浸式的体验。
- 使用场景:适用于需要用户专注查看页面内容的场景,比如展示图片、视频、文档等。
例如,在界面上点击右侧顶部的全屏按钮,就进入全屏模式。若要退出全展模式,需按Esc键:

② data-widget="control-sidebar"
- 含义:同样,data-widget="control-sidebar" 也是自定义的 HTML5 数据属性,用于标识这个元素与侧边栏控制功能相关。它表明该元素可以控制侧边栏的显示与隐藏。
- 作用:点击带有这个属性的元素时,会触发侧边栏的显示或隐藏操作。侧边栏通常用于放置一些额外的控制选项、导航菜单等,以节省页面的主要空间。
- 使用场景:常见于管理后台、仪表盘等页面,用于收纳一些不常用但又需要随时访问的功能。
例如,在界面上点击右侧顶部的侧边栏按钮,会出现一个黑色的区域,这是右侧的侧边栏:

如何使用:
①实现全屏功能
要实现 data-widget="fullscreen" 的全屏功能,需要结合 JavaScript 代码来监听点击事件,并触发全屏 API。以下是一个示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<title>Fullscreen Example</title>
</head>
<body>
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" data-widget="fullscreen" href="#" role="button">
<i class="fas fa-expand-arrows-alt"></i>
</a>
</li>
</ul>
<script>
document.addEventListener('click', function (event) {
if (event.target.getAttribute('data-widget') === 'fullscreen') {
const doc = document.documentElement;
if (doc.requestFullscreen) {
doc.requestFullscreen();
} else if (doc.webkitRequestFullscreen) {
doc.webkitRequestFullscreen();
} else if (doc.msRequestFullscreen) {
doc.msRequestFullscreen();
}
}
});
</script>
</body>
</html>
在这个示例中,我们通过监听点击事件,当点击带有 data-widget="fullscreen" 的元素时,调用浏览器的全屏 API 实现全屏功能。
② 实现侧边栏控制功能
要实现 data-widget="control-sidebar" 的侧边栏控制功能,同样需要结合 JavaScript 代码。以下是一个简单的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
.control-sidebar {
position: fixed;
top: 0;
right: -200px;
width: 200px;
height: 100%;
background-color: #0e2e4f;
transition: right 0.3s ease;
}
.control-sidebar.show {
right: 0;
}
</style>
<title>Control Sidebar Example</title>
</head>
<body>
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" data-widget="control-sidebar" data-slide="true" href="#" role="button">
<i class="fas fa-th-large"></i>
</a>
</li>
</ul>
<div class="control-sidebar"></div>
<script>
document.addEventListener('click', function (event) {
if (event.target.getAttribute('data-widget') === 'control-sidebar') {
const sidebar = document.querySelector('.control-sidebar');
sidebar.classList.toggle('show');
}
});
</script>
</body>
</html>
在这个示例中,我们定义了一个侧边栏元素,并通过 CSS 设置其初始位置为隐藏状态。当点击带有 data-widget="control-sidebar" 的元素时,通过 JavaScript 切换侧边栏的 show 类,从而实现侧边栏的显示与隐藏。
当然,在不关心JavaScript的实现细节的情况下,我们只需要像basse.html那样,在模板文件中,导入下面的代码:
<script src="{{ url_for('static',filename='plugins/jquery/jquery.min.js') }}"></script>
<script src="{{ url_for('static',filename='plugins/bootstrap/js/bootstrap.js') }}"></script>
(二)publish目录下的index.html模板文件
这个模板文件,位于mybook\templates\publish\ 下,是publish模块的首页展示文件。需要重点关注的代码有:
1、HTML5的<section>元素
index.html文件中,相应的代码如下:
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12 search-collapse">
<form id="search_form" method="get">
{# {% form.csrf_token %}#}
<div class="form-group row">
<label for="inputtext" class="col-form-label">出版社名称:</label>
<div class="col-md-4">
<input type="text" id="search_name" name="name" class="form-control"/>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-info"><i class="fa fa-plus"></i>查询</button>
<a class="btn btn-primary single" href="{{ url_for('pub_add') }}">
<i class="fa fa-plus"></i> 新增
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
这段代码,对应的是下面的界面打红框的部分:

HTML5的<section>元素是用于结构化文档内容的语义化标签,主要作用是为页面内容提供逻辑分块。以下是其核心作用和正确使用方法:
核心作用:
①语义化分块
<section>表示文档中具有独立主题的内容区块(如章节、标签页等),需包含标题(<h1>-<h6>),帮助浏览器、屏幕阅读器和搜索引擎理解内容结构
②组织关联内容
适合将相关联的内容分组(例如同一主题下的多个段落),强调内容的逻辑关联性而非独立性。
③优化可访问性
屏幕阅读器可通过<section>识别内容结构,提升视障用户的访问体验。
基本语法:
<section>
<h2>章节标题</h2>
<p>相关段落或其他内容...</p>
</section>
典型场景:
- 文章章节:划分长文的不同部分(如“引言”“方法”“结论”)。
- 标签页内容:每个标签页的内容区块。
- 专题内容组:如产品介绍页的“功能”“参数”“评价”模块。
嵌套规则:
允许嵌套使用以创建层级结构:
<section>
<h2>主章节</h2>
<section>
<h3>子章节</h3>
<p>子内容...</p>
</section>
</section>
与其他元素对比:
- 与<div>的区别<div>是无语义的通用容器,仅用于布局或样式;<section>强调内容的逻辑分组。
- 与<article>的区别<article>表示独立完整的内容(如一篇博客),而<section>侧重关联性内容的分组。注意事项:
①避免滥用
- 不要仅为了样式需求使用<section>,应优先选择<div>。
- 若无明确标题,应使用<div>替代。
②替代方案优先级
若内容更适合用<article>(独立内容)、<aside>(侧边栏)或<nav>(导航),则无需使用<section>。
错误用法示例:
<!-- 错误:缺少标题 -->
<section>
<p>无标题的段落...</p>
</section>
<!-- 错误:仅用于样式容器 -->
<section class="red-box">
<p>仅为了红色边框而使用section</p>
</section>
通过合理使用<section>,可提升网页的语义化程度和可维护性。
2、信息的删除与确认
在每一条信息的展示上,有一个删除按钮:

当点击相应的删除按钮,将弹出一个确认框:
这个功能,对应了两段代码。这两段代码是Ajax异步请求数据的关键。
代码1:
<td width="20%">
<a class="btn btn-primary single" href="{{ url_for('pub_edit',p_id=per.p_id) }}">
<i class="fa fa-edit"></i> 修改
</a>
<a class="btn btn-danger" href="javascript:void(0)" onclick="showDeleteModal(this)">删除</a>
<input type="hidden" id="id_hidden" value={{ per.p_id }}>
</td>
代码2:
<!-- 信息删除确认 -->
<div class="modal fade" id="delModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" style="float:left">提示信息</h4>
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p id="info">您确认要删除当前数据吗?</p>
<input type="hidden" id="del_id">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<a id="delButton" class="btn btn-success" data-dismiss="modal">确定</a>
</div>
</div>
</div>
</div>
<script>
// 打开模态框并设置需要删除的ID
function showDeleteModal(obj) {
var $tds = $(obj).parent().children();// 获取到删除元素的所在列
var delete_id = $($tds[2]).val();// 获取隐藏控件的ID
console.log(delete_id)
$("#del_id").val(delete_id);// 给模态框中需要删除的ID赋值
$("#delModal").modal({
backdrop: 'static',
keyboard: false
});
};
$(function () {
// 模态框的确定按钮的点击事件
$("#delButton").click(function () {
var id = $("#del_id").val();
console.log("del" + id)
// ajax异步删除
$.ajax({
url: "/pub_del",
type: "POST",
data:{p_id:id},
success: function (result) {
if (result.code == "200") {
$("#delModal").modal("hide");
alert(result.message);
window.location.href = "{{ url_for('pub_list') }}";
}
else{
alert(result.message);
}
}
})
});
});
</script>
在代码1中,语句
<a class="btn btn-danger" href="javascript:void(0)" onclick="showDeleteModal(this)">删除</a>
通过点击“删除”链节,触发了showDeleteModa(this) 函数,导致Bootstrap的模态框被打开,下面的JavScript代码:
function showDeleteModal(obj) {
var $tds = $(obj).parent().children();// 获取到删除元素的所在列
var delete_id = $($tds[2]).val();// 获取隐藏控件的ID
console.log(delete_id)
$("#del_id").val(delete_id);// 给模态框中需要删除的ID赋值
$("#delModal").modal({
backdrop: 'static',
keyboard: false
});
};
语句:
var $tds = $(obj).parent().children();// 获取到删除元素的所在列
- obj是用户点击的删除按钮元素(即<a class="btn btn-danger">标签的DOM对象)。当用户点击删除按钮时,οnclick="showDeleteModal(this)"中的this指向被点击的按钮本身,作为参数传递给函数。
- $(obj):将obj(删除按钮)转换为jQuery对象。
- .parent():获取删除按钮的父元素,即所在的操作列<td>。
- .children():获取该<td>下的所有直接子元素(包括修改按钮、删除按钮、隐藏的<input>)。
最终结果:$tds是一个包含三个元素的jQuery集合,按顺序为:
第一个子元素:修改按钮的<a>标签,
第二个子元素:删除按钮的<a>标签(即obj自身),
第三个子元素:隐藏的<input>(存储角色ID)。
后续通过$($tds[2]).val()获取隐藏输入框的值(即角色ID)。
语句:
var delete_id = $($tds[2]).val();// 获取隐藏控件的ID
- $("#del_id"):通过 jQuery 的 ID 选择器找到 HTML 中 id="del_id" 的元素。
- .val(delete_id):将变量 delete_id 的值(即要删除的角色 ID)设置到该元素中(等价于修改其 value 属性)
语句:
$("#delModal").modal({
backdrop: 'static',
keyboard: false
});
代码作用:
这段代码通过 jQuery 调用 Bootstrap 的模态框(Modal)组件,并配置了两个关键参数:
- backdrop: 'static'
- keyboard: false
它的目的是以 强制交互模式 弹出一个模态框,用户必须明确点击模态框内的按钮(如“确认”或“取消”)才能关闭模态框,无法通过点击背景或按键盘 ESC 键关闭。
参数详解:
① backdrop: 'static'
- 默认行为:当 backdrop: true(默认值)时,模态框会显示一个半透明背景层,且点击背景层会自动关闭模态框。
- static的行为:
- 背景层依然显示,但点击背景层不会关闭模态框。
- 用户只能通过模态框内的按钮操作关闭模态框。
- 适用场景:需要强制用户明确操作(如确认删除)时使用。
②keyboard: false
- 默认行为:当 keyboard: true(默认值)时,按下键盘的 ESC 键 会关闭模态框。
- false的行为:
- 禁用 ESC 键关闭模态框的功能。
- 适用场景:防止用户误触 ESC 键导致意外关闭关键操作(如删除确认)。
对应的 HTML 元素:
在 HTML 中,需存在一个 id 为 delModal 的模态框结构,例如:
<!-- 删除确认模态框 -->
<div class="modal fade" id="delModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<!-- 模态框内容(标题、消息、按钮等) -->
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
</div>
<div class="modal-body">
<p>确定要删除此数据吗?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
</div>
</div>
</div>
</div>
实际效果:
- 用户点击删除按钮 → 弹出 delModal 模态框。
- 用户无法通过以下方式关闭模态框:
- 点击模态框外的背景区域(backdrop: 'static')。
- 按下键盘 ESC 键(keyboard: false)。
- 用户必须明确选择:
- 点击“取消” 按钮(关闭模态框)。
- 点击“确认删除” 按钮(执行删除操作)。
在信息删除确认的模态框中,有这样的代码:
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<a id="delButton" class="btn btn-success" data-dismiss="modal">确定</a>
</div>
其中的data-dismiss="modal" 是 Bootstrap 框架中用于关闭模态框(Modal)的一个关键属性。
作用与用途:
- 功能:当用户点击带有 data-dismiss="modal" 属性的元素(如按钮或链接)时,Bootstrap 会自动关闭当前显示的模态框。
- 目的:无需编写额外 JavaScript 代码即可实现模态框的关闭逻辑,简化开发流程。
在 Bootstrap 模态框中的应用:
假设有以下模态框结构(简化示例):
<!-- 模态框容器 -->
<div class="modal fade" id="exampleModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">标题</h5>
<!-- 关闭按钮(右上角的 ×) -->
<button type="button" class="close" data-dismiss="modal">
<span>×</span>
</button>
</div>
<div class="modal-body">
<p>模态框内容...</p>
</div>
<div class="modal-footer">
<!-- 关闭按钮(底部) -->
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary">保存</button>
</div>
</div>
</div>
</div>
- 关键元素:
- 右上角的 “×” 按钮和底部的 “关闭” 按钮均添加了data-dismiss="modal"。
- 用户点击这两个按钮时,Bootstrap 会直接关闭模态框。
注意事项
- 依赖关系:
- 必须正确引入Bootstrap 的 JavaScript 文件,否则 data-dismiss 无法生效。
- 模态框的 HTML 结构需符合 Bootstrap 规范(如包含.modal 类)。
- Bootstrap 版本兼容性:
- Bootstrap 4 和 5:均支持 data-dismiss="modal"。
- Bootstrap 5 的变动:在 Bootstrap 5 中,某些 JavaScript 方法和属性可能有调整,但data-dismiss 的行为保持不变。
- 自定义关闭逻辑:
- 如果需要执行额外操作(如关闭前验证数据),可以结合 JavaScript 事件监听:
$('#exampleModal').on('hide.bs.modal', function (e) {
if (!validateForm()) {
e.preventDefault(); // 阻止关闭
}
});
示例场景:
- 场景:用户点击 “关闭” 按钮后,模态框消失。
- 代码实现:直接在按钮上添加 data-dismiss="modal":
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
在模态框的确定按钮的点击事件的JavaScript代码中:
$(function () {
// 模态框的确定按钮的点击事件
$("#delButton").click(function () {
var id = $("#del_id").val();
console.log("del" + id)
// ajax异步删除
$.ajax({
url: "/pub_del",
type: "POST",
data:{p_id:id},
success: function (result) {
if (result.code == "200") {
$("#delModal").modal("hide");
alert(result.message);
window.location.href = "{{ url_for('pub_list') }}";
}
else{
alert(result.message);
}
}
})
});
});
语句:
$("#delModal").modal("hide");
- $("#delModal"):通过 jQuery 选择器选中 HTML 中 id="delModal" 的模态框容器元素。
- .modal("hide"):调用 Bootstrap 的 modal 方法,并传递参数 "hide",表示关闭(隐藏)该模态框。
与其他关闭方式的区别:
| 关闭方式 | 描述 | 是否需要编码 |
| $("#delModal").modal("hide") | 通过 JavaScript 主动触发关闭逻辑,需手动调用。 | 是(需编写代码) |
| data-dismiss="modal" | 点击按钮或链接时自动关闭模态框(依赖 HTML 属性)。 | 否(无需编码) |
| 点击模态框外部(背景) | 默认行为下,点击模态框外的背景区域会关闭模态框(需设置backdrop: true)。 | 否(依赖配置) |
关于《Flask2+Vue.js实战派》第8章的笔记暂时就写到这里,欢迎各位同学补充。

1439

被折叠的 条评论
为什么被折叠?



