SQLAlchemy¶
SQLAlchemy - самая популярная и годная ORM для Python.
Типы состояний объектов в сессии¶
-
Transient: Объект находится во временном состоянии, если он был только что создан и еще не связан с сессией SQLAlchemy. Такой объект еще не были сохранён в БД и не имеет, например, идентификатора.
-
Pending: Как только объект добавляется в сессию с помощью метода
add()
, он переходит в состояние ожидания. В этом состоянии объект готов к сохранению в БД. -
Persistent: Такой объект уже присутствует в сессии и имеет запись в БД. Любые изменения объекта автоматически отслеживаются сессией и при необходимости будут отправлены в БД.
-
Deleted: Если объект был помечен для удаления из базы данных во время текущей сессии, он переходит в состояние удаления. Объект будет удалён при следующем коммите.
-
Detached: Если объект отсоединен от сессии (например, после закрытия сессии), он переходит в "отсоединённое" состояние. В этом состоянии объект по-прежнему ассоциирован с записью в базе данных, но изменения, внесенные в объект, не будут автоматически отслеживаться или синхронизироваться с БД.
Различие между flush() и commit()¶
flush()
и commit()
обновляют состояние БД, но часто создается путаница где что надо использовать.
Рассмотрим их работу и применение.
flush()¶
flush()
синхронизирует состояние сессии с БД. Это значит, что все изменения в сессии перенесутся в БД, но не зафиксируются, поэтому их можно будет откатить при помощи rollback()
.
Транзакция при этом остаётся открытой, поэтому мы можем дальше добавлять изменения.
Использовать flush()
можно тогда, когда мы работаем со связанными объектами.
Например мы создаем запись в таблице User
, а далее нам необходимо создать объект профиля нашего User
, в котором требуется его идентификатор. Использование flush()
позволяет Алхимии обновить идентификаторы и статусы объектов.
В некоторых случаях flush()
используют для валидации целостности данных. Его вызов позволяет найти ошибки целостности и исправить их до фиксации изменений.
commit()¶
commit()
применяет все изменения в БД и закрывает транзакцию. Такие изменения уже нельзя откатить при помощи rollback()
, они становятся постоянными.
Если в сессии присутствуют изменения, которые не были отправлены в БД, то SQLAlchemy сделает вызов flush()
.
Рекомендации по организация кода и использование session.begin()¶
Допустим, у нас есть некоторая функция, которая создает пользователя:
def create_user(session: Session, email: str, username: str) -> UserSchema:
user = User(email=email, username=username)
session.add(user)
session.commit()
return user.to_schema()
def create_profile(session: Session, user_id: int) -> ProfileSchema:
profile = Profile(user_id=user_id)
session.add(user)
session.commit()
return user.to_schema()
def create_user_case(session: Session, email: str, username: str):
user = create_user(session, email, user)
profile = create_profile(user.id)
return {"user": user, "profile": profile}
Для этого можно использовать контекстный менеджер with session.begin()
. Он автоматически начнёт транзакцию, сделает commit() при успешности выполнения всего блока кода под ним, или вызовет rollback()
в случае ошибок.
def create_user_case(session: Session, email: str, username: str):
with session.begin():
user = create_user(session, email, user)
profile = create_profile(user.id)
return {"user": user, "profile": profile}
И так как в create_user()
мы исполняем commit()
, наша транзакция автоматически закроется, вызов create_profile()
будет невозможен.
Что делать в этой ситуации? Очевидно, переписывать код функций create_user()
и create_profile()
без использования commit()
внутри.
Для синхронизации изменений внутри транзакции и получения идентификаторов можно использовать flush()
:
def create_user(session: Session, email: str, username: str) -> UserSchema:
user = User(email=email, username=username)
session.add(user)
session.flush()
return user.to_schema()
def create_profile(session: Session, user_id: int) -> ProfileSchema:
profile = Profile(user_id=user_id)
session.add(user)
session.flush()
return user.to_schema()
Теперь вызов create_user_case()
пройдет как надо, рассмотрим шаги внимательнее:
def create_user_case(session: Session, email: str, username: str):
# стартуем транзакцию
with session.begin():
# 1. создаем пользователя и синхронизируем состояние с БД
# - таким образом у нас появится пользователь c ID,
# который мы сможем использовать для создания профиля
user = create_user(session, email, user)
# 2. создаем профиль используя ID пользователя
# в случае ошибки при создании профиля транзакция откатится
# и пользователь не создастся - то что нам и нужно
profile = create_profile(user.id)
# 3. Транзакция завершится, менеджер сделает commit() и закроет сессию.
return {"user": user, "profile": profile}
Исходя из этого можно выделить несколько правил организации кода:
- По возможности стоит делать запросы, которые потом можно будет переиспользовать.
- Запросы не должны содержать в себе операций, которые, например, закрывают транзакцию.
- Работа с транзакцией должна проводиться в специальных функциях-сервисах (use cases, сервисный слой и так далее), которые и описывают логику.