当前位置: 首页 > news >正文

Flask 笔记十:把查询逻辑抽到 service,让 views 变薄

上一篇我们做了登录、Session 和@login_required。路由能保护了,但views.py往往还会越来越长:读参数、拼 SQL、分页、再render_template全挤在一个函数里。

这一篇做一件事:把「怎么查数据」从视图里挪出去,视图只负责「读请求 → 调函数 → 选模板」。

例子仍是通用的Note备忘录,不涉及任何真实业务。


1. 学完后你能做什么

  • 分清 视图该写什么、service 该写什么
  • 新建note_service.py,把列表查询抽成函数
  • 登录后 「只看自己的备忘录」 也放在 service 里
  • 同一个查询函数,列表页和导出 可以共用
  • 知道什么时候 不必再抽一层

2. 视图变胖,通常长什么样

第五篇你可能已经写过类似代码:

@home.route("/notes/")

@login_required

def note_list():

q = (request.args.get("q") or "").strip()

page = request.args.get("page", 1, type=int)

user_id = session.get("user_id")

query = Note.query.filter_by(user_id=user_id).order_by(Note.addtime.desc())

if q:

like = f"%{q}%"

query = query.filter(

or_(Note.title.like(like), Note.content.like(like))

)

page_data = query.paginate(page=page, per_page=10)

return render_template(

"home/note_list.html",

page_data=page_data,

delete_form=DeleteForm(),

q=q,

)

能跑。但再加「日期筛选」「置顶优先」「admin 后台也要同逻辑」时,这段查询会 复制粘贴好几份,改一处漏一处。

问题不在 SQLAlchemy,而在 职责混在一起:

层次该关心什么

视图 views

HTTP:读参数、鉴权、redirect、选模板

service

业务查询:过滤谁的数据、拼条件、分页

模板

展示


3. 先建app/note_service.py

新建文件,专门放和Note有关的查询:

from sqlalchemy import or_

from app.models import Note

def list_notes_for_user(

user_id: int,

*,

q: str = "",

date_from: str = "",

date_to: str = "",

page: int = 1,

per_page: int = 10,

):

"""某用户的备忘录列表(支持搜索、日期、分页)。"""

query = (

Note.query

.filter_by(user_id=user_id)

.order_by(Note.addtime.desc())

)

q = (q or "").strip()

if q:

like = f"%{q}%"

query = query.filter(

or_(Note.title.like(like), Note.content.like(like))

)

date_from = (date_from or "").strip()

date_to = (date_to or "").strip()

if date_from:

query = query.filter(Note.addtime >= date_from)

if date_to:

query = query.filter(Note.addtime <= date_to + " 23:59:59")

return query.paginate(page=page, per_page=per_page)

几个习惯:

  • 关键字参数(*, q=...):调用时一眼能看出传了什么
  • 返回数据(这里是page_data),不render_template
  • 函数名说清用途:list_notes_for_user,不是含糊的get_notes

4. 视图变薄

app/home/views.py

from flask import request, session, render_template

from app.auth_utils import login_required

from app.forms import DeleteForm

from app.note_service import list_notes_for_user

@home.route("/notes/")

@login_required

def note_list():

q = (request.args.get("q") or "").strip()

date_from = (request.args.get("date_from") or "").strip()

date_to = (request.args.get("date_to") or "").strip()

page = request.args.get("page", 1, type=int)

page_data = list_notes_for_user(

session["user_id"],

q=q,

date_from=date_from,

date_to=date_to,

page=page,

)

return render_template(

"home/note_list.html",

page_data=page_data,

delete_form=DeleteForm(),

q=q,

date_from=date_from,

date_to=date_to,

)

对比之前:中间一大段 SQL 没了,读起来像目录——先读参数,再调 service,再渲染。

登录保护仍放在 视图 + 装饰器;service 假定「调用方已经知道 user_id」,不读session(后面会说为什么)。


5. 单条查询也抽出来

编辑、删除前都要「按 id 取一条,且必须是本人的」:

def get_note_for_user(note_id: int, user_id: int):

"""取一条备忘录;不存在或不属于该用户则返回 None。"""

return Note.query.filter_by(id=note_id, user_id=user_id).first()

编辑视图:

@home.route("/notes/edit/<int:note_id>/", methods=["GET", "POST"])

@login_required

def note_edit(note_id):

user_id = session["user_id"]

row = get_note_for_user(note_id, user_id)

if not row:

flash("记录不存在或无权访问", "err")

return redirect(url_for("home.note_list"))

form = NoteForm()

if request.method == "GET":

form.title.data = row.title

form.content.data = row.content

if form.validate_on_submit():

row.title = form.title.data.strip()

row.content = (form.content.data or "").strip()

db.session.commit()

flash("保存成功", "ok")

return redirect(url_for("home.note_list"))

return render_template("home/note_form.html", form=form, title="编辑备忘录")

Note.query.get_or_404(note_id)更安全:别人的 id 不会误改,直接当「没有」处理。


6. service 为什么不读 session

新手常写:

def list_notes_for_user(...):

user_id = session.get("user_id") # 不推荐

短期省事,长期麻烦:

  • 批处理脚本、定时任务没有 HTTP 请求,没有 session
  • 单元测试要 mock session
  • 同一个函数不好区分「查 A 用户」还是「查 B 用户」

更好做法:谁调用谁传user_id。视图从 session 取,脚本从参数取,service 只认数字 id。

鉴权(有没有登录)留在 装饰器 / 视图;数据归属(这条是不是你的)放在 service 或视图里显式传 user_id。


7. 一个查询,多处复用

以后若要 导出 CSV,不必复制 SQL:

from app.note_service import list_notes_for_user

def export_my_notes_csv(user_id):

# 不分页,取全量:per_page 设大,或另写 list_notes_for_user_all

page_data = list_notes_for_user(user_id, per_page=10000)

rows = page_data.items

# 写 CSV ...

列表页、导出、后台统计,共用同一套过滤规则,改搜索逻辑只改 service 一处。


8. 文件怎么摆(入门够用)

不必搞复杂目录,小项目常见:

app/

├── home/

│ └── views.py # 前台路由

├── admin/

│ └── views.py # 后台路由(下一篇可拆 Blueprint)

├── models.py

├── forms.py

├── auth_utils.py # login_required

├── note_service.py # Note 相关查询

└── user_service.py # User 相关(可选)

命名习惯:xxx_service.pyxxx_queries.py都行,团队统一即可。

一个文件对应一块业务,别把所有表的查询塞进一个service.py几千行。


9. 流程示意

GET /notes/?q=会议

@login_required 确认已登录

views.note_list 读 request.args、session["user_id"]

list_notes_for_user(user_id, q="会议", ...)

返回 paginate 结果(不碰模板)

render_template("note_list.html", page_data=...)

GET /notes/edit/99/(别人的 id)

get_note_for_user(99, my_user_id) → None

flash + redirect(不暴露「有这条但你看不见」)


10. 新手常踩的 5 个坑

坑 1:service 里render_template

service 应 返回数据;渲染是视图的事。混在一起以后没法给 JSON API 复用。

坑 2:service 里flash/redirect

同上,属于 HTTP 层。service 返回None或抛自定义异常,由视图决定怎么提示用户。

坑 3:过度抽象

只有一条Note.query.get(id),不必再包三层。 重复第二次时再抽。

坑 4:忘记在 service 里过滤user_id

登录只保证「你是谁」,不保证「你能动别人的数据」。写操作、按 id 查单条都要带 user_id。

坑 5:service 之间循环 import

note_serviceuser_serviceuser_service又调note_service会炸。
共用小逻辑可放modelsutils.py;大模块之间尽量 单向依赖。


11. 和「大项目」的关系

真实项目里常见分层名字更多(Repository、DAO、Domain),但入门阶段记住一条就够:

视图处理 Web,service 处理「查什么、怎么查」。

你项目里若看到load_novel_chapter_view()search_notes()这类函数,套路相同:视图短、查询集中、名字说清楚用途。

不必急着学 Application Factory 或复杂架构;先把重复的 SQL 从 views 挪出去,收益已经很大。


12. 小结

记住四件事:

  1. views — 读参数、鉴权、redirect、render_template
  2. service — 拼查询、分页、返回数据;不读 session
  3. user_id显式传入 — 列表、单条查询都要管数据归属
  4. 重复再抽 — 别为单行查询建十层抽象
http://www.jsqmd.com/news/1076709/

相关文章:

  • 解锁GIS开发超能力:ArcObjects SDK 227个实战案例深度解析
  • 基于session的登录、登出(退出登录)、记住我
  • 目前正规的健身房推雪橇毯制造商哪家好
  • TorchDrift实战:PyTorch原生MMD数据漂移检测指南
  • 【AI大模型】国产模型入门:文心一言/通义千问API调用教程
  • Web登录绕过漏洞深度剖析:从信任链条断裂到服务器端权威验证的修复实践
  • 5分钟极速上手:FigmaCN中文翻译插件让设计工作流效率翻倍
  • ResNet50、YOLOv8与点云:民宿房源实景核验三大平台算法落地对比与工程实践
  • 2027最新软件工程毕业设计选题推荐
  • 115、PCIE surprise移除处理:一次真实的硬件调试笔记
  • AI写论文优选!4款AI论文写作工具,为写期刊论文提供新思路!
  • 反序列化漏洞深度解析:从原理到实战的攻防指南
  • 豆包生态GEO优化实战:EEAT信源体系下的品牌可见度提升策略
  • HR实操教程:怎样在招聘网站高效发布招聘信息
  • Netty第一章NIO,ByteBuffer 中,‌limit解释
  • 移动云主要服务哪些用户群体?
  • 如何在10分钟内搭建AI驱动的自动化测试平台:Testsigma完整实战指南
  • IDA Pro逆向分析:挖掘加密认证绕过漏洞的实战指南
  • Python毕业设计-基于 Python 的个性化书籍推荐管理系统设计与实现 基于 Python 的智能图书推荐管理系统设计与实现(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • TVA在物流分拣领域的独特价值(10)
  • 3步快速上手:无需训练的AI换脸工具终极指南
  • SVM实战调参指南:从过拟合到工业部署的27次踩坑总结
  • 计算机毕业设计之jsp基于SSM技术的定额成本管理系统设计与实现
  • 计算机Python毕设实战-基于 Python 的个性化阅读书籍推送系统设计与实现 基于 Python 的用户偏好书籍推荐管理系统设计与实现【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 电脑文件不小心删了怎么恢复?7种高分恢复技巧(2026年全新)
  • 剖析主流选型:微信小程序开发平台综合对比指南
  • Apex Legends压枪宏完整指南:告别后坐力困扰的终极解决方案
  • 我写了一个AI图像视频生成工具,免费API+本地部署,分享给大家
  • 2026年AI大模型接口中转服务全维度实测推荐:主流服务商性能成本场景适配完整指南
  • 高度测量用三维光学轮廓仪推荐:国产与进口能力对比分析