simplified a bit
This commit is contained in:
@@ -33,7 +33,7 @@ Use the same vocabulary at every layer:
|
||||
| Update | `update_widget()` | `update()` | Owned transaction | `None` |
|
||||
| Delete | `delete_widget()` | `delete()` | Owned transaction | `None` |
|
||||
|
||||
Functions and repository methods both put domain arguments first. Database configuration, factory overrides, and sessions are keyword-only infrastructure arguments. This keeps call sites analogous and makes ownership choices visible.
|
||||
Functions and repository methods both put domain arguments first. Database configuration and sessions are keyword-only infrastructure arguments. This keeps call sites analogous and makes ownership choices visible.
|
||||
|
||||
---
|
||||
|
||||
@@ -62,7 +62,6 @@ Functions are the simplest default when grouping state or behavior in an object
|
||||
|
||||
```python
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from sqlmodel import select
|
||||
|
||||
from .session import session_scope
|
||||
@@ -75,12 +74,10 @@ async def create_widget(
|
||||
*,
|
||||
database_url: str,
|
||||
session: AsyncSession | None = None,
|
||||
session_factory: async_sessionmaker[AsyncSession] | None = None,
|
||||
) -> Widget:
|
||||
async with transaction_scope(
|
||||
database_url=database_url,
|
||||
session=session,
|
||||
session_factory=session_factory,
|
||||
) as active_session:
|
||||
widget = Widget(name=name, description=description)
|
||||
active_session.add(widget)
|
||||
@@ -93,12 +90,10 @@ async def get_widget(
|
||||
*,
|
||||
database_url: str,
|
||||
session: AsyncSession | None = None,
|
||||
session_factory: async_sessionmaker[AsyncSession] | None = None,
|
||||
) -> Widget | None:
|
||||
async with session_scope(
|
||||
database_url=database_url,
|
||||
session=session,
|
||||
session_factory=session_factory,
|
||||
) as active_session:
|
||||
return await active_session.get(Widget, widget_id)
|
||||
|
||||
@@ -109,7 +104,6 @@ async def list_widgets(
|
||||
offset: int = 0,
|
||||
limit: int = 100,
|
||||
session: AsyncSession | None = None,
|
||||
session_factory: async_sessionmaker[AsyncSession] | None = None,
|
||||
) -> list[Widget]:
|
||||
if offset < 0:
|
||||
raise ValueError("offset must be non-negative")
|
||||
@@ -119,7 +113,6 @@ async def list_widgets(
|
||||
async with session_scope(
|
||||
database_url=database_url,
|
||||
session=session,
|
||||
session_factory=session_factory,
|
||||
) as active_session:
|
||||
statement = select(Widget).order_by(Widget.id).offset(offset).limit(limit)
|
||||
return list(await active_session.scalars(statement))
|
||||
@@ -132,12 +125,10 @@ async def update_widget(
|
||||
*,
|
||||
database_url: str,
|
||||
session: AsyncSession | None = None,
|
||||
session_factory: async_sessionmaker[AsyncSession] | None = None,
|
||||
) -> Widget | None:
|
||||
async with transaction_scope(
|
||||
database_url=database_url,
|
||||
session=session,
|
||||
session_factory=session_factory,
|
||||
) as active_session:
|
||||
widget = await active_session.get(Widget, widget_id)
|
||||
if widget is None:
|
||||
@@ -154,12 +145,10 @@ async def delete_widget(
|
||||
*,
|
||||
database_url: str,
|
||||
session: AsyncSession | None = None,
|
||||
session_factory: async_sessionmaker[AsyncSession] | None = None,
|
||||
) -> Widget | None:
|
||||
async with transaction_scope(
|
||||
database_url=database_url,
|
||||
session=session,
|
||||
session_factory=session_factory,
|
||||
) as active_session:
|
||||
widget = await active_session.get(Widget, widget_id)
|
||||
if widget is None:
|
||||
@@ -178,20 +167,14 @@ Update and delete load the row through the same session that mutates it. This av
|
||||
|
||||
## Repository Object
|
||||
|
||||
A repository can provide a stable domain-facing interface when several callers need the same grouped operations. It stores repeatable database configuration and an optional factory override, never a mutable session. Every method delegates to the analogous function and exposes the same optional-session contract.
|
||||
A repository can provide a stable domain-facing interface when several callers need the same grouped operations. It stores repeatable database configuration, never a mutable session. Every method delegates to the analogous function and exposes the same optional-session contract.
|
||||
|
||||
```python
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
class WidgetRepository:
|
||||
def __init__(
|
||||
self,
|
||||
database_url: str,
|
||||
session_factory: async_sessionmaker[AsyncSession] | None = None,
|
||||
) -> None:
|
||||
def __init__(self, database_url: str) -> None:
|
||||
self.database_url = database_url
|
||||
self.session_factory = session_factory
|
||||
|
||||
async def create(
|
||||
self,
|
||||
@@ -205,7 +188,6 @@ class WidgetRepository:
|
||||
description,
|
||||
database_url=self.database_url,
|
||||
session=session,
|
||||
session_factory=self.session_factory,
|
||||
)
|
||||
|
||||
async def get(
|
||||
@@ -218,7 +200,6 @@ class WidgetRepository:
|
||||
widget_id,
|
||||
database_url=self.database_url,
|
||||
session=session,
|
||||
session_factory=self.session_factory,
|
||||
)
|
||||
|
||||
async def list(
|
||||
@@ -233,7 +214,6 @@ class WidgetRepository:
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
session=session,
|
||||
session_factory=self.session_factory,
|
||||
)
|
||||
|
||||
async def update(
|
||||
@@ -250,7 +230,6 @@ class WidgetRepository:
|
||||
description,
|
||||
database_url=self.database_url,
|
||||
session=session,
|
||||
session_factory=self.session_factory,
|
||||
)
|
||||
|
||||
async def delete(
|
||||
@@ -263,11 +242,10 @@ class WidgetRepository:
|
||||
widget_id,
|
||||
database_url=self.database_url,
|
||||
session=session,
|
||||
session_factory=self.session_factory,
|
||||
)
|
||||
```
|
||||
|
||||
The object is intentionally thin. The factory override lets tests supply a maker bound to a test engine without FastAPI startup. A caller-provided session always wins and remains open after the method returns. A standalone operation closes its owned session before returning, so returned objects are detached; load every required scalar, deferred column, and relationship explicitly before the scope exits, and do not mutate those objects expecting persistence.
|
||||
The object is intentionally thin. Tests can construct it with a test database URL or pass a transaction-scoped test session to individual methods. A caller-provided session always wins and remains open after the method returns. A standalone operation closes its owned session before returning, so returned objects are detached; load every required scalar, deferred column, and relationship explicitly before the scope exits, and do not mutate those objects expecting persistence.
|
||||
|
||||
If a read participates in a later write, pass the same session and place both operations inside the explicit transaction. This avoids splitting one use case across sessions and keeps SQLAlchemy's autobegin behavior from obscuring transaction ownership. Add a repository only when its naming, shared query policy, dependency substitution, or domain boundary improves the application. Independent functions remain a valid and often clearer design.
|
||||
|
||||
@@ -294,7 +272,6 @@ async def replace_widget(
|
||||
async with transaction_scope(
|
||||
database_url=repository.database_url,
|
||||
session=session,
|
||||
session_factory=repository.session_factory,
|
||||
) as active_session:
|
||||
deleted_widget = await repository.delete(
|
||||
widget_id,
|
||||
|
||||
Reference in New Issue
Block a user