Подготовка к нескольким контекстам EF в единице работы — TransactionScope

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

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

Конечно, если мы обернем область транзакции разными контекстами, мы будем защищены, если второй context.SaveChanges() не удался...

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Transactions;

    namespace ConsoleApplication2
    {
        class Program
        {
            static void Main(string[] args)
            {
                using(UnitOfWork unitOfWork = new UnitOfWork())
                {

                    var repository = new EmployeeRepository(unitOfWork);

                    var employee = repository.CreateOrGetEmployee("Whatever Name");

                    Console.Write(employee.Id);

                    unitOfWork.SaveChanges();
                }
            }
        }

        class UnitOfWork : IDisposable
        {
            TestEntities _context;
            TransactionScope _scope;
            public UnitOfWork()
            {
                _scope = new TransactionScope();
                _context = new TestEntities();
            }

            public void SaveChanges()
            {
                _context.SaveChanges();
                _scope.Complete();
            }

            public TestEntities Context
            {
                get
                {
                    return _context;
                }
            }

            public void Dispose()
            {
                _scope.Dispose();
                _context.Dispose();
            }
        }

        class EmployeeRepository
        {
            UnitOfWork _unitOfWork;

            public EmployeeRepository(UnitOfWork unitOfWork)
            {
                _unitOfWork = unitOfWork;
            }

            public Employee GetEmployeeById(int employeeId)
            {
                return _unitOfWork.Context.Employees.SingleOrDefault(e => e.Id == employeeId);
            }

            public Employee CreateEmployee(string fullName)
            {
                Employee employee = new Employee();
                employee.FullName = fullName;
                _unitOfWork.Context.SaveChanges();
                return employee;
            }

            public Employee CreateOrGetEmployee(string fullName)
            {
                var employee = _unitOfWork.Context.Employees.FirstOrDefault(e => e.FullName == fullName);
                if (employee == null)
                {
                    employee = new Employee();
                    employee.FullName = fullName;
                    this.AddEmployee(employee);
                }
                return employee;
            }

            public Employee AddEmployee(Employee employee)
            {
                _unitOfWork.Context.Employees.AddObject(employee);
                _unitOfWork.Context.SaveChanges();
                return employee;
            }
        }
    }

person johnildergleidisson    schedule 23.05.2012    source источник


Ответы (1)


Почему вы запускаете TransactionScope в конструкторе? Он нужен только для сохранения изменений.

public void SaveChanges()
{
    // SaveChanges also uses transaction which uses by default ReadCommitted isolation
    // level but TransactionScope uses by default more restrictive Serializable isolation
    // level 
    using (var scope = new TransactionScope(TransactionScopeOption.Required,
                                            new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
    {
        _context.SaveChanges();
        scope.Complete();
    }
}

Если вы хотите иметь единицу работы с большим количеством контекстов, вы просто оберните весь этот контекст в одну и ту же единицу рабочего класса. Ваш SaveChanges станет немного сложнее:

public void SaveChanges()
{
    using (var scope = new TransactionScope(TransactionScopeOption.Required,
                                            new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
    {
        _contextA.SaveChanges(SaveOptions.DetectChangesBeforeSave);
        _contextB.SaveChanges(SaveOptions.DetectChangesBeforeSave);
        scope.Complete();
        _contextA.AcceptAllChanges();
        _contextB.AcceptAllChanges(); 
    }
}

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

person Ladislav Mrnka    schedule 24.05.2012
comment
Идея запуска TransactionScope в конструкторе единицы работы заключается в том, что когда вы добавляете объект в контекст, вы можете получить его из контекста еще до того, как вы зафиксируете изменения в базе данных. Так, например, если я вызову unitOfWork.Context.Employees.AddObject(employee), затем вызову context.SaveChanges(), а затем, не завершив единицу работы, context.Employees.ToList() вернет только что вставленного сотрудника, даже если он не существует в БД. - person johnildergleidisson; 24.05.2012
comment
Это довольно плохая идея, потому что, если вы вызываете SaveChanges, запись вставляется в базу данных, и эта таблица блокируется до тех пор, пока вы не зафиксируете или не отмените транзакцию, поэтому другой поток не сможет читать или писать в эту таблицу. - person Ladislav Mrnka; 24.05.2012
comment
+1 @LadislavMrnka Будет ли TransactionScope требоваться только в том случае, если сохранение в базы данных должно выполняться в определенном порядке? Другими словами, если мои несколько контекстов не связаны, можно ли просто вызвать _contextA.SaveChanges(); _contextB.SaveChanges(); в моем UoW? - person GFoley83; 11.06.2013
comment
@ GSoley83: Это зависит от того, хотите ли вы, чтобы эти вызовы были атомарными транзакциями. Если вы используете TransactionScope, оба вызова должны быть успешными. Если второй вызов терпит неудачу, первый откатывается. Без области транзакции первый вызов изменит базу данных, даже если второй вызов завершится ошибкой. - person Ladislav Mrnka; 11.06.2013
comment
@LadislavMrnka: Круто, именно то, что мне было нужно. Вы как Папа Entity Framework здесь, в стеке! Спасибо. - person GFoley83; 11.06.2013
comment
В этой ситуации, если оба контекста связаны с вашим одним соединением, координатор распределенных транзакций Microsoft (MDTC) потребуется для координации транзакций, включающих транзакцию в рамках двух разных соединений, будьте осторожны с этим подходом. - person will; 08.10.2020