Skip to content

Commit a80d896

Browse files
Unit of work 3: Transactional database: auto-commit or rollback per endpoint (#729)
Open a transaction per request, to ensure that everything will be either committed or rollback. ## Migration strategy Existing `db.commit()` were replaced by `db.flush()` even when it may not have been necessary to prevent creating unexpected regressions. ## Advanced usage The database transaction is automatically commited at the end. - If an HTTPException is raised during the request, we consider that the error was expected and managed by the endpoint. We commit the session. - If an other exception is raised, we rollback the session to avoid. > Cruds and endpoints should never call `db.commit()` or `db.rollback()` directly. >After adding an object to the session, calling `await db.flush()` will integrate the changes in the transaction without committing them. > If an endpoint needs to add objects to the sessions that should be committed even in case of an unexpected error, it should start a SAVEPOINT after adding the object. > ```python > # Add here the object that should always be committed, even in case of an unexpected error > await db.add(object) > await db.flush() > # Start a SAVEPOINT. If the code in the following context manager raises an exception, the changes will be rolled back to this point. > async with db.begin_nested(): > # Add objects that may be rolled back in case of an error here > ``` --- Supersede #498
1 parent 327287c commit a80d896

39 files changed

+870
-1620
lines changed

app/core/auth/cruds_auth.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from datetime import UTC, datetime
44

55
from sqlalchemy import delete, select, update
6-
from sqlalchemy.exc import IntegrityError
76
from sqlalchemy.ext.asyncio import AsyncSession
87

98
from app.core.auth import models_auth
@@ -29,13 +28,8 @@ async def create_authorization_token(
2928
"""Create a new group in database and return it"""
3029

3130
db.add(db_authorization_code)
32-
try:
33-
await db.commit()
34-
except IntegrityError:
35-
await db.rollback()
36-
raise
37-
else:
38-
return db_authorization_code
31+
await db.flush()
32+
return db_authorization_code
3933

4034

4135
async def delete_authorization_token_by_token(
@@ -49,7 +43,7 @@ async def delete_authorization_token_by_token(
4943
models_auth.AuthorizationCode.code == code,
5044
),
5145
)
52-
await db.commit()
46+
await db.flush()
5347
return None
5448

5549

@@ -71,13 +65,8 @@ async def create_refresh_token(
7165
"""Create a new refresh token in database and return it"""
7266

7367
db.add(db_refresh_token)
74-
try:
75-
await db.commit()
76-
except IntegrityError:
77-
await db.rollback()
78-
raise
79-
else:
80-
return db_refresh_token
68+
await db.flush()
69+
return db_refresh_token
8170

8271

8372
async def revoke_refresh_token_by_token(
@@ -94,7 +83,7 @@ async def revoke_refresh_token_by_token(
9483
)
9584
.values(revoked_on=datetime.now(UTC)),
9685
)
97-
await db.commit()
86+
await db.flush()
9887
return None
9988

10089

@@ -114,7 +103,7 @@ async def revoke_refresh_token_by_client_and_user_id(
114103
)
115104
.values(revoked_on=datetime.now(UTC)),
116105
)
117-
await db.commit()
106+
await db.flush()
118107

119108

120109
async def revoke_refresh_token_by_user_id(
@@ -131,4 +120,4 @@ async def revoke_refresh_token_by_user_id(
131120
)
132121
.values(revoked_on=datetime.now(UTC)),
133122
)
134-
await db.commit()
123+
await db.flush()

app/core/core_endpoints/cruds_core.py

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from collections.abc import Sequence
22

33
from sqlalchemy import delete, select
4-
from sqlalchemy.exc import IntegrityError
54
from sqlalchemy.ext.asyncio import AsyncSession
65

76
from app.core.core_endpoints import models_core
@@ -87,11 +86,7 @@ async def create_module_group_visibility(
8786
"""Create a new module visibility in database and return it"""
8887

8988
db.add(module_visibility)
90-
try:
91-
await db.commit()
92-
except IntegrityError:
93-
await db.rollback()
94-
raise
89+
await db.flush()
9590

9691

9792
async def create_module_account_type_visibility(
@@ -101,11 +96,7 @@ async def create_module_account_type_visibility(
10196
"""Create a new module visibility in database and return it"""
10297

10398
db.add(module_visibility)
104-
try:
105-
await db.commit()
106-
except IntegrityError:
107-
await db.rollback()
108-
raise
99+
await db.flush()
109100

110101

111102
async def delete_module_group_visibility(
@@ -119,7 +110,7 @@ async def delete_module_group_visibility(
119110
models_core.ModuleGroupVisibility.allowed_group_id == allowed_group_id,
120111
),
121112
)
122-
await db.commit()
113+
await db.flush()
123114

124115

125116
async def delete_module_account_type_visibility(
@@ -134,7 +125,7 @@ async def delete_module_account_type_visibility(
134125
== allowed_account_type,
135126
),
136127
)
137-
await db.commit()
128+
await db.flush()
138129

139130

140131
async def get_core_data_crud(
@@ -164,13 +155,8 @@ async def add_core_data_crud(
164155
To manipulate core data, prefer using the `get_core_data` and `set_core_data` utils.
165156
"""
166157
db.add(core_data)
167-
try:
168-
await db.commit()
169-
except IntegrityError:
170-
await db.rollback()
171-
raise
172-
else:
173-
return core_data
158+
await db.flush()
159+
return core_data
174160

175161

176162
async def delete_core_data_crud(
@@ -182,4 +168,4 @@ async def delete_core_data_crud(
182168
models_core.CoreData.schema == schema,
183169
),
184170
)
185-
await db.commit()
171+
await db.flush()

app/core/google_api/cruds_google_api.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from sqlalchemy import delete, select
2-
from sqlalchemy.exc import IntegrityError
32
from sqlalchemy.ext.asyncio import AsyncSession
43

54
from app.core.google_api import models_google_api
@@ -10,11 +9,7 @@ async def create_oauth_flow_state(
109
db: AsyncSession,
1110
) -> None:
1211
db.add(oauth_flow_state)
13-
try:
14-
await db.commit()
15-
except IntegrityError:
16-
await db.rollback()
17-
raise
12+
await db.flush()
1813

1914

2015
async def get_oauth_flow_state_by_state(

app/core/groups/cruds_groups.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from collections.abc import Sequence
44

55
from sqlalchemy import delete, select, update
6-
from sqlalchemy.exc import IntegrityError
76
from sqlalchemy.ext.asyncio import AsyncSession
87
from sqlalchemy.orm import selectinload
98

@@ -54,13 +53,8 @@ async def create_group(
5453
"""Create a new group in database and return it"""
5554

5655
db.add(group)
57-
try:
58-
await db.commit()
59-
except IntegrityError:
60-
await db.rollback()
61-
raise
62-
else:
63-
return group
56+
await db.flush()
57+
return group
6458

6559

6660
async def delete_group(db: AsyncSession, group_id: str):
@@ -69,7 +63,7 @@ async def delete_group(db: AsyncSession, group_id: str):
6963
await db.execute(
7064
delete(models_groups.CoreGroup).where(models_groups.CoreGroup.id == group_id),
7165
)
72-
await db.commit()
66+
await db.flush()
7367

7468

7569
async def create_membership(
@@ -79,12 +73,8 @@ async def create_membership(
7973
"""Add a user to a group using a membership"""
8074

8175
db.add(membership)
82-
try:
83-
await db.commit()
84-
return await get_group_by_id(db, membership.group_id)
85-
except IntegrityError:
86-
await db.rollback()
87-
raise
76+
await db.flush()
77+
return await get_group_by_id(db, membership.group_id)
8878

8979

9080
async def delete_membership_by_group_id(
@@ -96,7 +86,7 @@ async def delete_membership_by_group_id(
9686
models_groups.CoreMembership.group_id == group_id,
9787
),
9888
)
99-
await db.commit()
89+
await db.flush()
10090

10191

10292
async def delete_membership_by_group_and_user_id(
@@ -110,7 +100,7 @@ async def delete_membership_by_group_and_user_id(
110100
models_groups.CoreMembership.user_id == user_id,
111101
),
112102
)
113-
await db.commit()
103+
await db.flush()
114104

115105

116106
async def update_group(
@@ -123,4 +113,4 @@ async def update_group(
123113
.where(models_groups.CoreGroup.id == group_id)
124114
.values(**group_update.model_dump(exclude_none=True)),
125115
)
126-
await db.commit()
116+
await db.flush()

app/core/memberships/endpoints_memberships.py

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,7 @@ async def create_association_membership(
132132
db=db,
133133
membership=db_association_membership,
134134
)
135-
try:
136-
await db.commit()
137-
except Exception:
138-
await db.rollback()
139-
raise
135+
await db.flush()
140136
return db_association_membership
141137

142138

@@ -170,13 +166,7 @@ async def update_association_membership(
170166
membership=membership,
171167
)
172168

173-
try:
174-
await db.commit()
175-
except Exception:
176-
raise HTTPException(
177-
status_code=500,
178-
detail="Failed to update membership",
179-
)
169+
await db.flush()
180170

181171

182172
@router.delete(
@@ -219,14 +209,6 @@ async def delete_association_membership(
219209
membership_id=association_membership_id,
220210
)
221211

222-
try:
223-
await db.commit()
224-
except Exception:
225-
raise HTTPException(
226-
status_code=500,
227-
detail="Failed to delete membership",
228-
)
229-
230212

231213
@router.get(
232214
"/memberships/users/{user_id}",
@@ -324,13 +306,9 @@ async def create_user_membership(
324306
await validate_user_new_membership(db_user_membership, db)
325307

326308
cruds_memberships.create_user_membership(db=db, user_membership=db_user_membership)
327-
try:
328-
await db.commit()
329-
except Exception:
330-
raise HTTPException(
331-
status_code=500,
332-
detail="Failed to create user membership",
333-
)
309+
310+
await db.flush()
311+
334312
return schemas_memberships.UserMembershipComplete(
335313
**db_user_membership.__dict__,
336314
user=schemas_users.CoreUserSimple(
@@ -401,11 +379,7 @@ async def add_batch_membership(
401379
end_date=detail.end_date,
402380
),
403381
)
404-
try:
405-
await db.commit()
406-
except Exception:
407-
await db.rollback()
408-
raise
382+
await db.flush()
409383
return unknown_users
410384

411385

@@ -448,11 +422,7 @@ async def update_user_membership(
448422
user_membership_edit=user_membership,
449423
)
450424

451-
try:
452-
await db.commit()
453-
except Exception:
454-
await db.rollback()
455-
raise
425+
await db.flush()
456426

457427

458428
@router.delete(
@@ -481,11 +451,3 @@ async def delete_user_membership(
481451
db=db,
482452
user_membership_id=membership_id,
483453
)
484-
485-
try:
486-
await db.commit()
487-
except Exception:
488-
raise HTTPException(
489-
status_code=500,
490-
detail="Failed to delete user membership",
491-
)

app/core/myeclpay/cruds_myeclpay.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ async def init_structure_manager_transfer(
6868
confirmation_token=confirmation_token,
6969
),
7070
)
71-
await db.commit()
7271

7372

7473
async def get_structure_manager_transfer_by_secret(
@@ -196,7 +195,6 @@ async def update_store(
196195
.where(models_myeclpay.Store.id == store_id)
197196
.values(**store_update.model_dump(exclude_none=True)),
198197
)
199-
await db.commit()
200198

201199

202200
async def delete_store(

0 commit comments

Comments
 (0)