Мне нужно сделать несколько Breeze SaveChanges за одну транзакцию

У меня есть веб-страница, которая принимает оплату, и она должна ВСТАВИТЬ 6 строк, распределенных по 4 таблицам. Мне пришлось разбить INSERTS на два отдельных SaveChanges, но мне нужно, чтобы они оба находились в одной и той же транзакции базы данных, чтобы все INSERTS и UPDATES откатывались в случае возникновения проблемы.

Я использую Breeze 1.6 поверх Entity Framework 6.2 поверх Oracle Mgd Data Access 12.2 в шаблоне SPA.

Четыре таблицы — это A, B, C и D. Таблицы B, C и D являются дочерними таблицами A, каждая из которых содержит PK A в качестве внешнего ключа. Первоначально я закодировал INSERTS в этой последовательности, как того требует мое приложение A1, B1, C1, C2, C3, D1, а затем один Breeze SaveChanges. C3 имеет триггеры Oracle, которые обновляют несколько столбцов в A1 и B1 во время INSERTED C3. Моя проблема в том, что Entity Framework НЕ ВСТАВЛЯЕТСЯ в последовательность, которую я закодировал (и я понимаю, что не могу контролировать последовательность). На самом деле я получал эту последовательность: A1, C1, C2, C3, B1, D1, и, поскольку у C3 есть триггер, который обновляет A и B, он столкнулся с проблемой, потому что B еще не был ВСТАВЛЕН.

Итак, я нашел этот вопрос: Какая логика определяет вставку порядок Entity Framework 6 и предлагает использовать несколько SaveChanges, чтобы получить некоторый контроль. Получил, что работает следующим образом:

  • A1, B1 Сохранить изменения
  • C1, C2, C3, D1 SaveChanges Все триггеры, включая обновление C3 для A и B, теперь работают отлично.

НО Breeze/Entity Framework рассматривает каждое SaveChanges как транзакцию, поэтому теперь я не могу откатить обе части в случае ошибок во втором SaveChanges. Придется ли мне самостоятельно кодировать откат части 1 в случае сбоя части 2? Или есть секрет, которого я не знаю?

Я знаком с BeginTransaction, Commit и Rollback, но не уверен, как/где их можно сочетать с Breeze и Entity Framework. Breeze захватывает каждое SaveChanges и автоматически включает его в транзакцию.

Мне нужно, чтобы несколько SaveChanges были сгруппированы в одну транзакцию.

Спасибо за любую помощь.


person Debbie A    schedule 06.01.2020    source источник
comment
ОБНОВЛЕНИЕ: это Visual Studio 2017, .Net FW 4.6.2, и для базы данных я использую Breeze 1.6 поверх Entity Framework 6.2 поверх Oracle Mgd Data Access 19.3 в шаблоне SPA.   -  person Debbie A    schedule 11.05.2020


Ответы (2)


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

Позвольте Breeze ContextProvider десериализовать пакет сохранения, а затем используйте хук BeforeSaveEntities чтобы вручную внести изменения. Наконец, пометьте объекты как Unchanged, чтобы ContextProvider не пытался их снова сохранить.

Вот пример реализации.

Сервер

На сервере в Breeze Controller создайте новый метод сохранения, который добавляет BeforeSaveEntitiesDelegate., который используется только для этого специального сохранения:

[HttpPost]
public SaveResult SaveFeePayment(JObject saveBundle) {
{
    contextProvider.BeforeSaveEntitiesDelegate += BeforeSaveFeePayment;
    return contextProvider.SaveChanges(saveBundle);
}

private Dictionary<Type, List<EntityInfo>> BeforeSaveFeePayment(Dictionary<Type, List<EntityInfo>> entities)
{
    var context = contextProvider.Context;

    // Get the list of EntityInfo for each type.  Throws exception if not found.
    // A fee payment save bundle must have A, B, C, and D or it is invalid.
    var ainfos = entities[typeof(A)];
    var binfos = entities[typeof(B)];
    var cinfos = entities[typeof(C)];
    var dinfos = entities[typeof(D)];

    using (var tx = context.Database.BeginTransaction()) 
    {               
        // Save A and B
        Attach(context, ainfos);
        Attach(context, binfos);
        context.SaveChanges();

        // Save C and D
        Attach(context, cinfos);
        Attach(context, dinfos);
        context.SaveChanges();

        tx.Commit();
    }

    // Set all states to Unchanged, so Breeze won't try to save them again
    ainfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Unchanged);
    binfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Unchanged);
    cinfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Unchanged);
    dinfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Unchanged);

    // Return the entities, so Breeze will return them back to the client
    return entities;
}

// Map Breeze EntityState to EF EntityState
private static Dictionary<Breeze.ContextProvider.EntityState, System.Data.Entity.EntityState> entityStateMap =
    new Dictionary<Breeze.ContextProvider.EntityState, System.Data.Entity.EntityState> {
      { Breeze.ContextProvider.EntityState.Added, System.Data.Entity.EntityState.Added },
      { Breeze.ContextProvider.EntityState.Deleted, System.Data.Entity.EntityState.Deleted },
      { Breeze.ContextProvider.EntityState.Detached, System.Data.Entity.EntityState.Detached },
      { Breeze.ContextProvider.EntityState.Modified, System.Data.Entity.EntityState.Modified },
      { Breeze.ContextProvider.EntityState.Unchanged, System.Data.Entity.EntityState.Unchanged }
    };

// Attach entities to the DbContext in the correct entity state 
private static void Attach(DbContext context, List<EntityInfo> infos)
{
    foreach(var info in infos)
    {
        var efState = entityStateMap[info.EntityState];
        context.Entry(info.Entity).State = efState;
    }
}

Клиент

На клиенте вам нужно будет вызвать конечную точку SaveFeePayment с помощью именованного сохранения. . Вы сделаете это только при сохранении платежа комиссии. Продолжайте использовать обычную конечную точку SaveChanges для других сохранений.

Вызовите специальную конечную точку, указав resourceName:

var saveOptions = new SaveOptions({ resourceName: "SaveFeePayment" });
return myEntityManager.saveChanges(null, saveOptions);

Транзакции?

Я не тестировал этот пример на 100% о поведении транзакции. Я не уверен, следует ли нам использовать существующий contextProvider.Context или создавать новый DbContext в начале метода. Разница заключается в том, как обрабатываются соединения с базой данных. См. руководство Microsoft и надеюсь, что оно работает так же с Оракул.

Надеемся, что ваше предыдущее решение для управления транзакциями можно применить к описанному выше методу BeforeSaveFeePayment.

Надеюсь это поможет.

person Steve Schmitt    schedule 07.01.2020
comment
Вау, спасибо, Стив. Я только что видел это. Забавно, но я только что разговаривал с нашим архитектором-разработчиком, который сказал, что мне нужно десериализовать пакет сохранения на сервере и поместить его в коллекции различных типов. Затем создайте несколько SaveChanges, обернутых в транзакцию. Разве это не то, что ты делаешь? (Мне потребуется некоторое время, чтобы изучить и понять все, что у вас есть здесь) Спасибо, что нашли время, чтобы помочь мне. - person Debbie A; 08.01.2020
comment
Как сюда вписываются BeginTransaction и Commit/Rollback? - person Debbie A; 08.01.2020
comment
Да, мы позволяем Breeze помещать набор сохранений в коллекции различных типов, а затем выполнять несколько SaveChanges. Я добавил в пример блок транзакций, НО я не проверял его, и я не уверен, следует ли нам использовать существующий contextProvider.Context или создавать новый DbContext в начале метода. - person Steve Schmitt; 09.01.2020
comment
Отличная работа, Стив. Все заработало, как вы написали. Одна транзакция, содержащая 2 saveChanges, и обе откатывают все в случае сбоя. Я многому научился. Любите Бриз НАЗВАНИЕ СОХРАНЯЕТ. И было очень полезно понять, как обращаться с частями saveBundle. Хорошая вещь. Спасибо большое. - person Debbie A; 12.01.2020
comment
Привет, Стив, я только что использовал этот же сценарий для удаления тех же 4 таблиц A, B, C, D, потому что триггер C проверял, чтобы убедиться, что D больше не было, и D не был удален, хотя это был первым, для которого я включил статус Deleted. Поэтому мне нужно было удалить D, а затем все ABC в 2 сохранить изменения. - person Debbie A; 30.01.2020
comment
Однако на этот раз произошло кое-что, что не было проблемой для вкладышей. saveBundle не очистился после фиксации, и он оставил некоторые строки там помеченными как Deleted. Так что в следующий раз, когда я попытался добавить или удалить плату, они все еще зависали в saveBundle, и он попытался снова удалить те строки, которые уже были удалены, и, конечно же, их там не было. Итак, я получил ошибку параллелизма - я думаю, потому что предполагалось, что кто-то другой опередил меня в удалении. - person Debbie A; 30.01.2020
comment
Есть ли способ отсоединить эти объекты от контекста или повторно инициализировать контекст? Любое другое удаление отдельной таблицы очищает контекст, и удаленная строка не сохраняется. Просто что-то с этим процессом вызывает проблемы. Забавно то, что буква D исчезла, но буквы A-B-C все еще висят со статусом "удалено". Однако строки исчезли из базы данных, как и хотелось, и триггеры довольны. Кстати, все возвращенные объекты были помечены как неизмененные, как и хотелось, во время возврата. - person Debbie A; 30.01.2020
comment
Правильно, нужно вернуть удаленный статус обратно клиенту. Я думаю, что это должно быть deletedInfos.ForEach(info => info.EntityState = Breeze.ContextProvider.EntityState.Detached);, но, возможно, это должно быть EntityState.Deleted. Пожалуйста, попробуйте оба. - person Steve Schmitt; 31.01.2020
comment
Спасибо. Detached почти решил проблему!! Я наткнулся на это значение прошлой ночью и попробовал его. Большинство моих удалений тоже работают. - person Debbie A; 01.02.2020
comment
Есть одна интересная проблема, когда я делаю вставку, а затем удаляю один и тот же набор строк. По какой-то причине строка таблицы D (которая сначала удаляется сама) оказывается в saveBundle дважды, но только в том случае, если она была только что добавлена. Он есть один раз со статусом «Удалено» и ключом вторичного столбца, равным 1, что является правильным, но также один раз со статусом «Изменено» и дополнительным ключом идентификации, равным -1. Я собираюсь попытаться выяснить, что там происходит. Попробуйте прикрепить D и A и сохранить изменения. Затем B и C и сохраните изменения, чтобы посмотреть, получу ли я тоже 2 A. Просто слишком странно. Спасибо за вашу помощь. - person Debbie A; 01.02.2020
comment
Все работает, как для 2 SaveChanges (INSERTS, так и для DELETES). У меня была дополнительная строка, которая не удалялась, и в следующий раз это вызвало проблемы с параллелизмом. Виноват. Но установка всех статусов в DETACHED после удаления была необходима, а не в UNCHANGED в конце. - person Debbie A; 03.02.2020
comment
Отлично, рад, что вы решили проблему. И рад, что смог помочь. - person Steve Schmitt; 03.02.2020

В моем случае, используя Breeze на .NET core 3.1, мне пришлось установить IsTemporary=true, как показано в следующем примере кода:

private static void Attach(BreezeContext context, List<EntityInfo> infos)
{
  foreach (var info in infos)
  {
    var efState = entityStateMap[info.EntityState];

    context.Entry(info.Entity).Property("Id").IsTemporary = true; //Explicitly set to true
    context.Entry(info.Entity).State = efState;
  }
}

По умолчанию установлено значение false и, следовательно, SaveChanges() выдавало следующую ошибку:

"невозможно вставить явное значение для столбца идентификации"

Это может быть связано с ошибкой в ​​Breeze. Между тем, установка IsTemporary на true, по крайней мере, решает проблему.

person Fayeeg    schedule 10.05.2020