Как заставить только одну транзакцию в нескольких классах DbContext? - PullRequest
2 голосов
/ 06 января 2012

Справочная информация:
Из другого вопроса здесь, в SO, у меня есть решение Winforms (финансы) со многими проектами (фиксированные проекты для решения).Теперь один из моих клиентов попросил меня «обновить» решение и добавить проекты / модули, которые будут получены из другого решения Winforms (HR).

Я действительно не хочу оставлять эти проекты фиксированными насуществующее финансовое решение.Для этого я пытаюсь создать плагины, которые будут загружать GUI, бизнес-логику и слой данных, используя MEF.

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

Допустим, у меня есть это:

public class MainContext : DbContext
{
   public List<IPluginContext> ExternalContexts { get; set; }

   // other stuff here
}

и

public class PluginContext_A : DbContext, IPluginContext
{ /* Items from this context */ }

public class PluginContext_B : DbContext, IPluginContext
{ /* Items from this context */ }

и внутри класса MainContext, уже загруженного, у меня есть оба внешних контекста (из плагинов).

Имея это в виду, допустим, у меня есть транзакцияэто повлияет на и MainContext и PluginContext_B.

Как выполнить обновление / вставку / удаление в обоих контекстах в рамках одной транзакции (единство работы)?

Используя IUnityOfWork, я могу установить SaveChanges () для последнего элемента, но, насколько я знаю, у меня должен быть один контекст, чтобы он работал как одна транзакция.

Есть способиспользование MSDTC (TransactionScope), но этот подход ужасен, и я бы вообще не использовал его вообще (в том числе потому, что мне нужно включить MSDTC на клиентах и ​​на сервере, и у меня постоянно возникали сбои и утечки).

Обновление:
Системы используют SQL 2008 R2.Никогда не ругайтесь.
Если возможно использовать TransactionScope так, чтобы он не масштабировался до MSDTC, это нормально, но я никогда этого не достиг.Все время, когда я использовал TransactionScope, он входит в MSDTC.Согласно другому сообщению о SO, есть некоторые случаи, когда TS не пойдет в MSDTC: отметьте здесь .Но я бы предпочел пойти другим путем вместо TransactionScope ...

Ответы [ 4 ]

3 голосов
/ 07 января 2012

Если вы используете несколько контекстов, каждый из которых использует отдельное соединение, и вы хотите сохранить данные в этот контекст в одной транзакции, вы должны использовать TransactionScope с распределенной транзакцией (MSDTC).

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

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

using (var connection = new SqlConnection(connnectionString))
{
    var c1 = new Context(connection);
    var c2 = new Context(connection);

    c1.MyEntities.Add(new MyEntity() { Name = "A" });
    c2.MyEntities.Add(new MyEntity() { Name = "B" });

    connection.Open(); 

    using (var scope = new TransactionScope())
    {
        // This is necessary because DbContext doesnt't contain necessary methods
        ObjectContext obj1 = ((IObjectContextAdapter)c1).ObjectContext;
        obj1.SaveChanges(SaveOptions.DetectChangesBeforeSave);

        ObjectContext obj2 = ((IObjectContextAdapter)c2).ObjectContext;
        obj2.SaveChanges(SaveOptions.DetectChangesBeforeSave);

        scope.Complete();

        // Only after successful commit of both save operations we can accept changes
        // otherwise in rollback caused by second context the changes from the first
        // context will be already accepted = lost

        obj1.AcceptAllChanges();
        obj2.AcceptAllChanges();
    }
}

Конструктор контекста определен как:

public Context(DbConnection connection) : base(connection,false) { }

Сам пример работал для меня, ноу него много проблем:

  • Первое использование контекстов должно быть сделано при закрытом соединении.Вот почему я добавляю сущности перед открытием соединения.
  • Я скорее открываю соединение вручную вне транзакции, но, возможно, оно не требуется.
  • Оба сохранения успешно выполнены, и Transaction.Current имеет пустой идентификатор распределенной транзакции, поэтому он должен оставаться локальным.
  • Сохранение намного сложнее, и вы должны использовать ObjectContext, потому что DbContext не имеет всех необходимых методов.
  • Не обязательно работать в каждом сценарии.Даже MSDN заявляет следующее:

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

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

0 голосов
/ 23 июня 2014

Чтобы не использовать MSDTC (распределенная транзакция):

Это должно заставить вас использовать одно соединение внутри транзакции, а также только одну транзакцию. В противном случае следует выдать исключение.

Примечание: требуется минимум EF6

class TransactionsExample 
 { 
    static void UsingExternalTransaction() 
    { 
        using (var conn = new SqlConnection("...")) 
        { 
           conn.Open(); 

           using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot)) 
           { 
               try 
               { 
                   var sqlCommand = new SqlCommand(); 
                   sqlCommand.Connection = conn; 
                   sqlCommand.Transaction = sqlTxn; 
                   sqlCommand.CommandText = 
                       @"UPDATE Blogs SET Rating = 5" + 
                        " WHERE Name LIKE '%Entity Framework%'"; 
                   sqlCommand.ExecuteNonQuery(); 

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false)) 
                    { 
                        context.Database.UseTransaction(sqlTxn); 

                        var query =  context.Posts.Where(p => p.Blog.Rating >= 5); 
                        foreach (var post in query) 
                        { 
                            post.Title += "[Cool Blog]"; 
                        } 
                       context.SaveChanges(); 
                    } 

                    sqlTxn.Commit(); 
                } 
                catch (Exception) 
                { 
                    sqlTxn.Rollback(); 
                } 
            } 
        } 
    } 
} 

Источник: http://msdn.microsoft.com/en-us/data/dn456843.aspx#existing

0 голосов
/ 19 октября 2012

Итак, есть ли шанс, что это изменилось к 19 октября?Во всех разделах люди предлагают следующий код, и он не работает:

    (_contextA as IObjectContextAdapter).ObjectContext.Connection.Open();
    (_contextB as IObjectContextAdapter).ObjectContext.Connection.Open();

    using (var transaction = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions{IsolationLevel = IsolationLevel.ReadUncommitted, Timeout = TimeSpan.MaxValue}))
{
    _contextA.SaveChanges();
    _contextB.SaveChanges();

    // Commit the transaction
    transaction.Complete();
}

    // Close open connections
    (_contextA as IObjectContextAdapter).ObjectContext.Connection.Close();
    (_contextB as IObjectContextAdapter).ObjectContext.Connection.Close();

Это серьезное препятствие для реализации одного класса Unit of Work в репозиториях.Любой новый способ обойти это?

0 голосов
/ 17 января 2012

@ Ladislav Mrnka Вы были правы с самого начала: я должен использовать MSDTC.

Я пробовал несколько вещей здесь, включая пример кода, который я предоставил.Я проверял это много раз с измененным зайцем и там, но это не будет работать.Ошибка глубоко связана с тем, как работают EF и DbContext, и для того, чтобы это изменить, я наконец-то найду свой собственный инструмент ORM.Это не тот случай.

Я также поговорил с другом (MVP), который тоже много знает об EF.Мы проверили некоторые другие вещи здесь, но это не сработает так, как я хочу.В итоге я получу несколько изолированных транзакций (я пытался собрать их вместе с моим примером кода), и с этим подходом у меня нет никакого способа автоматически выполнить полный откат, и мне придется создавать много общих/ Пользовательский код для ручного отката изменений, и здесь возникает другой вопрос: что, если этот вид отката завершится неудачно (это не откат, а просто обновление)?

Итак, единственный найденный нами способ - использовать MSDTCи создайте некоторые инструменты, которые помогут отладке / тестированию, если DTC включен, если брандмауэры клиент / сервер в порядке и все такое.

В любом случае, спасибо.=)

...