I have a CRUD with insert and update functions with commit
at the end of the each one as follows:
@staticmethod
def insert(db: Session, item: Item) -> None:
db.add(item)
db.commit()
@staticmethod
def update(db: Session, item: Item) -> None:
...
db.commit()
I have an endpoint which receives a sqlalchemy session from a FastAPI dependency and needs to insert and update atomically (DB transaction).
What's the best practice when working with transactions? I can't work with the CRUD since it does more than one commit
.
How should I handle the transactions? Where do you commit your session? in the CRUD? or only once in the FastAPI dependency function for each request?
Getting a ConnectionThe transaction is not committed automatically; when we want to commit data we normally need to call Connection. commit() as we'll see in the next section. “autocommit” mode is available for special cases. The section Setting Transaction Isolation Levels including DBAPI Autocommit discusses this.
commit() is used to commit the current transaction. It always issues Session.
begin() method may also be used to begin the Session level transaction; calling upon Session. connection() subsequent to that call may be used to set up the per-connection-transaction isolation level: sess = Session(bind=engine) with sess. begin(): # call connection() with options before any other operations proceed.
ORMs. FastAPI works with any database and any style of library to talk to the database. A common pattern is to use an "ORM": an "object-relational mapping" library. An ORM has tools to convert ("map") between objects in code and database tables ("relations").
However, the recommended approach for using SQLAlchemy’s ORM with FastAPI has evolved over time to reflect both insights from the community and the addition of new features to FastAPI. The fastapi_restful.session module contains an implementation making use of the most up-to-date best practices for managing SQLAlchemy sessions with FastAPI.
It is common to use Flask with a package called Flask-SQLAlchemy. Flask-SQLAlchemy isn’t necessary and has problems of its own. For more information on this, give this article a read! There is no FastAPI-SQLALchemly because FastAPI integrates well with vanilla SQLAlchemy!
SQLAlchemy’s dialects support settable isolation modes on a per- Engine or per- Connection basis, using flags at both the create_engine () level as well as at the Connection.execution_options () level. When using the ORM Session, it acts as a facade for engines and connections, but does not expose transaction isolation directly.
SQLAlchemy includes a helper object that helps with the establishment of user-defined Session scopes. This is useful for eliminating threading issues across your app. To create a session, below we use the sessionmaker function and pass it a few arguments. Sessionmaker is a factory for initializing new Session objects.
I had the same problem while using FastAPI. I couldn't find a way to use commit in separate methods and have them behave transactionally.
What I ended up doing was a flush instead of the commit
, which sends the changes to the db, but doesn't commit the transaction.
One thing to note, is that in FastAPI every request opens a new session and closes it once its done. This would be a rough example of what is happening using the example in the SQLAlchemy docs.
def run_my_program():
# This happens in the `database = SessionLocal()` of the `get_db` method below
session = Session()
try:
ThingOne().go(session)
ThingTwo().go(session)
session.commit()
except:
session.rollback()
raise
finally:
# This is the same as the `get_db` method below
session.close()
The session that is generated for the request is already a transaction. When you commit that session what is actually doing is this
When using the Session in its default mode of autocommit=False, a new transaction will be begun immediately after the commit, but note that the newly begun transaction does not use any connection resources until the first SQL is actually emitted.
In my opinion after reading that it makes sense handling the commit
and rollback
at the endpoint scope.
I created a dummy example of how this would work. I use everything form the FastAPI guide.
def create_user(db: Session, user: UserCreate):
"""
Create user record
"""
fake_hashed_password = user.password + "notreallyhashed"
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
db.add(db_user)
db.flush() # Changed this to a flush
return db_user
And then use the crud operations in the endpoint as follows
from typing import List
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
...
def get_db():
"""
Get SQLAlchemy database session
"""
database = SessionLocal()
try:
yield database
finally:
database.close()
@router.post("/users", response_model=List[schemas.User])
def create_users(user_1: schemas.UserCreate, user_2: schemas.UserCreate, db: Session = Depends(get_db)):
"""
Create two users
"""
try:
user_1 = crud.create_user(db=db, user=user_1)
user_2 = crud.create_user(db=db, user=user_2)
db.commit()
return [user_1, user_2]
except:
db.rollback()
raise HTTPException(status_code=400, detail="Duplicated user")
In the future I might investigate moving this to a middleware, but I don't think that using commit
you can get the behavior you want.
A more pythonic approach is to let a context manager perform a commit or rollback depending on whether or not there was an exception.
A Transaction
is a nice abstraction of what we are trying to accomplish.
class Transaction:
def __init__(self, session: Session = Depends(get_session)):
self.session = session
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
# rollback and let the exception propagate
self.session.rollback()
return False
self.session.commit()
return True
And, use it in your APIs, like so:
def some_api(tx: Transaction = Depends(Transaction)):
with tx:
ThingOne().go()
ThingTwo().go()
No need to pass session to ThingOne and ThingTwo. Inject it into them, like so:
class ThingOne:
def __init__(self, session: Session = Depends(get_session)):
...
class ThingTwo:
def __init__(self, session: Session = Depends(get_session)):
...
I would also inject ThingOne and ThingTwo in the APIs as well:
def some_api(tx: Transaction = Depends(Transaction),
one: ThingOne = Depends(ThingOne),
two: ThingTwo = Depends(ThingTwo)):
with tx:
one.go()
two.go()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With