Перейти к содержанию

SQLAlchemy

SQLAlchemy - самая популярная и годная ORM для Python.

Типы состояний объектов в сессии

  1. Transient: Объект находится во временном состоянии, если он был только что создан и еще не связан с сессией SQLAlchemy. Такой объект еще не были сохранён в БД и не имеет, например, идентификатора.

  2. Pending: Как только объект добавляется в сессию с помощью метода add(), он переходит в состояние ожидания. В этом состоянии объект готов к сохранению в БД.

  3. Persistent: Такой объект уже присутствует в сессии и имеет запись в БД. Любые изменения объекта автоматически отслеживаются сессией и при необходимости будут отправлены в БД.

  4. Deleted: Если объект был помечен для удаления из базы данных во время текущей сессии, он переходит в состояние удаления. Объект будет удалён при следующем коммите.

  5. 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, сервисный слой и так далее), которые и описывают логику.

Полезные ссылки