Паттерн Unit of Work в Python с SQLAlchemy

от автора

Привет, Хабр!

Unit of Work отслеживает все объекты, которые были загружены в память и изменены в ходе выполнения программы. Он управляет их состояниями и сохраняет изменения в базе данных в конце транзакции. Это делается с использованием сессий, которые действуют как контейнеры для всех изменений.

Когда работа завершена, Unit of Work выполняет commit для всех изменений, сохраняя их в базе данных. Если что-то пошло не так, выполняется rollback, и база данных возвращается в состояние до начала транзакции.

В данной статье рассмотрим, как реализовать паттерн Unit of Work с использованием SQLAlchemy.

Установим саму библиотеку:

pip install sqlalchemy

Основные компоненты Unit of Work в SQLAlchemy

Сессия — это главный компонент, через который проходят все взаимодействия с базой данных. Она отвечает за управление объектами, отслеживание изменений и выполнение транзакций. Можно сказать, что сессия — это основной рабочий инструмент, через который ORM управляет состоянием объектов и синхронизацией их с базой данных.

Сессия в SQLAlchemy создается с использованием фабрики сессий, обычно через sessionmaker, который связывает сессию с определенным engine — объектом, который управляет подключением к базе данных. Пример:

from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker  # создаем подключение к базе данных engine = create_engine('postgresql://user:password@localhost/mydatabase')  # создаем фабрику сессий Session = sessionmaker(bind=engine)  # создаем экземпляр сессии session = Session()

Здесь engine управляет низкоуровневыми аспектами подключения к базе данных, а сессия действует как обертка, которая управляет объектами и транзакциями на более высоком уровне.

Одна из основных задач сессии — отслеживание изменений, которые происходят с объектами. SQLAlchemy автоматически отслеживает состояние объектов, которые были загружены в сессию, и любые изменения, сделанные с этими объектами. Например, изменить атрибут объекта, SQLAlchemy пометит этот объект как «измененный«:

# изменение объекта user = session.query(User).filter_by(name='Volodya').first() user.nickname = 'Vladimir'  # сессия отслеживает это изменение

Сессия хранит список всех измененных объектов и применяет эти изменения к базе данных при выполнении транзакции.

SQLAlchemy использует транзакции для атомарности операций. Т.е все изменения, сделанные в рамках одной транзакции, либо успешно применяются к базе данных, либо полностью откатываются, если что-то пошло не так. Сессия в SQLAlchemy автоматически управляет транзакциями.

Коммит — это момент, когда все изменения, накопленные сессией, применяются к базе данных. Если в процессе коммита возникает ошибка, SQLAlchemy автоматически выполнит откат транзакции:

try:     session.commit()  # попытка записать изменения в базу данных except:     session.rollback()  # в случае ошибки откат изменений     raise finally:     session.close()  # закрываем сессию

Механизмы работы сессии

SQLAlchemy приджеривается некоторого механизма управления состоянием объектов в сессии:

  1. Transient: Объект создан, но еще не привязан к сессии и не существует в базе данных.

  2. Pending: Объект добавлен в сессию с помощью метода add(), но изменения еще не зафиксированы в базе данных.

  3. Persistent: Объект связан с сессией и уже существует в базе данных.

  4. Detached: Объект был связан с сессией, но сейчас отсоединен от нее (например, после закрытия сессии).

Рассмотрим, как эти состояния проявляются:

# Создаем новый объект User (состояние Transient) new_user = User(name='Alice', fullname='Alice Wonderland', nickname='AliceW')  # Добавляем объект в сессию (состояние Pending) session.add(new_user)  # Коммит изменений (объект становится Persistent) session.commit()  # Теперь объект является Persistent, но если мы закроем сессию session.close()  # Объект перейдет в состояние Detached

Интеграция Unit of Work с паттерном Repository

Зачем это?

Паттерн Repository предоставляет абстракцию для доступа к данным, позволяя отделить бизнес-логику от деталей доступа к базе данных. Он действует как хранилище объектов домена, предоставляя интерфейсы для операций CRUD.

Паттерн Unit of Work управляет транзакциями, гарантируя, что все операции с данными в рамках одной транзакции выполняются атомарно.

Для начала создадим базовый интерфейс репозитория, который будет определять основные методы для работы с данными. Этот интерфейс должен быть максимально абстрактным, чтобы его можно было легко использовать с разными типами данных и баз данных:

from typing import Generic, TypeVar, List  T = TypeVar('T')  class Repository(Generic[T]):     def add(self, entity: T) -> None:         raise NotImplementedError      def remove(self, entity: T) -> None:         raise NotImplementedError      def get_by_id(self, id) -> T:         raise NotImplementedError      def list(self) -> List[T]:         raise NotImplementedError

Этот интерфейс задает основу для конкретных репозиториев, которые будут работать с определенными типами сущностей.

Теперь создадим конкретную реализацию репозитория, используя SQLAlchemy. В этом репозитории будем управлять сессиями с помощью паттерна Unit of Work:

from sqlalchemy.orm import Session from typing import List  class SQLAlchemyRepository(Repository[T]):     def __init__(self, session: Session):         self.session = session      def add(self, entity: T) -> None:         self.session.add(entity)      def remove(self, entity: T) -> None:         self.session.delete(entity)      def get_by_id(self, id) -> T:         return self.session.query(T).filter_by(id=id).one()      def list(self) -> List[T]:         return self.session.query(T).all()

SQLAlchemyRepository полагается на Session для выполнения всех операций с базой данных. Это позволяет репозиторию быть абстрактным по отношению к типу сущностей, с которыми он работает.

А теперь реализуем саму интеграцию с Unit of Work. Создадим класс, который будет управлять несколькими репозиториями и сессией одновременно:

from contextlib import contextmanager  class UnitOfWork:     def __init__(self, session_factory):         self.session_factory = session_factory         self._session = None         self._repositories = {}      @contextmanager     def start(self):         self._session = self.session_factory()         try:             yield self             self._session.commit()         except:             self._session.rollback()             raise         finally:             self._session.close()      def register_repository(self, entity_type, repository):         self._repositories[entity_type] = repository(self._session)      def get_repository(self, entity_type):         return self._repositories[entity_type]

Класс UnitOfWork управляет сессией, создавая и закрывая её, а также управляет репозиториями.

Теперь посмотрим, как это будет использоваться в приложении:

from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker  engine = create_engine('sqlite:///example.db') Session = sessionmaker(bind=engine)  uow = UnitOfWork(session_factory=Session)  # регистрируем репозиторий uow.register_repository(User, SQLAlchemyRepository)  with uow.start() as uow_session:     user_repo = uow_session.get_repository(User)     new_user = User(name='John Doe', email='john@example.com')     user_repo.add(new_user)      # все изменения будут зафиксированы автоматически в конце блока

Здесь вся работа с базой данных проходит через репозиторий, который в свою очередь управляется Unit of Work.

Пример реализации паттерна Unit of Work

Начнем с создания моделей для базы данных. В нашем случае это будут Book, User и Rental:

from sqlalchemy import Column, Integer, String, ForeignKey, DateTime from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base import datetime  Base = declarative_base()  class Book(Base):     __tablename__ = 'books'     id = Column(Integer, primary_key=True)     title = Column(String)     author = Column(String)     available_copies = Column(Integer)  class User(Base):     __tablename__ = 'users'     id = Column(Integer, primary_key=True)     name = Column(String)     email = Column(String)  class Rental(Base):     __tablename__ = 'rentals'     id = Column(Integer, primary_key=True)     user_id = Column(Integer, ForeignKey('users.id'))     book_id = Column(Integer, ForeignKey('books.id'))     rental_date = Column(DateTime, default=datetime.datetime.utcnow)     return_date = Column(DateTime, nullable=True)      user = relationship('User')     book = relationship('Book')

Эти модели представляют структуру таблиц в базе данных. Пользователи могут брать книги напрокат, и для этого создается запись в таблице rentals.

Далее, создадим репозитории для управления данными. Репозитории будут использоваться для работы с конкретными сущностями:

from sqlalchemy.orm import Session  class BookRepository:     def __init__(self, session: Session):         self.session = session      def add(self, book: Book):         self.session.add(book)      def get_by_id(self, book_id: int) -> Book:         return self.session.query(Book).filter_by(id=book_id).first()      def list_all(self):         return self.session.query(Book).all()  class UserRepository:     def __init__(self, session: Session):         self.session = session      def add(self, user: User):         self.session.add(user)      def get_by_id(self, user_id: int) -> User:         return self.session.query(User).filter_by(id=user_id).first()  class RentalRepository:     def __init__(self, session: Session):         self.session = session      def add(self, rental: Rental):         self.session.add(rental)      def get_active_rentals_by_user(self, user_id: int):         return self.session.query(Rental).filter_by(user_id=user_id, return_date=None).all()

Каждый репозиторий инкапсулирует логику работы с конкретной сущностью. Например, BookRepository управляет объектами Book, а RentalRepository — арендными операциями.

Теперь создадим класс Unit of Work, который будет управлять сессией и репозиториями:

from contextlib import contextmanager  class UnitOfWork:     def __init__(self, session_factory):         self.session_factory = session_factory         self._session = None      @contextmanager     def start(self):         self._session = self.session_factory()         try:             yield self             self._session.commit()         except Exception as e:             self._session.rollback()             raise e         finally:             self._session.close()      @property     def books(self) -> BookRepository:         return BookRepository(self._session)      @property     def users(self) -> UserRepository:         return UserRepository(self._session)      @property     def rentals(self) -> RentalRepository:         return RentalRepository(self._session)

UnitOfWork управляет сессией и предоставляет доступ к репозиториям. Вся работа с данными происходит в контексте одного блока with.

Теперь посмотрим, как можно использовать Unit of Work для выполнения операций, например, аренды книги юзером:

from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker  # настройка подключения к базе данных engine = create_engine('sqlite:///library.db') SessionFactory = sessionmaker(bind=engine)  # создаем Unit of Work uow = UnitOfWork(session_factory=SessionFactory)  # аренда книги def rent_book(user_id: int, book_id: int):     with uow.start() as uow_session:         user = uow_session.users.get_by_id(user_id)         book = uow_session.books.get_by_id(book_id)                  if book.available_copies > 0:             book.available_copies -= 1             rental = Rental(user_id=user.id, book_id=book.id)             uow_session.rentals.add(rental)         else:             raise Exception("No available copies of this book")  # пример использования rent_book(1, 2)  # пользователь с ID 1 берет напрокат книгу с ID 2

Все операции происходят в рамках одного блока with, и если что-то пойдет не так (например, книга уже занята), изменения будут автоматически откатаны.


Системная аналитика: что важно и откуда начать? Обсудим на открытом уроке 22 августа, на котором:

  • Проведем обзор ключевых навыков системного аналитика;

  • Поделимся рекомендациями по началу карьеры и путям развития;

  • Дадим практические советы и инструменты для повышения квалификации.

Записаться на урок можно по ссылке.


ссылка на оригинал статьи https://habr.com/ru/articles/836128/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *