SQLModel 使用 Decimal 精确处理金额与财务数据:从 Field 配置到数据库存储的完整实践
2026/9/21 19:16:50 网站建设 项目流程
  • ORM
  • 数据库
  • 后端

【免费下载链接】sqlmodel

SQL databases in Python, designed for simplicity, compatibility, and robustness.

项目地址:https://gitcode.com/gh_mirrors/sq/sqlmodel
点击查看免费下载

本指南以 SQLModel 官方文档 Decimal Numbers 为核心,系统讲解如何在 SQLModel 模型中使用 Python 标准库decimal.Decimal类型来存储货币、价格、账户余额等对精度有严格要求的财务数据。读完本文,你将掌握Field()max_digitsdecimal_places参数的精确配置规则、合法的数值范围判断方法,以及 SQLModel 底层如何将Decimal映射为 SQLAlchemy 的DECIMAL/Numeric数据库列类型。

为什么财务数据需要 Decimal

在二进制计算机中,浮点数(float)的存储方式决定了它无法精确表达所有十进制小数。以最常见的例子来说,在 Python 中执行1.1 + 2.2,直觉上应该得到3.3,实际却得到:

>>> 1.1 + 2.2 3.3000000000000003

这是因为1.12.2在二进制中都是无限循环小数,只能近似存储,累加后误差便暴露出来。Python 提供了decimal标准库模块和Decimal类型来解决这类问题,它可以以十进制的形式严格保存数值,从而保证运算结果的确定性。

数据库在底层同样以二进制存储数据,因此也存在相同的问题,这也正是主流数据库都提供专用decimal类型的原因。对多数场景(如统计视频播放量、游戏角色血条)来说,浮点误差通常无关紧要;但对于货币、价格、账户余额这类涉及金钱与财务计算的业务,四舍五入误差是绝对不能接受的,此时就必须使用 Decimal。

SQLModel 中 Decimal 的类型基础

Pydantic 对Decimal类型有专门支持。当你在 SQLModel 模型字段上使用Decimal时,可以通过Field()函数指定该数值允许的总位数(digits)和小数位数(decimal places):

  • max_digits:数值允许的最大总位数,同时包含整数部分和小数部分
  • decimal_places:小数点右侧允许的小数位数。

这两个参数一方面由 Pydantic 在校验阶段(例如配合 FastAPI 做请求体校验时)强制执行,另一方面会被 SQLModel 原样传递给数据库列定义,让数据库层面的精度约束与模型层面的校验保持一致。

从源码看,SQLModel 的Field()签名完整接收max_digitsdecimal_places两个参数(见 sqlmodel/main.py),并在构造字段信息时将其原样转发给底层 Pydantic 元数据(见 sqlmodel/main.py)。在内部兼容层中,SQLModel 通过FakeMetadata类持有这两个属性供后续类型映射读取(见 sqlmodel/_compat.py)。

底层数据库类型映射

SQLModel 在将模型字段转换为数据库列时,会通过get_sqlalchemy_type()函数完成 Python 类型到 SQLAlchemy 类型的映射。当字段类型是Decimal时,映射逻辑如下(见 sqlmodel/main.py):

if issubclass(type_, Decimal): return Numeric( precision=getattr(metadata, "max_digits", None), scale=getattr(metadata, "decimal_places", None), )

也就是说,SQLModel 使用 SQLAlchemy 的DECIMAL类型(即Numeric)来表示 Decimal 字段,并将max_digits映射为 SQL 层面的precision(精度),将decimal_places映射为scale(小数位)。这意味着你在 Python 模型中声明的精度约束,会直接体现在数据库表结构中,形成端到端的精度保证。

在模型中使用 Decimal 字段

假设数据库中的每个 hero(英雄)都有一笔钱,我们可以将该字段声明为Decimal,并用Field()参数配置最大位数与小数位。完整示例代码如下(见 docs_src/advanced/decimal/tutorial001_py310.py):

from decimal import Decimal from sqlmodel import Field, Session, SQLModel, create_engine, select class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) money: Decimal = Field(default=0, max_digits=5, decimal_places=3) sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url, echo=True) def create_db_and_tables(): SQLModel.metadata.create_all(engine) def create_heroes(): hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1) hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001) hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2) with Session(engine) as session: session.add(hero_1) session.add(hero_2) session.add(hero_3) session.commit()

其中money: Decimal = Field(default=0, max_digits=5, decimal_places=3)的含义是:

  • max_digits=5money字段最多允许5 位数字,这 5 位同时包括整数部分(小数点左侧)和小数部分(小数点右侧)
  • decimal_places=3:小数点右侧最多3 位小数

因此,该字段的整数部分最多只能是5 - 3 = 2位,即数值范围为099.999之间(考虑符号则为-99.99999.999)。

合法的数值示例

✅ 以下数值对money字段都是合法的:

  • 12.345—— 5 位数字,其中整数 2 位、小数 3 位;
  • 12.3—— 不足 3 位小数时按 3 位小数存储(即12.300);
  • 12—— 无小数部分,整数 2 位;
  • 1.2—— 整数 1 位、小数 1 位;
  • 0.123—— 整数 0 位、小数 3 位;
  • 0—— 全零值。

非法的数值示例

🚫 以下数值对money字段都是非法的:

  • 1.2345—— 小数位数超过 3 位(4 位小数);
  • 123.234—— 总位数超过 5 位(整数部分 3 位 + 小数部分 3 位);
  • 123—— 虽然没有任何小数位,但字段仍为小数保留了 3 位,因此整数部分只能使用max_digits - decimal_places = 2位,而123有 3 位整数数字,超出限制。

提示:请务必根据自己应用的实际业务需求调整位数和小数位配置。例如货币场景通常需要decimal_places=2(分),而科学计算或高精度统计可能需要更多小数位。

创建带 Decimal 字段的模型数据

创建模型实例时,你完全可以直接传入普通的float数字,Pydantic 会自动将其转换为Decimal类型,SQLModel 再通过 SQLAlchemy 以Decimal类型存入数据库。例如上面的示例代码中:

hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1) hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001) hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)

这里传入的1.10.0012.2都是 Pythonfloat,经过 Pydantic 校验后被规范化为Decimal('1.100')Decimal('0.001')Decimal('2.200')存储。注意1.1被自动补零为1.100,以满足decimal_places=3的精度约定。

查询 Decimal 数据并验证精度

写入数据之后,再读取 Decimal 字段并参与运算,即可验证它确实规避了浮点数的舍入误差:

def select_heroes(): with Session(engine) as session: statement = select(Hero).where(Hero.name == "Deadpond") results = session.exec(statement) hero_1 = results.one() print("Hero 1:", hero_1) statement = select(Hero).where(Hero.name == "Rusty-Man") results = session.exec(statement) hero_2 = results.one() print("Hero 2:", hero_2) total_money = hero_1.money + hero_2.money print(f"Total money: {total_money}") def main(): create_db_and_tables() create_heroes() select_heroes() if __name__ == "__main__": main()

注意这里的hero_1.money + hero_2.money是两个Decimal对象直接相加,其结果依然是Decimal,因此不会产生浮点累加误差。

运行程序(示例中使用了 uv 作为包管理器,若你的环境不同可替换为python app.py):

$ uv run python app.py // 部分样板输出已省略 // The type of money is Decimal('1.100') Hero 1: id=1 secret_name='Dive Wilson' age=None name='Deadpond' money=Decimal('1.100') // 更多输出已省略 // The type of money is Decimal('1.100') Hero 2: id=3 secret_name='Tommy Sharp' age=48 name='Rusty-Man' money=Decimal('2.200') // 没有舍入误差,就是 3.3! Total money: 3.300

可以看到,最终输出是精确的3.300,而不是浮点运算时出现的3.3000000000000003。这正是 Decimal 类型在财务计算中的核心价值。

测试用例佐证

仓库中的测试文件 tests/test_advanced/test_decimal/test_tutorial001.py 对上述行为做了完整验证。测试用内存型 SQLite 引擎(sqlite://)替换示例中的文件数据库,然后运行示例的main(),并断言:

  • Hero 1money等于Decimal("1.100")
  • Hero 2money等于Decimal("2.200")
  • 两笔钱相加的结果打印为Total money: 3.300

这从自动化测试层面确认了:从 Python 类型转换、数据库存取到算术运算,Decimal 的精度在整个链路上都得到了保持。

重要警告:SQLite 不支持 Decimal

虽然 Decimal 类型在 Python 侧得到完整支持,但并非所有数据库都支持 Decimal 类型。需要特别注意的是:

SQLite 不支持 Decimal。在 SQLite 中,Decimal 会被转换为它支持的浮点型NUMERIC类型,这意味着精度保证在 SQLite 下无法落实。

这一点在示例代码中也能得到印证——示例默认使用sqlite:///database.db作为数据库引擎,因此实际的精度行为取决于底层数据库。好消息是,绝大多数其他 SQL 数据库(如 PostgreSQL、MySQL 等)都原生支持 Decimal 类型,在这些数据库上,SQLModel 会以真正的DECIMAL(precision, scale)列来存储,精度约束由数据库引擎强制执行。

因此,如果你的应用涉及金额等财务数据,建议在生产环境选择支持 Decimal 的数据库,并在部署前用真实数据库验证精度行为。

实战要点总结

  1. 何时使用 Decimal:涉及货币、价格、账户余额、税率等对舍入误差敏感的财务场景,一律使用Decimal;普通计数、度量等场景使用float即可。
  2. 字段声明方式money: Decimal = Field(default=0, max_digits=5, decimal_places=3),其中max_digits包含整数与小数全部位数,整数部分上限为max_digits - decimal_places
  3. 写入无需手动转换:创建模型时可以传float,Pydantic 自动转换为Decimal并补足小数位。
  4. 运算保持精确Decimal之间的加、减、乘、除不会引入浮点舍入误差,适合直接用于财务计算。
  5. 底层映射:SQLModel 将Decimal映射为 SQLAlchemy 的Numeric/DECIMAL类型,max_digits对应precisiondecimal_places对应scale(见 sqlmodel/main.py)。
  6. 数据库选型:SQLite 会把 Decimal 降级为浮点NUMERIC,精度无法保证;生产环境请选用支持 Decimal 的 SQL 数据库。
  7. 自动化验证:可以参考 tests/test_advanced/test_decimal/test_tutorial001.py 的写法,为财务字段编写精度断言测试,防止回归。
  • ORM
  • 数据库
  • 后端

【免费下载链接】sqlmodel

SQL databases in Python, designed for simplicity, compatibility, and robustness.

项目地址:https://gitcode.com/gh_mirrors/sq/sqlmodel
点击查看免费下载

相关推荐

上一篇:Android 12精确位置权限革命:AndPermission新特性深度解析
下一篇:k-skill 的 lotto-results 技能与 k-lotto 包:韩国 로또 6/45 开奖结果查询与号码大对照实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询