SQLAlchemy Many to Many – Unique Constraint

Question:

I am using FastAPI and SQLAlchemy to save a many to many relationship. I am getting an issue where the pre-existing child element is being inserted by SQLAlchemy as a new item rather than updating (or preferably leaving it alone).

I have put together the below code which replicates the issue.

The first time you run it, it works and creates the parent, the child and the relationship in the relationship table.

The second time your run it (which replicates my issue) it attempts to create the child again with the provided ID, which obviously causes an error as the ID already exists and is the primary key.

from fastapi import Depends, FastAPI
from sqlalchemy import Column, ForeignKey, Integer, create_engine, String, Integer, 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)
 
# Run route "/" twice and get:
# ----------------------------------------
 
# sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: children.id
# [SQL: INSERT INTO children (id, name) VALUES (?, ?)]
# [parameters: (1, 'Child name')]
# (Background on this error at: https://sqlalche.me/e/14/gkpj)
 
 
@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
Asked By: Steven

||

Answers:

Yeah, that might be confusing. When adding a m2m object with its related object, you either need that child object to be a result of a query or a new object of its own. SQLAlchemy (unfortunately) doesn’t work like you want it to work; creating a new Child object from scratch does not perform an ‘upsert’ but will try an insert. An easy way to circumvent this is to see if the (Child) object already exists and if not then only create a new instance. Like so:

from fastapi import Depends, FastAPI
from sqlalchemy import Column, ForeignKey, Integer, create_engine, String, Integer, 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, backref="parents")
 
class Child(Base):
    __tablename__ = "children"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
 
 
app = FastAPI()
 
Base.metadata.create_all(bind=engine)

 
@app.get("/")
async def root(db: SessionLocal = Depends(get_db)):
    child_1 = db.query(Child).filter(Child.id==1).first()
    if not child_1:
        child_1 = Child(id=1, name="Child name")

    parent = Parent(name="Parent name", children=[
        child_1])
 
    db.add(parent)
    db.commit()
    db.refresh(parent)
    return parent

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

(I simplified your model as well if that is not to your liking; it has nothing to do with the question at hand so ignore it if you will :))

Answered By: JarroVGIT
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.