Дизайн слоя абстракции базы данных. Правильный выбор IRepository?

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

Многие примеры, которые я видел, описывают и используют шаблон репозитория (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 контроллер на приложение), поэтому я не уверен в том, что лучший курс действий.

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

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

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

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

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

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

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

Ответы

Ответ 1

Некоторые очень очевидные проблемы с идеей общего 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);
}

И так далее - вы получаете идею. Таким образом, у нас есть "общий" репозиторий, если нам когда-либо понадобится невероятно упрощенная семантика retrieve-by-id, но в общем мы никогда не собираемся передавать это, конечно, не к классу контроллера.


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

Контроллер должен извлечь свой репозиторий из внешнего мира. Причина, по которой вы создали эти хранилища, заключается в том, что вы можете сделать какую-то инверсию управления. Ваша конечная цель здесь состоит в том, чтобы иметь возможность менять один репозиторий для другого - например, выполнять модульное тестирование, или если вы решите перейти от 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 есть беззадачные конструкторы, которые создают конкретные репозитории, заключается в том, что сайты должны иметь возможность компилироваться и запускаться, не заставляя вас настраивать всю инфраструктуру внедрения Injection.

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

[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>();

To:

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

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


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

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

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

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

Ответ 2

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

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

Используют ли они статический репозиторий factory? Я предпочитаю инъекцию экземпляра репозитория через контейнер IOC

Ответ 3

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

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

и я использую также выделенный ITRepository для каждого aggregate для операции, отличной от этого репозитория. Например, для пользователя я буду использовать 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;
    }
}

Чем создается экземпляр репозитория с использованием шаблона ввода зависимостей с использованием пользовательского контроллера factory. Я использую StructureMap как мой уровень инъекции зависимостей.

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

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

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

Ответ 4

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

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

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

Используют ли они общий IRepository?

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

Используют ли они статический репозиторий factory?

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

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

Ответ 5

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

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