Привет, Хабр!
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 приджеривается некоторого механизма управления состоянием объектов в сессии:
Transient: Объект создан, но еще не привязан к сессии и не существует в базе данных.
Pending: Объект добавлен в сессию с помощью метода
add()
, но изменения еще не зафиксированы в базе данных.Persistent: Объект связан с сессией и уже существует в базе данных.
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 августа, на котором:
Проведем обзор ключевых навыков системного аналитика;
Поделимся рекомендациями по началу карьеры и путям развития;
Дадим практические советы и инструменты для повышения квалификации.
nronnie
Вообще не понял зачем смешивать репозиторий и UoW. Совершенно перпендикулярные паттерны.