复合主键必须在Column定义时显式声明primary_key=True,所有主键列均需标注,不可后期追加;UniqueConstraint仅提供唯一性校验,不替代主键语义。

复合主键必须在 Column 定义时声明 primary_key=True
SQLAlchemy 不支持后期用 __table_args__ 或其他方式“追加”复合主键。主键列必须在 Column 初始化时就带上 primary_key=True,否则 ORM 会报错或生成错误的 DDL。
常见错误现象:sqlalchemy.exc.ArgumentError: Mapper mapped class X could not assemble any primary key columns,本质是漏标了任一主键列。
- 所有参与复合主键的列,都要显式写
primary_key=True - 不能只靠
UniqueConstraint(..., name="uq_x_y")模拟主键行为 —— 它不提供主键语义(如自动NOT NULL、外键引用能力) - 若用 Core 的
Table方式定义,同样需在每个主键Column上设primary_key=True
示例(正确):
class OrderItem(Base):
__tablename__ = "order_items"
order_id = Column(Integer, primary_key=True)
product_id = Column(Integer, primary_key=True)
quantity = Column(Integer)
联合唯一索引要用 UniqueConstraint,不是 Index
Index 和 UniqueConstraint 在 DDL 层级效果看似一样,但语义和 ORM 行为完全不同:前者仅加速查询,后者才触发数据库级唯一性校验与 Python 层异常捕获。
立即学习“Python免费学习笔记(深入)”;
使用场景:你想防止插入重复的 (user_id, role) 组合,但这两列本身不构成主键(比如还有自增 id 主键)。
- 必须用
UniqueConstraint("user_id", "role", name="uq_user_role"),写在__table_args__里 - 别用
Index("ix_user_role", "user_id", "role", unique=True)—— 它不会阻止重复插入,只建索引 - MySQL/PostgreSQL 对
UniqueConstraint的违反会抛出IntegrityError,可被捕获处理
示例:
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
class UserRole(Base):
__tablename__ = "user_roles"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, nullable=False)
role = Column(String(32), nullable=False)
__table_args__ = (
UniqueConstraint("user_id", "role", name="uq_user_role"),
)
复合主键影响 relationship 和 foreign key 声明方式
一旦用了复合主键,外键引用就必须对齐全部主键列,且 ForeignKeyConstraint 是唯一合法写法 —— 单个 ForeignKey 只能指向单列主键。
容易踩的坑:直接写 ForeignKey("orders.order_id") 而忽略 ref_num,导致外键约束无效或迁移失败。
- 被引用表的主键是多列 → 引用方必须用
ForeignKeyConstraint显式列出所有列及其目标 -
relationship()的foreign_keys参数也要传对应列对象列表,不能只传一个 - 如果被引用表是 Core
Table(非 ORM 类),复合主键定义必须完全一致,否则ForeignKeyConstraint无法解析
示例(引用复合主键表 invoice):
class InvoiceItem(Base):
__tablename__ = "invoice_items"
item_id = Column(Integer, primary_key=True)
invoice_id = Column(Integer, nullable=False)
ref_num = Column(Integer, nullable=False)
__table_args__ = (
ForeignKeyConstraint(
["invoice_id", "ref_num"],
["invoice.invoice_id", "invoice.ref_num"]
),
)
联合唯一约束的 NULL 处理要特别注意
SQL 标准中,NULL 不参与唯一性比较 —— 所以 (1, NULL) 和 (1, NULL) 不算重复。但不同数据库实现有差异,尤其 PostgreSQL 默认把多个 NULL 视为不冲突,而 MySQL 5.7+ 在某些模式下可能表现不同。
如果你的业务逻辑要求“某列为空时也需全局唯一”,不能依赖 UniqueConstraint,得用函数索引或应用层控制。
- 避免在
UniqueConstraint中混用可空列,除非你明确接受多个NULL共存 - 想让
NULL也被视为值来去重?PostgreSQL 可建函数索引:Index("ix_uq_nonnull", func.coalesce(user_id, -1), role, unique=True) - MySQL 没有函数索引(8.0+ 支持,但限制多),更稳妥的是加检查约束或默认值兜底
真正难处理的从来不是怎么写约束,而是当约束冲突发生时,你有没有准备好捕获 IntegrityError 并区分到底是主键冲突、唯一索引冲突,还是外键缺失 —— 这些异常在不同方言里 message 字段格式不统一,靠字符串匹配不可靠,建议用 exc.orig 和 SQLSTATE 码做判断。

















