sqlmodel
sqlmodel copied to clipboard
🐛 Fix instance of related object added to session on validation
Previously, when validating instance when a session was open and the model instance had a related object a new instance of this related object was created and added to the session.
I encountered similar issue described in https://github.com/tiangolo/sqlmodel/discussions/897 and narrowed it down to this replicatable issue:
UPD YuriiMotov: code example provided by author was invalid. Here is a working MRE from tests:
from typing import List, Optional
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
heroes: List["Hero"] = Relationship(back_populates="team")
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
engine = create_engine("sqlite://")
SQLModel.metadata.create_all(engine)
team = Team(name="team")
hero = Hero(name="hero", team=team)
with Session(engine) as session:
session.add(team)
session.add(hero)
session.commit()
with Session(engine) as session:
hero = session.get(Hero, 1)
assert session._is_clean()
new_hero = Hero.model_validate(hero)
assert session._is_clean() # AssertionError
assert id(new_hero) != id(hero)
assert id(new_hero.team) == id(hero.team)
original code example provided by author
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
heroes: list["Hero"] = Relationship(
back_populates="team",
)
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship(back_populates="heroes")
if __name__ == "__main__":
engine = create_engine(
"postgresql+psycopg://<user>:<password>@localhost:5432/sqlmodel-test",
)
# SQLModel.metadata.create_all(engine)
with Session(engine) as session:
hero = session.exec(select(Hero)).one()
print(f"{session.dirty=}") # prints `session.dirty=IdentitySet([])`
Hero.model_validate(hero, session=session)
print(f"{session.dirty=}") # prints `session.dirty=IdentitySet([Team(id=1, name='Team 1')])`