Разработка уровня абстракции базы данных - правильно ли использовать IRepository? - PullRequest
14 голосов
/ 27 февраля 2010

Я нахожусь в процессе разработки своего приложения ASP.NET MVC и натолкнулся на пару интересных мыслей.

Множество примеров, которые я видел, описывают и используют шаблон Repository (IRepository), поэтому я так и делал, изучая MVC.

Теперь я знаю, что все это делает, я начинаю смотреть на свой текущий дизайн и задаюсь вопросом, лучший ли это путь.

В настоящее время у меня есть базовый IUserRepository, который определяет такие методы, как FindById(), SaveChanges() и т. Д.

В настоящее время, когда я хочу загрузить / запросить пользовательскую таблицу в БД, я делаю что-то вроде следующего:

    private IUserRepository Repository;

    public UserController()
        : this(new UserRepository())
    { }

    [RequiresAuthentication]
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Edit(string ReturnUrl, string FirstRun)
    {
        var user = Repository.FindById(User.Identity.Name);

        var viewModel = Mapper.Map<User, UserEditViewModel>(user);
        viewModel.FirstRun = FirstRun == "1" ? true : false;

        return View("Edit", viewModel);
    }

    [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken(Salt = "SaltAndPepper")]
    public ActionResult Edit(UserEditViewModel viewModel, string ReturnUrl)
    {
        //Map the ViewModel to the Model
        var user = Repository.FindById(User.Identity.Name);

        //Map changes to the user
        Mapper.Map<UserEditViewModel, User>(viewModel, user);

        //Save the DB changes
        Repository.SaveChanges();

        if (!string.IsNullOrEmpty(ReturnUrl))
            return Redirect(ReturnUrl);
        else
            return RedirectToAction("Index", "User");
    }

Теперь я не до конца понимаю, как MVC работает в отношении создания контроллера, когда пользователь создает ссылку (не уверен, есть ли 1 контроллер на пользователя или 1 контроллер на приложение), поэтому я не уверен в лучший курс действий.

Я обнаружил большой вопрос, касающийся использования универсального интерфейса репозитория IRepository<T> здесь , а также мне показалось, что в ряде блогов существует статическая RepositoryFactory В основном, только 1 экземпляр хранилища хранится когда-либо, и он получен через эту фабрику

Так что мой вопрос вращается вокруг того, как люди делают это в своих приложениях, и что считается хорошей практикой.

Есть ли у людей отдельные репозитории на основе каждой таблицы (IUserRepository)?
Они используют общий IRepository<T>?
Они используют фабрику статических хранилищ?
Или что-то еще полностью?

EDIT: Я только что понял, что должен спросить:

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

BOUNTY EDIT: Я открываю награду, чтобы получить больше перспектив (не то, чтобы Тим не помог).

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

Ответы [ 5 ]

24 голосов
/ 04 марта 2010

Некоторые очень очевидные проблемы с идеей общего IRepository<T>:

  • Предполагается, что каждая сущность использует один и тот же тип ключа, что неверно почти в любой нетривиальной системе. Некоторые объекты будут использовать GUID, другие могут иметь какой-то натуральный и / или составной ключ. NHibernate может поддерживать это довольно хорошо, но Linq to SQL довольно плохо справляется с этим - вам нужно написать много хакерского кода для автоматического сопоставления ключей.

  • Это означает, что каждый репозиторий может иметь дело только с одним типом сущности и поддерживает только самые тривиальные операции. Когда хранилище отправляется в такую ​​простую оболочку CRUD, оно совсем мало используется. Вы также можете просто вручить клиенту IQueryable<T> или Table<T>.

  • Предполагается, что вы выполняете абсолютно одинаковые операции с каждой сущностью. На самом деле это будет очень далеко от истины. Конечно, возможно вы хотите получить этот Order по его идентификатору, но, скорее всего, вы хотите получить список Order объектов для конкретного клиента и в некотором диапазоне дат. Понятие полностью универсального IRepository<T> не учитывает тот факт, что вы почти наверняка захотите выполнять разные типы запросов для разных типов сущностей.

Весь смысл шаблона репозитория заключается в создании абстракции поверх общих шаблонов доступа к данным. Я думаю, что некоторым программистам надоедает создавать репозитории, поэтому они говорят: «Эй, я знаю, я создам один über-репозиторий, который может обрабатывать любой тип сущности!» Это здорово, за исключением того, что репозиторий практически бесполезен для 80% того, что вы пытаетесь сделать. Это хорошо в качестве базового класса / интерфейса, но если это полный объем работы, которую вы делаете, то вы просто ленивы (и гарантируете будущие головные боли).


В идеале я мог бы начать с общего репозитория, который выглядит примерно так:

public interface IRepository<TKey, TEntity>
{
    TEntity Get(TKey id);
    void Save(TEntity entity);
}

Вы заметите, что у этого нет функции List или GetAll - это потому, что абсурдно думать, что допустимо извлекать данные из всей таблицы одновременно в любом месте в код. Это когда вам нужно начать заходить в определенные репозитории:

public interface IOrderRepository : IRepository<int, Order>
{
    IEnumerable<Order> GetOrdersByCustomer(Guid customerID);
    IPager<Order> GetOrdersByDate(DateTime fromDate, DateTime toDate);
    IPager<Order> GetOrdersByProduct(int productID);
}

И так далее - вы поняли идею. Таким образом, у нас есть «общий» репозиторий, если нам когда-нибудь понадобится невероятно упрощенная семантика извлечения по идентификатору, но в целом мы никогда не собираемся передавать это, конечно, не классу контроллера.


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

Контроллер должен взять свой репозиторий из внешнего мира . Причина, по которой вы создали эти репозитории, заключается в том, что вы можете сделать какую-то инверсию контроля. Ваша конечная цель заключается в том, чтобы иметь возможность поменять один репозиторий на другой - например, выполнить модульное тестирование или если вы решите переключиться с Linq на SQL на Entity Framework в какой-то момент в будущем.

Пример этого принципа:

public class OrderController : Controller
{
    public OrderController(IOrderRepository orderRepository)
    {
        if (orderRepository == null)
            throw new ArgumentNullException("orderRepository");
        this.OrderRepository = orderRepository;
    }

    public ActionResult List(DateTime fromDate, DateTime toDate) { ... }
    // More actions

    public IOrderRepository OrderRepository { get; set; }
}

Другими словами, Контроллер не знает, как создать хранилище , и не должен этого делать. Если у вас там есть какая-то конструкция репозитория, она создает связь, которая вам действительно не нужна. Причина того, что образцы контроллеров ASP.NET MVC имеют конструкторы без параметров, которые создают конкретные репозитории, заключается в том, что сайты должны иметь возможность компилировать и запускать, не вынуждая настраивать всю инфраструктуру внедрения зависимостей.

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

[TestMethod]
public void Can_add_order()
{
    OrderController controller = new OrderController();
    FakeOrderRepository fakeRepository = new FakeOrderRepository();
    controller.OrderRepository = fakeRepository; //<-- Important!
    controller.SubmitOrder(...);
    Assert.That(fakeRepository.ContainsOrder(...));
}

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


Это еще не DI, заметьте, это просто притворство / издевательство. DI приходит на ум, когда вы решаете, что Linq to SQL недостаточно для вас, и вы действительно хотите использовать HQL в NHibernate, но вам понадобится 3 месяца, чтобы портировать все, и вы захотите иметь возможность сделайте это одно хранилище за один раз. Так, например, используя DI Framework, например Ninject , все, что вам нужно сделать, это изменить это:

Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<LinqToSqlOrderRepository>();
Bind<IProductRepository>().To<LinqToSqlProductRepository>();

Кому:

Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<NHibernateOrderRepository>();
Bind<IProductRepository>().To<NHibernateProductRepository>();

И вот, теперь все, что зависит от IOrderRepository, использует версию NHibernate, вам нужно было всего лишь изменить одну строку кода в отличие от потенциально сотен строк. И мы работаем над версиями Linq для SQL и NHibernate бок о бок, портируя функциональность по частям, не нарушая ничего посередине.


Итак, подведем итог всем сделанным мной пунктам:

  1. Не полагайтесь строго на общий IRepository<T> интерфейс. Большая часть функциональности, которую вы хотите получить из репозитория, специфична , а не универсальная . Если вы хотите включить IRepository<T> на верхних уровнях иерархии классов / интерфейсов, это нормально, но контроллеры должны зависеть от определенных репозиториев, чтобы вам не пришлось менять свой код в 5 в разных местах, когда вы обнаружите, что в общем хранилище отсутствуют важные методы.

  2. Контроллеры должны принимать репозитории извне, а не создавать свои собственные. Это важный шаг в устранении связи и улучшении тестируемости.

  3. Обычно вам нужно подключить контроллеры с помощью инфраструктуры Dependency Injection, и многие из них могут быть легко интегрированы с ASP.NET MVC. Если это слишком много для вас, то по крайней мере вы должны использовать какой-то статический поставщик услуг, чтобы вы могли централизовать всю логику создания хранилища. (В долгосрочной перспективе вам, вероятно, будет проще просто изучить и использовать DI-фреймворк).

3 голосов
/ 27 февраля 2010

Есть ли у людей отдельные репозитории на основе каждой таблицы (IUserRepository)? У меня, как правило, есть хранилище для каждого агрегата, а не для каждой таблицы.

Используют ли они общий IRepository? если возможно да

Они используют фабрику статических хранилищ? Я предпочитаю внедрение экземпляра репозитория через контейнер IOC

1 голос
/ 03 марта 2010

Вот как я это использую. Я использую IRepository для всех операций, которые являются общими для всех моих репозиториев.

public interface IRepository<T> where T : PersistentObject
{
    T GetById(object id);
    T[] GetAll();
    void Save(T entity);
}

и я также использую выделенный ITRepository для каждого агрегата для операции, которая отличается от этого репозитория. Например, для пользователя я буду использовать IUserRepository для добавления метода, отличного от UserRepository:

public interface IUserRepository : IRepository<User>
{
    User GetByUserName(string username);
}

Реализация будет выглядеть так:

public class UserRepository : RepositoryBase<User>, IUserRepository
{
    public User GetByUserName(string username)
    {
        ISession session = GetSession();
        IQuery query = session.CreateQuery("from User u where u.Username = :username");
        query.SetString("username", username);

        var matchingUser = query.UniqueResult<User>();

        return matchingUser;
    }
}


public class RepositoryBase<T> : IRepository<T> where T : PersistentObject
{
    public virtual T GetById(object id)
    {
        ISession session = GetSession();
        return session.Get<T>(id);
    }

    public virtual T[] GetAll()
    {
        ISession session = GetSession();
        ICriteria criteria = session.CreateCriteria(typeof (T));
        return criteria.List<T>().ToArray();
    }

    protected ISession GetSession()
    {
        return new SessionBuilder().GetSession();
    }

    public virtual void Save(T entity)
    {
        GetSession().SaveOrUpdate(entity);
    }
}

Чем в UserController будет выглядеть так:

public class UserController : ConventionController
{
    private readonly IUserRepository _repository;
    private readonly ISecurityContext _securityContext;
    private readonly IUserSession _userSession;

    public UserController(IUserRepository repository, ISecurityContext securityContext, IUserSession userSession)
    {
        _repository = repository;
        _securityContext = securityContext;
        _userSession = userSession;
    }
}

Чем создается репозиторий с использованием шаблона внедрения зависимостей с использованием фабрики пользовательских контроллеров. Я использую StructureMap в качестве слоя внедрения зависимостей.

Уровень базы данных - NHibernate. ISession является шлюзом для базы данных в этом сеансе.

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

Еще один проект, о котором вы можете узнать, это Кто может мне помочь . Которого я пока недостаточно копаю.

0 голосов
/ 04 марта 2010

Вы можете найти превосходную библиотеку Generic Repopsitory , которая была написана для того, чтобы использовать ее в качестве asp: ObjectDataSource WebForms в codeplex: MultiTierLinqToSql

У каждого из моих контроллеров есть частные репозитории для действий, которые они должны поддерживать.

0 голосов
/ 03 марта 2010

Есть ли у людей отдельные репозитории на основе каждой таблицы (IUserRepository)?

Да, это был лучший выбор по 2 причинам:

  • мой DAL основан на Linq-to-Sql (но мои DTO - это интерфейсы, основанные на объектах LTS)
  • выполненные операции атомарные (добавление - атомарная операция, сохранение - еще одна и т. Д.)

Используют ли они общий IR-репозиторий?

Да, исключительно , на основе схемы DDD Entity / Value. Я создал IRepositoryEntity / IRepositoryValue и общий IRepository для других вещей.

Они используют фабрику статического хранилища?

Да и нет: я использую IOC-контейнер , вызываемый через статический класс. Ну ... мы можем сказать, что это своего рода фабрика.

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

...