Бд

Если вам нужно хранить много файлов/документов и вы не хотите задумываться (до разумных пределов, разумеется) о структуре хранения, иерархии, связях, вам может подойти одна из документо-ориентированных баз данных. Дополнительным преимуществом являются широкие возможности для масштабирования.

Виды баз данных. Большой обзор типов СУБД

Часто, в обзорах видов баз данных упоминают реляционные и “другие”, “NoSQL” и т.д., либо приводят самые основные типы СУБД (базы данных), забывая о редких. В данной статье я постараюсь описать максимально полно виды баз данных и привести примеры конкретных реализаций. Разумеется, статья не претендует на всеохватность и классифицировать базы данных можно по разному, в том числе по типам оптимальной нагрузки и т.д., и все виды баз данных будут рассмотрены очень кратко. Но надеюсь, статья даст базовое представление о видах СУБД и принципах их работы.

В статье мы рассмотрим следующие типы баз данных:

  • Реляционные
  • Ключ-значение
  • Документо-ориентированные
  • Базы данных временных рядов
  • Графовые базы данных
  • Поисковые базы данных (Search Engines)
  • Объектно-ориентированные базы данных
  • RDF (Resource Description Framework)
  • Wide Column Stores
  • Мультимодальные СУБД
  • Native XML СУБД
  • GEO/GIS (пространственные) и специализированные СУБД
  • Event СУБД (баз данных переходов состояний)
  • Контентные СУБД
  • Навигационные (Navigational) СУБД
  • Векторные базы данных

Начнем с самого распространенного типа — реляционных СУБД.

Структура реляционных баз данных

Реляционная модель подразумевает логическую структуру данных: таблицы, представления и индексы. Логическая структура отличается от физической структуры хранения. Такое разделение дает возможность администраторам управлять физической системой хранения, не меняя данных, содержащихся в логической структуре. Например, изменение имени файла базы данных не повлияет на хранящиеся в нем таблицы.

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

Чтобы обеспечить точность и доступность данных, в реляционных базах должны соблюдаться определенные правила целостности. Например, в правилах целостности можно запретить использование дубликатов строк в таблицах, чтобы устранить вероятность попадания неправильной информации в базу данных.

Что такое СУБД и SQL? Разбираемся с понятиями

Система управления базами данных (СУБД) — это программное обеспечение, предназначенное для создания, управления, обновления и анализа баз данных. Она обеспечивает интерфейс для взаимодействия пользователя или приложения с данными, хранящимися в базе данных. СУБД позволяют структурировать данные таким образом, чтобы обеспечить их легкий доступ, безопасность и эффективное использование.

Взаимодействие СУБД и БД

СУБД предоставляют различные функции.

  1. Позволяют определить структуру БД, включая таблицы, поля, связи и ограничения.
  2. Обеспечивает добавление, изменение и удаление записей в БД с помощью стандартных команд.
  3. Поддерживают правила, гарантирующие, что данные остаются согласованными и целостными.
  4. Позволяют группировать операции в транзакции для обеспечения атомарности, согласованности, изолированности и долговечности (ACID-свойства).
  5. Управляет доступом к информации и защищает их от несанкционированного доступа.
  6. Оптимизирует запросы и структуру информации, чтобы обеспечить быстрое выполнение запросов.

Функции СУБД

SQL (Structured Query Language) — это специализированный язык программирования, предназначенный для взаимодействия с реляционными базами данных. С помощью SQL можно осуществлять различные операции с информацией с помощью запросов, включая добавление, изменение и удаление записей, а также создание и модификацию таблиц и прочие операции.

Взаимодействие SQL и БД

Подытожим, СУБД и SQL взаимодействуют между собой, обеспечивая эффективное управление и работу с информацией. СУБД предоставляют средства для создания и управления БД, а SQL позволяет выполнять различные операции с записями в этих базах, что делает их основой современных информационных систем.

Слои

Разделение кода на слои — это довольно распространенный подход, независимо от того, сколько слоев вы используете и как называете этот подход (чистая архитектура, шестиугольная, порты и адаптеры, луковая и т. д.). Это имеет смысл по многим причинам: разделение ответственности, возможность параллельной работы и упрощение тестирования. Мы уже писали об этих концепциях в других статьях.

В оставшейся части статьи я предполагаю, что ваш SQL-код находится в отдельной структуре, файле или пакете, отдельно от логики приложения, и вы инжектируете одно в другое. В примерах кода я для простоты разделяю “слои” в разных файлах, но в одном пакете.

http.go — HTTP-обработчик (мы не будем заострять внимание на этом).

app.go — логика приложения.

repository.go — репозиторий (хранилище данных).

три слоя счастья

В логике приложения я определяю команду UsePointsAsDiscount и обработчик для неё (подробнее см. статью Роберта о CQRS). Вы можете предпочесть что-то немного другое, например, структуру сервиса с методами или use cases. Всё это нормально. Главное, что эта часть кода ничего не знает о базе данных. Репозиторий инжектируется в командный обработчик через интерфейс, определенный рядом с самим обработчиком.

Вот как может выглядеть код без транзакций:

// UsePointsAsDiscount описывает команду, в которой пользователь тратит свои баллы на скидку. type UsePointsAsDiscount struct < UserID int // UserID — это ID пользователя Points int // Points — количество баллов для списания. >// UsePointsAsDiscountHandler отвечает за обработку команды UsePointsAsDiscount. // Он взаимодействует с двумя репозиториями: userRepository для пользователей и discountRepository для скидок. type UsePointsAsDiscountHandler struct < userRepository UserRepository discountRepository DiscountRepository >// UserRepository описывает интерфейс для работы с пользователями. // GetPoints возвращает количество баллов у пользователя по его ID. // TakePoints уменьшает количество баллов у пользователя. type UserRepository interface < GetPoints(ctx context.Context, userID int) (int, error) TakePoints(ctx context.Context, userID int, points int) error >// DiscountRepository описывает интерфейс для работы со скидками. // AddDiscount увеличивает скидку пользователя для следующего заказа. type DiscountRepository interface < AddDiscount(ctx context.Context, userID int, discount int) error >// NewUsePointsAsDiscountHandler создает новый UsePointsAsDiscountHandler, // принимая два репозитория: userRepository для работы с пользователями и // discountRepository для работы со скидками. func NewUsePointsAsDiscountHandler( userRepository UserRepository, discountRepository DiscountRepository, ) UsePointsAsDiscountHandler < return UsePointsAsDiscountHandler< userRepository: userRepository, discountRepository: discountRepository, >> // Handle обрабатывает команду UsePointsAsDiscount. // Проверяет, что количество баллов положительное, что у пользователя достаточно баллов, // списывает баллы и добавляет соответствующую скидку. func (h UsePointsAsDiscountHandler) Handle(ctx context.Context, cmd UsePointsAsDiscount) error < // Валидация: количество баллов должно быть больше 0 if cmd.Points // Получаем текущее количество баллов пользователя currentPoints, err := h.userRepository.GetPoints(ctx, cmd.UserID) if err != nil < return fmt.Errorf("could not get points: %w", err) >// Проверяем, что у пользователя достаточно баллов if currentPoints < cmd.Points < return errors.New("not enough points") >// Списываем баллы у пользователя err = h.userRepository.TakePoints(ctx, cmd.UserID, cmd.Points) if err != nil < return fmt.Errorf("could not take points: %w", err) >// Добавляем скидку пользователю err = h.discountRepository.AddDiscount(ctx, cmd.UserID, cmd.Points) if err != nil < return fmt.Errorf("could not add discount: %w", err) >// Если всё прошло успешно, возвращаем nil, что означает отсутствие ошибок return nil >

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

Репозитории выглядят так:

// PostgresDiscountRepository — реализация DiscountRepository для работы с PostgreSQL. // Он хранит ссылку на соединение с базой данных. type PostgresDiscountRepository struct < db *sql.DB >// NewPostgresDiscountRepository создаёт новый экземпляр PostgresDiscountRepository, // принимая соединение с базой данных в качестве аргумента. func NewPostgresDiscountRepository(db *sql.DB) *PostgresDiscountRepository < return &PostgresDiscountRepository< db: db, >> // AddDiscount увеличивает скидку пользователя в базе данных. // Она выполняет SQL-запрос для обновления поля скидки для следующего заказа. func (r *PostgresDiscountRepository) AddDiscount(ctx context.Context, userID int, discount int) error < // Выполняем SQL-запрос для увеличения скидки на следующую покупку _, err := r.db.ExecContext(ctx, "UPDATE user_discounts SET next_order_discount = next_order_discount + $1 WHERE user_id = $2", discount, userID) // Возвращаем ошибку, если что-то пошло не так return err >

Мы бы хотели, чтобы все это выполнялось в рамках одной транзакции, но у нас есть куча логики, смешанной с двумя репозиториями. Давайте посмотрим, как к этому подойти.

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

Все примеры доступны в репозитории go-web-app-anti patterns. Существует Docker Compose, который позволяет запускать их все локально. В качестве базы данных используется Postgres.

Транзакции в слое логики (избегайте, если можете)

Первое решение для работы с транзакциями через репозитории — это передача объекта транзакции по всему коду.

Для выполнения кода в транзакции мы можем использовать вспомогательную функцию, такую как runInTx

func runInTx(db *sql.DB, fn func(tx *sql.Tx) error) error < tx, err := db.Begin() if err != nil < return err >err = fn(tx) if err == nil < return tx.Commit() >rollbackErr := tx.Rollback() if rollbackErr != nil < return errors.Join(err, rollbackErr) >return err >

Эта функция принимает другую функцию (вы увидите много анонимных функций в этой статье), которая получает объект транзакции. Что бы ни происходило внутри функции fn, она не знает и не заботится о том, как транзакция запускается, фиксируется или откатывается.

Вот как это работает внутри хендлера:

func (h UsePointsAsDiscountHandler) Handle(ctx context.Context, cmd UsePointsAsDiscount) error < return runInTx(h.db, func(tx *sql.Tx) error < if cmd.Points currentPoints, err := h.userRepository.GetPoints(ctx, tx, cmd.UserID) if err != nil < return fmt.Errorf("could not get points: %w", err) >if currentPoints < cmd.Points < return errors.New("not enough points") >err = h.userRepository.TakePoints(ctx, tx, cmd.UserID, cmd.Points) if err != nil < return fmt.Errorf("could not take points: %w", err) >err = h.discountRepository.AddDiscount(ctx, tx, cmd.UserID, cmd.Points) if err != nil < return fmt.Errorf("could not add discount: %w", err) >return nil >) >

Хорошая новость в том, что этот подход работает. Но нельзя игнорировать тот факт, что он смешивает логику приложения с деталями реализации (SQL-транзакцией). На первый взгляд, это может не казаться серьезной проблемой: мы, вероятно, не будем менять базу данных, так что зачем беспокоиться?

Во-первых, управление транзакцией с последовательностью коммит/откат усложняет поток кода. Вместо работы с чистой логикой приложения вам нужно учитывать дополнительное поведение, которое не имеет прямого отношения к тому, что делает ваше приложение. По мере роста логики вы должны тщательно обдумывать, что должно выполняться внутри транзакции, а что — вне её. Откат повлияет на всё, что вы поместите внутрь функции.

Не говоря уже о странном аргументе tx, который нужно передавать в методы репозитория. Тестирование становится неудобным, потому что нужно передавать транзакцию даже тогда, когда метод её не требует (метод GetPoints может работать без транзакции в другом контексте). Вы не можете мокировать SQL-соединение для тестирования командного обработчика, поэтому это становится интеграционным тестом, а не простым юнит-тестом, проверяющим логику.

Можно попытаться обойти это с помощью абстрактного объекта транзакции или, возможно, передавать его через Context, но это только маскирует корневую проблему смешения деталей реализации с логикой.

Анти-паттерн: Транзакции смешаны с логикой

Избегайте смешивания транзакций с логикой приложения. Это усложняет понимание работы кода, тестирование логики и отладку проблем.

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

Транзакции внутри репозитория (лучше, но далеко не идеально)

Если транзакция принадлежит слою базы данных, почему бы не оставить её там? Сложность заключается в том, что мы работаем с двумя репозиториями, и им нужно как-то разделить объект транзакции. Иногда это действительно необходимо (и я покажу, как это сделать чуть позже). Но сначала стоит задуматься, нужны ли нам два репозитория.

Часто мы рассматриваем таблицы базы данных как сущности. В результате у нас появляется один репозиторий на каждую таблицу, и логика приложения управляет их взаимодействием. Часто такое разделение репозиториев не приносит практической пользы.

Table Driven Design

Анти-паттерн: Один репозиторий на каждую таблицу базы данных

Не создавайте репозиторий для каждой таблицы базы данных. Вместо этого думайте о данных, которые нужно хранить транзакционно вместе.

Данные, которые должны быть транзакционно согласованными, должны также быть когерентными и храниться как одно целое. Domain-Driven Design предлагает идею агрегата — набора данных, которые всегда должны быть согласованными. Следуя этой идее, вы создаете репозиторий не для каждой SQL-таблицы, а для каждого агрегата. (Это краткое изложение всей идеи. Скоро у нас выйдет полноценная статья об агрегатах.)

Мы рассматриваем пользователей и скидки как отдельные концепции (сущности, структуры и т. д.). И это логично, так как концептуально это разные вещи. Пользователь используется для идентификации и аутентификации. Размещение заказов со скидками — это лишь одна из множества вещей, которые может делать пользователь на сайте. Но мы также хотим поддерживать согласованность баллов и скидок пользователя. Мы можем рассматривать их как часть одного агрегата — набора объектов, которые хранятся вместе транзакционно.

На практике можно считать скидки частью агрегата пользователя, даже если они хранятся в другой таблице SQL. В таком случае нам нужен только один репозиторий. И это позволяет нам перенести обработку транзакции туда.

Теперь логика приложения становится тривиальной.

func (h UsePointsAsDiscountHandler) Handle(ctx context.Context, cmd UsePointsAsDiscount) error < if cmd.Points err := h.userRepository.UsePointsForDiscount(ctx, cmd.UserID, cmd.Points) if err != nil < return fmt.Errorf("could not use points as discount: %w", err) >return nil >

А репозиторий предоставляет метод, специфичный для этой операции.

func (r *PostgresUserRepository) UsePointsForDiscount(ctx context.Context, userID int, points int) error < return runInTx(r.db, func(tx *sql.Tx) error < row := tx.QueryRowContext(ctx, "SELECT points FROM users WHERE FOR UPDATE", userID) var currentPoints int err := row.Scan(&currentPoints) if err != nil < return err >if currentPoints < points < return errors.New("not enough points") >_, err = tx.ExecContext(ctx, "UPDATE users SET points = points - $1 WHERE points, userID) if err != nil < return err >_, err = tx.ExecContext(ctx, "UPDATE user_discounts SET next_order_discount = next_order_discount + $1 WHERE user_id = $2", points, userID) if err != nil < return err >return nil >) >

Тактика: Агрегаты

Храните данные, которые должны быть строго согласованными, в одном агрегате. Каждому агрегату по репозиторию!

Транзакции в репозитории

Этот подход всё ещё имеет некоторые недостатки. Один из них — необходимость перемещения логики проверки баллов (“достаточно ли баллов”) в репозиторий. Название метода (UsePointsForDiscount) делает очевидным, что репозиторий знает что-то о логике, и иногда это может быть допустимо. Но в идеале эта логика должна находиться в обработчике.

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

Хотя этот подход полезен, он плохо масштабируется. Кажется, что нам нужно более универсальное решение с методом обновления.

Паттерн UpdateFn (наше основное решение)

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

Во-первых, нам нужна модель, которая объединяет пользователя и скидки (наш агрегат). Я использую инкапсуляцию для скрытия полей, поскольку мы хотим, чтобы модель всегда была валидной в памяти. Единственный способ изменить состояние — это вызвать экспортированные методы.

// User представляет пользователя с его уникальным ID, email, баллами (points) и скидками (discounts). type User struct < id int // уникальный идентификатор пользователя email string // email пользователя points int // количество баллов пользователя, которые он может потратить discounts *Discounts // информация о скидках пользователя >// ID возвращает уникальный идентификатор пользователя. func (u *User) ID() int < return u.id >// Email возвращает email пользователя. func (u *User) Email() string < return u.email >// Points возвращает текущее количество баллов пользователя. func (u *User) Points() int < return u.points >// Discounts возвращает структуру скидок, связанную с пользователем. func (u *User) Discounts() *Discounts < return u.discounts >// Discounts представляет скидки пользователя, например, скидку на следующий заказ. type Discounts struct < nextOrderDiscount int // скидка на следующий заказ >// NextOrderDiscount возвращает значение скидки на следующий заказ пользователя. func (c *Discounts) NextOrderDiscount() int

Добавление скидки осуществляется вызовом метода UsePointsAsDiscount.

func (u *User) UsePointsAsDiscount(points int) error < if points if u.points < points < return errors.New("not enough points") >u.points -= points u.discounts.nextOrderDiscount += points return nil >

Легко понять, тривиально тестировать. Нет необходимости думать о транзакциях базы данных при чтении этого кода. На самом деле, нет необходимости думать даже о таблицах базы данных. Именно с таким кодом я хочу работать.

Теперь введем интерфейс метода обновления.

type UserRepository interface

Аргумент updateFn — это то, как мы отделяем логику от деталей базы данных. Функция получает модель User, которую можно изменить. Любые изменения сохраняются в хранилище, если не возвращается ошибка.

updateFn возвращает значение updated типа bool, указывающее, был ли пользователь обновлен. Если возвращается true, репозиторий сохраняет пользователя. Пока у нас нет случая для возврата false, но это может быть полезно при обновлении нескольких значений (например, при использовании PATCH-запросов). Тогда обновление может быть ненужным и пропускаться.

Теперь обработчик выглядит так:

func (h UsePointsAsDiscountHandler) Handle(ctx context.Context, cmd UsePointsAsDiscount) error < return h.userRepository.UpdateByID(ctx, cmd.UserID, func(user *User) (bool, error) < err := user.UsePointsAsDiscount(cmd.Points) if err != nil < return false, err >return true, nil >) >

Теперь давайте посмотрим на реализацию репозитория.

func (r *PostgresUserRepository) UpdateByID(ctx context.Context, userID int, updateFn func(user *User) (bool, error)) error < return runInTx(r.db, func(tx *sql.Tx) error < row := tx.QueryRowContext(ctx, "SELECT email, points FROM users WHERE FOR UPDATE", userID) var email string var currentPoints int err := row.Scan(&email, &currentPoints) if err != nil < return err >row = tx.QueryRowContext(ctx, "SELECT next_order_discount FROM user_discounts WHERE user_id = $1 FOR UPDATE", userID) var discount int err = row.Scan(&discount) if err != nil < return err >discounts := UnmarshalDiscounts(discount) user := UnmarshalUser(userID, email, currentPoints, discounts) updated, err := updateFn(user) if err != nil < return err >if !updated < return nil >_, err = tx.ExecContext(ctx, "UPDATE users SET email = $1, points = $2 WHERE user.Email(), user.Points(), user.ID()) if err != nil < return err >_, err = tx.ExecContext(ctx, "UPDATE user_discounts SET next_order_discount = $1 WHERE user_id = $2", user.Discounts().NextOrderDiscount(), user.ID()) if err != nil < return err >return nil >) >

В репозитории нет никакой логики. Он извлекает все данные из таблиц пользователей и скидок, переводит их в модели приложения (с помощью функций Unmarshal), а затем вызывает функцию updateFn. Затем он сохраняет все изменения обратно в базу данных. Всё это происходит в рамках одной транзакции.

Совет

Сейчас репозиторий довольно многословен. ORM может помочь избавиться от шаблонного кода. Но будьте осторожны при выборе библиотеки, так как некоторые ORM могут принести больше вреда, чем пользы. Проверьте библиотеки, которые никогда нас не подводили.

Таким образом, мы достигли цели: логика остаётся в обработчике, а транзакция остаётся в репозитории.

Тактика: Паттерн UpdateFn

Используйте метод Update, который загружает и сохраняет агрегат. Логику оставляйте в замыкании updateFn.

Распространенное беспокойство по поводу метода Update заключается в том, что он обновляет все поля, даже если они не изменились (например, в приведённом выше примере email).

Если вы не сохраняете огромные наборы данных или не работаете с большим масштабом, это не должно иметь большого значения. Вы можете отслеживать, какие поля изменились, и обновлять только их, но это, скорее всего, только усложнит вашу архитектуру без фактической выгоды для производительности. Будьте прагматичными и избегайте преждевременной оптимизации. Если сомневаетесь, проведите нагрузочные тесты.

Паттерн Transaction Provider — решение для особых случаев

Этот паттерн полезен, когда нужно управлять транзакцией, которая затрагивает несколько репозиториев, хотя это не всегда является лучшей практикой. Прежде всего, стоит задаться вопросом: “Почему это не единый репозиторий?”, но иногда действительно бывают случаи, когда транзакция должна охватывать несколько репозиториев.

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

Как работает Transaction Provider:

1. Transaction Provider содержит метод Transact, который принимает функцию как аргумент. В этой функции можно работать с адаптерами (структура с репозиториями), и всё это будет происходить в пределах одной транзакции.

2. Адаптеры включают репозитории, которые зависят от транзакции, например, UserRepository и AuditLogRepository. Эти репозитории работают в рамках одной транзакции, но сама транзакция явно не передаётся в коде приложения.

type UsePointsAsDiscountHandler struct < txProvider txProvider >type txProvider interface < Transact(txFunc func(adapters Adapters) error) error >type Adapters struct < UserRepository UserRepository AuditLogRepository AuditLogRepository >func (h UsePointsAsDiscountHandler) Handle(ctx context.Context, cmd UsePointsAsDiscount) error < return h.txProvider.Transact(func(adapters Adapters) error < // Обновляем баллы пользователя err := adapters.UserRepository.UpdateByID(ctx, cmd.UserID, func(user *User) (bool, error) < err := user.UsePointsAsDiscount(cmd.Points) if err != nil < return false, err >return true, nil >) if err != nil < return fmt.Errorf("could not use points as discount: %w", err) >// Записываем действия пользователя в аудит-лог log := fmt.Sprintf("used %d points as discount for user %d", cmd.Points, cmd.UserID) err = adapters.AuditLogRepository.StoreAuditLog(ctx, log) if err != nil < return fmt.Errorf("could not store audit log: %w", err) >return nil >) >

В этом примере UserRepository и AuditLogRepository работают в рамках одной транзакции, но явный объект транзакции не передаётся в коде обработчика.

Основные моменты:

Адаптеры предоставляют необходимые зависимости (репозитории) для работы внутри транзакции.

• Мы не передаём объект транзакции напрямую в методы репозиториев. Вместо этого репозитории принимают транзакцию через конструктор.

Транзакция управляется “в фоне” через TransactionProvider, что делает код чище и читабельнее.

Пример реализации репозитория:

type db interface < QueryRowContext(ctx context.Context, query string, args . interface<>) *sql.Row ExecContext(ctx context.Context, query string, args . interface<>) (sql.Result, error) > type PostgresUserRepository struct < db db >func NewPostgresUserRepository(db db) *PostgresUserRepository < return &PostgresUserRepository< db: db, >>

Реализация Transaction Provider:

type TransactionProvider struct < db *sql.DB >func NewTransactionProvider(db *sql.DB) *TransactionProvider < return &TransactionProvider< db: db, >> func (p *TransactionProvider) Transact(txFunc func(adapters Adapters) error) error < return runInTx(p.db, func(tx *sql.Tx) error < adapters := Adapters< UserRepository: NewPostgresUserRepository(tx), AuditLogRepository: NewPostgresAuditLogRepository(tx), >return txFunc(adapters) >) >

Важные моменты:

Не злоупотребляй паттерном: хотя паттерн Transaction Provider может быть полезен в некоторых случаях, злоупотребление им может привести к тому, что логика, связанная с несколькими репозиториями, начнёт слишком тесно переплетаться.

Опасности при использовании FOR UPDATE: если ты используешь FOR UPDATE в своих запросах, будьте осторожны с этим паттерном, так как нужно тщательно следить за тем, чтобы все SELECT включали этот флаг для корректной работы транзакции.

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

Когда использовать Transaction Provider:

• Когда тебе действительно нужно объединить несколько репозиториев в рамках одной транзакции, но логика не требует их объединения в единый агрегат.

• Использовать паттерн стоит только для особых случаев, например, для технических задач, таких как ведение аудит-лога, когда их интеграция в основной агрегат будет чрезмерной.

Этот паттерн может быть полезным, но нужно использовать его с осторожностью, чтобы не усложнить логику приложения.

Это завершает рассмотрение работы с SQL-транзакциями в рамках одного сервиса. Однако существует смежная тема, которая часто возникает и требует особого внимания: транзакции между сервисами.

  • transactions
  • repository pattern
  • transaction manager
  • layers

Источники:

https://habr.com/ru/companies/amvera/articles/754702/&rut=0c838a8dd7d91a4ede2bcdc259919d23ff5dbe43fee0f54b9271826a0f2f385c
https://www.oracle.com/cis/database/what-is-a-relational-database/&rut=ab73a056c13cd81b0df998c272e2676682bf80be731d722c44c942dc84d5683b
https://habr.com/ru/companies/first/articles/755832/&rut=d01f54ff4147a4fbf289c65902f89006b16533f487575efcd34c831f399c9b24
https://habr.com/ru/articles/848596/&rut=c1a981a16bb037a7dcdf824f64f95ab37c2e628f910acb22223aa2a4ca7c130d