Подтвердить что ты не робот

Единица работы + шаблон хранилища: падение концепции бизнес-транзакции

Комбинирование Unit of Work и Repository Pattern - это то, что используется довольно широко в наши дни. Поскольку Martin Fowler говорит, цель использования UoW заключается в создании Бизнес-транзакции, хотя она не знает, как хранилища фактически работа (постоянная неосведомленность). Я рассмотрел множество реализаций; и игнорируя конкретные детали (конкретный/абстрактный класс, интерфейс,...), они более или менее похожи на следующие:

public class RepositoryBase<T>
{
    private UoW _uow;
    public RepositoryBase(UoW uow) // injecting UoW instance via constructor
    {
       _uow = uow;
    }
    public void Add(T entity)
    {
       // Add logic here
    }
    // +other CRUD methods
}

public class UoW
{
    // Holding one repository per domain entity

    public RepositoryBase<Order> OrderRep { get; set; }
    public RepositoryBase<Customer> CustomerRep { get; set; }
    // +other repositories

    public void Commit()
    {
       // Psedudo code: 
       For all the contained repositories do:
           store repository changes.
    }
}

Теперь моя проблема:

UoW предоставляет открытый метод Commit для сохранения изменений. Кроме того, поскольку каждый репозиторий имеет общий экземпляр UoW, каждый Repository может получить доступ к методу Commit в UoW. Вызов его одним репозиторием делает все остальные хранилища также сохраняют свои изменения; следовательно, вся концепция транзакции обрушивается:

class Repository<T> : RepositoryBase<T>
{
    private UoW _uow;
    public void SomeMethod()
    {
        // some processing or data manipulations here
        _uow.Commit(); // makes other repositories also save their changes
    }
}

Я думаю, что это должно быть запрещено. Учитывая цель UoW (бизнес-транзакция), метод Commit должен быть открыт только тем, кто запустил Бизнес-транзакция, например Business Layer. Меня удивило то, что я не мог найти статей, посвященных этой проблеме. Во всех из них Commit может быть вызвано любым репо, которое вводится.

PS: Я знаю, что я могу сказать моим разработчикам не называть Commit в Repository, но надежная архитектура более надежна, чем надежные разработчики!

4b9b3361

Ответ 1

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

Реализация используемого UnitOfWorkScope сильно вдохновлена ​​тем, как работает TransactionScope. Использование подхода, основанного на окружающей среде/области применения, также устраняет необходимость в инъекции зависимостей.

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

public static Entities.Car GetCar(int id)
{
    using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Reading))
    {
        return uow.DbContext.Cars.Single(c => c.CarId == id);
    }
}

Метод, который пишет, выглядит следующим образом:

using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Writing))
{
    Car c = SharedQueries.GetCar(carId);
    c.Color = "White";
    uow.SaveChanges();
}

Обратите внимание, что вызов uow.SaveChanges() будет выполнять фактическое сохранение в базе данных, если это корневая (верхняя) область. В противном случае это интерпретируется как "добросовестное голосование", которое позволит сохранить изменения в корневой области.

Вся реализация UnitOfWorkScope доступна по адресу: http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/

Ответ 2

Сделайте свои репозитории членами своего UoW. Не позволяйте вашим репозиториям "видеть" ваш UoW. Пусть UoW обрабатывает транзакцию.

Ответ 3

Не проходите в UnitOfWork, переходите в интерфейс, который имеет нужные вам методы. Вы можете реализовать этот интерфейс в исходной реализации UnitOfWork, если хотите:

public interface IDbContext
{
   void Add<T>(T entity);
}

public interface IUnitOfWork
{
   void Commit();
}

public class UnitOfWork : IDbContext, IUnitOfWork
{
   public void Add<T>(T entity);
   public void Commit();
}

public class RepositoryBase<T>
{
    private IDbContext _c;

    public RepositoryBase(IDbContext c) 
    {
       _c = c;
    }

    public void Add(T entity)
    {
       _c.Add(entity)
    }
}

ИЗМЕНИТЬ

После публикации этого я передумал. Выявление метода Add в реализации UnitOfWork означает, что это комбинация двух шаблонов.

Я использую Entity Framework в своем собственном коде, а DbContext используется как "комбинация Unit-Of- Работа и репозиторий".

Я думаю, что лучше разбить два, и это означает, что мне нужно два обертки вокруг DbContext один для бита Unit Of Work и один для бит репозитория. И я делаю обертку репозитория в RepositoryBase.

Ключевым отличием является то, что я не передаю UnitOfWork в репозитории, я передаю DbContext. Это означает, что BaseRepository имеет доступ к SaveChanges в DbContext. И поскольку намерение заключается в том, что пользовательские репозитории должны наследовать BaseRepository, они также получают доступ к DbContext. Поэтому возможно, что разработчик мог добавить код в пользовательский репозиторий, который использует этот DbContext. Поэтому я предполагаю, что моя "обертка" немного протекает...

Так стоит ли создавать еще одну оболочку для DbContext, которая может быть передана конструкторам репозитория, чтобы закрыть это? Не уверен, что это...

Примеры передачи DbContext:

Реализация хранилища и единицы работы

Репозиторий и блок работы в инфраструктуре Entity

Оригинальный исходный код John Papa

Ответ 4

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

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

Это показывает, как создать область транзакции в С#:

using (TransactionScope scope = new TransactionScope())
{
    // Your logic here. Save inside the transaction as much as you want.

    scope.Complete(); // <-- This will complete the transaction and make the changes permanent.
}

Ответ 5

Я тоже недавно изучал этот шаблон дизайна, и, используя шаблон Unit of Work и Generic Repository, я смог извлечь раздел "Сохранить изменения" для реализации репозитория. Мой код выглядит следующим образом:

public class GenericRepository<T> where T : class
{
  private MyDatabase _Context;
  private DbSet<T> dbset;

  public GenericRepository(MyDatabase context)
  {
    _Context = context;
    dbSet = context.Set<T>();
  }

  public T Get(int id)
  {
    return dbSet.Find(id);
  }

  public IEnumerable<T> GetAll()
  {
    return dbSet<T>.ToList();
  }

  public IEnumerable<T> Where(Expression<Func<T>, bool>> predicate)
  {
    return dbSet.Where(predicate);
  }
  ...
  ...
}

По существу, все, что мы делаем, это передача в контексте данных и использование методов сущности framework dbSet для базовых Get, GetAll, Add, AddRange, Remove, RemoveRange и Where.

Теперь мы создадим общий интерфейс, чтобы разоблачить эти методы.

public interface <IGenericRepository<T> where T : class
{
  T Get(int id);
  IEnumerable<T> GetAll();
  IEnumerabel<T> Where(Expression<Func<T, bool>> predicate);
  ...
  ...
}

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

Пример:

public interface ITable1 : IGenericRepository<table1>
{
}

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

Для репозиториев мы реализуем их следующим образом.

public class Table1Repository : GenericRepository<table1>, ITable1
{
  private MyDatabase _context;

  public Table1Repository(MyDatabase context) : base(context)
  {
    _context = context;
  }
} 

В приведенном выше примере репозитория я создаю репозиторий table1 и наследую GenericRepository с типом "table1", после чего наследую интерфейс ITable1. Это автоматически реализует общие методы dbSet для меня, что позволяет мне сосредоточиться только на моих пользовательских методах репозитория, если таковые имеются. Когда я передаю dbContext в конструктор, я также должен передать dbContext в базовый общий репозиторий.

Теперь я пойду и создаю репозиторий и интерфейс Unit of Work.

public interface IUnitOfWork
{
  ITable1 table1 {get;}
  ...
  ...
  list all other repository interfaces here.

  void SaveChanges();
} 

public class UnitOfWork : IUnitOfWork
{
  private readonly MyDatabase _context;
  public ITable1 Table1 {get; private set;}

  public UnitOfWork(MyDatabase context)
  {
    _context = context; 

    // Initialize all of your repositories here
    Table1 = new Table1Repository(_context);
    ...
    ...
  }

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

Я обрабатываю область транзакций на настраиваемом контроллере, из которого наследуются все остальные контроллеры в моей системе. Этот контроллер наследует от контроллера MVC по умолчанию.

public class DefaultController : Controller
{
  protected IUnitOfWork UoW;

  protected override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    UoW = new UnitOfWork(new MyDatabase());
  }

  protected override void OnActionExecuted(ActionExecutedContext filterContext) 
  {
    UoW.SaveChanges();
  }
}

Внедряя свой код таким образом. Каждый раз, когда запрос направляется на сервер в начале действия, создается новый UnitOfWork и автоматически создает все репозитории и делает их доступными для переменной UoW в вашем контроллере или классах. Это также удалит вашу SaveChanges() из ваших репозиториев и поместит ее в репозиторий UnitOfWork. И последний этот шаблон способен использовать только один dbContext по всей системе через инъекцию зависимости.

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

Ответ 6

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

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

  • Реализовать единицу рабочего интерфейса, описанную в книге Fawler P книги EAA, но вводить репозиторий в каждый метод UoW.
  • Внедрить единицу работы в каждую операцию репозитория.
  • Каждая операция репозитория вызывает соответствующую операцию UoW и вводит сам.
  • Реализовать два метода фиксации транзакций CanCommit(), Commit() и Откат() в репозиториях.
  • Если требуется, фиксация на UoW может запускать Commit в каждом репозитории или может быть зафиксирована в самом хранилище данных. Он также может реализовать 2-фазную фиксацию, если это то, что вы хотите.

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

interface IEntity
{
    int Id {get;set;}
}

interface IUnitOfWork()
{
    void RegisterNew(IRepsitory repository, IEntity entity);
    void RegisterDirty(IRepository respository, IEntity entity);
    //etc.
    bool Commit();
    bool Rollback();
}

interface IRepository<T>() : where T : IEntity;
{
    void Add(IEntity entity, IUnitOfWork uow);
    //etc.
    bool CanCommit(IUnitOfWork uow);
    void Commit(IUnitOfWork uow);
    void Rollback(IUnitOfWork uow);
}

Пользовательский код всегда один и тот же, независимо от реализации БД, и выглядит так:

// ...
var uow = new MyUnitOfWork();

repo1.Add(entity1, uow);
repo2.Add(entity2, uow);
uow.Commit();

Вернуться к исходному сообщению. Поскольку мы вводим метод UoW в каждую операцию репо, UoW не нужно хранить в каждом репозитории, то есть Commit() в репозитории может быть заглушен, с Commit на UoW, выполняющем фактическую фиксацию БД.

Ответ 7

Да, этот вопрос вызывает у меня беспокойство, и вот как я справляюсь с этим.

Прежде всего, в моем понимании модель домена не должна знать об Единице работы. Модель домена состоит из интерфейсов (или абстрактных классов), которые не подразумевают существование транзакционного хранилища. Фактически, он вообще не знает о существовании какого-либо хранилища. Следовательно, термин Domain Model.

Единица работы присутствует на уровне реализации модели домена. Я предполагаю, что это мой термин, и под этим я подразумеваю слой, который реализует интерфейсы модели домена, путем включения уровня доступа к данным. Обычно я использую ORM как DAL, поэтому он поставляется со встроенным UoW в нем (Entity Framework SaveChanges или метод SubmitChanges для фиксации ожидающих изменений). Тем не менее, он принадлежит DAL и не нуждается в магии изобретателя.

С другой стороны, вы имеете в виду UoW, который вам необходим для реализации на уровне реализации модели домена, потому что вам нужно абстрагироваться от части "совершения изменений в DAL". Для этого я бы пошел с решением Андерса Абеля (рекурсивные scropes), потому что это касается двух вещей, которые нужно решить за один выстрел:

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