Feel Good.

11 мая 2010

Совместное использование Repository с Unit Of Work.

В этом посте я решил поделиться мыслями о том, как можно применить паттерн Repository совместно с паттерном Unit Of Work (UOW). Рассмотрим это на конкретном примере. Для этого введем две сущности: Customer и Order, состоящим в отношении один-ко-многим соответственно.

Разумеется, все CRUD действия над сущностями будут производиться в классах репозитариях, не содержащих Save/Commit методов:

// Класс репозиторий для ICustomer

interface ICustomerRepository : IRepository<ICustomer>

{

}

 

// Класс репозиторий для IOrder

interface IOrderRepository : IRepository<IOrder>

{

    // Связанный с ICustomer список IOrder.

    IQueryable<IOrder> GetCustomerOrders(ICustomer customer);

}


где IRepository общий для всех репозитариев тип:

interface IRepository<TEntity>

{

    TEntity GetById(int id);

    void Add(TEntity entity);

 

    // И прочие общие CRUD методы...

    // Но НЕТ никаких "Save" или "Commit" методов!

}


Заметьте, так как указанные репозитории не содержат методы Save и Commit, то данную функциональность берет на себя Unit of work контейнер:

// Наш Unit Of Work контейнер

public interface IUnitOfWork : IDisposable

{

    // Здесь можно указать Undo/Rollback функции.

 

    void Save();

}


Например, реализуем "наш" UOW для Entity FrameWork:

// Реализация для Entity Framework либо Linq To Sql

partial class MyDataContext : ObjectContext

{

    // То что нам сгенерировал кодогенератор...       

}

 

 

// Расширим через partial

partial class MyDataContext : IUnitOfWork

{

    public void Save()

    {

        // Вызываем внутренний SaveChanges:

        this.SaveChanges();

    }               

}


Приступим к реализации класса репозитария для сущности ICustomer, конструктор которого инжектирует "наш" абстрактный IUnitOfWork:

class EFCustomerRepository : ICustomerRepository

{

    // Далее, в классе используем конкретный context.

    MyDataContext DataContext { get; set; }

 

    // Инжектируем IUnitOfWork

    public EFCustomerRepository(IUnitOfWork uof)

    {

        DataContext = uof as MyDataContext;

    }

}


Видно, что в примере существует жесткая связь EFCustomerRepository от MyDataContext, которую можно исключить, только расширив интерфейс IUnitOfWork. И аналогично для репозитария IOrderRepository. С учетом сказанного выше, приведу пример использования:

// все действия будем производить в одном UOW

using (IUnitOfWork uof = new MyDataContext())

{

    ICustomerRepository cr = new EFCustomerRepository(uof);

    IOrderRepository or = new EFOrderRepository(uof);

 

    // CRUD действия с объектами

 

    // Завершаем транзакцию.

    uof.Save();

}

16 комментариев:

  1. Выбрав правильный agregate root ты избавишь себя от необходимости в одном из двух репозиториев.

    Здесь ты в коде задаешь зависимость uow-repository, как ты будешь разрешать ее при использовании IoC контейнера?

    ОтветитьУдалить
  2. @hazzik

    Хороший вопрос, например, через MS Unity использовать класс ParameterOverrides, задав [key="uof", value=uofobj], предварительно разрешив IUnitOfWork на uofobj. Хотя, конечно со стороны это смотрится не очень элегантно.

    ОтветитьУдалить
  3. Не получится так. Если можно приведите пример.

    ОтветитьУдалить
  4. IUnitOfWork uof = container.Resolve();
    ParameterOverrides p = new ParameterOverrides();
    p.Add("uof", uof);
    ICustomerRepository cr = container.Resolve(p);
    IOrderRepository or = container.Resolve(p);

    ОтветитьУдалить
  5. О ужс... А если более элегантно? А если этот код будет в нескольких местах, а он будет?

    ОтветитьУдалить
  6. Во первых, хочу сказат спасибо за пост.
    Во вторых, не могли бы вы в дальшейнем прикреплять solution с исходными кодами к постам. В теории все расписано хорошо, но хотелось бы взглянуть и на практическое использование этого (и других) подходов.

    ОтветитьУдалить
  7. @hazzik
    Я Вас понимаю, смотрится ужасно, но Unity пока еще не настолько универсален пока. Если у Вас появится решение, мне было бы интересно на него взглянуть.
    Как вариант, можно положить все ICustomerRepository и IOrderRepository в IUnityOfWork. И уже в самом IUnityOfWork разрешать типы для репозитариев.

    @Dmitry Sukhovilin
    Спасибо. Идея с Solution хорошая, но так как я в основном прототипирую его, то редкий Solution скомпилируется :)

    Еще есть один интересный подход быстрой разработки, это использование UoW напрямую, а репозитарии (которые реализованы в виде exstention-методов) выполняют роль ТОЛЬКО фильтров данных. Примерно так: UoW+Filters.

    ОтветитьУдалить
  8. @Илья Дубаденко здесь дело не в Unity и не в любом другом контейнере. Для решения этой проблемы нужно смотреть в архитектуру.

    ОтветитьУдалить
  9. Ваше решение из примера вносит зависимость от конкретных реализаций. А решение из комментариев вносит зависимость от конкретной реализации IoC контейнера.

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

    И поэтому из 2х ваших решений и решения, где Save это обязанность репозитория - я выберу с репозиторием.

    ОтветитьУдалить
  10. @hazzik

    Согласен, шероховатость есть.
    Хорошо, ключевой вопрос, как быть если мы работаем с 2,3,5 и более сущностями, который из репозитариев выполнит конечный Save так, чтобы все изменения выполнились в одной транзакции?

    ОтветитьУдалить
  11. @Илья Дубаденко
    Если эти сущности в одном agregate root - мы просто сохраняем его. А если нет - то, скорее всего все-равно в одной или в 2х/3х/4х транзакциях.

    ОтветитьУдалить
  12. @hazzik

    Тоесть, если не в root, то объект DbTransaction будет инжектироваться в репозитарии? Опять та же проблема. Или все-таки TransactionScope ?

    ОтветитьУдалить
  13. @Илья Дубаденко
    Я к тому, что в большинстве приложений никакой UoW не нужен.

    ОтветитьУдалить
  14. @hazzik
    > Выбрав правильный agregate root ты избавишь себя от необходимости в одном из двух репозиториев.

    а как вы предлагаете не разделяя агрегат Кастомер/Ордер добираться доступа к отдельным элементам Ордер-списка?

    а если например надо отредактировать отдельный Ордер у конкретного Кастомера?

    ОтветитьУдалить
  15. Жалко что на данном вопросе дискуссия закончилась. С одной стороны интересна концепция aggregate root, но слабость данного подхода в проблеме выборки по нужному листу агрегата. Отсюда имеем дополнительные репозитории, количество которых может сравнятся кол-ву сущностей из агрегата.

    ОтветитьУдалить
  16. Лично мое мнение, что Repository исчерпал себя как абстракция доступа к данным с появлением UnitofWork, но по привычке мы все еще продолжаем использовать Repository. "Переходным" решением является подход описанный в этой статье (если коротко то Repository over UoW).
    А проблема выбора aggregate root это уже проблема UoW.
    Я не исключаю, что уже внутри UoW применяется подход на основе репозитариев, ведь кто-то должен все-таки делать CRUD.

    ОтветитьУдалить