-
Notifications
You must be signed in to change notification settings - Fork 0
/
db_manager.py
131 lines (113 loc) · 6.29 KB
/
db_manager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""Creating thread safe and managed sessions using SQLAlchemy.
The sessions that are created are expected to be:
- thread safe
- handle committing
- handle rolling back on errors
- handle session removal/releasing once context or thread is closed.
Author: Nitish Reddy Koripalli
License: MIT
# Resources for creating thread safe sessions.
SOURCE: https://docs.sqlalchemy.org/en/latest/orm/session_basics.html#getting-a-session
SOURCE: https://docs.sqlalchemy.org/en/latest/orm/contextual.html#unitofwork-contextual
SOURCE: https://stackoverflow.com/questions/21078696/why-is-my-scoped-session-raising-an-attributeerror-session-object-has-no-attr
################################################################################
# Documentation & Motivations for this entire module.
################################################################################
#######################################
# Motivations
#######################################
* What we would like to have is a unified way to get a database session in order to make transactions with the database.
* At this very moment we are not interested in the techinical details of the interactions our code has with the database.
What we want is a way to interact with a way without worrying about:
- thread safety
- memory leaks i.e. not returning connections to the connection pools etc.
* Finally we would want a method that returns a database session object that inherently takes care of the above concerns
without the user having to take care of those concerns.
* Although it is not enforced by SQLAlchemy, it does recommended to separate individual db operations from session
creation. That is, it recommends not create sessions right next to where you make a single database query.
We will try to follow this pattern recommendation blindly.
* We want a universal database session creation for offline and online (flask) applications.
#######################################
# Implementation & Notes
#######################################
* SQLAlchemy provides the sessionmaker function which generates a new Session object whenever it is called.
The recommended way to use it is to have a global variable that has a configured sessionmaker object which can
then be used by other parts of the application to create sessions.
```
session_factory = sessionmaker(bind=engine)
session = session_factory() # This can be called from elsewhere in the application.
```
However the sessionmaker does not generate Sessions that are meant to be used in a multi-threaded way for example in
a webserver.
You can release resources for such a session by using `session.close()`.
* In order to abstract away thread handling from the user, SQLAlchemy provides scoped_session functionality, that
maintains a session per thread registry. That is, (if I'm not mistaken) scoped_session always maintain one session per
threading.local(). If somehow in the same thread, we created a new session using scoped_session, it will return
the same session object unless the session object has been explicity removed/closed in between.
The recommended way to use it is to have a global variable that has a configured scoped_session object which can
then be used by other parts of the application to create sessions.
```
thread_safe_session_factory = scoped_session(sessionmaker(bind=engine))
session = thread_safe_session_factory() # This can be called from elsewhere in the application from any thread.
```
You can release resources for such a session by using `thread_safe_session_factory.remove()` and not `session.remove()`
since `thread_safe_session_factory` maintains the registry.
* Finally we provided a ManagedSession function that returns thread safe sessions and takes care committing, closing,
removing and rolling back the session on errors. We expect the ManagedSession to be used as follows:
```
with ManagedSession() as session: # multiple db_operations are done within one session.
db_operations.select(session, **kwargs) # db_operations is expected not to worry about session handling.
db_operations.insert(session, **kwargs) # after the with statement, the session commits to the database.
```
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import scoped_session
import contextlib
engine = None
thread_safe_session_factory = None
def init_engine(uri, **kwargs):
"""Initialize the engine.
Args:
uri (str): The string database URI. Examples:
- sqlite:///database.db
- postgresql+psycopg2://username:[email protected]:5432/database
"""
global engine
if engine is None:
engine = create_engine(uri, **kwargs)
return engine
def init_session_factory():
"""Initialize the thread_safe_session_factory."""
global engine, thread_safe_session_factory
if engine is None:
raise ValueError("Initialize engine by calling init_engine before calling init_session_factory!")
if thread_safe_session_factory is None:
thread_safe_session_factory = scoped_session(sessionmaker(bind=engine))
return thread_safe_session_factory
@contextlib.contextmanager
def ManagedSession():
"""Get a session object whose lifecycle, commits and flush are managed for you.
Expected to be used as follows:
```
with ManagedSession() as session: # multiple db_operations are done within one session.
db_operations.select(session, **kwargs) # db_operations is expected not to worry about session handling.
db_operations.insert(session, **kwargs) # after the with statement, the session commits to the database.
```
"""
global thread_safe_session_factory
if thread_safe_session_factory is None:
raise ValueError("Call init_session_factory before using ManagedSession!")
session = thread_safe_session_factory()
try:
yield session
session.commit()
session.flush()
except Exception:
session.rollback()
# When an exception occurs, handle session session cleaning,
# but raise the Exception afterwards so that user can handle it.
raise
finally:
# source: https://stackoverflow.com/questions/21078696/why-is-my-scoped-session-raising-an-attributeerror-session-object-has-no-attr
thread_safe_session_factory.remove()