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

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

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

Пакет 1  - ChangeSet 1  - - Исправить действительный объект  - - Патч недействительный объект  - End Changeset 1  - ChangeSet 2  - - Вставить действительный объект  - Завершить ChangeSet 2 Конечная партия

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

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

Ответ 1

Следующая ссылка показывает реализацию OData Web 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/

Ответ 2

  • Теория: пусть мы говорим об одном и том же.
  • На практике: проблема с проблемой, если я может (нет окончательного ответа).
  • На практике действительно (обновление): канонический способ реализации частей, относящихся к бэкэнду.
  • Подождите, решает ли моя проблема?: не забывайте, что реализация (3) связана спецификацией (1).
  • Альтернативно: обычная "вам это действительно нужно?" (нет окончательного ответа).

Теория

Для записи, вот что спецификация 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) или Invocation.

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

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

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

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

На практике

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

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

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

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

Опасно rabling: вместо условного вызова 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.

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

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

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

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

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

В качестве альтернативы

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

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

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

Резюме

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

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

Удачи;).

Ответ 3

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

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

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

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

  • Исходный вопрос предполагал, что каждый вызванный контроллер получил свой собственный DbContext. Это не правда Время жизни DBContext распространяется на весь запрос. Посмотрите время существования зависимости в ASP.NET Core для получения дополнительной информации. Я подозреваю, что у исходного автора возникли проблемы, потому что каждый подзапрос в пакете вызывает свой назначенный метод контроллера, и каждый метод вызывает DbContext.SaveChanges() индивидуально, вызывая фиксацию этой единицы работы.
  • В первоначальном вопросе также задавался вопрос, есть ли "стандарт". Я понятия не имею, что я собираюсь предложить, это что-то вроде того, что кто-то считает "стандартом", но это работает для меня.
  • Я делаю предположения о первоначальном вопросе, который заставил меня отказаться от одного ответа как бесполезный. Мое понимание вопроса исходит из основы выполнения транзакций базы данных, т.е. (Ожидается псевдокод для 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 вероятно, является классом DefaultHttpBatchHandler. В соответствии с документацией Microsoft, представленной здесь. Представляя пакетную поддержку в Web API и OData Web API, пакетные транзакции, использующие DefaultHttpBatchHandler в OData, являются последовательными по умолчанию. У него есть свойство ExecutionOrder, которое можно настроить для изменения этого поведения, чтобы операции выполнялись одновременно.

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

  • Если ваша пакетная операция использует формат "старой школы", похоже, что пакетные операции по умолчанию выполняются последовательно (при условии, что я правильно интерпретирую исходный код).
  • ASP.Net Core предоставляет новую опцию. Новый DefaultODataBatchHandler заменил старый класс DefaultHttpBatchHandler. Поддержка ExecutionOrder была упущена в пользу принятия модели, в которой метаданные в полезной нагрузке сообщают, должны ли какие пакетные операции выполняться по порядку и/или могут ли выполняться одновременно. Чтобы использовать эту функцию, тип содержимого полезной нагрузки запроса изменяется на application/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());

Управление порядком выполнения в ASP.Net Core POST для пакетной загрузки

Полезная нагрузка для пакетного обработчика ASP.Net Core использует теги "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, если что-то не получается, все откатывается.

Ответ 5

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