Русский
Русский
English
Статистика
Реклама

О репозиториях замолвите слово

image

В последнее время на хабре, и не только, можно наблюдать интерес GO сообщества к луковой/чистой архитектуре, энтерпрайз паттернам и прочему DDD. Читая статьи на данную тему и разбирая примеры кода, постоянно замечаю один момент когда дело доходит до хранения сущностей предметной области начинается изобретение своих велосипедов, которые зачастую еле едут. Код вроде бы состоит из набора паттернов: сущности, репозитории, value objectы и так далее, но кажется, что они для того там чтобы были, а не для решения поставленных задач.
В данной статье я бы хотел не только показать, что, по моему мнению, не так с типичными DDD-примерами на GO, но также продемонстрировать собственную ORM для реализации персистентности доменных сущностей.


Дисклеймер.


Прежде чем приступить к теме статьи, есть несколько моментов, которые необходимо осветить:


  • Данная статья о том, как писать приложения с богатой бизнес логикой. Сервисы на GO зачастую такими не являются, не нужно применять к ним DDDшные подходы.
  • Исходя из того, что я не являюсь ярым фанатом ORM, считаю, что зачастую использование этой технологии попросту излишне. Кроме того, необходимо брать ее лишь в том случае, когда вы отдаете себе отчет в ее целесообразном использовании в проекте, иначе вы попросту используете инструмент для галочки, для того, чтоб был.
  • Оппонировать я буду подходам из этой статьи и (раз, два) примерам проектов.
  • Я буду иллюстрировать свои мысли на примере типичного приложения wish list.

А теперь можно начинать.


Энтерпрайз паттерны в GO и что с ними не так.


Речь здесь пойдет о таких паттернах как: репозиторий, сущность, агрегат и способах их приготовления. Для начала, давайте разберемся, что же это за паттерны такие. Я не буду придумывать определения в стиле от себя, а буду использовать слова признанных мастеров: Ерика Эванса и Мартина Фаулера.


Сущность.


Начнем с сущности. По Эвансу:


Entity: Objects that have a distinct identity that runs through time and different representations. You also hear these called "reference objects".

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


type Wish struct {    id       sql.NullInt64    content  string    createAt time.Time}

Агрегат.


А вот про этот шаблон как то незаслуженно забывают, особенно в контексте GO. А забывают, между прочим, абсолютно зря. Чуть позже мы разберем почему агрегаты намеренно не используются в различных примерах DDD проектов на GO. Итак, определение по Эвансу:


Cluster the entities and value objects into aggregates and define boundaries around each. Choose one entity to be the root of each aggregate and control all access to the objects inside the boundary through the root

Рассмотрим пример aggregate root:


type User struct {    id      sql.NullInt64         name    string                email   Email                  wishes  []*Wish     friends []*User }

Тут у нас агрегат пользователь, который включает в себя сущность User, а также набор желаний этого пользователя и набор друзей.
Ну пока все ок, скажете вы, разве есть какие-то проблемы с реализацией? Я считаю есть, перейдем к репозиториям.


Репозиторий.


A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.

Определение емкое, поэтому выделю основные моменты:


  • Репозиторий абстрагирует конкретное хранилище ну обычно на этом в GO проектах все и заканчивается. Да, конечно, это важно, например, при написании юнит тестов, но это далеко не вся суть репозиториев.
  • Репозитории создаются только для aggregate root. Это исходит из определения агрегата, потому как все, что мы делаем в доменном слое, должно быть сделано через корень агрегата.
  • Репозиторий предоставляет интерфейс схожий с интерфейсом коллекции.

Давайте рассмотрим типичный для GO пример репозитория и как он используется:


type UserRepository interface {    Save(*User)    Update(*User)    FindById(*User, error)}user1 := &User{}userRepo.Save(user1) // Saveuser2, _ := userRepo.FindById(1) // FindByIduser2.Name = new useruserRepo.Update(user2 ) // Update

Вопросы, которые сразу же возникают для таких репозиториев:


  • должен ли FindById загружать коллекции друзей и желаний (что ведет к расходам на дополнительные запросы)? Что, если для решения конкретной бизнес задачи эти коллекции мне не нужны?
  • Должен ли Update каждый раз проверять список друзей и желаний не изменилось ли там что-то? Как мне отслеживать эти изменения?
  • Как быть с транзакционностью? В одном кейсе я хочу сделать Save одного пользователя, а в другом кейсе я хочу, чтобы в транзакции было два Savea. Очевидно, в таком случае управление транзакцией должно быть вне метода Save. Как в данном случае избежать протечки инфраструктурной логики в домен?

Обычно в примерах GO кода такие вопросы принято обходить всеми возможными способами:


  • делаем репозитории и для агрегатов и для сущностей, ведь чем меньше (и проще) структура тем проще ее сохранить
  • сознательно избегаем кейсов, где наша реализация начинает протекать в бизнес логику, так же поступаем с кейсами, реализация которых будет выглядеть крайне громоздкой
  • берем базу данных в которую можно положить агрегат целиком (например mongo) и делаем вид, что других хранилищ не бывает и транзакции нам не нужны

А как насчет схожести интерфейса репозитория к интерфейсу GO-коллекций? Ниже представлен пример работы с коллекцией пользователей реализованной через slice:


var users []*Useruser1 := &User{}users = append(users, user1) // Saveuser2 = users[1] // FindByIduser2.Name = new user // Update

Как видите, эквивалент методу Update для слайса users просто не требуется, потому, что изменения внесенные в агрегат User применяются сразу же.


Обобщим проблемы, которые не дают DDD-like GO коду быть достаточно выразительным, тестируемым и вообще классным:


  • Типичные GO-репозитории создаются для всего подряд, агрегат, сущность может value object who cares? Причина нет ORM или других инструментов позволяющая грамотно работать сразу с графом объектов.
  • Типичные GO-репозитории не стараются походить на коллекции. В результате страдает выразительность и тестируемость кода. Знание о базе данных может протечь в бизнес логику. Причина вновь упираемся в отсутствие подходящей ORM. Можно, опять же, все делать руками, но как показывает практика это слишком неудобно.

D3 ORM. Зачем оно мне?


Хм, похоже что написать свою ORM не самая плохая идея, что я и сделал. Рассмотрим как же она помогает решить описанные выше проблемы. Для начала, как выглядит сущность Wish и агрегат User:


//d3:entity//d3_table:lw_wishtype Wish struct {    id       sql.NullInt64 `d3:"pk:auto"`    content  string    createAt time.Time}//d3:entity//d3_table:lw_usertype User struct {    id      sql.NullInt64      `d3:"pk:auto"`    name    string             `d3:"column:name"`    email   Email              `d3:"column:email"`    wishes  *entity.Collection `d3:"one_to_many:<target_entity:Wish,join_on:user_id,delete:cascade>"`    friends *entity.Collection `d3:"many_to_many:<target_entity:User,join_on:u1_id,reference_on:u2_id,join_table:lw_friend>"`}

Как видите изменений не много, но они есть. Во первых появились аннотации, с помощью которых описывается мета-информация (имя таблицы в БД, маппинг полей структуры на поля в БД, индексы). Во вторых вместо обычных для GO коллекций sliceов D3 ORM накладывает требования на использование своих коллекций. Данное требование исходит из желания иметь фичу lazy/eager loading. Можно сказать, что, если не брать в расчет кастомные коллекции, то описание бизнес сущностей делается полностью нативными средствами.


Ну что ж, а теперь перейдем непосредственно к тому, как выглядят работа с репозиториями в D3ORM:


userRepo, _:= d3orm.MakeRepository(&domain.User{})userRepo.Persists(ctx, user1) // Saveuser2, _ := userRepo.FindOne(ctx, userRepo.Select().AndWhere("id", "=", 1)) // FindByIduser2.Name = new user // Update

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


orm.Session(ctx).Flush()

Если вы работали с такими инструментами как: hybernate или doctrine то, для вас это не будет неожиданностью. Так же для вас не должно быть неожиданностью то, что вся работа выполняется в рамках логических транзакций сессий. Для удобства работы с сессиями в D3 ORM есть ряд функций, которые позволяют положить и вынуть их из контекста.


Разберем еще некоторые примеры кода для демонстрации тех или иных фич:


  • lazy loading, в данном примере запрос на извлечение из БД желаний пользователя будет создан и выполнен в момент непосредственного обращения к коллекции (в последней строке)

u, _ := userRepo.FindOne(ctx, userRepo.Select().AndWhere("id", "=", 1)) // будет сгенерирован запрос только для таблицы lw_userwishes := u.wishes.ToSlice() // cгенерируется запрос для таблицы lw_wish

  • transactions D3 ORM использует концепцию UnitOfWork или другими словами транзакции на уровне приложения. Все изменения накапливаются пока не будет вызван Flush(). Кроме того транзакцией можно управлять вручную, объединяя несколько Flushей в одну транзакцию

userRepo.Persists(ctx, user1)userRepo.Persists(ctx, user2)orm.Session(ctx).Flush() // стандартное поведение - при вызове Flush создается физическая транзакция, в рамках которой выполняется два insertasession := orm.Session(ctx)session.BeginTx() // переводим в ручной режим управления транзакциейuserRepo.Persists(ctx, user1)userRepo.Persists(ctx, user2)session.Flush() // в ручном режиме тут не будет сгенерировано запросов к базеuserRepo.Persists(ctx, user3)session.Flush()session.CommitTx() // на этой строчке будет сгенерирована транзакция в рамках которой выполняется три inserta 

  • при вызове Persists сохраняются все объекты от корневого (то есть граф объектов). При этом запросы в базу данных на вставку/обновление генерируются только для тех, которые действительно изменились

Подробно о том, как работать с ORM, есть документация, а также демо проект. Краткий список фич:


  • кодогенерация вместо рефлексии
  • автогенерация схемы базы данных на основе сущностей
  • один к одному, один ко многим и многие ко многим связи между сущностями
  • lazy/eager загрузка связей
  • query builder
  • загрузка связей в одном запросе к базе (используется join)
  • кэш сущностей
  • каскадное удаление и обновление связанных сущностей
  • application-level transactions (UnitOfWork)
  • DB transactions
  • поддерживается UUID

А зачем оно вам?


Резюмируя, чем вам может быть полезна D3 ORM:


  • у вас много бизнес логики и вы хотите: чтобы ваш код был как можно ближе к языку доменной области и чтобы все инварианты и вообще весь доменный слой был покрыт юнит тестами

В противном случае не могу советовать использовать D3 ORM.
А еще бы хотел описать случаи, где, по моему мнению, использовать любую ORM плохая идея:


  • если вам действительно важна производительность и вы боритесь за каждую аллокацию
  • если ваше приложение выполняет в основном READ операции. Ну право дело, для этого у нас есть отличный инструмент SQL, зачем нам что-то другое?
  • если ваше приложение тонкий клиент к базе данных. Зачем вводить ненужные абстракции?

Заключение.


Надеюсь данной статьей мне удалось хотя бы немного поставить под сомнение типичный GO-style написания бизнес логики. Кроме того, я постарался показать и альтернативу этому подходу. В любом случае решать, как писать код, вам, ну что ж, удачи в этом нелегком деле!

Источник: habr.com
К списку статей
Опубликовано: 26.10.2020 14:14:03
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Проектирование и рефакторинг

Go

Orm

Ddd

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru