Транзакционная пакетная обработка с OData

работая с Web API OData, у меня есть $ batch processing, однако постоянство в базе данных не является транзакционным. Если я включаю несколько запросов в набор изменений в моем запросе, и один из этих элементов терпит неудачу, другой все еще завершается, потому что каждый отдельный вызов контроллера имеет свой собственный DbContext.

например, если я отправлю пакет с двумя наборами изменений:

пакет 1 - Изменений 1 - - Исправьте действительный объект - - Исправьте недопустимый объект - Конец Изменений 1 - 2 набора - - Вставить Допустимый Объект - Конец Набора 2 Конец Партии

Я ожидал бы, что первый допустимый патч будет откат, поскольку набор изменений не может быть завершен полностью, однако, поскольку каждый вызов получает свой собственный DbContext, первый патч фиксируется, второй-нет, и вставка фиксируется.

существует ли стандартный способ поддержки транзакций через запрос $ batch с OData?

5 ответов


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

http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/OData/v3/ODataEFBatchSample/

обновление Исходная ссылка, похоже, исчезла - следующая ссылка включает аналогичную логику (и для v4) для транзакционной обработка:

https://damienbod.com/2014/08/14/web-api-odata-v4-batching-part-10/


  1. теория: давайте убедимся, что мы говорим об одном и том же.
  2. на практике: решение проблемы насколько я can (нет окончательного ответа) может.
  3. на практике, действительно (обновление): канонический способ реализации частей, специфичных для бэкэнда.
  4. Подождите, это решает мою проблему?: не будем забывать, что реализация (3) связана спецификацией (1).
  5. альтернативно: обычный " вы действительно нужно?(окончательного ответа нет).

теория

для записи, вот что спецификация OData должна сказать об этом (акцент мой):

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

http://docs.oasis-open.org/odata/odata/v4.0/cos01/part1-protocol/odata-v4.0-cos01-part1-protocol.html#_Toc372793753

это V4, который едва обновляет V3 относительно пакетных запросов, поэтому те же соображения применяются для служб V3 AFAIK.

чтобы понять это, вам нужно немного фона:

  • пакетные запросы-это наборы упорядоченных запросов и изменений наборы.
  • Изменить Наборы сами по себе являются атомарными единицами работы, состоящими из наборов неупорядоченных запросов, хотя эти запросы могут быть только Модификация Данных запросы (POST, PUT, PATCH, DELETE, но не GET) или Вызова Действий запросы.

вы можете поднять бровь на тот факт, что запросы в наборах изменений неупорядочены, и, честно говоря, у меня нет надлежащего обоснования. Примеры в спецификации четко показывают запросы, ссылающиеся друг на друга, что подразумевает, что порядок их обработки должен быть выведен. На самом деле, я предполагаю, что наборы изменений действительно должны рассматриваться как отдельные запросы (отсюда атомарное требование), которые анализируются вместе и, возможно, сворачиваются в одну операцию бэкэнда (в зависимости от бэкэнда, конечно). В большинстве баз данных SQL, мы можем разумно начать операцию и применить каждый подзапрос в определенном порядке, определенных их взаимозависимость, но для некоторых других бэкэндов, может потребоваться, чтобы эти субчастоты были искажены и имели смысл вместе, прежде чем отправлять какие-либо изменения на тарелки. Это объясняет, почему они не должны применяться по порядку (само понятие может не иметь смысла для некоторых бэкэндов).

подразумевается, что все ваши наборы изменений должны быть логически согласованы сами по себе; например, вы не можете иметь PUT и патч, которые касаются одного и того же свойства в одном наборе изменений. Это было бы двусмысленно. Таким образом, клиент несет ответственность за максимально эффективное объединение операций перед отправкой запросов на сервер. Это всегда должно быть возможно.

(я хотел бы, чтобы кто-то подтвердил это.) теперь я вполне уверен, что это правильная интерпретация.

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

на практике

чтобы вернуться к вашему вопросу, который специфичен для ASP.NET веб-API, кажется они требуют полной поддержки пакетных запросов OData. подробнее здесь. Это также кажется правдой, что, как вы говорите, новый экземпляр контроллера создается для каждого подзапроса (ну, я верю вам на слово), что, в свою очередь, приносит новый контекст и нарушает требование атомарности. Так кто же прав?

Ну, как вы справедливо указываете тоже, если вы собираетесь SaveChanges вызовы в ваших обработчиках, никакое количество framework hackery не поможет. Похоже, вы должны обрабатывать эти подзапросы самостоятельно с учетом соображений, которые я изложил выше (глядя на несогласованные наборы изменений). Вполне очевидно, что вам нужно, чтобы (1) определить, что вы обрабатываете подзапрос, который является частью набора (так что вы можете условно commit) и (2) сохранить состояние между вызовами.

Update: см. следующий раздел о том, как сделать (2), не обращая внимания на функциональность контроллеров (нет необходимости в (1)). следующие два абзаца могут по-прежнему представлять интерес, если вы хотите больше контекста о проблемах, которые решаются the HttpMessageHandler решение.

я не знаю, можете ли вы определить, находитесь ли вы в наборе изменений или нет (1) с текущими API, которые они предоставляют. Я не знаю, сможешь ли ты заставить. ASP.NET чтобы сохранить контроллер в живых для (2). Что вы можете сделать для последнего, однако (если вы не можете сохранить его в живых), это сохранить ссылку на контекст в другом месте (например, в какое-то состояние сеанса Request.Properties) и использовать его условно (обновление: или безоговорочно, если вы управление транзакцией на более высоком уровне, см. ниже). Я понимаю, что это, вероятно, не так полезно, как вы могли бы надеяться, но, по крайней мере, теперь у вас должны быть правильные вопросы для разработчиков/авторов документации вашей реализации.

опасно бессвязный: вместо условного вызова SaveChanges, вы можете условно создать и завершить TransactionScope для каждого набора. Это не устраняет необходимость в (1) или (2), просто другой способ делать вещи. Из этого следует, что фреймворк может технически реализовать это автоматически (до тех пор, пока один и тот же экземпляр контроллера может быть повторно использован), но, не зная достаточно внутренних элементов, я бы не стал пересматривать свое утверждение о том, что фреймворк еще не достаточно, чтобы сделать все сам. В конце концов, семантика TransactionScope может быть слишком конкретным, неуместным или даже нежелательным для определенных бэкэндов.

обновление: этот это действительно то, как выглядит правильный способ делать вещи. В следующем разделе показан пример реализации, использующей явный API транзакций Entity Framework вместо TransactionScope, но это имеет тот же самый конечный результат. Хотя я чувствую, что есть способы сделать общую реализацию Entity Framework, в настоящее время ASP.NET не предоставляет никаких функций, специфичных для EF, поэтому вы должны реализовать это самостоятельно. Если вы когда-либо извлекали свой код, чтобы сделать его многоразовым, поделитесь им за пределами ASP.NET проект, если вы можете (или убедить ASP.NET команда, которую они должны включить в свое дерево).

на практике, действительно (обновление)

см. полезный ответ snow_FFFFFF, который ссылается на пример проекта.

, чтобы положить его в контексте этого ответа, он показывает, как использовать HttpMessageHandler реализовать требование #2, которое я описал выше (сохранение состояния между вызовами контроллеров в рамках одного запроса). Это работает путем закрепления на более высоком уровне чем контроллеры и разделение запроса на несколько "подзапросов", все время сохраняя состояние, не обращая внимания на контроллеры (транзакции) и даже подвергая состояние контроллерам (контекст Entity Framework, в этом случае через HttpRequestMessage.Properties). Контроллеры с радостью обрабатывают каждый запрос, не зная, являются ли они обычными запросами, частью пакетного запроса или даже частью набора изменений. Все, что им нужно сделать, это использовать контекст Entity Framework в свойствах запроса вместо используя свои собственные.

обратите внимание, что у вас на самом деле есть много встроенной поддержки для достижения этого. Эта реализация строится поверх DefaultODataBatchHandler, который строится поверх ODataBatchHandler код, который строится поверх HttpBatchHandler код, который является HttpMessageHandler. Соответствующие запросы явно направляются этому обработчику с помощью Routes.MapODataServiceRoute.

как эта реализация сопоставляется с теорией? Хорошо, на самом деле. Вы можете видеть, что каждый subrequest либо отправляется, чтобы быть обрабатывается соответствующим контроллером, если это" операция " (обычный запрос), или обрабатывается более конкретным кодом, если это набор изменений. На этом уровне, они обрабатываются в целях, а не атомарно.

код обработки набора изменений, однако, действительно обертывает каждый из своих собственных вложенных запросов в транзакцию (одна транзакция для каждого набора изменений). Хотя код может на данный момент попытаться выяснить порядок, в котором выполнять операторы в транзакция рассматривая заголовки Content-ID каждого подзапроса для построения графика зависимостей, эта реализация использует более простой подход, требующий от клиента упорядочить эти подзапросы в правильном порядке, что достаточно справедливо.

Подождите, это решает мою проблему?

если вы можете обернуть все свои операции в один набор изменений, то да, запрос будет транзакционным. Если вы не можете, то вы должны изменить эту реализацию, так что он обертывает весь пакет в одну транзакцию. Хотя спецификация предположительно не исключает этого, существуют очевидные соображения производительности, которые необходимо учитывать. Вы также можете добавить нестандартный HTTP-заголовок, чтобы указать, хотите ли вы, чтобы пакетный запрос был транзакционным или нет, и соответственно выполнить действие реализации.

в любом случае, это не будет стандартным, и вы не можете рассчитывать на это, если вы когда-либо хотели использовать другие серверы OData в совместимом манера. Чтобы исправить это, вам нужно оспорить дополнительные атомарные пакетные запросы в комитет OData в OASIS.

как вариант

если вы не можете найти способ ветвления кода при обработке набора изменений, или вы не можете убедить разработчиков предоставить вам способ сделать это, или вы не можете сохранить состояние набора изменений удовлетворительным образом, то похоже, что вы должны [вы можете альтернативно захотеть] предоставить новый HTTP-ресурс с семантикой специфический для операции, которую вам нужно выполнить.

вы, вероятно, знаете это, и это, вероятно, то, чего вы пытаетесь избежать, но это включает в себя использование DTOs (объекты передачи данных) для заполнения данными в запросах. Затем вы интерпретируете эти DTOs для управления вашими сущностями в пределах одного проводник действие регулятора и следовательно с полным контролем над атомарностью приводя к деятельностей.

обратите внимание, что некоторые люди на самом деле предпочитают этот подход (более ориентированные на процесс, менее ориентированный на данные), хотя это может быть очень трудно моделировать. Правильного ответа нет, он всегда зависит от домена и вариантов использования, и легко попасть в ловушки, которые сделают ваш API не очень спокойным. Это искусство дизайна API. несвязанные: те же замечания можно сказать о моделировании данных, которые некоторые люди на самом деле найти сложнее. YMMV.

резюме

есть несколько подходов для изучения,информация получить от разработчиков используемый канонический метод реализации, возможность создания универсальной реализации Entity Framework и непатентованная альтернатива.

было бы неплохо, если бы вы могли обновить эту тему, когда вы собираете ответы в другом месте (ну, если вы чувствуете себя достаточно мотивированным) и с тем, что вы в конечном итоге решите сделать, поскольку кажется, что многие люди хотели бы иметь какое-то окончательное руководство.

хорошее удача.);


для пакетного запроса OData должен быть только один DbContext. Службы данных WCF и HTTP Web API поддерживают пакетный сценарий OData и обрабатывают его транзакционным способом. Вы можете проверить этот пример: http://blogs.msdn.com/b/webdev/archive/2013/11/01/introducing-batch-support-in-web-api-and-web-api-odata.aspx


Я использовал то же самое из V3 образцов Odata, я видел, что моя транзакция.был вызван откат, но данные не откатились. чего-то не хватает, но я не могу понять чего. это может быть проблема с каждым вызовом Odata с помощью save changes, и они действительно видят транзакцию как в области. нам может понадобиться гуру из команды Entity Framework, чтобы помочь решить эту проблему.


я несколько новичок в использовании OData и Web API. Я на пути к познанию себя, так что принимайте мой ответ за то, что он стоит для вас.

редактировать - когда-нибудь так верно. Я только что узнал о TransactionScope класс и решил, что многое из того, что я опубликовал, неправильно. Итак, я обновляю в пользу лучшего решения.

этот вопрос тоже довольно старый, и с тех пор ASP.Net ядро пришло, поэтому некоторые изменения будут необходимы в зависимости от цели. Я только публикую ответ для будущих Gogglers, которые приземлились здесь, как и я: -)

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

  • исходный вопрос полагал, что каждый контроллер называется получил собственный DbContext. Это неправда. Время жизни DBContext ограничено на всю просьбу. Обзор время жизни зависимости в ASP.NET ядро для более подробная информация. Я подозреваю, что оригинальный плакат возникают проблемы, потому что каждый подзапрос в пакете вызывает свой назначенный метод контроллера, и каждый метод вызывает DbContext.Метод SaveChanges() индивидуально - вызывает эта единица работы должна быть совершена.
  • исходный вопрос также спросил, есть ли"стандарт". У меня нет идея, если то, что я собираюсь предложить, похоже на кого-то рассмотрим "стандарт", но он работает для меня.
  • я делаю предположения о первоначальном вопросе, который заставили меня вниз проголосуйте за ответ tne как не полезный. Мое понимание вопрос исходит из основы выполнения транзакций базы данных, т. е. (ожидается псевдо-код для SQL):

    BEGIN TRAN
        DO SOMETHING
        DO MORE THINGS
        DO EVEN MORE THINGS
    IF FAILURES OCCURRED ROLLBACK EVERYTHING.  OTHERWISE, COMMIT EVERYTHING.
    

    это разумный запрос, который я ожидал бы, что OData сможет выполнить с одним POST операции [base URL]/odata/$batch.

Порядок Выполнения Партии Касается

для наших целей, мы можем или не обязательно заботиться какой порядок работы на этом DbContext. Мы определенно заботимся о том, что выполняемая работа выполняется как часть партии. Мы хотим, чтобы все это удалось или все было откатано в обновляемой базе данных.

если вы используете веб-API старой школы (другими словами, до ASP.Net Core), то ваш класс пакетного обработчика, скорее всего,DefaultHttpBatchHandler класса. Согласно документации Microsoft здесь введение пакетной поддержки в Web API и Web API OData , пакетные транзакции с использованием DefaultHttpBatchHandler в OData являются последовательными по умолчанию. У него есть ExecutionOrder свойство, которое можно задать для изменения этого поведения, чтобы операции выполнялись одновременно.

если вы используете ASP.Net Core, похоже, у нас есть два варианта:

  • если ваша пакетная операция использует формат полезной нагрузки "старой школы", это по умолчанию пакетные операции выполняются последовательно (предполагая, что я интерпретирую исходный код правильно).
  • ASP.Net Core предоставляет новый вариант. Новый DefaultODataBatchHandler заменил старый DefaultHttpBatchHandler класс. Поддержка ExecutionOrder было отброшено в пользу принятия модель, в которой метаданные в полезной нагрузке передают, какой пакет операции должны выполняться в порядке и / или могут выполняться одновременно. К используйте эту функцию, тип содержимого полезной нагрузки запроса изменяется на приложение/JSON и сама нагрузка в JSON формат (см. ниже). Поток управление устанавливается в пределах полезной нагрузки путем добавления зависимостей и групп директивы для управления порядком выполнения, чтобы пакетные запросы можно было разделить в несколько групп отдельных запросов, которые могут быть выполнены асинхронно и параллельно, где нет зависимостей, или в порядке, где зависимости существуют. Мы можем воспользоваться этим фактом и просто создать Теги" Id"," atomicityGroup "и" DependsOn " в полезной нагрузке для обеспечения операции выполняются в соответствующий приказ.

Управление Транзакциями

как указано ранее, ваш код, скорее всего, использует либо DefaultHttpBatchHandler или DefaultODataBatchHandler класса. В любом случае эти классы не запечатаны, и мы можем легко извлечь из них, чтобы обернуть работу, выполняемую в TransactionScope. По умолчанию, если в области не произошло необработанных исключений, транзакция фиксируется когда он будет утилизирован. В противном случае он откатывается назад:

/// <summary>
/// An OData Batch Handler derived from <see cref="DefaultODataBatchHandler"/> that wraps the work being done 
/// in a <see cref="TransactionScope"/> so that if any errors occur, the entire unit of work is rolled back.
/// </summary>
public class TransactionedODataBatchHandler : DefaultODataBatchHandler
{
    public override async Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler)
    {
        using (TransactionScope scope = new TransactionScope( TransactionScopeAsyncFlowOption.Enabled))
        {
            await base.ProcessBatchAsync(context, nextHandler);
        }
    }
}

просто замените свой класс по умолчанию экземпляром этого, и вы хорошо пойдете!

routeBuilder.MapODataServiceRoute("ODataRoutes", "odata", 
  modelBuilder.GetEdmModel(app.ApplicationServices),
  new TransactionedODataBatchHandler());

Contolling порядок выполнения в ASP.Net основной пост для пакетной полезной нагрузки

полезная нагрузка для ASP.Net основной пакетный обработчик использует теги" Id"," atomicityGroup "и" DependsOn " для управления порядком выполнения подзапросов. Мы также получаем преимущество в том, что граница параметр в заголовке Content-Type не нужен, как это было в предыдущих версиях:

    HEADER

    Content-Type: application/json

    BODY

    {
        "requests": [
            {
                "method": "POST",
                "id": "PIG1",
                "url": "http://localhost:50548/odata/DoSomeWork",
                "headers": {
                    "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
                    "odata-version": "4.0"
                },
                "body": { "message": "Went to market and had roast beef" }
            },
            {
                "method": "POST",
                "id": "PIG2",
                "dependsOn": [ "PIG1" ],
                "url": "http://localhost:50548/odata/DoSomeWork",
                "headers": {
                    "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
                    "odata-version": "4.0"
                },
                "body": { "message": "Stayed home, stared longingly at the roast beef, and remained famished" }
            },
            {
                "method": "POST",
                "id": "PIG3",
                "dependsOn": [ "PIG2" ],
                "url": "http://localhost:50548/odata/DoSomeWork",
                "headers": {
                    "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
                    "odata-version": "4.0"
                },
                "body": { "message": "Did not play nice with the others and did his own thing" }
            },
            {
                "method": "POST",
                "id": "TEnd",
                "dependsOn": [ "PIG1", "PIG2", "PIG3" ],
                "url": "http://localhost:50548/odata/HuffAndPuff",
                "headers": {
                    "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
                    "odata-version": "4.0"
                }
            }
        ]
    }

и это в значительной степени все. При пакетных операциях, обернутых в TransactionScope, если что-то не удается, все это откатывается.