Link Tables getting duplicate entries only under async? #1220
-
First Check
Commit to Help
Example Code"""Sample to show issues between Session and AsyncSession.
# Setup environment
uv init sqlmodel-link-table
cd sqlmodel-link-table
uv venv
source .venv/bin/activate
uv pip install sqlmodel aiosqlite greelet pytest pytest-asyncio
Copy this file to sqlmodel-link-table/test_session.py and run pytest.
$ pytest test_session.py -v
"""
from contextlib import asynccontextmanager, contextmanager
from typing import AsyncGenerator, Generator
from uuid import UUID, uuid4
import pytest
from sqlalchemy import UniqueConstraint
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine
class LinkOrgUser(SQLModel, table=True):
"""Roles a user has in an Organization."""
__tablename__: str = "organization_user" # type: ignore
org_id: UUID | None = Field(
default=None, foreign_key="organization.id", primary_key=True
)
user_id: UUID | None = Field(default=None, foreign_key="user.id", primary_key=True)
role: int # enum.IntFlag
org: "Organization" = Relationship(back_populates="users")
user: "User" = Relationship(back_populates="orgs")
class User(SQLModel, table=True):
"""User with relationships."""
__table_args__ = (UniqueConstraint("email"),)
id: UUID = Field(primary_key=True, default_factory=uuid4)
name: str = Field(max_length=64)
email: str | None = Field(default=None, max_length=255)
# Relationships
orgs: list[LinkOrgUser] = Relationship(
back_populates="user",
)
class Organization(SQLModel, table=True):
"""Organization with users."""
id: UUID = Field(primary_key=True, default_factory=uuid4)
name: str = Field(max_length=80)
users: list[LinkOrgUser] = Relationship(back_populates="org")
###########################################################
## pytest with synchronous database session ##
@contextmanager
def memory_session() -> Generator[Session]:
"""Syncronious database session."""
engine = create_engine("sqlite://", connect_args={"check_same_thread": False})
SQLModel.metadata.create_all(bind=engine)
with Session(engine) as session:
yield session
def test_link_sync():
with memory_session() as session:
org = Organization(name="Example", id=UUID(int=1))
aa = User(name="AA", id=UUID(int=2))
org.users.append(LinkOrgUser(org=org, user=aa, role=1))
session.add(org)
session.commit()
assert org.name == "Example"
assert 1 == len(org.users)
assert aa.id in [_.user.id for _ in org.users]
###########################################################
## pytest with asynchronous database session ##
@asynccontextmanager
async def async_memory_session() -> AsyncGenerator[AsyncSession]:
"""Async database session."""
url = "sqlite+aiosqlite:///:memory:"
engine = create_async_engine(url, echo=False, future=True)
async with async_sessionmaker(engine, expire_on_commit=False)() as session:
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
try:
yield session
finally:
print(f"{url} finished.")
await engine.dispose()
@pytest.mark.asyncio
async def test_link_async():
async with async_memory_session() as async_session:
org = Organization(name="Example", id=UUID(int=5))
aa = User(name="AA", id=UUID(int=6))
org.users.append(LinkOrgUser(org=org, user=aa, role=1))
async_session.add(org)
await async_session.commit()
assert org.name == "Example"
assert 1 == len(org.users)
assert aa.id in [_.user.id for _ in org.users] DescriptionI create an Organization and User objects. I then create a link table object using the two objects and the user's role. I commit and check the number of users in the Organization.users object. I expect to see one user, which I do when using a synchronous database session. However under an asynchronous database session, I see two duplicates.
There are no errors other than the assert in the failed test. I don't understand why one session works while the other doesn't. I must be doing something wrong, but what? Operating SystemLinux Operating System DetailsUp to date Arch SQLModel Version0.0.22 Python Version3.13.0 Additional ContextPackage Version aiosqlite 0.20.0 |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
The problem is in the line org.users.append(LinkOrgUser(org=org, user=aa, role=1)) Creating an instance of I have no idea why it works differently with non-async session. async with async_memory_session() as async_session:
org = Organization(name="Example", id=UUID(int=5))
aa = User(name="AA", id=UUID(int=6))
LinkOrgUser(org=org, user=aa, role=1) # <- this line was changed
async_session.add(org)
await async_session.commit()
assert org.name == "Example"
assert 1 == len(org.users)
assert aa.id in [_.user.id for _ in org.users] Are there any not lazy people here who want to check how it works with pure SQLAlchemy? |
Beta Was this translation helpful? Give feedback.
The problem is in the line
Creating an instance of
LinkOrgUser
will already linkUser
toOrganization
, but you append this object toorg.users
and it creates duplicates.I have no idea why it works differently with non-async session.
To fix your code example, just remove
org.users.append()
: