
字节笔记本
2026年10月7日 · 约 11 分钟读完
会话用完要关,编号别用字符串
把查询从手写 SQL 换成 SQLAlchemy 时,先把会话的开关写对。对话里的 get_db 创建 SessionLocal(),用 yield 交给路径函数,finally 里 close()。请求结束会话一定关。模型和多对多表、app_id 怎么递增,都依赖这次会话还活着,但不能依赖它在响应发出后还活着。

连接串不要把口令写死在默认值里
database.py 用 create_engine。URL 由环境变量拼出来:用户、口令、主机、端口、库名。样本里 getenv 的默认值是用户 root、口令 root、主机 localhost、端口 3306、库名 test。这组默认值只适合本机空库。进程若在没配环境变量时启动,会拿着 root 去连库。上线前拿掉口令的默认值,缺变量就拒绝启动。
sessionmaker(autocommit=False, autoflush=False, bind=engine) 之后,get_db 是依赖:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()路径函数写 db: Session = Depends(get_db)。不要在模块顶上建一个全局会话反复用。并发请求会串在同一个会话上。create_all 在样本里于导入时执行,它按模型建表,不是迁移工具。已有表的列变更它不会改。
多对多用中间表,序列化打开 orm_mode
Application 和 Tag 通过表 application_tags 相连。两边的列是 applications.app_id 和 tags.tag_id,类型都是 String(50)。关系用 relationship(..., secondary="application_tags", back_populates=...)。查应用时要标签,就走这条关系,而不是再写一条只按名字模糊匹配的 SQL。
Pydantic 模型上 class Config: orm_mode = True,响应模型才能从 ORM 对象读属性。没有这行,返回 db_app 时字段对不上。Application 的响应里带 tags: List[Tag]。列表接口若把标签一起序列化,每一行都会碰到关系。样本的列表查询没有预加载,标签会在序列化时再查。行数上来之后这是额外的查询。列表不需要标签时,响应模型不要带 tags。
创建应用时,样本取 app_id 最大的一行:order_by(app_id.desc()).first(),再 int(...) + 1,没有记录时从 "10000" 起。app_id 是字符串。按字符串降序,"999" 会排在 "10000" 前面,因为字符 '9' 大于 '1'。下一次编号会从错误的最大值加一。编号要用整数列,或者固定宽度再排序。两个请求同时读到同一个最大值,会插入同一个 app_id。列上有 unique=True,后一个会在 commit 失败。失败时样本没有 rollback。会话回到池里之前应 rollback,否则下一次使用会带着失败状态。
更新用 app.dict().items() 对模型 setattr。传入的键会整份覆盖,包括 status。调用方少传一个字段时,看 Pydantic 模型是不是有默认值。ApplicationBase 里 status 默认 1。更新时没写状态,可能被写回 1。部分更新要和全量更新分开模型。
删除是 db.delete 然后 commit。中间表若没有级联,先删应用会碰到外键。样本没有先清 application_tags。库若拒绝删除,错误会冒到框架的 500,而不是样本里的 404。404 只在 first() 为空时抛出,detail 是 Application not found。
过滤和分页共用同一个 query
列表先按 search、type、status 往 query 上加 filter,再 count(),再 order_by(id.desc()).offset().limit().all()。total_pages 用 (total + page_size - 1) // page_size。page 从 1 起,page_size 在 1 和 100 之间。搜索用 like(f"%{search}%") 打在名称和描述上。用户输入里的 % 会变成通配,匹配会变宽。这是 LIKE 的语义,不是另一套查询。
CORS 样本只放行 http://localhost:3000,并带上凭据。换域名时改这份列表,不要写成任意来源再同时 allow_credentials=True。

把一次创建走完再看失败分支
启动时若环境变量缺失,样本会连到 localhost:3306 的库 test,用户和口令都是 root。用一组错误口令启动,应当在进程起来之前失败,而不是连上之后再在第一个请求里报错。做到这一点就要去掉 getenv 的口令默认值。
请求进来,Depends(get_db) 执行生成器,路径函数拿到 db。创建样本的步骤是:按 app_id 字符串降序取第一行,没有则从 "10000" 起,加一后得到新的字符串 id,db.add,commit,refresh,返回。空表第一次得到 "10001"。表里若已有 "999" 和 "1000",字符串降序会把 "999" 排前,加一得到 "1000",和已有行冲突。这就是不要用字符串排序当序号的原因。改成整数列之后,desc() 才是数值意义下的最大。
两个请求同时计算同一个下一号,后提交的那个会撞上 unique=True。commit 抛错时样本没有 rollback。在 except 里 rollback 再抛出,会话才能在 finally 的 close() 之前回到干净状态。只 close 不 rollback,连接归还时可能仍带着失败的事务。
更新路径用 app.dict() 遍历键再 setattr。ApplicationBase.status 默认是 1。客户端只想改名称、JSON 里没写状态时,模型仍可能带上默认的 1,把原来的状态盖掉。部分更新用一份没有默认值的模型,没出现的字段不 setattr。
删除前 first() 为空则 404,detail 为 Application not found。找到了就 delete 并 commit。中间表 application_tags 还指着这个 app_id 时,数据库会拒绝。要先删关联行,或在外键上定义删除行为。样本两者都没写,所以删除可能以 500 结束,而不是返回 {"message": "Application deleted"}。
列表的 search 用 like 包上百分号,打在 name 和 description。type、status 用等号,且只在不是 None 时加条件。count 必须发生在 offset 之前,并且用的是已经过滤的同一个 query。先分页再计数,总数会变成当前页的行数。page_size 最大 100,page 最小 1。CORS 只列了 http://localhost:3000。上线域名要加进 allow_origins,不要改成星号再打开凭据。
会话关闭,编号用整数
每个请求用 get_db 打开会话,在 finally 里关闭。口令不要默认成 root。应用和标签走 application_tags。响应模型打开 orm_mode。app_id 不要按字符串取最大值再加一。并发插入撞上唯一约束时要 rollback。更新不要用带默认值的模型把没传的状态写回 1。列表的 count 和 offset 用同一条已经加过过滤的 query。
空库第一次创建,样本从 "10000" 加一得到 "10001"。表里同时有 "999" 和 "1000" 时,按字符串降序会选错最大值,随后的唯一约束会让后写入的请求失败。失败路径要 rollback。更新若走 ApplicationBase,记得 status 默认是 1,没传状态也可能被写回。删除找不到行时 404,文案是 Application not found。找得到但中间表仍引用 app_id 时,要先处理 application_tags,否则不是样本里的删除成功消息。口令默认值 root 不能留到上线。CORS 目前只有 http://localhost:3000。orm_mode 打开之后,列表模型若不需要标签,就不要包含 tags,避免每行再查一次关系。
连接默认值只能留在本机空库。端口、库名、用户和口令都有一份看起来能跑的默认。其中口令过于常见,缺环境变量时会真的拿去连接。上线构建应在缺少口令时直接退出。每个请求单独打开会话,结束时务必关闭,不要模块级复用。应用和标签的关联放在中间表,两边都是字符串标识。用字符串的字典序取最大编号,在三位和四位数字同时存在时会取错。随后唯一约束让后一次提交失败,失败时要先回滚再关闭。部分更新不要借用带默认状态的模型,否则没写状态也会被写回初始值。找不到记录时给出未找到。删除前先看中间表是否还引用它。列表先加条件,再数总数,再偏移,页大小有上限。打开凭据时,允许的来源保持明确的地址列表。会话用完即关。编号改成整数列之后,降序才是数值意义下的最大。



