Administrator
Administrator
发布于 2026-10-31 / 0 阅读
0
0

Python 从入门到初级(16):SQLite 数据库:保存订单与参数化查询

本篇是《Python 从入门到初级》第 16 / 20 篇。示例使用 Python 3.12 兼容语法,完整目录结构与数据见配套源码包。

CSV 和 JSON 适合交换文件,但当数据需要反复查询、按条件更新并长期保存时,数据库会更方便。SQLite 以文件方式存储,通常不需要单独启动数据库服务,适合本地工具和学习。

订单数据写入本地 SQLite,再通过参数查询形成统计

1. Python 和数据库各负责什么

Python 负责组织流程、验证输入和处理结果,SQL 负责描述数据库中的写入与查询。本篇把已有订单 CSV 导入本地数据库,再按状态汇总分类金额。

示例继续使用 ops_tools/orders.py 验证数据,因此要保留第 13 篇的包结构。数据库放在自己的 output/orders.sqlite3,不连接线上服务。

先查看实际 SQLite 库版本:

python -c "import sqlite3; print(sqlite3.sqlite_version)"

本例使用的 UPSERT 语法需要 SQLite 3.24.0 或更新版本。Python 版本和底层 SQLite 版本不是同一个数字,特别是在自行编译或特殊发行环境中要区分。

2. 完整示例:导入与参数化查询

运行 python lesson16.py:

import sqlite3
from pathlib import Path
from ops_tools.orders import read_orders


def main():
    base = Path(__file__).resolve().parent
    output = base / "output"
    output.mkdir(exist_ok=True)
    orders = read_orders(base / "data" / "orders.csv")
    connection = sqlite3.connect(output / "orders.sqlite3")
    try:
        with connection:
            connection.execute("""
                CREATE TABLE IF NOT EXISTS orders (
                    order_id TEXT PRIMARY KEY,
                    category TEXT NOT NULL,
                    amount_cents INTEGER NOT NULL CHECK(amount_cents >= 0),
                    status TEXT NOT NULL
                )
            """)
            connection.executemany("""
                INSERT INTO orders(order_id, category, amount_cents, status)
                VALUES (:order_id, :category, :amount_cents, :status)
                ON CONFLICT(order_id) DO UPDATE SET
                    category=excluded.category,
                    amount_cents=excluded.amount_cents,
                    status=excluded.status
            """, orders)
        rows = connection.execute("""
            SELECT category, SUM(amount_cents)
            FROM orders WHERE status = ?
            GROUP BY category ORDER BY category
        """, ("paid",)).fetchall()
        print("已支付分类统计:", rows)
        print("数据库订单数:", connection.execute("SELECT COUNT(*) FROM orders").fetchone()[0])
        unsafe_looking_text = "paid' OR 1=1 --"
        count = connection.execute("SELECT COUNT(*) FROM orders WHERE status = ?",
                                   (unsafe_looking_text,)).fetchone()[0]
        print("特殊字符串作为参数的匹配数:", count)
    finally:
        connection.close()


if __name__ == "__main__":
    main()

干净的教学数据库中,预期订单数为 5,已支付分类金额仍是图书 5900、办公 12500、数码 19900 分。数据库输出的分类顺序不必和 CSV 输入顺序一致。

看起来像 SQL 的特殊字符串,作为 status 参数传入后,预期匹配数为 0,而不会变成另一段查询条件。

3. 为什么重复导入没有变成十笔

order_id 是主键,用来标识同一笔订单。插入遇到同一主键时,ON CONFLICT ... DO UPDATE 更新该记录的字段,而不是额外增加一行。

其中 excluded 表示本次准备写入的新值。这个模式便于重复运行教学导入,但不意味着数据库会自动与 CSV 完全同步:CSV 中没有出现的旧数据库记录,不会被本例删除。

语法与限制见 SQLite UPSERT 文档。真实导入还需要明确新旧数据优先级、删除规则和审计要求。

4. 值通过参数传,不拼接 SQL

WHERE status = ? 中的问号是占位符,具体值由后面的元组传入。单元素元组写成 ("paid",),末尾逗号不能忘。

不要把外部输入通过 f-string 或字符串相加拼进 SQL。参数化不仅处理引号,更让数据保持为数据。

占位符用于值,不能直接代替表名或列名。如果需要动态选择字段,通常要从程序自己定义的允许列表选择,而不是接受任意输入。

Python 数据库接口说明见 sqlite3 文档。

5. 提交、回滚和关闭是不同动作

with connection 管理事务的正常提交和异常回滚,但不会替你关闭连接。因此示例用 finally 调用 close。

表结构语句和写入语句的事务细节受连接模式影响,不应把一个 with 简单理解为“文件里所有操作都能整体撤销”。先明确哪些写入处于同一事务,再讨论失败后会留下什么。

此处 executemany 为每条记录绑定参数;fetchall 将查询结果读成列表。数据很大时,读取和写入方式都需要重新评估。

6. 练习与验证

连续运行两次,保持原始 CSV 不变,预期订单数仍为 5。把 A001 的金额改为 6000 分再运行,预期图书的已支付金额更新为 6000 分。练习后恢复 CSV,重新导入恢复基准。

如果自己另加过其他主键,数据库可能超过五行,这不是重复导入造成的。通过查询查明来源,再决定怎样维护教学数据,不要把清库当成唯一的验证方式。


上一篇:HTTP 与接口:读取商品并发送 JSON · 系列目录 · 下一篇:文件整理实战:先预览,再按类型复制


评论