qinghuan
← 回到知识探索

后端现场 / 可下载小项目

列名后面写着 INTEGER,为什么还能存进一段文字?

对照普通表、STRICT 表和业务约束表,观察同一输入如何被转换、保留或拒绝,再判断前导零是否应该保留下来。

展开本文目录
  1. 下载完整练习包
  2. 普通列的声明,先影响转换倾向
  3. STRICT 拒绝不能转换的值,但仍允许合适的转换
  4. 类型、必填和取值范围要各自写清
  5. 编号中的前导零有含义时,先别把它当数量
  6. 一次运行,保存三张表的接受矩阵
  7. 已有表要调整时,先盘点现在存了什么
  8. 记住这三点
  9. 别把例子用过头
  10. 自己试一小步
  11. 查原始资料
开始前,会这些就够了
  • 知道数据库的一列可以保存多个值。
  • 了解字符串、整数和空值的区别。
  • Python 3.10+;内置 SQLite 必须为 3.37.0 或更新版本。
  • 需要补基础时:先熟悉 SQL 与数据库约束
遇到不熟悉的词,先看这 1 个解释
类型亲和
SQLite 普通表的列类型会影响它尝试怎样存储和转换输入,但不一定把每个值限制为一种存储类型。要区分列的声明、值实际存成什么,以及业务允许什么。
打开完整术语小抄 ↗

库存数量列写着 INTEGER,导入程序却成功放进了 many。也许驱动没有忽略建表语句,而是你把 SQLite 普通表的类型规则理解成了另一种数据库的规则。

这次把五个完全相同的输入送进三张表,并同时读取值与 typeof 结果。这样能分别看见:数据存成什么类型、能否为空,以及数值是否满足库存规则。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0;内置 SQLite 需 3.37.0 或更新版本。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • demo.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -c "import sqlite3; print(sqlite3.sqlite_version)"
python -B demo.py

解压到独立练习文件夹,在含 demo.py 的目录打开终端运行。SQLite内存库三种表规则、ANY差异与18项检查;未测试其他数据库和旧表迁移

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

普通列的声明,先影响转换倾向

在 SQLite 普通表中,INTEGER 声明让这一列偏向用整数保存合适的输入,这种偏向叫类型亲和。它并不单独构成“任何文字都必须拒绝”的承诺。参数 '012' 可以被解释为整数十二,于是落库后读到的是 12。

'many' 无法按这里的规则转为整数,普通 INTEGER 列会把它作为文本保存。脚本同时查询 qty 和 typeof(qty),就是为了避免只看打印结果猜类型;数字十二与字符串 '12' 在某些显示界面上看起来一样,后续比较和汇总却未必一样。

示例中的值全部通过占位符绑定,没有用拼接 SQL 制造错误。参数绑定解决的是值如何进入 SQL,不负责替你决定这一列可以接受什么业务数据。这两层职责要分别验证。

STRICT 拒绝不能转换的值,但仍允许合适的转换

STRICT 是按表启用的严格类型模式,从 SQLite 3.37.0 起提供。先运行 python --version,再运行 python -c "import sqlite3; print(sqlite3.sqlite_version)";第二条显示当前 Python 实际使用的 SQLite 版本,必须至少为 3.37.0。若版本过旧,请换用内置 SQLite 符合要求的 Python 环境,再执行本例。

同样输入 '012' 和 '12.0',STRICT INTEGER 列都能保存为整数 12;输入 'many' 则被拒绝。因此 STRICT 不是“Python 传来的对象必须本来就是 int”,也不是不做任何转换。数据库检查的是是否能按目标类型接受该值。

如果接口契约规定数量必须直接提交 JSON 整数,字符串 '12' 也应拒绝,那项检查需要在接口解析和验证层表达。不能仅通过这一张 STRICT 表反推出原始请求使用了哪一种 JSON 类型。

类型、必填和取值范围要各自写清

第二张表的 STRICT INTEGER 仍能接受 None 和 -2。空值是否允许由 NOT NULL 决定,负数是否允许由业务约束决定。第三张表因此同时声明 INTEGER NOT NULL CHECK(qty >= 0)。

三列输出构成一个小矩阵:'many' 在严格类型这一步被拦住;None 需要非空要求;-2 本来就是整数,却不符合非负库存。看到某次插入失败时,应说明究竟违反了哪一类要求,而不要把所有拒绝都归功于 STRICT。

本例的三张表都故意只有一个字段,便于对照。真实业务还可能需要跨字段规则,例如售出数量不能高于总数;类型检查并不会自动建立这些关系。数据库约束与入口校验也可互相补充,而不是二选一。

编号中的前导零有含义时,先别把它当数量

脚本另建两个 ANY 列,输入 '00123'。普通表的 ANY 按其亲和规则得到整数 123,STRICT 表的 ANY 保留文本 '00123'。相同类型名在这两种表模式下的行为并不相同,不能只看英文名字猜测。

如果 '00123' 是门店编码或固定格式编号,前导零可能属于标识本身;如果它是库存数量,转换为 123 可能正合要求。选类型前先写下这些字符是否可以无损丢掉,而不是见到全是数字就一律用整数。

本例用 ANY 观察保留行为,并不建议把所有列改成 ANY。一个明确只允许文本编码的业务字段,通常应直接表达其文本类型及长度、格式要求;保留多种存储类型是另一种明确的设计选择。

一次运行,保存三张表的接受矩阵

保存为 demo.py 后运行 python -B demo.py。程序只建立内存数据库,运行结束即释放。输出每一行都保留原始 Python 输入的表示,并打印数据库读出的值与存储类型。

本次实测 Python 3.14.0、SQLite 3.50.4,18 项检查全部完成。最后的 integrity_check 只用于确认当前练习数据库;最有解释力的证据仍是每个输入分别在三张表中的结果。

展开收起代码 · 小实验python · 63 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Observe SQLite storage classes, coercion, STRICT, and business constraints."""
from contextlib import closing
import sqlite3


def main():
    if sqlite3.sqlite_version_info < (3, 37, 0):
        raise SystemExit("This experiment requires SQLite 3.37.0 or later.")
    checks = 0

    def check(condition, message):
        nonlocal checks
        if not condition:
            raise AssertionError(message)
        checks += 1

    with closing(sqlite3.connect(":memory:", isolation_level=None)) as db:
        db.execute("CREATE TABLE ordinary(qty INTEGER)")
        db.execute("CREATE TABLE strict_qty(qty INTEGER) STRICT")
        db.execute("CREATE TABLE contract(qty INTEGER NOT NULL CHECK(qty >= 0)) STRICT")

        def insert(table, value):
            # Table names are fixed program constants, not user input.
            try:
                db.execute(f"INSERT INTO {table}(qty) VALUES(?)", (value,))
            except sqlite3.IntegrityError:
                return "rejected"
            row = db.execute(
                f"SELECT qty, typeof(qty) FROM {table} ORDER BY rowid DESC LIMIT 1"
            ).fetchone()
            return row

        cases = [
            ("012", (12, "integer"), (12, "integer"), (12, "integer")),
            ("12.0", (12, "integer"), (12, "integer"), (12, "integer")),
            ("many", ("many", "text"), "rejected", "rejected"),
            (None, (None, "null"), (None, "null"), "rejected"),
            (-2, (-2, "integer"), (-2, "integer"), "rejected"),
        ]
        for value, expected_plain, expected_strict, expected_contract in cases:
            ordinary = insert("ordinary", value)
            strict = insert("strict_qty", value)
            contract = insert("contract", value)
            check(ordinary == expected_plain, f"ordinary case {value!r}")
            check(strict == expected_strict, f"strict case {value!r}")
            check(contract == expected_contract, f"business contract case {value!r}")
            print(f"input={value!r}: ordinary={ordinary}; strict={strict}; contract={contract}")

        db.execute("CREATE TABLE any_plain(value ANY)")
        db.execute("CREATE TABLE any_strict(value ANY) STRICT")
        for table in ("any_plain", "any_strict"):
            db.execute(f"INSERT INTO {table} VALUES(?)", ("00123",))
        any_plain = db.execute("SELECT value, typeof(value) FROM any_plain").fetchone()
        any_strict = db.execute("SELECT value, typeof(value) FROM any_strict").fetchone()
        check(any_plain == (123, "integer"), "ordinary ANY applies numeric affinity")
        check(any_strict == ("00123", "text"), "STRICT ANY preserves the input storage class")
        print(f"ANY input='00123': ordinary={any_plain}; strict={any_strict}")
        check(db.execute("PRAGMA integrity_check").fetchone()[0] == "ok", "integrity check")
    print(f"PASS checks={checks}")


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中执行 python -B demo.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。SQLite内存库三种表规则、ANY差异与18项检查;未测试其他数据库和旧表迁移

运行后对照结果
input='012': ordinary=(12, 'integer'); strict=(12, 'integer'); contract=(12, 'integer')
input='12.0': ordinary=(12, 'integer'); strict=(12, 'integer'); contract=(12, 'integer')
input='many': ordinary=('many', 'text'); strict=rejected; contract=rejected
input=None: ordinary=(None, 'null'); strict=(None, 'null'); contract=rejected
input=-2: ordinary=(-2, 'integer'); strict=(-2, 'integer'); contract=rejected
ANY input='00123': ordinary=(123, 'integer'); strict=('00123', 'text')
PASS checks=18

已有表要调整时,先盘点现在存了什么

准备收紧现有数据库之前,可先按 typeof 和业务条件统计异常值,并抽查其来源。直接假设“列早就写了 INTEGER,所以里面都是真整数”,会把待清理的数据遗漏掉。

本文没有对旧业务库执行表迁移,也没有测 MySQL 或 PostgreSQL 的转换规则。迁移时应另外核对目标数据库版本、驱动绑定方式、旧数据修复和失败回退;这份 SQLite 矩阵只能作为你设计对应实验的方法。

记住这三点

  • SQLite 普通列的类型亲和与严格拒绝不兼容值是不同规则。
  • STRICT 仍可能进行允许的转换,也不代替 NOT NULL 和业务范围。
  • 检查落库类型与原始输入,才能解释前导零为什么消失。

别把例子用过头

  • 把绑定参数与业务校验当成同一件事。
  • 认为 STRICT INTEGER 会拒绝所有看起来像数字的字符串。
  • 认为存成整数就一定是合法库存。
  • 把本例 SQLite 规则套用到未实际测试的其他数据库。

自己试一小步

一份商品导入表同时有库存数量与六位门店编码,怎样让 '000123' 在两列中得到各自需要的处理?

  1. 先分别写清两个字段是否允许空值、前导零和小数。
  2. 为普通表、STRICT 表及带业务约束的表列出预测结果。
  3. 加入整数、数字字符串、无法转换的文本、空值与一个超出范围的输入。
  4. 运行后同时保存原始输入、数据库值、typeof 和拒绝原因。

做到这些,就算完成

  • 编码与数量不能只因为都由数字组成就使用相同规则。
  • 说明哪些要求在接口层检查,哪些由数据库负责。
  • 逐项对照预测与实测;若有差异,解释转换发生在哪里;若全部一致,选择一组最容易混淆的输入,说明判断依据。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。