Repository: litestar-org/litestar

Issue:
Question: problems with sqlalchemy repository update and lazy loading
I'm having trouble reading a field representing a many-to-many relationship after using the [repo `update` method](https://github.com/litestar-org/litestar/blob/main/litestar/contrib/sqlalchemy/repository/_async.py#L270). This is what I found out so far. 

My sqla repository has `statement=select(Item).options(selectinload(Item.tags))` in order to include the tags in the response, which works great for the `get` and `list` methods. 

When setting for the `lazy` arg for the `tags` relationship defined for the `Item` model, AKA configuring the loader strategy at [mapping time](https://docs.sqlalchemy.org/en/20/orm/queryguide/relationships.html#configuring-loader-strategies-at-mapping-time) :

- If I set the `lazy` arg to `"noload"`, the one that I desire, the response ends up with an empty tags list in the case I'm updating a field other than `tags`. I only get a valid response when the `tags` field itself is being updated.  

- If I set the `lazy` arg to `"selectin"`, then it works as expected for `update`, but then I also get tags for regular `select(Item)` statement, which is undesired. 

- If I leave the `lazy` arg empty/default/`"select"`, then I get:

```
sqlalchemy.orm.exc.DetachedInstanceError: Parent instance <Item at 0x7fb31aaea590> is not bound to a Session; lazy load operation of attribute 'tags' cannot proceed (Background on this error at: https://sqla
lche.me/e/20/bhk3)
```

I read through the above [error help](https://docs.sqlalchemy.org/en/20/errors.html#error-bhk3). I already have `expire_on_commit=False` and my sqla repository has `statement=select(Item).options(selectinload(Item.tags))`, but for some reason `update` doesn't care about the statement options even though it calls `get` internally, and, from what I can tell, instead uses the loader strategies configured at mapping time. 


Here's some of my code:

```python
item_tag = Table(
    "item_tag",
    UUIDBase.metadata,
    Column("item_id", ForeignKey("item.id"), primary_key=True),
    Column("tag_id", ForeignKey("tag.id"), primary_key=True),
)


class Item(UUIDBase):
    name: Mapped[str] = mapped_column(String(), unique=True)
    description: Mapped[str | None]
    tags: Mapped[list[Tag]] = relationship(
        secondary=item_tag,
        back_populates="items"
        lazy="noload"  # <-- here be problems
    )


class Tag(UUIDBase):
    name: Mapped[str] = mapped_column(String(), unique=True,
    items: Mapped[list[Item]] = relationship(
        secondary=item_tag,
        back_populates="tags",
        lazy="noload"
    )


@patch(
    path="/{item_id:uuid}",
)
async def update_item(
    item_repo: ItemRepository,
    tag_repo: TagRepository,
    data: ItemUpdate,
    item_id: UUID = Parameter(
        title="Item ID",
        description="The item to update",
    ),
) -> ItemDB:
    dd = data.dict(exclude_unset=True)
    dd.update({"id": item_id})
    if "tag_names" in dd:
        if len(dd["tag_names"]) != 0:
            dd["tags"] = await tag_repo.list(
                CollectionFilter("name", dd["tag_names"])
            )
        else:
            dd["tags"] = []
        del dd["tag_names"]
    obj = await item_repo.update(Item(**dd))
    await item_repo.session.commit()
    return parse_obj_as(ItemDB, obj)  # <-- here be errors

```

Hopefully that was clear and I provided enough context. I'm a bit lost on how to solve this and wondering if it may indicate an issue with the method's implementation itself. 

<!-- POLAR PLEDGE BADGE START -->
## Funding
* You can sponsor this specific effort via a [Polar.sh](https://polar.sh) pledge below
* We receive the pledge once the issue is completed & verified

<a href="https://polar.sh/litestar-org/litestar/issues/1895">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/1895/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/1895/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->

StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:
Out of curiosity, does this issue persist if the `refresh` method on the `update` is not called?

@peterschutt, this error looks familiar to me, but I haven't seen it in a while.  This may have something to do with why I removed the `refresh`.
> Out of curiosity, does this issue persist if the `refresh` method on the `update` is not called?
> 
> @peterschutt, this error looks familiar to me, but I haven't seen it in a while. This may have something to do with why I removed the `refresh`.

I initially noticed this on [v2.0.0beta2](https://github.com/litestar-org/litestar/releases/tag/v2.0.0beta2), that didn't have the refresh. I updated the litestar dep to point to main and the behavior persists. 

I'll try to set up a test repo. 
I've actually almost finished setting up the test case. I'll link the PR here in just a moment in case you want to test as well.
I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/contrib/sqlalchemy/repository/_async.py b/litestar/contrib/sqlalchemy/repository/_async.py
--- a/litestar/contrib/sqlalchemy/repository/_async.py
+++ b/litestar/contrib/sqlalchemy/repository/_async.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Generic, Literal, cast
+from typing import TYPE_CHECKING, Any, Generic, Iterable, Literal, cast
 
 from sqlalchemy import Result, Select, delete, over, select, text, update
 from sqlalchemy import func as sql_func
@@ -31,16 +31,31 @@ class SQLAlchemyAsyncRepository(AbstractAsyncRepository[ModelT], Generic[ModelT]
 
     match_fields: list[str] | str | None = None
 
-    def __init__(self, *, statement: Select[tuple[ModelT]] | None = None, session: AsyncSession, **kwargs: Any) -> None:
+    def __init__(
+        self,
+        *,
+        statement: Select[tuple[ModelT]] | None = None,
+        session: AsyncSession,
+        auto_expunge: bool = False,
+        auto_refresh: bool = True,
+        auto_commit: bool = False,
+        **kwargs: Any,
+    ) -> None:
         """Repository pattern for SQLAlchemy models.
 
         Args:
             statement: To facilitate customization of the underlying select query.
             session: Session managing the unit-of-work for the operation.
+            auto_expunge: Remove object from session before returning.
+            auto_refresh: Refresh object from session before returning.
+            auto_commit: Commit objects before returning.
             **kwargs: Additional arguments.
 
         """
         super().__init__(**kwargs)
+        self.auto_expunge = auto_expunge
+        self.auto_refresh = auto_refresh
+        self.auto_commit = auto_commit
         self.session = session
         self.statement = statement if statement is not None else select(self.model_type)
         if not self.session.bind:
@@ -49,44 +64,63 @@ def __init__(self, *, statement: Select[tuple[ModelT]] | None = None, session: A
             raise ValueError("Session improperly configure")
         self._dialect = self.session.bind.dialect
 
-    async def add(self, data: ModelT) -> ModelT:
+    async def add(
+        self,
+        data: ModelT,
+        **kwargs: Any,
+    ) -> ModelT:
         """Add `data` to the collection.
 
         Args:
             data: Instance to be added to the collection.
+            **kwargs: Additional arguments.
 
         Returns:
             The added instance.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
+        auto_refresh = kwargs.pop("auto_refresh", self.auto_refresh)
         with wrap_sqlalchemy_exception():
             instance = await self._attach_to_session(data)
-            await self.session.flush()
-            await self.session.refresh(instance)
-            self.session.expunge(instance)
+            await self._flush_or_commit(auto_commit=auto_commit)
+            await self._refresh(instance, auto_refresh=auto_refresh)
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
-    async def add_many(self, data: list[ModelT]) -> list[ModelT]:
+    async def add_many(
+        self,
+        data: list[ModelT],
+        **kwargs: Any,
+    ) -> list[ModelT]:
         """Add Many `data` to the collection.
 
         Args:
             data: list of Instances to be added to the collection.
-
+            **kwargs: Additional arguments.
 
         Returns:
             The added instances.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             self.session.add_all(data)
-            await self.session.flush()
+            await self._flush_or_commit(auto_commit=auto_commit)
             for datum in data:
-                self.session.expunge(datum)
+                self._expunge(datum, auto_expunge=auto_expunge)
             return data
 
-    async def delete(self, item_id: Any) -> ModelT:
+    async def delete(
+        self,
+        item_id: Any,
+        **kwargs: Any,
+    ) -> ModelT:
         """Delete instance identified by ``item_id``.
 
         Args:
             item_id: Identifier of instance to be deleted.
+            **kwargs: Additional arguments.
 
         Returns:
             The deleted instance.
@@ -94,23 +128,32 @@ async def delete(self, item_id: Any) -> ModelT:
         Raises:
             NotFoundError: If no instance found identified by ``item_id``.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             instance = await self.get(item_id)
             await self.session.delete(instance)
-            await self.session.flush()
-            self.session.expunge(instance)
+            await self._flush_or_commit(auto_commit=auto_commit)
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
-    async def delete_many(self, item_ids: list[Any]) -> list[ModelT]:
+    async def delete_many(
+        self,
+        item_ids: list[Any],
+        **kwargs: Any,
+    ) -> list[ModelT]:
         """Delete instance identified by `item_id`.
 
         Args:
             item_ids: Identifier of instance to be deleted.
+            **kwargs: Additional arguments.
 
         Returns:
             The deleted instances.
 
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             instances: list[ModelT] = []
             chunk_size = 450
@@ -133,9 +176,9 @@ async def delete_many(self, item_ids: list[Any]) -> list[ModelT]:
                     await self.session.execute(
                         delete(self.model_type).where(getattr(self.model_type, self.id_attribute).in_(chunk))
                     )
-            await self.session.flush()
+            await self._flush_or_commit(auto_commit=auto_commit)
             for instance in instances:
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
             return instances
 
     async def exists(self, **kwargs: Any) -> bool:
@@ -164,12 +207,13 @@ async def get(self, item_id: Any, **kwargs: Any) -> ModelT:
         Raises:
             NotFoundError: If no instance found identified by `item_id`.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             statement = kwargs.pop("statement", self.statement)
             statement = self._filter_select_by_kwargs(statement=statement, **{self.id_attribute: item_id})
             instance = (await self._execute(statement)).scalar_one_or_none()
             instance = self.check_not_found(instance)
-            self.session.expunge(instance)
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
     async def get_one(self, **kwargs: Any) -> ModelT:
@@ -184,12 +228,13 @@ async def get_one(self, **kwargs: Any) -> ModelT:
         Raises:
             NotFoundError: If no instance found identified by `item_id`.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             statement = kwargs.pop("statement", self.statement)
             statement = self._filter_select_by_kwargs(statement=statement, **kwargs)
             instance = (await self._execute(statement)).scalar_one_or_none()
             instance = self.check_not_found(instance)
-            self.session.expunge(instance)
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
     async def get_one_or_none(self, **kwargs: Any) -> ModelT | None:
@@ -201,27 +246,38 @@ async def get_one_or_none(self, **kwargs: Any) -> ModelT | None:
         Returns:
             The retrieved instance or None
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             statement = kwargs.pop("statement", self.statement)
             statement = self._filter_select_by_kwargs(statement=statement, **kwargs)
             instance = (await self._execute(statement)).scalar_one_or_none()
             if instance:
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
             return instance  # type: ignore
 
     async def get_or_create(
-        self, match_fields: list[str] | str | None = None, upsert: bool = True, **kwargs: Any
+        self,
+        match_fields: list[str] | str | None = None,
+        upsert: bool = True,
+        attribute_names: Iterable[str] | None = None,
+        with_for_update: bool | None = None,
+        **kwargs: Any,
     ) -> tuple[ModelT, bool]:
         """Get instance identified by ``kwargs`` or create if it doesn't exist.
 
         Args:
             match_fields: a list of keys to use to match the existing model.  When empty, all fields are matched.
             upsert: When using match_fields and actual model values differ from `kwargs`, perform an update operation on the model.
+            attribute_names: an iterable of attribute names to pass into the ``update`` method.
+            with_for_update: indicating FOR UPDATE should be used, or may be a dictionary containing flags to indicate a more specific set of FOR UPDATE flags for the SELECT
             **kwargs: Identifier of the instance to be retrieved.
 
         Returns:
             a tuple that includes the instance and whether or not it needed to be created.  When using match_fields and actual model values differ from `kwargs`, the model value will be updated.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
+        auto_refresh = kwargs.pop("auto_refresh", self.auto_refresh)
         match_fields = match_fields or self.match_fields
         if isinstance(match_fields, str):
             match_fields = [match_fields]
@@ -242,9 +298,11 @@ async def get_or_create(
                 if field and field != new_field_value:
                     setattr(existing, field_name, new_field_value)
             existing = await self._attach_to_session(existing, strategy="merge")
-            await self.session.flush()
-            await self.session.refresh(existing)
-            self.session.expunge(existing)
+            await self._flush_or_commit(auto_commit=auto_commit)
+            await self._refresh(
+                existing, attribute_names=attribute_names, with_for_update=with_for_update, auto_refresh=auto_refresh
+            )
+            self._expunge(existing, auto_expunge=auto_expunge)
         return existing, False
 
     async def count(self, *filters: FilterTypes, **kwargs: Any) -> int:
@@ -267,12 +325,21 @@ async def count(self, *filters: FilterTypes, **kwargs: Any) -> int:
         results = await self._execute(statement)
         return results.scalar_one()  # type: ignore
 
-    async def update(self, data: ModelT) -> ModelT:
+    async def update(
+        self,
+        data: ModelT,
+        attribute_names: Iterable[str] | None = None,
+        with_for_update: bool | None = None,
+        **kwargs: Any,
+    ) -> ModelT:
         """Update instance with the attribute values present on `data`.
 
         Args:
             data: An instance that should have a value for `self.id_attribute` that exists in the
                 collection.
+            attribute_names: an iterable of attribute names to pass into the ``update`` method.
+            with_for_update: indicating FOR UPDATE should be used, or may be a dictionary containing flags to indicate a more specific set of FOR UPDATE flags for the SELECT
+            **kwargs: Additional arguments.
 
         Returns:
             The updated instance.
@@ -280,18 +347,27 @@ async def update(self, data: ModelT) -> ModelT:
         Raises:
             NotFoundError: If no instance found with same identifier as `data`.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
+        auto_refresh = kwargs.pop("auto_refresh", self.auto_refresh)
         with wrap_sqlalchemy_exception():
             item_id = self.get_id_attribute_value(data)
             # this will raise for not found, and will put the item in the session
             await self.get(item_id)
             # this will merge the inbound data to the instance we just put in the session
             instance = await self._attach_to_session(data, strategy="merge")
-            await self.session.flush()
-            await self.session.refresh(instance)
-            self.session.expunge(instance)
+            await self._flush_or_commit(auto_commit=auto_commit)
+            await self._refresh(
+                instance, attribute_names=attribute_names, with_for_update=with_for_update, auto_refresh=auto_refresh
+            )
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
-    async def update_many(self, data: list[ModelT]) -> list[ModelT]:
+    async def update_many(
+        self,
+        data: list[ModelT],
+        **kwargs: Any,
+    ) -> list[ModelT]:
         """Update one or more instances with the attribute values present on `data`.
 
         This function has an optimized bulk insert based on the configured SQL dialect:
@@ -301,6 +377,7 @@ async def update_many(self, data: list[ModelT]) -> list[ModelT]:
         Args:
             data: A list of instances to update.  Each should have a value for `self.id_attribute` that exists in the
                 collection.
+            **kwargs: Additional arguments.
 
         Returns:
             The updated instances.
@@ -308,6 +385,8 @@ async def update_many(self, data: list[ModelT]) -> list[ModelT]:
         Raises:
             NotFoundError: If no instance found with same identifier as `data`.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         data_to_update: list[dict[str, Any]] = [v.to_dict() if isinstance(v, self.model_type) else v for v in data]  # type: ignore
         with wrap_sqlalchemy_exception():
             if self._dialect.update_executemany_returning and self._dialect.name != "oracle":
@@ -319,15 +398,15 @@ async def update_many(self, data: list[ModelT]) -> list[ModelT]:
                         # https://github.com/sqlalchemy/sqlalchemy/discussions/9925
                     )
                 )
-                await self.session.flush()
+                await self._flush_or_commit(auto_commit=auto_commit)
                 for instance in instances:
-                    self.session.expunge(instance)
+                    self._expunge(instance, auto_expunge=auto_expunge)
                 return instances
             await self.session.execute(
                 update(self.model_type),
                 data_to_update,
             )
-            await self.session.flush()
+            await self._flush_or_commit(auto_commit=auto_commit)
             return data
 
     async def list_and_count(
@@ -348,6 +427,25 @@ async def list_and_count(
             return await self._list_and_count_basic(*filters, **kwargs)
         return await self._list_and_count_window(*filters, **kwargs)
 
+    def _expunge(self, instance: ModelT, auto_expunge: bool) -> None:
+        return self.session.expunge(instance) if auto_expunge else None
+
+    async def _flush_or_commit(self, auto_commit: bool) -> None:
+        return await self.session.commit() if auto_commit else await self.session.flush()
+
+    async def _refresh(
+        self,
+        instance: ModelT,
+        auto_refresh: bool,
+        attribute_names: Iterable[str] | None = None,
+        with_for_update: bool | None = None,
+    ) -> None:
+        return (
+            await self.session.refresh(instance, attribute_names=attribute_names, with_for_update=with_for_update)
+            if auto_refresh
+            else None
+        )
+
     async def _list_and_count_window(
         self,
         *filters: FilterTypes,
@@ -362,6 +460,7 @@ async def _list_and_count_window(
         Returns:
             Count of records returned by query using an analytical window function, ignoring pagination.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         statement = kwargs.pop("statement", self.statement)
         statement = statement.add_columns(over(sql_func.count(self.get_id_attribute_value(self.model_type))))
         statement = self._apply_filters(*filters, statement=statement)
@@ -371,7 +470,7 @@ async def _list_and_count_window(
             count: int = 0
             instances: list[ModelT] = []
             for i, (instance, count_value) in enumerate(result):
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
                 instances.append(instance)
                 if i == 0:
                     count = count_value
@@ -391,6 +490,7 @@ async def _list_and_count_basic(
         Returns:
             Count of records returned by query using 2 queries, ignoring pagination.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         statement = kwargs.pop("statement", self.statement)
         statement = self._apply_filters(*filters, statement=statement)
         statement = self._filter_select_by_kwargs(statement, **kwargs)
@@ -404,7 +504,7 @@ async def _list_and_count_basic(
             result = await self._execute(statement)
             instances: list[ModelT] = []
             for (instance,) in result:
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
                 instances.append(instance)
             return instances, count
 
@@ -418,6 +518,7 @@ async def list(self, *filters: FilterTypes, **kwargs: Any) -> list[ModelT]:
         Returns:
             The list of instances, after filtering applied.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         statement = kwargs.pop("statement", self.statement)
         statement = self._apply_filters(*filters, statement=statement)
         statement = self._filter_select_by_kwargs(statement, **kwargs)
@@ -426,10 +527,16 @@ async def list(self, *filters: FilterTypes, **kwargs: Any) -> list[ModelT]:
             result = await self._execute(statement)
             instances = list(result.scalars())
             for instance in instances:
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
             return instances
 
-    async def upsert(self, data: ModelT) -> ModelT:
+    async def upsert(
+        self,
+        data: ModelT,
+        attribute_names: Iterable[str] | None = None,
+        with_for_update: bool | None = None,
+        **kwargs: Any,
+    ) -> ModelT:
         """Update or create instance.
 
         Updates instance with the attribute values present on `data`, or creates a new instance if
@@ -439,6 +546,9 @@ async def upsert(self, data: ModelT) -> ModelT:
             data: Instance to update existing, or be created. Identifier used to determine if an
                 existing instance exists is the value of an attribute on `data` named as value of
                 `self.id_attribute`.
+            attribute_names: an iterable of attribute names to pass into the ``update`` method.
+            with_for_update: indicating FOR UPDATE should be used, or may be a dictionary containing flags to indicate a more specific set of FOR UPDATE flags for the SELECT
+            **kwargs: Instance attribute value filters.
 
         Returns:
             The updated or created instance.
@@ -446,11 +556,16 @@ async def upsert(self, data: ModelT) -> ModelT:
         Raises:
             NotFoundError: If no instance found with same identifier as `data`.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
+        auto_refresh = kwargs.pop("auto_refresh", self.auto_refresh)
         with wrap_sqlalchemy_exception():
             instance = await self._attach_to_session(data, strategy="merge")
-            await self.session.flush()
-            await self.session.refresh(instance)
-            self.session.expunge(instance)
+            await self._flush_or_commit(auto_commit=auto_commit)
+            await self._refresh(
+                instance, attribute_names=attribute_names, with_for_update=with_for_update, auto_refresh=auto_refresh
+            )
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
     def filter_collection_by_kwargs(  # type:ignore[override]
diff --git a/litestar/contrib/sqlalchemy/repository/_sync.py b/litestar/contrib/sqlalchemy/repository/_sync.py
--- a/litestar/contrib/sqlalchemy/repository/_sync.py
+++ b/litestar/contrib/sqlalchemy/repository/_sync.py
@@ -2,7 +2,7 @@
 # litestar/contrib/sqlalchemy/repository/_async.py
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Generic, Literal, cast
+from typing import TYPE_CHECKING, Any, Generic, Iterable, Literal, cast
 
 from sqlalchemy import Result, Select, delete, over, select, text, update
 from sqlalchemy import func as sql_func
@@ -33,16 +33,31 @@ class SQLAlchemySyncRepository(AbstractSyncRepository[ModelT], Generic[ModelT]):
 
     match_fields: list[str] | str | None = None
 
-    def __init__(self, *, statement: Select[tuple[ModelT]] | None = None, session: Session, **kwargs: Any) -> None:
+    def __init__(
+        self,
+        *,
+        statement: Select[tuple[ModelT]] | None = None,
+        session: Session,
+        auto_expunge: bool = False,
+        auto_refresh: bool = True,
+        auto_commit: bool = False,
+        **kwargs: Any,
+    ) -> None:
         """Repository pattern for SQLAlchemy models.
 
         Args:
             statement: To facilitate customization of the underlying select query.
             session: Session managing the unit-of-work for the operation.
+            auto_expunge: Remove object from session before returning.
+            auto_refresh: Refresh object from session before returning.
+            auto_commit: Commit objects before returning.
             **kwargs: Additional arguments.
 
         """
         super().__init__(**kwargs)
+        self.auto_expunge = auto_expunge
+        self.auto_refresh = auto_refresh
+        self.auto_commit = auto_commit
         self.session = session
         self.statement = statement if statement is not None else select(self.model_type)
         if not self.session.bind:
@@ -51,44 +66,63 @@ def __init__(self, *, statement: Select[tuple[ModelT]] | None = None, session: S
             raise ValueError("Session improperly configure")
         self._dialect = self.session.bind.dialect
 
-    def add(self, data: ModelT) -> ModelT:
+    def add(
+        self,
+        data: ModelT,
+        **kwargs: Any,
+    ) -> ModelT:
         """Add `data` to the collection.
 
         Args:
             data: Instance to be added to the collection.
+            **kwargs: Additional arguments.
 
         Returns:
             The added instance.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
+        auto_refresh = kwargs.pop("auto_refresh", self.auto_refresh)
         with wrap_sqlalchemy_exception():
             instance = self._attach_to_session(data)
-            self.session.flush()
-            self.session.refresh(instance)
-            self.session.expunge(instance)
+            self._flush_or_commit(auto_commit=auto_commit)
+            self._refresh(instance, auto_refresh=auto_refresh)
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
-    def add_many(self, data: list[ModelT]) -> list[ModelT]:
+    def add_many(
+        self,
+        data: list[ModelT],
+        **kwargs: Any,
+    ) -> list[ModelT]:
         """Add Many `data` to the collection.
 
         Args:
             data: list of Instances to be added to the collection.
-
+            **kwargs: Additional arguments.
 
         Returns:
             The added instances.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             self.session.add_all(data)
-            self.session.flush()
+            self._flush_or_commit(auto_commit=auto_commit)
             for datum in data:
-                self.session.expunge(datum)
+                self._expunge(datum, auto_expunge=auto_expunge)
             return data
 
-    def delete(self, item_id: Any) -> ModelT:
+    def delete(
+        self,
+        item_id: Any,
+        **kwargs: Any,
+    ) -> ModelT:
         """Delete instance identified by ``item_id``.
 
         Args:
             item_id: Identifier of instance to be deleted.
+            **kwargs: Additional arguments.
 
         Returns:
             The deleted instance.
@@ -96,23 +130,32 @@ def delete(self, item_id: Any) -> ModelT:
         Raises:
             NotFoundError: If no instance found identified by ``item_id``.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             instance = self.get(item_id)
             self.session.delete(instance)
-            self.session.flush()
-            self.session.expunge(instance)
+            self._flush_or_commit(auto_commit=auto_commit)
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
-    def delete_many(self, item_ids: list[Any]) -> list[ModelT]:
+    def delete_many(
+        self,
+        item_ids: list[Any],
+        **kwargs: Any,
+    ) -> list[ModelT]:
         """Delete instance identified by `item_id`.
 
         Args:
             item_ids: Identifier of instance to be deleted.
+            **kwargs: Additional arguments.
 
         Returns:
             The deleted instances.
 
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             instances: list[ModelT] = []
             chunk_size = 450
@@ -135,9 +178,9 @@ def delete_many(self, item_ids: list[Any]) -> list[ModelT]:
                     self.session.execute(
                         delete(self.model_type).where(getattr(self.model_type, self.id_attribute).in_(chunk))
                     )
-            self.session.flush()
+            self._flush_or_commit(auto_commit=auto_commit)
             for instance in instances:
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
             return instances
 
     def exists(self, **kwargs: Any) -> bool:
@@ -166,12 +209,13 @@ def get(self, item_id: Any, **kwargs: Any) -> ModelT:
         Raises:
             NotFoundError: If no instance found identified by `item_id`.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             statement = kwargs.pop("statement", self.statement)
             statement = self._filter_select_by_kwargs(statement=statement, **{self.id_attribute: item_id})
             instance = (self._execute(statement)).scalar_one_or_none()
             instance = self.check_not_found(instance)
-            self.session.expunge(instance)
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
     def get_one(self, **kwargs: Any) -> ModelT:
@@ -186,12 +230,13 @@ def get_one(self, **kwargs: Any) -> ModelT:
         Raises:
             NotFoundError: If no instance found identified by `item_id`.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             statement = kwargs.pop("statement", self.statement)
             statement = self._filter_select_by_kwargs(statement=statement, **kwargs)
             instance = (self._execute(statement)).scalar_one_or_none()
             instance = self.check_not_found(instance)
-            self.session.expunge(instance)
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
     def get_one_or_none(self, **kwargs: Any) -> ModelT | None:
@@ -203,27 +248,38 @@ def get_one_or_none(self, **kwargs: Any) -> ModelT | None:
         Returns:
             The retrieved instance or None
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         with wrap_sqlalchemy_exception():
             statement = kwargs.pop("statement", self.statement)
             statement = self._filter_select_by_kwargs(statement=statement, **kwargs)
             instance = (self._execute(statement)).scalar_one_or_none()
             if instance:
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
             return instance  # type: ignore
 
     def get_or_create(
-        self, match_fields: list[str] | str | None = None, upsert: bool = True, **kwargs: Any
+        self,
+        match_fields: list[str] | str | None = None,
+        upsert: bool = True,
+        attribute_names: Iterable[str] | None = None,
+        with_for_update: bool | None = None,
+        **kwargs: Any,
     ) -> tuple[ModelT, bool]:
         """Get instance identified by ``kwargs`` or create if it doesn't exist.
 
         Args:
             match_fields: a list of keys to use to match the existing model.  When empty, all fields are matched.
             upsert: When using match_fields and actual model values differ from `kwargs`, perform an update operation on the model.
+            attribute_names: an iterable of attribute names to pass into the ``update`` method.
+            with_for_update: indicating FOR UPDATE should be used, or may be a dictionary containing flags to indicate a more specific set of FOR UPDATE flags for the SELECT
             **kwargs: Identifier of the instance to be retrieved.
 
         Returns:
             a tuple that includes the instance and whether or not it needed to be created.  When using match_fields and actual model values differ from `kwargs`, the model value will be updated.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
+        auto_refresh = kwargs.pop("auto_refresh", self.auto_refresh)
         match_fields = match_fields or self.match_fields
         if isinstance(match_fields, str):
             match_fields = [match_fields]
@@ -244,9 +300,11 @@ def get_or_create(
                 if field and field != new_field_value:
                     setattr(existing, field_name, new_field_value)
             existing = self._attach_to_session(existing, strategy="merge")
-            self.session.flush()
-            self.session.refresh(existing)
-            self.session.expunge(existing)
+            self._flush_or_commit(auto_commit=auto_commit)
+            self._refresh(
+                existing, attribute_names=attribute_names, with_for_update=with_for_update, auto_refresh=auto_refresh
+            )
+            self._expunge(existing, auto_expunge=auto_expunge)
         return existing, False
 
     def count(self, *filters: FilterTypes, **kwargs: Any) -> int:
@@ -269,12 +327,21 @@ def count(self, *filters: FilterTypes, **kwargs: Any) -> int:
         results = self._execute(statement)
         return results.scalar_one()  # type: ignore
 
-    def update(self, data: ModelT) -> ModelT:
+    def update(
+        self,
+        data: ModelT,
+        attribute_names: Iterable[str] | None = None,
+        with_for_update: bool | None = None,
+        **kwargs: Any,
+    ) -> ModelT:
         """Update instance with the attribute values present on `data`.
 
         Args:
             data: An instance that should have a value for `self.id_attribute` that exists in the
                 collection.
+            attribute_names: an iterable of attribute names to pass into the ``update`` method.
+            with_for_update: indicating FOR UPDATE should be used, or may be a dictionary containing flags to indicate a more specific set of FOR UPDATE flags for the SELECT
+            **kwargs: Additional arguments.
 
         Returns:
             The updated instance.
@@ -282,18 +349,27 @@ def update(self, data: ModelT) -> ModelT:
         Raises:
             NotFoundError: If no instance found with same identifier as `data`.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
+        auto_refresh = kwargs.pop("auto_refresh", self.auto_refresh)
         with wrap_sqlalchemy_exception():
             item_id = self.get_id_attribute_value(data)
             # this will raise for not found, and will put the item in the session
             self.get(item_id)
             # this will merge the inbound data to the instance we just put in the session
             instance = self._attach_to_session(data, strategy="merge")
-            self.session.flush()
-            self.session.refresh(instance)
-            self.session.expunge(instance)
+            self._flush_or_commit(auto_commit=auto_commit)
+            self._refresh(
+                instance, attribute_names=attribute_names, with_for_update=with_for_update, auto_refresh=auto_refresh
+            )
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
-    def update_many(self, data: list[ModelT]) -> list[ModelT]:
+    def update_many(
+        self,
+        data: list[ModelT],
+        **kwargs: Any,
+    ) -> list[ModelT]:
         """Update one or more instances with the attribute values present on `data`.
 
         This function has an optimized bulk insert based on the configured SQL dialect:
@@ -303,6 +379,7 @@ def update_many(self, data: list[ModelT]) -> list[ModelT]:
         Args:
             data: A list of instances to update.  Each should have a value for `self.id_attribute` that exists in the
                 collection.
+            **kwargs: Additional arguments.
 
         Returns:
             The updated instances.
@@ -310,6 +387,8 @@ def update_many(self, data: list[ModelT]) -> list[ModelT]:
         Raises:
             NotFoundError: If no instance found with same identifier as `data`.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         data_to_update: list[dict[str, Any]] = [v.to_dict() if isinstance(v, self.model_type) else v for v in data]  # type: ignore
         with wrap_sqlalchemy_exception():
             if self._dialect.update_executemany_returning and self._dialect.name != "oracle":
@@ -321,15 +400,15 @@ def update_many(self, data: list[ModelT]) -> list[ModelT]:
                         # https://github.com/sqlalchemy/sqlalchemy/discussions/9925
                     )
                 )
-                self.session.flush()
+                self._flush_or_commit(auto_commit=auto_commit)
                 for instance in instances:
-                    self.session.expunge(instance)
+                    self._expunge(instance, auto_expunge=auto_expunge)
                 return instances
             self.session.execute(
                 update(self.model_type),
                 data_to_update,
             )
-            self.session.flush()
+            self._flush_or_commit(auto_commit=auto_commit)
             return data
 
     def list_and_count(
@@ -350,6 +429,25 @@ def list_and_count(
             return self._list_and_count_basic(*filters, **kwargs)
         return self._list_and_count_window(*filters, **kwargs)
 
+    def _expunge(self, instance: ModelT, auto_expunge: bool) -> None:
+        return self.session.expunge(instance) if auto_expunge else None
+
+    def _flush_or_commit(self, auto_commit: bool) -> None:
+        return self.session.commit() if auto_commit else self.session.flush()
+
+    def _refresh(
+        self,
+        instance: ModelT,
+        auto_refresh: bool,
+        attribute_names: Iterable[str] | None = None,
+        with_for_update: bool | None = None,
+    ) -> None:
+        return (
+            self.session.refresh(instance, attribute_names=attribute_names, with_for_update=with_for_update)
+            if auto_refresh
+            else None
+        )
+
     def _list_and_count_window(
         self,
         *filters: FilterTypes,
@@ -364,6 +462,7 @@ def _list_and_count_window(
         Returns:
             Count of records returned by query using an analytical window function, ignoring pagination.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         statement = kwargs.pop("statement", self.statement)
         statement = statement.add_columns(over(sql_func.count(self.get_id_attribute_value(self.model_type))))
         statement = self._apply_filters(*filters, statement=statement)
@@ -373,7 +472,7 @@ def _list_and_count_window(
             count: int = 0
             instances: list[ModelT] = []
             for i, (instance, count_value) in enumerate(result):
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
                 instances.append(instance)
                 if i == 0:
                     count = count_value
@@ -393,6 +492,7 @@ def _list_and_count_basic(
         Returns:
             Count of records returned by query using 2 queries, ignoring pagination.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         statement = kwargs.pop("statement", self.statement)
         statement = self._apply_filters(*filters, statement=statement)
         statement = self._filter_select_by_kwargs(statement, **kwargs)
@@ -406,7 +506,7 @@ def _list_and_count_basic(
             result = self._execute(statement)
             instances: list[ModelT] = []
             for (instance,) in result:
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
                 instances.append(instance)
             return instances, count
 
@@ -420,6 +520,7 @@ def list(self, *filters: FilterTypes, **kwargs: Any) -> list[ModelT]:
         Returns:
             The list of instances, after filtering applied.
         """
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
         statement = kwargs.pop("statement", self.statement)
         statement = self._apply_filters(*filters, statement=statement)
         statement = self._filter_select_by_kwargs(statement, **kwargs)
@@ -428,10 +529,16 @@ def list(self, *filters: FilterTypes, **kwargs: Any) -> list[ModelT]:
             result = self._execute(statement)
             instances = list(result.scalars())
             for instance in instances:
-                self.session.expunge(instance)
+                self._expunge(instance, auto_expunge=auto_expunge)
             return instances
 
-    def upsert(self, data: ModelT) -> ModelT:
+    def upsert(
+        self,
+        data: ModelT,
+        attribute_names: Iterable[str] | None = None,
+        with_for_update: bool | None = None,
+        **kwargs: Any,
+    ) -> ModelT:
         """Update or create instance.
 
         Updates instance with the attribute values present on `data`, or creates a new instance if
@@ -441,6 +548,9 @@ def upsert(self, data: ModelT) -> ModelT:
             data: Instance to update existing, or be created. Identifier used to determine if an
                 existing instance exists is the value of an attribute on `data` named as value of
                 `self.id_attribute`.
+            attribute_names: an iterable of attribute names to pass into the ``update`` method.
+            with_for_update: indicating FOR UPDATE should be used, or may be a dictionary containing flags to indicate a more specific set of FOR UPDATE flags for the SELECT
+            **kwargs: Instance attribute value filters.
 
         Returns:
             The updated or created instance.
@@ -448,11 +558,16 @@ def upsert(self, data: ModelT) -> ModelT:
         Raises:
             NotFoundError: If no instance found with same identifier as `data`.
         """
+        auto_commit = kwargs.pop("auto_commit", self.auto_commit)
+        auto_expunge = kwargs.pop("auto_expunge", self.auto_expunge)
+        auto_refresh = kwargs.pop("auto_refresh", self.auto_refresh)
         with wrap_sqlalchemy_exception():
             instance = self._attach_to_session(data, strategy="merge")
-            self.session.flush()
-            self.session.refresh(instance)
-            self.session.expunge(instance)
+            self._flush_or_commit(auto_commit=auto_commit)
+            self._refresh(
+                instance, attribute_names=attribute_names, with_for_update=with_for_update, auto_refresh=auto_refresh
+            )
+            self._expunge(instance, auto_expunge=auto_expunge)
             return instance
 
     def filter_collection_by_kwargs(  # type:ignore[override]


Test patch:
diff --git a/tests/unit/test_contrib/test_sqlalchemy/models_bigint.py b/tests/unit/test_contrib/test_sqlalchemy/models_bigint.py
--- a/tests/unit/test_contrib/test_sqlalchemy/models_bigint.py
+++ b/tests/unit/test_contrib/test_sqlalchemy/models_bigint.py
@@ -4,7 +4,7 @@
 from datetime import date, datetime
 from typing import List
 
-from sqlalchemy import FetchedValue, ForeignKey, String, func
+from sqlalchemy import Column, FetchedValue, ForeignKey, String, Table, func
 from sqlalchemy.orm import Mapped, mapped_column, relationship
 
 from litestar.contrib.sqlalchemy.base import BigIntAuditBase, BigIntBase
@@ -43,14 +43,39 @@ class BigIntEventLog(BigIntAuditBase):
 class BigIntModelWithFetchedValue(BigIntBase):
     """The ModelWithFetchedValue BigIntBase."""
 
-    val: Mapped[int]
-    updated: Mapped[datetime] = mapped_column(
+    val: Mapped[int]  # pyright: ignore
+    updated: Mapped[datetime] = mapped_column(  # pyright: ignore
         server_default=func.current_timestamp(),
         onupdate=func.current_timestamp(),
         server_onupdate=FetchedValue(),
     )
 
 
+bigint_item_tag = Table(
+    "bigint_item_tag",
+    BigIntBase.metadata,
+    Column("item_id", ForeignKey("big_int_item.id"), primary_key=True),
+    Column("tag_id", ForeignKey("big_int_tag.id"), primary_key=True),
+)
+
+
+class BigIntItem(BigIntBase):
+    name: Mapped[str] = mapped_column(String(length=50))  # pyright: ignore
+    description: Mapped[str] = mapped_column(String(length=100), nullable=True)  # pyright: ignore
+    tags: Mapped[List[BigIntTag]] = relationship(  # pyright: ignore  # noqa: UP
+        secondary=lambda: bigint_item_tag, back_populates="items"
+    )
+
+
+class BigIntTag(BigIntBase):
+    """The event log domain object."""
+
+    name: Mapped[str] = mapped_column(String(length=50))  # pyright: ignore
+    items: Mapped[List[BigIntItem]] = relationship(  # pyright: ignore  # noqa: UP
+        secondary=lambda: bigint_item_tag, back_populates="tags"
+    )
+
+
 class BigIntRule(BigIntAuditBase):
     """The rule domain object."""
 
@@ -88,6 +113,18 @@ class ModelWithFetchedValueAsyncRepository(SQLAlchemyAsyncRepository[BigIntModel
     model_type = BigIntModelWithFetchedValue
 
 
+class TagAsyncRepository(SQLAlchemyAsyncRepository[BigIntTag]):
+    """Tag repository."""
+
+    model_type = BigIntTag
+
+
+class ItemAsyncRepository(SQLAlchemyAsyncRepository[BigIntItem]):
+    """Item repository."""
+
+    model_type = BigIntItem
+
+
 class AuthorSyncRepository(SQLAlchemySyncRepository[BigIntAuthor]):
     """Author repository."""
 
@@ -116,3 +153,15 @@ class ModelWithFetchedValueSyncRepository(SQLAlchemySyncRepository[BigIntModelWi
     """ModelWithFetchedValue repository."""
 
     model_type = BigIntModelWithFetchedValue
+
+
+class TagSyncRepository(SQLAlchemySyncRepository[BigIntTag]):
+    """Tag repository."""
+
+    model_type = BigIntTag
+
+
+class ItemSyncRepository(SQLAlchemySyncRepository[BigIntItem]):
+    """Item repository."""
+
+    model_type = BigIntItem
diff --git a/tests/unit/test_contrib/test_sqlalchemy/models_uuid.py b/tests/unit/test_contrib/test_sqlalchemy/models_uuid.py
--- a/tests/unit/test_contrib/test_sqlalchemy/models_uuid.py
+++ b/tests/unit/test_contrib/test_sqlalchemy/models_uuid.py
@@ -6,9 +6,10 @@
 from typing import List
 from uuid import UUID
 
-from sqlalchemy import FetchedValue, ForeignKey, String, func
+from sqlalchemy import Column, FetchedValue, ForeignKey, String, Table, func
 from sqlalchemy.orm import Mapped, mapped_column, relationship
 
+from litestar.contrib.sqlalchemy import base
 from litestar.contrib.sqlalchemy.base import UUIDAuditBase, UUIDBase
 from litestar.contrib.sqlalchemy.repository import SQLAlchemyAsyncRepository, SQLAlchemySyncRepository
 
@@ -43,14 +44,39 @@ class UUIDEventLog(UUIDAuditBase):
 class UUIDModelWithFetchedValue(UUIDBase):
     """The ModelWithFetchedValue UUIDBase."""
 
-    val: Mapped[int]
-    updated: Mapped[datetime] = mapped_column(
+    val: Mapped[int]  # pyright: ignore
+    updated: Mapped[datetime] = mapped_column(  # pyright: ignore
         server_default=func.current_timestamp(),
         onupdate=func.current_timestamp(),
         server_onupdate=FetchedValue(),
     )
 
 
+uuid_item_tag = Table(
+    "uuid_item_tag",
+    base.orm_registry.metadata,
+    Column("item_id", ForeignKey("uuid_item.id"), primary_key=True),
+    Column("tag_id", ForeignKey("uuid_tag.id"), primary_key=True),
+)
+
+
+class UUIDItem(UUIDBase):
+    name: Mapped[str] = mapped_column(String(length=50))  # pyright: ignore
+    description: Mapped[str] = mapped_column(String(length=100), nullable=True)  # pyright: ignore
+    tags: Mapped[List[UUIDTag]] = relationship(  # pyright: ignore  # noqa: UP
+        secondary=lambda: uuid_item_tag, back_populates="items"
+    )
+
+
+class UUIDTag(UUIDAuditBase):
+    """The event log domain object."""
+
+    name: Mapped[str] = mapped_column(String(length=50))  # pyright: ignore
+    items: Mapped[List[UUIDItem]] = relationship(  # pyright: ignore  # noqa: UP
+        secondary=lambda: uuid_item_tag, back_populates="tags"
+    )
+
+
 class UUIDRule(UUIDAuditBase):
     """The rule domain object."""
 
@@ -88,6 +114,18 @@ class ModelWithFetchedValueAsyncRepository(SQLAlchemyAsyncRepository[UUIDModelWi
     model_type = UUIDModelWithFetchedValue
 
 
+class TagAsyncRepository(SQLAlchemyAsyncRepository[UUIDTag]):
+    """Tag repository."""
+
+    model_type = UUIDTag
+
+
+class ItemAsyncRepository(SQLAlchemyAsyncRepository[UUIDItem]):
+    """Item repository."""
+
+    model_type = UUIDItem
+
+
 class AuthorSyncRepository(SQLAlchemySyncRepository[UUIDAuthor]):
     """Author repository."""
 
@@ -116,3 +154,15 @@ class ModelWithFetchedValueSyncRepository(SQLAlchemySyncRepository[UUIDModelWith
     """ModelWithFetchedValue repository."""
 
     model_type = UUIDModelWithFetchedValue
+
+
+class TagSyncRepository(SQLAlchemySyncRepository[UUIDTag]):
+    """Tag repository."""
+
+    model_type = UUIDTag
+
+
+class ItemSyncRepository(SQLAlchemySyncRepository[UUIDItem]):
+    """Item repository."""
+
+    model_type = UUIDItem
diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_repository/conftest.py b/tests/unit/test_contrib/test_sqlalchemy/test_repository/conftest.py
--- a/tests/unit/test_contrib/test_sqlalchemy/test_repository/conftest.py
+++ b/tests/unit/test_contrib/test_sqlalchemy/test_repository/conftest.py
@@ -292,7 +292,7 @@ def engine(request: FixtureRequest) -> Engine:
 
 @pytest.fixture()
 def session(engine: Engine) -> Generator[Session, None, None]:
-    session = sessionmaker(bind=engine)()
+    session = sessionmaker(bind=engine, expire_on_commit=False)()
     try:
         yield session
     finally:
@@ -402,7 +402,7 @@ def async_engine(request: FixtureRequest) -> AsyncEngine:
 
 @pytest.fixture()
 async def async_session(async_engine: AsyncEngine) -> AsyncGenerator[AsyncSession, None]:
-    session = async_sessionmaker(bind=async_engine)()
+    session = async_sessionmaker(bind=async_engine, expire_on_commit=False)()
     try:
         yield session
     finally:
diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_repo_bigint.py b/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_repo_bigint.py
--- a/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_repo_bigint.py
+++ b/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_repo_bigint.py
@@ -20,14 +20,20 @@
     AuthorSyncRepository,
     BigIntAuthor,
     BigIntBook,
+    BigIntItem,
     BigIntModelWithFetchedValue,
     BigIntRule,
+    BigIntTag,
     BookAsyncRepository,
     BookSyncRepository,
+    ItemAsyncRepository,
+    ItemSyncRepository,
     ModelWithFetchedValueAsyncRepository,
     ModelWithFetchedValueSyncRepository,
     RuleAsyncRepository,
     RuleSyncRepository,
+    TagAsyncRepository,
+    TagSyncRepository,
 )
 
 from .helpers import maybe_async, update_raw_records
@@ -102,6 +108,20 @@ def model_with_fetched_value_repo(
     return ModelWithFetchedValueSyncRepository(session=any_session)
 
 
+@pytest.fixture()
+def tag_repo(any_session: AsyncSession | Session) -> TagAsyncRepository | TagSyncRepository:
+    if isinstance(any_session, AsyncSession):
+        return TagAsyncRepository(session=any_session)
+    return TagSyncRepository(session=any_session)
+
+
+@pytest.fixture()
+def item_repo(any_session: AsyncSession | Session) -> ItemAsyncRepository | ItemSyncRepository:
+    if isinstance(any_session, AsyncSession):
+        return ItemAsyncRepository(session=any_session)
+    return ItemSyncRepository(session=any_session)
+
+
 def test_filter_by_kwargs_with_incorrect_attribute_name(author_repo: AuthorAsyncRepository) -> None:
     """Test SQLALchemy filter by kwargs with invalid column name.
 
@@ -506,3 +526,36 @@ async def test_repo_fetched_value(model_with_fetched_value_repo: ModelWithFetche
     assert obj.updated is not None
     assert obj.val == 2
     assert obj.updated != first_time
+
+
+async def test_lazy_load(item_repo: ItemAsyncRepository, tag_repo: TagAsyncRepository) -> None:
+    """Test SQLALchemy fetched value in various places.
+
+    Args:
+        item_repo (ItemAsyncRepository): The item mock repository
+        tag_repo (TagAsyncRepository): The tag mock repository
+    """
+
+    tag_obj = await maybe_async(tag_repo.add(BigIntTag(name="A new tag")))
+    assert tag_obj
+    new_items = await maybe_async(
+        item_repo.add_many([BigIntItem(name="The first item"), BigIntItem(name="The second item")])
+    )
+    await maybe_async(item_repo.session.commit())
+    await maybe_async(tag_repo.session.commit())
+    assert len(new_items) > 0
+    first_item_id = new_items[0].id
+    new_items[1].id
+    update_data = {
+        "name": "A modified Name",
+        "tag_names": ["A new tag"],
+        "id": first_item_id,
+    }
+    tags_to_add = await maybe_async(tag_repo.list(CollectionFilter("name", update_data.pop("tag_names", []))))  # type: ignore
+    assert len(tags_to_add) > 0
+    assert tags_to_add[0].id is not None
+    update_data["tags"] = tags_to_add
+    updated_obj = await maybe_async(item_repo.update(BigIntItem(**update_data), auto_refresh=False))
+    await maybe_async(item_repo.session.commit())
+    assert len(updated_obj.tags) > 0
+    assert updated_obj.tags[0].name == "A new tag"
diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_repo_uuid.py b/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_repo_uuid.py
--- a/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_repo_uuid.py
+++ b/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_repo_uuid.py
@@ -25,14 +25,20 @@
     AuthorSyncRepository,
     BookAsyncRepository,
     BookSyncRepository,
+    ItemAsyncRepository,
+    ItemSyncRepository,
     ModelWithFetchedValueAsyncRepository,
     ModelWithFetchedValueSyncRepository,
     RuleAsyncRepository,
     RuleSyncRepository,
+    TagAsyncRepository,
+    TagSyncRepository,
     UUIDAuthor,
     UUIDBook,
+    UUIDItem,
     UUIDModelWithFetchedValue,
     UUIDRule,
+    UUIDTag,
 )
 
 from .helpers import maybe_async, update_raw_records
@@ -189,6 +195,20 @@ def book_repo(any_session: AsyncSession | Session) -> BookAsyncRepository | Book
     return BookSyncRepository(session=any_session)
 
 
+@pytest.fixture()
+def tag_repo(any_session: AsyncSession | Session) -> TagAsyncRepository | TagSyncRepository:
+    if isinstance(any_session, AsyncSession):
+        return TagAsyncRepository(session=any_session)
+    return TagSyncRepository(session=any_session)
+
+
+@pytest.fixture()
+def item_repo(any_session: AsyncSession | Session) -> ItemAsyncRepository | ItemSyncRepository:
+    if isinstance(any_session, AsyncSession):
+        return ItemAsyncRepository(session=any_session)
+    return ItemSyncRepository(session=any_session)
+
+
 @pytest.fixture()
 def model_with_fetched_value_repo(
     any_session: AsyncSession | Session,
@@ -612,3 +632,36 @@ async def test_repo_fetched_value(model_with_fetched_value_repo: ModelWithFetche
     assert obj.updated is not None
     assert obj.val == 2
     assert obj.updated != first_time
+
+
+async def test_lazy_load(item_repo: ItemAsyncRepository, tag_repo: TagAsyncRepository) -> None:
+    """Test SQLALchemy fetched value in various places.
+
+    Args:
+        item_repo (ItemAsyncRepository): The item mock repository
+        tag_repo (TagAsyncRepository): The tag mock repository
+    """
+
+    tag_obj = await maybe_async(tag_repo.add(UUIDTag(name="A new tag")))
+    assert tag_obj
+    new_items = await maybe_async(
+        item_repo.add_many([UUIDItem(name="The first item"), UUIDItem(name="The second item")])
+    )
+    await maybe_async(item_repo.session.commit())
+    await maybe_async(tag_repo.session.commit())
+    assert len(new_items) > 0
+    first_item_id = new_items[0].id
+    new_items[1].id
+    update_data = {
+        "name": "A modified Name",
+        "tag_names": ["A new tag"],
+        "id": first_item_id,
+    }
+    tags_to_add = await maybe_async(tag_repo.list(CollectionFilter("name", update_data.pop("tag_names", []))))  # type: ignore
+    assert len(tags_to_add) > 0
+    assert tags_to_add[0].id is not None
+    update_data["tags"] = tags_to_add
+    updated_obj = await maybe_async(item_repo.update(UUIDItem(**update_data), auto_refresh=False))
+    await maybe_async(item_repo.session.commit())
+    assert len(updated_obj.tags) > 0
+    assert updated_obj.tags[0].name == "A new tag"
diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_sqlalchemy_async.py b/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_sqlalchemy_async.py
--- a/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_sqlalchemy_async.py
+++ b/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_sqlalchemy_async.py
@@ -122,8 +122,8 @@ async def test_sqlalchemy_repo_add(mock_repo: SQLAlchemyAsyncRepository) -> None
     assert instance is mock_instance
     mock_repo.session.add.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.refresh.assert_called_once_with(mock_instance)
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.refresh.assert_called_once_with(mock_instance, attribute_names=None, with_for_update=None)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -151,7 +151,7 @@ class BigIntModel(base.BigIntAuditBase):
     assert len(instances) == 3
     for row in instances:
         assert row.id is not None
-    mock_repo.session.expunge.assert_called()
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -194,7 +194,7 @@ async def test_sqlalchemy_repo_delete(mock_repo: SQLAlchemyAsyncRepository, monk
     assert instance is mock_instance
     mock_repo.session.delete.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -236,7 +236,7 @@ async def test_sqlalchemy_repo_get_member(mock_repo: SQLAlchemyAsyncRepository,
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
     instance = await mock_repo.get("instance-id")
     assert instance is mock_instance
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -249,7 +249,7 @@ async def test_sqlalchemy_repo_get_one_member(mock_repo: SQLAlchemyAsyncReposito
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
     instance = await mock_repo.get_one(id="instance-id")
     assert instance is mock_instance
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -267,7 +267,7 @@ async def test_sqlalchemy_repo_get_or_create_member_existing(
     instance, created = await mock_repo.get_or_create(id="instance-id", upsert=False)
     assert instance is mock_instance
     assert created is False
-    mock_repo.session.expunge.assert_called_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.merge.assert_not_called()
     mock_repo.session.refresh.assert_not_called()
 
@@ -286,10 +286,10 @@ async def test_sqlalchemy_repo_get_or_create_member_existing_upsert(
     instance, created = await mock_repo.get_or_create(id="instance-id", upsert=True, an_extra_attribute="yep")
     assert instance is mock_instance
     assert created is False
-    mock_repo.session.expunge.assert_called_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo._attach_to_session.assert_called_once()
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.refresh.assert_called_once_with(mock_instance)
+    mock_repo.session.refresh.assert_called_once_with(mock_instance, attribute_names=None, with_for_update=None)
 
 
 async def test_sqlalchemy_repo_get_or_create_member_existing_no_upsert(
@@ -304,7 +304,7 @@ async def test_sqlalchemy_repo_get_or_create_member_existing_no_upsert(
     instance, created = await mock_repo.get_or_create(id="instance-id", upsert=False, an_extra_attribute="yep")
     assert instance is mock_instance
     assert created is False
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.add.assert_not_called()
     mock_repo.session.refresh.assert_not_called()
 
@@ -320,9 +320,9 @@ async def test_sqlalchemy_repo_get_or_create_member_created(
     instance, created = await mock_repo.get_or_create(id="new-id")
     assert instance is not None
     assert created is True
-    mock_repo.session.expunge.assert_called_once_with(instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.add.assert_called_once_with(instance)
-    mock_repo.session.refresh.assert_called_once_with(instance)
+    mock_repo.session.refresh.assert_called_once_with(instance, attribute_names=None, with_for_update=None)
 
 
 async def test_sqlalchemy_repo_get_one_or_none_member(
@@ -336,7 +336,7 @@ async def test_sqlalchemy_repo_get_one_or_none_member(
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
     instance = await mock_repo.get_one_or_none(id="instance-id")
     assert instance is mock_instance
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -364,7 +364,7 @@ async def test_sqlalchemy_repo_list(mock_repo: SQLAlchemyAsyncRepository, monkey
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
     instances = await mock_repo.list()
     assert instances == mock_instances
-    mock_repo.session.expunge.assert_has_calls(*mock_instances)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -378,7 +378,7 @@ async def test_sqlalchemy_repo_list_and_count(mock_repo: SQLAlchemyAsyncReposito
     instances, instance_count = await mock_repo.list_and_count()
     assert instances == mock_instances
     assert instance_count == mock_count
-    mock_repo.session.expunge.assert_has_calls(*mock_instances)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -475,9 +475,9 @@ async def test_sqlalchemy_repo_update(mock_repo: SQLAlchemyAsyncRepository, monk
     assert instance is mock_instance
     mock_repo.session.merge.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
-    mock_repo.session.refresh.assert_called_once_with(mock_instance)
+    mock_repo.session.refresh.assert_called_once_with(mock_instance, attribute_names=None, with_for_update=None)
 
 
 async def test_sqlalchemy_repo_upsert(mock_repo: SQLAlchemyAsyncRepository) -> None:
@@ -488,9 +488,9 @@ async def test_sqlalchemy_repo_upsert(mock_repo: SQLAlchemyAsyncRepository) -> N
     assert instance is mock_instance
     mock_repo.session.merge.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
-    mock_repo.session.refresh.assert_called_once_with(mock_instance)
+    mock_repo.session.refresh.assert_called_once_with(mock_instance, attribute_names=None, with_for_update=None)
 
 
 async def test_attach_to_session_unexpected_strategy_raises_valueerror(
diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_sqlalchemy_sync.py b/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_sqlalchemy_sync.py
--- a/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_sqlalchemy_sync.py
+++ b/tests/unit/test_contrib/test_sqlalchemy/test_repository/test_sqlalchemy_sync.py
@@ -121,8 +121,8 @@ def test_sqlalchemy_repo_add(mock_repo: SQLAlchemySyncRepository) -> None:
     assert instance is mock_instance
     mock_repo.session.add.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.refresh.assert_called_once_with(mock_instance)
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.refresh.assert_called_once_with(mock_instance, attribute_names=None, with_for_update=None)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -150,7 +150,7 @@ class BigIntModel(base.BigIntAuditBase):
     assert len(instances) == 3
     for row in instances:
         assert row.id is not None
-    mock_repo.session.expunge.assert_called()
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -193,7 +193,7 @@ def test_sqlalchemy_repo_delete(mock_repo: SQLAlchemySyncRepository, monkeypatch
     assert instance is mock_instance
     mock_repo.session.delete.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -235,7 +235,7 @@ def test_sqlalchemy_repo_get_member(mock_repo: SQLAlchemySyncRepository, monkeyp
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
     instance = mock_repo.get("instance-id")
     assert instance is mock_instance
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -248,7 +248,7 @@ def test_sqlalchemy_repo_get_one_member(mock_repo: SQLAlchemySyncRepository, mon
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
     instance = mock_repo.get_one(id="instance-id")
     assert instance is mock_instance
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -267,7 +267,7 @@ def test_sqlalchemy_repo_get_or_create_member_existing(
     instance, created = mock_repo.get_or_create(id="instance-id", upsert=False)
     assert instance is mock_instance
     assert created is False
-    mock_repo.session.expunge.assert_called_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.merge.assert_not_called()
     mock_repo.session.refresh.assert_not_called()
 
@@ -286,10 +286,10 @@ def test_sqlalchemy_repo_get_or_create_member_existing_upsert(
     instance, created = mock_repo.get_or_create(id="instance-id", upsert=True, an_extra_attribute="yep")
     assert instance is mock_instance
     assert created is False
-    mock_repo.session.expunge.assert_called_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo._attach_to_session.assert_called_once()
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.refresh.assert_called_once_with(mock_instance)
+    mock_repo.session.refresh.assert_called_once_with(mock_instance, attribute_names=None, with_for_update=None)
 
 
 def test_sqlalchemy_repo_get_or_create_member_existing_no_upsert(
@@ -304,7 +304,7 @@ def test_sqlalchemy_repo_get_or_create_member_existing_no_upsert(
     instance, created = mock_repo.get_or_create(id="instance-id", upsert=False, an_extra_attribute="yep")
     assert instance is mock_instance
     assert created is False
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.add.assert_not_called()
     mock_repo.session.refresh.assert_not_called()
 
@@ -320,9 +320,9 @@ def test_sqlalchemy_repo_get_or_create_member_created(
     instance, created = mock_repo.get_or_create(id="new-id")
     assert instance is not None
     assert created is True
-    mock_repo.session.expunge.assert_called_with(instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.add.assert_called_once_with(instance)
-    mock_repo.session.refresh.assert_called_once_with(instance)
+    mock_repo.session.refresh.assert_called_once_with(instance, attribute_names=None, with_for_update=None)
 
 
 def test_sqlalchemy_repo_get_one_or_none_member(mock_repo: SQLAlchemySyncRepository, monkeypatch: MonkeyPatch) -> None:
@@ -334,7 +334,7 @@ def test_sqlalchemy_repo_get_one_or_none_member(mock_repo: SQLAlchemySyncReposit
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
     instance = mock_repo.get_one_or_none(id="instance-id")
     assert instance is mock_instance
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -362,7 +362,7 @@ def test_sqlalchemy_repo_list(mock_repo: SQLAlchemySyncRepository, monkeypatch:
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
     instances = mock_repo.list()
     assert instances == mock_instances
-    mock_repo.session.expunge.assert_has_calls(*mock_instances)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -376,7 +376,7 @@ def test_sqlalchemy_repo_list_and_count(mock_repo: SQLAlchemySyncRepository, mon
     instances, instance_count = mock_repo.list_and_count()
     assert instances == mock_instances
     assert instance_count == mock_count
-    mock_repo.session.expunge.assert_has_calls(*mock_instances)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
 
 
@@ -471,9 +471,9 @@ def test_sqlalchemy_repo_update(mock_repo: SQLAlchemySyncRepository, monkeypatch
     assert instance is mock_instance
     mock_repo.session.merge.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
-    mock_repo.session.refresh.assert_called_once_with(mock_instance)
+    mock_repo.session.refresh.assert_called_once_with(mock_instance, attribute_names=None, with_for_update=None)
 
 
 def test_sqlalchemy_repo_upsert(mock_repo: SQLAlchemySyncRepository) -> None:
@@ -484,9 +484,9 @@ def test_sqlalchemy_repo_upsert(mock_repo: SQLAlchemySyncRepository) -> None:
     assert instance is mock_instance
     mock_repo.session.merge.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
-    mock_repo.session.expunge.assert_called_once_with(mock_instance)
+    mock_repo.session.expunge.assert_not_called()
     mock_repo.session.commit.assert_not_called()
-    mock_repo.session.refresh.assert_called_once_with(mock_instance)
+    mock_repo.session.refresh.assert_called_once_with(mock_instance, attribute_names=None, with_for_update=None)
 
 
 def test_attach_to_session_unexpected_strategy_raises_valueerror(