diff --git a/text/0000-transactional-data-service.md b/text/0000-transactional-data-service.md new file mode 100644 index 0000000..abc0e7f --- /dev/null +++ b/text/0000-transactional-data-service.md @@ -0,0 +1,69 @@ +- Дата создания: 2018-12-14 +- RFC PR: (оставьте изначально пустым) +- RFC Issue: (оставьте пустым, если для RFC предварительно не создавалось обсуждение (issue) в RFC-репозитории) +- Flexberry Issue: (оставьте пустым, если для соответствующей проблемы предварительно не создавалась задача (issue) в одном из репозиториев платформы) + +# TransactionalDataService + +## Краткое описание + +Предлагается реализовать в Flexberry ORM возможность выполнять бизнес-операции в рамках одной транзакции, чтобы все вычитки и обновления объектов через сервис данных внутри бизнес-операции проходили в пределах одной транзакции. Для этого предлагается реализовать декорирующий TransactionalDataService, реализующий интерфейс IDataService и содержащий внутри одну из стандартных реализаций SQLDataService-а. TransactionalDataService должен для всех методов, работающих с БД, некоторым образом определять, создана ли прикладным программистом транзакция, в рамках которой необходимо выполнить это обращение к БД, и, если такая транзакция есть, вызывать подходящие методы внутреннего SQLDataService-а, выполняющиеся в транзакции (...ByExtConn(..., connection, transaction)) + +## Обоснование + +Сейчас бизнес-логика на наших проектах отрабатывает таким образом, что при вызове `dataService.UpdateObjects(objectsArray)` для каждого обновляемого объекта внутри ORM создается его бизнес-сервер, у которого вызывается метод `OnUpdateИмяОбъекта(UpdatedObject)`. Внутри этого метода может осуществляться множество проверок, в ходе которых происходят вычитки из БД с помощью методов `LoadObjects(lcs)`, а также могут изменяться другие объекты, которые пополнят список отправляемых в БД объектов. Прикладной разработчик имеет возможность отправить объекты на обновление, явно указав подключение к БД и транзакцию, с помощью метода `UpdateObjectsByExtConn`. Этот метод можно вызывать несколько раз по ходу бизнес-операции, передавая одно и то же подключение и транзакцию. В этом случае изменения в БД будут видны только в рамках заданной транзакции до момента вызова `transaction.Commit()` в прикладном коде. Однако все вычитки в методах проверок будут выполняться вне этой транзакции, что, на мой взгляд, противоречит намерению прикладного разработчика, и может привести к некорректным результатам проверок. + +### Пример +Представим себе следующую модель: +Есть справочник с полями Наименование (строка), Актуальность (логический), НовоеНаименование (ссылка на самого себя). При снятии Актуальности в бизнес-сервере проверяется, что нет других актуальных справочников, ссылающихся на деактуализируемую запись. При этом вызывается метод `DataService.LoadObjects(lcs)`, вычитывающий из БД связанные актуальные справочники. + +Нужно реализовать бизнес-операцию "Переименование", которая включает в себя: +1. Создание новой записи с новым значением Наименования +2. Перевешивание ссылок с других актуальных справочников со старой записи на новую +3. Простановка старой записи флага Актуальность = false и ссылки НовоеНаименование на созданную новую запись + +В такой постановке реализовать операцию, используя один вызов `UpdateObjects()` невозможно, поскольку некорректно сработает проверка на наличие ссылок на удаляемую запись. Необходимо использовать `UpdateObjectsByExtConn`, самостоятельно управляя моментом фиксации транзакции. Однако и в этом случае проверка все равно сработает некорректно, потому что `LoadObjects` в прикладном коде проверки не использует transaction и connection и нет готового способа их ему передать. + +## Детальное проектирование + +Предлагается внести следующие доработки в ORM: +1. Создать интерфейс `ITransactionManager` со свойствами `Connection`, `Transaction` и методом `IEnumerable ExecuteInTransaction(Func> operation)` +2. Создать реализацию этого интерфейса `TransactionManager` + 1. `Connection`, `Transaction` с приватными setter-ами + 2. `ExecuteInTransaction` проверяет, если `Transaction` и `Connection` `== null`, оборачивает вызов `operation` в создание коннекта и транзакции с try-catch-finally блоком с коммитом и роллбеком транзакции соответственно в try и catch и занулением `Transaction` и `Connection` в finally. Если `Transaction` и `Connection` не нуллы, просто возвращать результат `operation` (это означает, что где-то раньше по стеку вызовов транзакция уже была создана). + 3. Добавить конструктор, принимающий `IDataService`. Конструктор должен проверять, что переданный объект является наследником `SQLDataService`, чтобы мочь создавать транзакции. + 4. Для облегчения тестирования корректности работы и потенциальных усовершенствований в будущем необходимо добавить событие `BeforeCommitTransaction(IEnumerable operationResult)`. Вызывать это событие в `ExecuteInTransaction` перед вызовом `transaction.Commit()`. +3. Создать `sealed class TransactionalDataService:IDataService` + 1. Добавить ему конструктор, принимающий `IDataService` и `ITransactionManager`. В конструкторе проверять, что переданный `IDataService` является наследником `SQLDataService`, `ITransactionManager` не нулл. + 2. В методах сохранения и загрузки объектов проверять значения `_transactionManager.Connection`, `Transaction` и, если они не нуллы, вызывать методы `...ByExtConn(...)` внутреннего `_dataService`, иначе - простые методы внутреннего `_dataService`. +4. Реализовать интеграционные тесты метода `ExecuteInTransaction` в сочетании с `TransactionalDataService`, проверяющие, что вычитки и обновления внутри метода действительно выполняются в рамках транзакции, а явная вычитка из БД вне транзакции не видит результатов обновлений внутри метода до момента фиксации транзакции. + +В результате, если прикладной разработчик хочет иметь возможность корректно обрабатывать бизнес-операции в рамках транзакции, он должен сделать следующее: +1. В Unity-конфиге добавить + 1. именованную регистрацию `IDataService` - "decorableDataService". Зарегистрировать в ней какого-либо наследника `SQLDataService`. + 2. регистрацию `ITransactionManager`, заинжектив ему в конструктор "decorableDataService" + 3. регистрацию `TransactionalDataService` в качестве `IDataService`, заинжектив ему в конструктор "decorableDataService" +2. В прикладном коде перед вызовом метода, реализующего бизнес-операцию, получить реализацию `ITransactionManager` из Unity-контейнера +3. Обернуть вызов метода, реализующего бизнес-операцию, в метод `ITransactionManager.ExecuteInTransaction` + +В результате будет гарантироваться, что вся работа с базой внутри данного метода будет осуществляться в рамках одной транзакции + +## Документирование и обучение + +Необходимо будет дополнить раздел документации к ORM, описывающий сервисы данных, добавив туда описание `TransactionalDataService`. В статье про него необходимо также описать `ITransactionManager`, `TransactionManager`, метод `ExecuteInTransaction`, а также необходимые изменения конфигурации Unity. + +## Недостатки + +Не до конца ясны потенциальные сложности новой реализации `IDataService`. В частности, нет методов `LoadObjectsCountByExtConn`, `LoadStringed...`. Необходимо решить что с ними делать: какие-то реализовать, а какие-то возможно оставить нетронутыми, поскольку они обычно не вызываются в прикладном коде. + +Также не ясно как правильно построить работу с кешами внутри датасервиса. Возможно возникнут какие-то необычные баги. + +## Альтернативы + +Озвучивалось предложение не реализовывать отдельный `TransactionManager`, а реализовать метод `ExecuteInTransaction` внутри `TransactionalDataService`. На мой взгляд это решение нарушает принцип единой ответственности: сервис данных должен общаться с БД и осуществлять объектно-реляционный маппинг, а управление транзакциями - отдельная функциональность. Кроме того, разработчик все равно будет вынужден в прикладном коде каким-то образом резолвить объект, содержащий метод `ExecuteInTransaction`. Этот метод на фоне методов `LoadObjects` и `UpdateObjects` находится на ином уровне абстракции, поэтому считаю неправильным смешивать их в одном классе. + +Озвучивалось предложение реализовать `TransacionManager:IDisposable`, чтобы коннект и транзакция создавались в конструкторе, а удалялись в `Dispose`. Однако не ясно как затем инжектить его в сервис данных. Также не ясно как должны обрабатываться ошибки и в какой момент должна проиcходить фиксация или откат транзакции. Кроме того, это создает опасность утечек ресурсов при неаккуратном использовании. Описанный выше способ таких возможностей прикладному разработчику не оставляет. + +## Нерешенные вопросы + +Что делать с методами `IDataService`, для которых в `SQLDataService` нет транзакционных реализаций?