iikoFront API SDK

Редактирование данных

Под редактированием данных подразумеваются такие действия как создание заказов, манипуляция с их составом, резервирование столов, регистрация гостей и т. п. Каждое отдельно взятое действие вносит небольшое точечное изменение — например, AddOrderItemProduct добавляет в заказ блюдо, а AddOrderItemModifier добавляет к блюду модификатор. Многие действия сами по себе могут приводить данные к несогласованному состоянию — к примеру, если блюдо имеет обязательные модификаторы, то добавление блюда без модификаторов нарушило бы соответствующее правило предметной области. Комбинируя эти действия, можно добавить в заказ блюдо с модификаторами. При этом важно, чтобы набор действий выполнялся с соблюдением принципа «всё или ничего» и переводил данные из одного согласованного состояния в другое согласованное состояние. Для обеспечения транзакционности при изменении данных вводится понятие сессии редактирования.

Сессии редактирования

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

  1. Создаётся сессия IEditSession с помощью вызова PluginContext.Operations.CreateEditSession.
  2. В рамках сессии выполняется одно или несколько действий.
  3. Сделанные изменения сохраняются методом PluginContext.Operations.SubmitChanges.

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

Синхронизация

Доступ к данным синхронизирован с помощью блокировок и ревизий. На время редактирования объекты блокируются, но через API напрямую управлять блокировками невозможно — объекты блокируются автоматически при сохранении изменений (SubmitChanges). Это соответствует концепции оптимистических блокировок: на этапах создания сессии редактирования и выполнения действий объекты не блокируются, а при сохранении сделанных изменений происходит проверка, не изменились ли они в это же время кем-то другим. При каждом изменении объектам назначается новая ревизия, что позволяет различать разные версии одного и того же объекта. С учётом низкой конкуренции за параллельное редактирование одних и тех же объектов такой подход позволяет упростить программный интерфейс (нет методов управления блокировками, не надо заботиться об их корректном освобождении) и обеспечить доступность данных (невозможно надолго заблокировать объект, невозможно «забыть» отпустить блокировку, в случае аварийного завершения работы плагина объект не зависнет в заблокированном состоянии).

Следует учитывать некоторые особенности реализации:

Выполнение операций непрерываемой серией

Операции (IOperationService), требующие синхронизации для редактирования данных, блокируют и затем разблокируют объекты при каждом вызове. Соответственно, при последовательном вызове нескольких операций каждая из них будет независимо от других блокировать данные, вносить изменения и затем разблокировать, между вызовами операций могут «вклиниться» другие желающие редактировать эти же данные, в результате часть наших операций выполнится успешно, а часть может завершиться ошибками EntityAlreadyInUseException, EntityModifiedException, некоторые операции могут стать неприменимыми с учётом чужих правок.

Например, в плагине, принимающем из внешнего источника (веб-сайт, агрегатор) заказы на доставку, понадобилось создать доставку с проведённой внешней предоплатой. Выполнить всё это атомарно в рамках одной сессии редактирования невозможно, поскольку проведение платежа — необратимая операция, выполняется отдельно, поэтому сначала создаём доставку и заполняем её поля, включая добавление непроведённой внешней предоплаты (CreateEditSession, CreateDeliveryOrder, AddExternalPaymentItem и пр.), сохраняем эти изменения (SubmitChanges), а затем пытаемся провести предоплату (ProcessPrepay). Между этими операциями (SubmitChanges и ProcessPrepay) данные разблокированы и доступны для редактирования любому желающему. Те, кто подписан на изменения доставок, могут успеть получить уведомление о создании нами новой доставки и внести в неё изменения прежде, чем мы проведём предоплату. Писать код, который не боится быть прерванным и способен продолжать работать, подстраиваясь под чужие правки, трудоёмко. Для удобства реализации подобных сценариев добавлена возможность выполнения нескольких операций одной непрерываемой серией.

ExecuteContinuousOperation — специальная операция, внутри которой можно последовательно выполнить несколько других операций одной непрерываемой серией. В коде плагина нужно собрать серию операций в одну функцию или лямбду и передать её как callback в метод ExecuteContinuousOperation, который вызовет этот callback, передав ему на вход специальный экземпляр сервиса IOperationService, предназначенный для непрерываемого выполнения операций:

PluginContext.Operations.ExecuteContinuousOperation(
    operations =>
    {
        ...
        operations.SubmitChanges(...);
        ...
        operations.ProcessPrepay(...);
        ... 
    });

Следует обратить внимание, что корневая операция ExecuteContinuousOperation вызывается через общий сервис PluginContext.Operations, а вложенные операции — через полученный лямбдой на вход экземпляр сервиса (названный в примере выше operations). Технически, вызываемые через специальный экземпляр сервиса операции работают точно так же, но не разблокируют после себя данные, то есть каждая операция, требующая синхронизации, блокирует данные, если они ещё не были заблокированы предыдущими операциями, вносит изменения и оставляет данные заблокированными для последующих операций. Это гарантирует, что никто другой не сможет «угнать» блокировку и «вклиниться», и наши последующие операции операции над этими же объектами не столкнутся с EntityAlreadyInUseException. Данные разблокируются при возврате управления из лямбды.

Следует учесть следующие ограничения:

Заглушки

Так как результаты действий невозможно получить до сохранения всей сессии, при выполнении последовательности действий в рамках одной сессии бывает необходимо сослаться на создаваемый, но ещё не существующий объект. Например, вслед за созданием заказа нужна возможность добавить в него гостя, этому гостю — блюдо, а блюду — модификатор, хотя при этом ещё нет ни заказа, ни гостя, ни блюда. Для этой цели вводится понятие заглушек объектов — неких фиктивных, однако, однозначных указателей на объекты. Действия создания объектов, такие как CreateOrder или AddOrderGuest, возвращают заглушки вида INew...Stub, которые в рамках той же сессии можно использовать вместо будущих объектов.

Большинство методов редактирования принимают в качестве аргументов подобные заглушки, что позволяет передавать в них как существующие, так и новые объекты. Например, метод SetOrderType принимает IOrderStub, поэтому можно задать тип и уже существующему заказу (IOrder : IOrderStub), и только создаваемому (INewOrderStub : IOrderStub).

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

Ожидаемые исключения

При попытке сохранить изменения могут возникать различные исключения. Некоторые из них могут свидетельствовать об ошибке в коде плагина (например, ArgumentNullException или ArgumentOutOfRangeException), подавлять такие исключения не рекомендуется (лучше исправить ошибку в коде). Однако, некоторые исключения предугадать или предотвратить невозможно, их следует перехватывать и корректно обрабатывать:

Синтаксический сахар

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