Skip to content

Commit 9b95abd

Browse files
authored
Merge pull request #30 from febus982/update_multithread_docs
Update docs
2 parents 26afc5f + bc950cd commit 9b95abd

File tree

5 files changed

+87
-117
lines changed

5 files changed

+87
-117
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ typing:
1313
poetry run mypy
1414

1515
format:
16-
poetry run black --check sqlalchemy_bind_manager tests
16+
poetry run black --check .
1717

1818
lint:
1919
poetry run ruff .
2020

2121
format-fix:
22-
poetry run black sqlalchemy_bind_manager tests
22+
poetry run black .
2323

2424
lint-fix:
2525
poetry run ruff . --fix

README.md

Lines changed: 18 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ pip install sqlalchemy-bind-manager
3131

3232
[//]: # (https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md)
3333
* [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta) **SQLAlchemy manager:** Implementation is mostly finalised, needs testing in production.
34-
* [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta) **Repository / Unit of work:** Implementation is mostly finalised, needs testing in production.
34+
* [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta) **Repository:** Implementation is mostly finalised, needs testing in production.
35+
* [![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#experimental) **Unit of work:** The implementation is working but limited to repositories using the same engine. Distributed transactions across different engines are not yet supported.
3536

37+
## Documentation
38+
39+
The complete documentation can be found [here](https://febus982.github.io/sqlalchemy-bind-manager)
3640

3741
## SQLAlchemy manager
3842

@@ -50,24 +54,16 @@ config = SQLAlchemyConfig(
5054
sa_manager = SQLAlchemyBindManager(config)
5155
```
5256

53-
🚨 NOTE: Using global variables is not thread-safe, please read the [Threading](#threading) section if your application uses multi-threading.
57+
🚨 NOTE: Using global variables is not thread-safe, please read the [Documentation](https://febus982.github.io/sqlalchemy-bind-manager/manager/session/#note-on-multithreaded-applications) if your application uses multi-threading.
5458

5559
The `engine_url` and `engine_options` dictionaries accept the same parameters as SQLAlchemy [create_engine()](https://docs.sqlalchemy.org/en/14/core/engines.html#sqlalchemy.create_engine)
5660

5761
The `session_options` dictionary accepts the same parameters as SQLALchemy [sessionmaker()](https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.sessionmaker)
5862

59-
Once the bind manager is initialised we can retrieve and use the SQLAlchemyBind using the method `get_bind()`
60-
61-
The `SQLAlchemyBind` and `SQLAlchemyAsyncBind` class has the following attributes:
62-
63-
* `engine`: The initialised SQLALchemy `Engine`
64-
* `model_declarative_base`: A base class that can be used to create [declarative models](https://docs.sqlalchemy.org/en/14/orm/mapping_styles.html#declarative-mapping)
65-
* `registry_mapper`: The `registry` associated with the `engine`. It can be used with Alembic or to setup [imperative mapping](https://docs.sqlalchemy.org/en/14/orm/mapping_styles.html#imperative-mapping)
66-
* `session_class`: The class built by [sessionmaker()](https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.sessionmaker), either `Session` or `AsyncSession`
63+
The `SQLAlchemyBindManager` provides some helper methods for common operations:
6764

68-
The `SQLAlchemyBindManager` provides some helper methods to quickly access some of the bind properties without using the `SQLAlchemyBind`:
69-
70-
* `get_session`: returns a Session object
65+
* `get_bind`: returns a `SQLAlchemyBind` or `SQLAlchemyAsyncBind` object
66+
* `get_session`: returns a `Session` object, which works also as a context manager
7167
* `get_mapper`: returns the mapper associated with the bind
7268

7369
Example:
@@ -76,40 +72,20 @@ Example:
7672
bind = sa_manager.get_bind()
7773

7874

79-
class DeclarativeModel(bind.model_declarative_base):
75+
class MyModel(bind.model_declarative_base):
8076
pass
8177

8278

83-
class ImperativeModel:
84-
id: int
85-
86-
87-
imperative_table = Table(
88-
"imperative",
89-
bind.registry_mapper.metadata,
90-
Column("id", Integer, primary_key=True),
91-
Column("name", String, primary_key=True),
92-
)
93-
94-
bind.registry_mapper.map_imperatively(ImperativeModel, imperative_table)
95-
96-
# or using the get_mapper() helper method
97-
sa_manager.get_mapper().map_imperatively(ImperativeModel, imperative_table)
98-
9979
# Persist an object
100-
o = ImperativeModel() # also o = DeclarativeModel()
80+
o = MyModel()
10181
o.name = "John"
102-
with sa_manager.get_bind().session_class()() as session:
103-
session.add(o)
104-
session.commit()
105-
106-
# or using the get_session() helper method for better readability
10782
with sa_manager.get_session() as session:
10883
session.add(o)
10984
session.commit()
110-
11185
```
11286

87+
[Imperative model declaration](https://febus982.github.io/sqlalchemy-bind-manager/manager/models/) is also supported.
88+
11389
### Multiple database binds
11490

11591
`SQLAlchemyBindManager` accepts also multiple databases configuration, provided as a dictionary. The dictionary keys are used as a reference name for each bind.
@@ -135,37 +111,10 @@ sa_manager = SQLAlchemyBindManager(config)
135111

136112
All the `SQLAlchemyBindManager` helper methods accept the `bind_name` optional parameter:
137113

138-
* `get_session(bind_name="default")`: returns a `Session` or `AsyncSession` object
114+
* `get_bind(bind_name="default")`: returns a `SQLAlchemyBind` or `SQLAlchemyAsyncBind` object
115+
* `get_session(bind_name="default")`: returns a `Session` or `AsyncSession` object, which works also as a context manager
139116
* `get_mapper(bind_name="default")`: returns the mapper associated with the bind
140117

141-
### Threading
142-
143-
Global variables are shared between different threads in python. If your application uses
144-
multiple threads, like spawning a thread per request, then you should not store an
145-
initialised session in a global variable, otherwise the state of your models will be shared
146-
among the threads and produce undesired changes in the database.
147-
148-
This is not thread safe:
149-
150-
```python
151-
session = sa_manager.get_session()
152-
session.add(model)
153-
session.commit()
154-
```
155-
156-
If you truly need to have a long-lived session you'll need to use a scoped session,
157-
something like this:
158-
159-
```python
160-
from sqlalchemy.orm import scoped_session
161-
162-
session = scoped_session(sa_manager.get_bind().session_class())
163-
```
164-
165-
Handling the life cycle of scoped sessions is not supported by this documentations.
166-
Please refer to [SQLAlchemy documentation](https://docs.sqlalchemy.org/en/20/orm/contextual.html)
167-
about this.
168-
169118
### Asynchronous database binds
170119

171120
Is it possible to supply configurations for asyncio supported engines.
@@ -176,7 +125,7 @@ config = SQLAlchemyAsyncConfig(
176125
)
177126
```
178127

179-
This will make sure we have an `AsyncEngine` and an `AsyncSession` are initialised.
128+
This will make sure we have an `AsyncEngine` and an `AsyncSession` are initialised, as an asynchronous context manager.
180129

181130
```python
182131
async with sa_manager.get_session() as session:
@@ -211,45 +160,16 @@ class ModelRepository(SQLAlchemyRepository[MyModel]):
211160
extended_repo_instance = ModelRepository(sqlalchemy_bind_manager.get_bind())
212161
```
213162

214-
The classes provide some common use methods:
163+
The repository classes provides methods for common use case:
215164

216-
* `get`: Retrieve a model by identifier
165+
* `get`: Retrieve a model by primary key
217166
* `save`: Persist a model
218167
* `save_many`: Persist multiple models in a single transaction
219168
* `delete`: Delete a model
220169
* `find`: Search for a list of models (basically an adapter for SELECT queries)
221170
* `paginated_find`: Search for a list of models, with pagination support
222171
* `cursor_paginated_find`: Search for a list of models, with cursor based pagination support
223172

224-
### Session lifecycle in repositories
225-
226-
[SQLAlchemy documentation](https://docs.sqlalchemy.org/en/20/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it)
227-
recommends we create `Session` object at the beginning of a logical operation where
228-
database access is potentially anticipated.
229-
230-
Doing this too soon might cause unexpected effects, like unexpected updates being committed,
231-
if the initialised session is shared among different repositories.
232-
233-
A `Repository` represents a generic interface to persist data object to a storage, not necessarily
234-
using SQLAlchemy. It makes sense that the lifecycle of a `Session` follows the one of the Repository
235-
(The assumption is: if we create a Repository, we're going to do a DB operation,
236-
otherwise we wouldn't need one).
237-
238-
Each Repository instance create an internal scoped session. The session gets
239-
automatically closed when the Repository instance is not referenced by any variable (and the
240-
garbage collector clean it up)
241-
242-
In this way we ensure the `Session` we use is isolated, and the same for all the operations we do with the
243-
same Repository.
244-
245-
This approach has a consequence: We can't use SQLAlchemy lazy loading, so we'll need to make sure relationship are always loaded eagerly,
246-
using either approach:
247-
* Setup your model/table relationships to always use always eager loading
248-
* Implement ad-hoc methods to deal with relationships as necessary
249-
250-
Note that `AsyncSession` has [the same limitation on lazy loading](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#asyncio-orm-avoid-lazyloads),
251-
even when keeping the session opened, so it makes sense that the two Repository implementations behave consistently.
252-
253173
### Use the Unit Of Work to share a session among multiple repositories
254174

255175
It is possible we need to run several operations in a single database transaction. While a single
@@ -271,9 +191,4 @@ uow = UnitOfWork(bind, (MyRepo, MyOtherRepo))
271191
with uow.transaction():
272192
uow.MyRepo.save(some_model)
273193
uow.MyOtherRepo.save(some_other_model)
274-
275-
# Optionally disable the commit/rollback handling
276-
with uow.transaction(read_only=True):
277-
model1 = uow.MyRepo.get(1)
278-
model2 = uow.MyOtherRepo.get(2)
279194
```

docs/manager/session.md

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
Using the session is the same as standard SQLAlchemy, we just retrieve the session class
2-
using the `get_session()` method.
1+
Using the session is the same as standard SQLAlchemy. The recommended approach
2+
is using the `get_session()` context manager, so you don't need to manage the
3+
session life cycle.
34

45
```python
56
# Persist an object
67
o = ImperativeModel()
78
o.name = "John"
8-
with sa_manager.get_bind().session_class()() as session:
9+
with sa_manager.get_session() as session:
910
session.add(o)
1011
session.commit()
1112

12-
# or using the get_session() helper method for better readability
13-
with sa_manager.get_session() as session:
13+
# We can also get the `session_class` property of the bind,
14+
# but t
15+
with sa_manager.get_bind().session_class()() as session:
1416
session.add(o)
1517
session.commit()
1618
```
@@ -24,14 +26,47 @@ among the threads and produce undesired changes in the database.
2426

2527
This is not thread safe:
2628

29+
/// note | db.py (a module to have an easy to use session)
2730
```python
2831
session = sa_manager.get_session()
32+
```
33+
///
34+
35+
36+
/// note | some_other_module.py (a module to have an easy-to=use session)
37+
```python
38+
from db import session
39+
2940
session.add(model)
3041
session.commit()
3142
```
43+
///
44+
45+
46+
This instead would be thread safe:
47+
48+
/// note | some_other_module.py (a module to have an easy-to=use session)
49+
50+
```python
51+
def do_something():
52+
session = sa_manager.get_session()
53+
session.add(model)
54+
session.commit()
55+
session.close()
56+
57+
do_something()
58+
```
59+
60+
The `do_something` function can be also in another method, as long as
61+
the `session` variable has no global scope it will be safe.
62+
///
63+
64+
/// tip | Using the `get_session()` context manager is much easier
65+
66+
///
3267

33-
If you truly need to have a long-lived session you'll need to use a scoped session,
34-
something like this:
68+
If you truly need to have a long-lived session in a variable with global scope,
69+
you'll need to use a scoped session like this:
3570

3671
```python
3772
from sqlalchemy.orm import scoped_session

docs/repository/uow.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ repository provide by itself an isolated session for single operations, we have
55
approach for multiple operations.
66

77
We can use the `UnitOfWork` or the `AsyncUnitOfWork` class to provide a shared session to
8-
be used for repository operations, **assuming the same bind is used for all the repositories**.
9-
10-
/// admonition | The direct use of `SQLAlchemyRepository` and `SQLAlchemyAsyncRepository` classes is not yet supported
11-
type: warning
12-
///
8+
be used for repository operations.
139

1410
```python
1511
class MyRepo(SQLAlchemyRepository):
@@ -30,6 +26,16 @@ with uow.transaction(read_only=True):
3026
model2 = uow.MyOtherRepo.get(2)
3127
```
3228

29+
/// admonition | The unit of work implementation is still experimental.
30+
type: warning
31+
32+
There are some limitations in the current implementation that could radically change
33+
the implementation:
34+
35+
* Distributed transactions are not yet supported.
36+
* The direct use of `SQLAlchemyRepository` and `SQLAlchemyAsyncRepository` classes is not yet supported.
37+
///
38+
3339
Both the UnitOfWork classes create an internal `scoped_session` or `async_scoped_session`, behaving
3440
in the same way at the repositories do. This provides the freedom to tune the session lifecycle based
3541
on our application requirements (e.g. one unit of work per http request, per domain, etc.)

pyproject.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,21 @@ plugins = "pydantic.mypy"
7474

7575
[tool.ruff]
7676
select = ["E", "F", "I"]
77+
extend-exclude = ["docs"]
7778

7879
[tool.ruff.per-file-ignores]
7980
"__init__.py" = ["F401"]
8081
"repository.py" = ["F401"]
82+
83+
[tool.black]
84+
files = '''
85+
(
86+
sqlalchemy_bind_manager
87+
tests
88+
)
89+
'''
90+
extend-exclude = '''
91+
(
92+
/docs
93+
)
94+
'''

0 commit comments

Comments
 (0)