Python 现代开发学习笔记

(十一)把读取与写入保留在业务函数

1. 查询记录和核对归属

先把数据库行变成普通业务数据:

# 续写同一文件:shelf_project/src/shelfweb/db.py
def public_book(row):
    # API 和模板使用普通字典,不把数据库连接留给模板迭代。
    return {
        "id": row["id"],
        "title": row["title"],
        "finished": bool(row["finished"]),
        "tags": json.loads(row["tags"]),
    }
# 项目文件:shelf_project/src/shelfweb/service.py
import json
import sqlite3

from flask import abort

from .db import get_db, public_book


def list_books(owner_id, q="", finished="all"):
    clauses, parameters = ["owner_id=?"], [owner_id]
    if q:
        # SQL 参数绑定和 LIKE 通配符转义分别处理不同的解释过程。
        escaped = q.replace("!", "!!").replace("%", "!%").replace("_", "!_")
        clauses.append("title LIKE ? ESCAPE '!'")
        parameters.append(f"%{escaped}%")
    if finished != "all":
        clauses.append("finished=?")
        parameters.append(int(finished))
    query = "SELECT * FROM books WHERE " + " AND ".join(clauses) + " ORDER BY id"
    return [public_book(row) for row in get_db().execute(query, parameters).fetchall()]


def owned_book(book_id, owner_id):
    if book_id > 2**63 - 1:
        abort(404, description="没有找到这本书")
    row = get_db().execute("SELECT * FROM books WHERE id=?", (book_id,)).fetchone()
    if row is None:
        abort(404, description="没有找到这本书")
    if row["owner_id"] != owner_id:
        abort(403, description="这本书属于其他用户")
    return public_book(row)

列表查询先加 owner_id 条件,再按书名和已读状态筛选,最后按编号排序。clauses 中的 SQL 片段由程序提供,用户文本放进参数列表;LIKE 里的 %、_ 另外用 ! 转义,使输入百分号时查找实际包含百分号的书名。

owned_book 对找不到的记录返回 404,对其他用户的书返回 403。路径编号很大时,也统一作为未找到处理,再按可保存的数据库编号查询。public_book 稍后将行转成普通字典,恢复标签 JSON 和布尔状态,供页面和 JSON 接口复用。

2. 在成功返回之前完成事务

# 续写同一文件:shelf_project/src/shelfweb/service.py
def save_book(owner_id, draft, book_id=None):
    connection = get_db()
    try:
        # 退出 with 时提交;SQL 或后续操作出错则回滚。
        with connection:
            fields = (
                draft.title,
                int(draft.finished),
                json.dumps(draft.tags, ensure_ascii=False),
            )
            if book_id is None:
                cursor = connection.execute(
                    "INSERT INTO books(title,finished,tags,owner_id) VALUES (?,?,?,?)",
                    (*fields, owner_id),
                )
                book_id = cursor.lastrowid
            else:
                # 地址中的编号需要核对归属,实际 UPDATE 也携带用户条件。
                owned_book(book_id, owner_id)
                connection.execute(
                    "UPDATE books SET title=?,finished=?,tags=? WHERE id=? AND owner_id=?",
                    (*fields, book_id, owner_id),
                )
    except sqlite3.IntegrityError as exc:
        if exc.sqlite_errorname == "SQLITE_CONSTRAINT_UNIQUE":
            # 事务已经回滚,视图用字段错误重新显示用户这次的尝试值。
            return None, {"title": "书架里已经有同名的书"}
        raise
    return book_id, {}


def delete_book(book_id, owner_id):
    # 未找到或归属不符时,在 DELETE 执行之前结束。
    owned_book(book_id, owner_id)
    connection = get_db()
    with connection:
        connection.execute(
            "DELETE FROM books WHERE id=? AND owner_id=?", (book_id, owner_id)
        )

新增、修改和删除进入 with connection:。正常结束提交,异常离开回滚;这个 with 管理事务,实际关闭连接由 teardown_appcontext 完成。

fields 统一准备书名、整数状态和标签 JSON,(*fields, owner_id) 使用前面已经学过的拆包构造参数。新增后从 cursor.lastrowid 取得编号;修改和删除都核对归属,并在实际 SQL 中再次带上 owner_id 条件。

同名冲突在离开事务之后返回 errors,页面可以显示“已经有同名的书”。其他数据库异常继续抛出,交给后面的统一处理器。

把书籍 B 改为已有书名 A 时触发唯一键,事务回滚后返回表单错误,新请求仍读取 A 和 B;正常改为 C 则提交后跳转

笔记正文与全部题解 · Ctrl / ⌘ K