Flask 笔记十:把查询逻辑抽到 service,让 views 变薄 上一篇我们做了登录、Session 和login_required。路由能保护了但views.py往往还会越来越长读参数、拼 SQL、分页、再render_template全挤在一个函数里。这一篇做一件事把「怎么查数据」从视图里挪出去视图只负责「读请求 → 调函数 → 选模板」。例子仍是通用的Note备忘录不涉及任何真实业务。1. 学完后你能做什么分清 视图该写什么、service 该写什么新建note_service.py把列表查询抽成函数登录后 「只看自己的备忘录」 也放在 service 里同一个查询函数列表页和导出 可以共用知道什么时候 不必再抽一层2. 视图变胖通常长什么样第五篇你可能已经写过类似代码home.route(/notes/)login_requireddef note_list():q (request.args.get(q) or ).strip()page request.args.get(page, 1, typeint)user_id session.get(user_id)query Note.query.filter_by(user_iduser_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(pagepage, per_page10)return render_template(home/note_list.html,page_datapage_data,delete_formDeleteForm(),qq,)能跑。但再加「日期筛选」「置顶优先」「admin 后台也要同逻辑」时这段查询会 复制粘贴好几份改一处漏一处。问题不在 SQLAlchemy而在 职责混在一起层次该关心什么视图 viewsHTTP读参数、鉴权、redirect、选模板service业务查询过滤谁的数据、拼条件、分页模板展示3. 先建app/note_service.py新建文件专门放和Note有关的查询from sqlalchemy import or_from app.models import Notedef 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_iduser_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(pagepage, per_pageper_page)几个习惯关键字参数*, q...调用时一眼能看出传了什么返回数据这里是page_data不render_template函数名说清用途list_notes_for_user不是含糊的get_notes4. 视图变薄app/home/views.pyfrom flask import request, session, render_templatefrom app.auth_utils import login_requiredfrom app.forms import DeleteFormfrom app.note_service import list_notes_for_userhome.route(/notes/)login_requireddef 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, typeint)page_data list_notes_for_user(session[user_id],qq,date_fromdate_from,date_todate_to,pagepage,)return render_template(home/note_list.html,page_datapage_data,delete_formDeleteForm(),qq,date_fromdate_from,date_todate_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(idnote_id, user_iduser_id).first()编辑视图home.route(/notes/edit/int:note_id/, methods[GET, POST])login_requireddef 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.titleform.content.data row.contentif 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, formform, 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不必复制 SQLfrom app.note_service import list_notes_for_userdef export_my_notes_csv(user_id):# 不分页取全量per_page 设大或另写 list_notes_for_user_allpage_data list_notes_for_user(user_id, per_page10000)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.py或xxx_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 个坑坑 1service 里render_templateservice 应 返回数据渲染是视图的事。混在一起以后没法给 JSON API 复用。坑 2service 里flash/redirect同上属于 HTTP 层。service 返回None或抛自定义异常由视图决定怎么提示用户。坑 3过度抽象只有一条Note.query.get(id)不必再包三层。 重复第二次时再抽。坑 4忘记在 service 里过滤user_id登录只保证「你是谁」不保证「你能动别人的数据」。写操作、按 id 查单条都要带 user_id。坑 5service 之间循环 importnote_service调user_serviceuser_service又调note_service会炸。共用小逻辑可放models或utils.py大模块之间尽量 单向依赖。11. 和「大项目」的关系真实项目里常见分层名字更多Repository、DAO、Domain但入门阶段记住一条就够视图处理 Webservice 处理「查什么、怎么查」。你项目里若看到load_novel_chapter_view()、search_notes()这类函数套路相同视图短、查询集中、名字说清楚用途。不必急着学 Application Factory 或复杂架构先把重复的 SQL 从 views 挪出去收益已经很大。12. 小结记住四件事views — 读参数、鉴权、redirect、render_templateservice — 拼查询、分页、返回数据不读 sessionuser_id显式传入 — 列表、单条查询都要管数据归属重复再抽 — 别为单行查询建十层抽象