sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

🐛 Fix instance of related object added to session on validation

Open mskrip opened this issue 1 year ago • 0 comments

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')])`

mskrip avatar Aug 07 '24 12:08 mskrip