2.4 常用字段类型与列参数
本节是模型定义的"零件库":各种字段类型该怎么选、枚举和 JSON 怎么存、时间字段的最佳实践。可当参考手册反复查阅。
一、字符串类:String / Text
from sqlalchemy import String, Text
class Article(Base):
__tablename__ = "articles"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200)) # VARCHAR(200):有长度上限
content: Mapped[str] = mapped_column(Text) # TEXT:不限长度的大文本
选择原则:
| 场景 | 用什么 |
|---|---|
| 用户名、标题、邮箱、URL 等短文本 | String(n),给个合理上限 |
| 文章正文、评论、日志等长文本 | Text |
⚠️ MySQL 中
String不指定长度会建表报错,所以养成习惯:String永远写长度。SQLite 虽然不强制,但为了可移植性也写上。
二、数字类:int / float / Decimal
from decimal import Decimal
from sqlalchemy import Numeric, BigInteger
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
stock: Mapped[int] # INTEGER
weight: Mapped[float] # FLOAT,有精度误差
price: Mapped[Decimal] = mapped_column(Numeric(10, 2)) # 精确小数:共10位,小数2位
big_num: Mapped[int] = mapped_column(BigInteger) # 超大整数(如雪花ID)
💰 金额永远用
Decimal+Numeric,不要用 float! float 是二进制浮点数,0.1 + 0.2 == 0.30000000000000004,算钱会出事故。Numeric(10, 2)表示最多 10 位数字、其中 2 位小数(最大 99999999.99)。
三、布尔类:bool
is_active: Mapped[bool] = mapped_column(default=True)
is_deleted: Mapped[bool] = mapped_column(default=False)
数据库里通常存为 BOOLEAN 或 TINYINT(1),SQLAlchemy 自动转换成 Python 的 True/False。
四、时间类:datetime / date / time
from datetime import datetime, date
from sqlalchemy import func, DateTime
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
# 创建时间:数据库自动填当前时间
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
# 更新时间:每次 UPDATE 自动刷新
updated_at: Mapped[datetime] = mapped_column(
server_default=func.now(), onupdate=func.now()
)
# 只有日期没有时间
delivery_date: Mapped[Optional[date]]
# 带时区的时间(跨国项目推荐)
paid_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
时间字段三个最佳实践:
- created_at / updated_at 几乎每张表都该有,排查问题全靠它们
default=datetime.now传的是函数本身(不带括号!)。写成default=datetime.now()就变成了"程序启动那一刻的固定时间",这是经典错误- 认真做国际化的项目用 UTC 时间 +
DateTime(timezone=True)
五、枚举类:Enum(有限选项)
状态、类型这种"只有几个固定值"的字段,用 Python 枚举最安全:
import enum
class OrderStatus(enum.Enum):
PENDING = "pending" # 待支付
PAID = "paid" # 已支付
SHIPPED = "shipped" # 已发货
COMPLETED = "completed" # 已完成
CANCELLED = "cancelled" # 已取消
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
status: Mapped[OrderStatus] = mapped_column(default=OrderStatus.PENDING)
使用时是真正的枚举对象,写错值 IDE 直接报错:
order.status = OrderStatus.PAID # ✅
order.status = "paidd" # ❌ 类型检查器标红
# 查询所有已支付订单
stmt = select(Order).where(Order.status == OrderStatus.PAID)
六、JSON 类:灵活的半结构化数据
配置、标签、扩展属性等结构不固定的数据,可以存 JSON:
from sqlalchemy import JSON
from typing import Any
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
specs: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
p = Product(name="手机", specs={"color": "黑色", "storage": "256G", "5g": True})
session.add(p)
session.commit()
print(p.specs["storage"]) # 256G —— 读出来就是 Python dict
⚠️ 两个注意点:
default=dict传的是 dict 构造函数,不要写default={}(所有行会共享同一个字典对象,经典 Python 陷阱)- 原地修改 JSON 内容(
p.specs["color"] = "白")默认不会被 Session 察觉! 最稳妥的做法是整体替换:p.specs = {**p.specs, "color": "白"}
七、列参数完整速查
mapped_column(
String(50), # 类型(可省略,由 Mapped[] 推导)
primary_key=True, # 主键
autoincrement=True, # 自增(整数主键默认开启)
unique=True, # 唯一约束
index=True, # 普通索引
nullable=False, # 非空(通常用 Mapped[Optional[]] 控制)
default=..., # Python 侧默认值
server_default=..., # 数据库侧默认值(DDL 中的 DEFAULT)
onupdate=..., # 每次 UPDATE 时自动设置的值
comment="字段说明", # 数据库注释
name="real_col_name", # 数据库里的真实列名(与属性名不同时用)
)
八、什么列该加索引?
索引像书的目录:加速查询,但拖慢写入、占用空间。原则:
✅ 应该加索引的列:
- 经常出现在
where条件里的列(如email、status) - 经常用来排序的列(如
created_at) - 外键列(第 4 章会讲,SQLAlchemy 不会自动为外键建索引)
❌ 不该加的:
- 几乎不用于查询的列
- 值重复度极高的列(如只有两个值的
is_active,单独加索引意义不大) - 小表(几百行全表扫描比走索引还快)
# 单列索引
email: Mapped[str] = mapped_column(String(120), index=True)
# 联合索引(多列组合查询时用)
from sqlalchemy import Index
class Log(Base):
__tablename__ = "logs"
__table_args__ = (
Index("ix_logs_user_time", "user_id", "created_at"),
)
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int]
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
九、综合示例:一个"什么都有"的模型
import enum
from datetime import datetime
from decimal import Decimal
from typing import Any, Optional
from sqlalchemy import JSON, Numeric, String, Text, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class ProductStatus(enum.Enum):
DRAFT = "draft"
ON_SALE = "on_sale"
SOLD_OUT = "sold_out"
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
sku: Mapped[str] = mapped_column(String(32), unique=True, comment="商品编码")
name: Mapped[str] = mapped_column(String(100), index=True)
description: Mapped[Optional[str]] = mapped_column(Text)
price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
stock: Mapped[int] = mapped_column(default=0)
status: Mapped[ProductStatus] = mapped_column(default=ProductStatus.DRAFT)
specs: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
is_deleted: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
server_default=func.now(), onupdate=func.now()
)
def __repr__(self) -> str:
return f"Product(id={self.id}, sku={self.sku!r}, name={self.name!r})"
📝 本节小结
- 短文本
String(n)(永远写长度),长文本Text - 金额用
Decimal+Numeric,绝不用 float - 每张表标配
created_at(server_default=func.now())和updated_at(+onupdate) - 固定选项用
enum.Enum,灵活数据用JSON default=datetime.now不带括号;default=dict不写{}- 常被 where/排序 的列加
index=True
核心基础到此完成!接下来进入日常开发的主战场 → 3.1 增:插入数据