You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI+SQLAlchemy多对多关联:重复插入子项触发唯一约束错误

解决FastAPI+SQLAlchemy多对多关系中重复插入已存在子项的问题

问题描述

使用FastAPI和SQLAlchemy实现多对多关系存储时,已存在的子项会被当作新条目插入,触发主键唯一约束错误。首次运行接口可正常创建父项、子项及关联关系;第二次运行时,因尝试插入已存在ID的子项,报错如下:

sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: children.id
[SQL: INSERT INTO children (id, name) VALUES (?, ?)]
[parameters: (1, 'Child name')]

复现代码:

from fastapi import Depends, FastAPI
from sqlalchemy import Column, ForeignKey, Integer, create_engine, String, Table
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base

SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

children_parents = Table(
    "children_parents",
    Base.metadata,
    Column("child_id", ForeignKey("children.id"), primary_key=True),
    Column("parent_id", ForeignKey("parents.id"), primary_key=True)
)

class Parent(Base):
    __tablename__ = "parents"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)

    children = relationship(
        "Child", secondary=children_parents, back_populates="parents")

class Child(Base):
    __tablename__ = "children"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)

    parents = relationship(
        "Parent", secondary=children_parents, back_populates="children")

app = FastAPI()

Base.metadata.create_all(bind=engine)

@app.get("/")
async def root(db: SessionLocal = Depends(get_db)):
    parent = Parent(name="Parent name", children=[
        Child(id=1, name="Child name")])
    db.add(parent)
    db.commit()
    db.refresh(parent)
    return parent

解决方案

方案1:先查询复用已存在的子项

在创建父项前,先查询数据库中是否存在目标子项,存在则直接使用该实例,不存在再创建新实例:

@app.get("/")
async def root(db: SessionLocal = Depends(get_db)):
    # 查询是否存在id为1的子项
    child = db.query(Child).filter(Child.id == 1).first()
    if not child:
        child = Child(id=1, name="Child name")
        db.add(child)
        db.commit()
        db.refresh(child)
    
    # 创建父项并关联子项
    parent = Parent(name="Parent name", children=[child])
    db.add(parent)
    db.commit()
    db.refresh(parent)
    return parent

方案2:使用session.merge()自动处理实例

merge()方法会根据主键判断实例是否存在:存在则更新属性并关联,不存在则插入新实例:

@app.get("/")
async def root(db: SessionLocal = Depends(get_db)):
    # 用merge处理子项,自动识别是否已存在
    child = db.merge(Child(id=1, name="Child name"))
    
    parent = Parent(name="Parent name", children=[child])
    db.add(parent)
    db.commit()
    db.refresh(parent)
    return parent

关键原因说明

直接创建Child(id=1, name="Child name")实例时,SQLAlchemy的session会将其标记为新对象,即使id已存在。必须通过查询让session加载已存在的实例,或使用merge()让session自动识别实例状态,避免重复插入。

内容的提问来源于stack exchange,提问作者Steven

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 08:15:30