跳到主要内容

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))

时间字段三个最佳实践:

  1. created_at / updated_at 几乎每张表都该有,排查问题全靠它们
  2. default=datetime.now 传的是函数本身(不带括号!)。写成 default=datetime.now() 就变成了"程序启动那一刻的固定时间",这是经典错误
  3. 认真做国际化的项目用 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

⚠️ 两个注意点:

  1. default=dict 传的是 dict 构造函数,不要写 default={}(所有行会共享同一个字典对象,经典 Python 陷阱)
  2. 原地修改 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 条件里的列(如 emailstatus
  • 经常用来排序的列(如 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 插入数据