Комментарии 7
// UnitOfWork --
func (ths *SQLStore) UnitOfWork(fn func(writer port.Writer) error) (err error) {
trx := ths.db.Begin()
if err != nil {
return err
}
А для чего здесь идёт проверка ошибки? err в данном случае объявлена как именованный результат, при входе в метод она безальтернативно будет рана nil. Код return err
в этом примере недостижим.
Вы всё же реализовали просто транзакционный метод, это не Unit of Work. Тем более, что у вас Writer вдруг знает о Reader и метод этот во Writer. Что даёт забавную возможность:
ths.writer.UnitOfWork(func(rTx port.Reader, wTx port.Writer) error {
wTx.UnitOfWork(func(rrTx port.Reader, wwTx port.Writer) error {
wwTx.UnitOfWork(func(rrrTx port.Reader, wwwTx port.Writer) error {
// и тд
})
})
})
Суть Unit of Work в том, чтобы там было для нескольких репозиториев ОДНО соединение к СУБД. К транзакциям он не имеет никакого отношения, это скорее приятное следствие. Если брать тот самый EF Core, из которого вы взяли понятие DbContext - именно он внутри отвечает там за транзакции.
По идее должно быть что-то типа такого:
type Repository1 struct {
connection *Connection
}
func (r *Repository1) FindById(id int) Entity {}
type Repository2 struct {
connection *Connection
}
func (r *Repository2) FindById(id int) Entity {}
type UnitOfWork struct {
connection *Connection
repo1 *Repository1
repo2 *Repository2
}
func (u *UnitOfWork) GetRepository1() *Repository1 {
if u.repo1 == nil {
u.repo1 = &Repository1{u.connection}
}
return u.repo1
}
func (u *UnitOfWork) GetRepository2() *Repository2 {
if u.repo2 == nil {
u.repo2 = &Repository2{u.connection}
}
return u.repo2
}
А для транзакций делаем уже, что нам удобнее
// Транзакционный метод
func (u *UnitOfWork) Transaction(fn func() error) {}
// Или методы управления транзакцией
func (u *UnitOfWork) BeginTransaction() *Transaction {}
func (u *UnitOfWork) RollbackTransaction() {}
func (u *UnitOfWork) CommitTransaction() {}
И соответственно интерфейсы на уровне домена/приложения
type Repository1 interface {
FindById(id int) Enity
}
type Repository2 interface {
FindById(id int) Entity
}
type UnitOfWork interface {
func (u *UnitOfWork) GetRepository1() *Repository1
func (u *UnitOfWork) GetRepository2() *Repository2
func (u *UnitOfWork) Transaction(fn func() error)
}
ну и использование
uow.Transaction(func () error {
uow.GetRepository1().FindById(1024)
uow.GetRepository2().FindById(1025)
})
Спасибо за статью!
SQLStore
точно реализует интерфейс Writer? Мне кажется там аргумента не хватает. Могу ошибаться.
А как бы вы реализовали ограничение для unit of work, сначала должны выполниться все операции чтения и только потом операции записи?
Так DDD или чистой архитектуры?
Статья полна бесполезных ничего не значащих слов типа "чистая архитектура", "DDD" (повторяется в статье 8 раз), "Domain-Driven Design (DDD)" (вся фраза целиком повторяется в статье 3 раза)
Своей статьёй вы же и нарушаете свои "принципы" (Do not Repeat Yourself)
разработать шаблон сервиса, который можно было бы использовать как для монолитной, так и для микро-сервисной архитектуры. Шаблон должен был придерживаться принципов Domain-Driven Design (DDD)
Монолит, микросервисы, DDD, всё это пустые слова, здесь буквально написано "мне нужно было написать шаблон программы" - это невозможно.
Все эти слова используются для описания готового приложения, чтобы легче объяснить человеку как оно работает, но невозможно следовать "принципам чистой архитектуры", так как они +- выглядят как "не делайте плохо, делайте хорошо"
В целом всю статью можно описать так:
" А давайте в интерфейс ДБ добавим методы .begin_transaction() .commit() .rollback() "
error_code my_transaction(....) {
auto transaction_handle = db.begin_transaction();
error_code err;
on_scope_exit {
if (err) transaction_handle.rollback();
else transaction_handle.commit();
};
....
}
Вставлю свои 5 копеек, но в терминах C# на примере моей демки https://github.com/KlestovAlexej/Wattle3.DemoServer
При создании UnitOfWork необязательно начинать транзакцию БД сразу и в лоб.
Так как даже работа с доменными объектами может не потребовать БД до самого момента комита UnitOfWork - и соединение к БД не будет держаться без дела паразитно.
Всё зависит от реализации UnitOfWork и той инфраструктуры на которой он живёт.
Не очень понятно, какое отношение результат имеет к DDD. Если в Golang хотите упариваться в DDD и есть проблема транзакций, то event sourcing и SAGA (да, можно применять не только в отношениях сервис-сервис) в контексте DDD более применима.
В ином случае решение от hello_my_name_is_dany работает эффективней.
Паттерн Unit of Work в разрезе чистой архитектуры DDD на языке Golang