Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransactionalDataService #9

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions text/0000-transactional-data-service.md
Original file line number Diff line number Diff line change
@@ -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<DataObject> ExecuteInTransaction(Func<IEnumerable<DataObject>> 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<DataObject> 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` нет транзакционных реализаций?