Альтернативы шаблону хранилища для инкапсуляции логики ORM?

24

Мне просто нужно было отключить ORM, и это было довольно сложной задачей, потому что логика запросов просачивалась повсюду. Если бы мне когда-либо приходилось разрабатывать новое приложение, я бы предпочел инкапсулировать всю логику запросов (используя ORM), чтобы защитить ее от изменений в будущем. Шаблон репозитория довольно сложен для кодирования и поддержки, поэтому мне было интересно, есть ли какие-либо другие шаблоны для решения проблемы?

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

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

public static class PersonExtensions
{
    public static IEnumerable<Person> GetRetiredPeople(this IRepository<Person> personRep)
    {
        // logic
    }
}
Dante
источник
5
Что именно в шаблоне репозитория вы считаете проблематичным в коде и поддержке?
Мэтью Флинн
1
Есть альтернативы там. Одним из таких примеров является использование шаблона Query Object, который, хотя я и не использовал, кажется хорошим подходом. Репозиторий IMHO - хороший шаблон, если использовать его правильно, но не быть всем и закончить все
dreza

Ответы:

14

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

public class UserRepository : EntityFrameworkRepository<User>, IUserRepository
{
    public UserRepository(DbContext context){}

    public ICollection<Person> FindRetired()
    {
        return context.Persons.Where(p => p.Status == "Retired").ToList();
    }
}

Во-вторых:

Не возвращайся IQueryable. Это дырявая абстракция. Попробуйте реализовать этот интерфейс самостоятельно или использовать его без ведома основного поставщика БД. Например, у каждого провайдера БД есть собственный API для активной загрузки сущностей. Вот почему это дырявая абстракция.

альтернатива

В качестве альтернативы вы можете использовать запросы (например, как определено шаблоном разделения Command / Query). CQS описана в википедии.

Вы в основном создаете классы запросов, которые вы вызываете:

public class GetMyMessages
{
    public GetMyMessages(IDbConnection connection)
    {
    }


    public Message[] Execute(DateTime minDate)
    {
        //query
    }
}

Самое замечательное в запросах заключается в том, что вы можете использовать разные методы доступа к данным для разных запросов, не загромождая код. например, вы можете использовать веб-сервис в одном, nhibernate в другом и ADO.NET в третьем.

Если вы заинтересованы в реализации .NET, прочитайте мою статью: http://blog.gauffin.org/2012/10/griffin-decoupled-the-queries/

jgauffin
источник
Разве альтернативный шаблон, который Вы предложили, не создал бы еще больше беспорядка в более крупных проектах, видя, как то, что будет методом в шаблоне репозитория, теперь представляет собой целый класс?
Данте
3
Если ваше определение наличия маленьких классов беспорядок, ну да. В этом случае вы не должны пытаться следовать принципам SOLID, так как их применение всегда приведет к появлению большего количества (меньших) классов.
jgauffin
2
Меньшие классы лучше, но разве навигация по существующим классам запросов не доставит больше хлопот, чем, скажем, просмотр intellisense (доменного) репозитория, чтобы увидеть, находится ли там необходимая функциональность и, если нет, реализовать ее.
Данте
4
Структура проекта становится более важной. Если вы поместите все запросы в пространство имен, как YourProject.YourDomainModelName.Queriesэто будет легко ориентироваться.
jgauffin
Одна вещь, которую я нахожу трудной для хорошего подхода к CQS, - это общие запросы. Например, для данного пользователя один запрос получает всех связанных учеников (различия в оценках, собственные дети и т. Д.). Теперь другой запрос должен получить дочерние элементы для пользователя, а затем выполнить с ним некоторую операцию - поэтому запрос 2 может использовать общую логику в запросе 1, но простого способа сделать это не существует. Мне не удалось найти какую-либо реализацию CQS, которая бы облегчала это. Хранилища очень помогают в этом отношении. Не фанат какой-либо модели, а просто делюсь своим мнением.
Mrchief
8

Сначала я думаю, что ORM - это уже достаточно большая абстракция над вашей базой данных. Некоторые ORM обеспечивают привязку ко всем распространенным реляционным базам данных (NHibernate имеет привязки к MSSQL, Oracle, MySQL, Postgress и т. Д.). Поэтому создание новой абстракции поверх этого не кажется мне выгодным. Кроме того, спорить о том, что вам нужно «абстрагироваться» от этого ORM, не имеет смысла.

Если вы все еще хотите построить эту абстракцию, я бы пошел против шаблона репозитория. Это было здорово в эпоху чистого SQL, но это довольно хлопотно для современного ORM. Главным образом потому, что вы в конечном итоге повторно внедрили большинство функций ORM, таких как операции CRUD и запросы.

Если бы я строил такие абстракции, я бы использовал эти правила / шаблоны

  • CQRS
  • Используйте как можно больше существующих функций ORM
  • Инкапсулировать только сложную логику и запросы
  • Постарайтесь, чтобы «сантехника» ORM была скрыта внутри архитектуры

В конкретной реализации я бы использовал CRUD-операции ORM напрямую, без каких-либо переносов. Я бы также делал простые запросы прямо в коде. Но сложные запросы будут заключены в свои собственные объекты. Чтобы «спрятать» ORM, я бы попытался прозрачно внедрить контекст данных в объекты службы / пользовательского интерфейса и сделать то же самое для запросов к объектам.

Последнее, что я хотел бы сказать, это то, что многие люди используют ORM, не зная, как его использовать и как извлечь из него максимальную «прибыль». Люди, которые рекомендуют репозитории, обычно бывают такого рода.

В качестве рекомендуемого чтения я бы сказал блог Айенде , особенно эту статью .

Euphoric
источник
+1 «Последнее, что я хотел бы сказать, это то, что многие люди используют ORM, не зная, как его использовать и как извлечь из него максимальную« выгоду ». Люди, которые рекомендуют репозитории, обычно бывают такого рода»
Misters,
1

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

Вы можете сузить этот API, если вы реализуете метод репозитория FindByExample и используете его следующим образом

// find all retired persons
Person filter = new Person {Status=PersonStatus.Retired};
IEnumerable<Person> found = personRepository.FindByExample(filter);


// find all persons who have a dog named "sam"
Person filter = new Person();
filter.AddPet(new Dog{Name="sam"});
IEnumerable<Person> found = personRepository.FindByExample(filter);
k3b
источник
0

Подумайте о том, чтобы заставить ваши расширения работать на IQueryable вместо IRepository.

public static class PersonExtensions
{
    public static IQueryable<Person> AreRetired(this IQueryable<Person> people)
    {
        return people.Where(p => p.Status == "Retired");
    }
}

Для модульного тестирования:

List<Person> people = new List<Person>();
people.Add(new Person() { Name = "Bob", Status = "Retired" });
people.Add(new Person() { Name = "Sam", Status = "Working" });

var retiredPeople = people.AsQueryable().AreRetired();
// assert that Bob is in the list
// assert that Sam is not in the list

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

Джаред С
источник
5
-1 а) IQueryableплохая мама, точнее интерфейс БОГА. Его реализация очень полезна, и есть очень мало (если таковые имеются) полных поставщиков LINQ to Sql. б) Методы расширения делают расширение (один из основных принципов ООП) невозможным.
jgauffin
0

Я написал довольно аккуратный шаблон объекта запроса для NHibernate здесь: https://github.com/shaynevanasperen/NHibernate.Sessions.Operations

Это работает с использованием интерфейса, подобного этому:

public interface IDatabases
{
    ISessionManager SessionManager { get; }

    T Query<T>(IDatabaseQuery<T> query);
    T Query<T>(ICachedDatabaseQuery<T> query);

    void Command(IDatabaseCommand command);
    T Command<T>(IDatabaseCommand<T> command);
}

Учитывая класс сущности POCO как это:

class Database1Poco
{
    public int Property1 { get; set; }
    public string Property2 { get; set; }
}

Вы можете создавать объекты запроса следующим образом:

class Database1PocoByProperty1 : DatabaseQuery<Database1Poco>
{
    public override Database1Poco Execute(ISessionManager sessionManager)
    {
        return sessionManager.Session.Query<Database1Poco>().SingleOrDefault(x => x.Property1 == Property1);
    }

    public int Property1 { get; set; }
}

А затем используйте их так:

var database1Poco = _databases.Query(new Database1PocoByProperty1 { Property1 = 1 });
Шэйн
источник
1
Хотя эта ссылка может ответить на вопрос, лучше включить сюда основные части ответа и предоставить ссылку для справки. Ответы, содержащие только ссылки, могут стать недействительными, если связанная страница изменится.
Дэн Пичельман
Мои извинения, я спешил. Ответ обновлен, чтобы показать пример кода.
Шейн
1
+1 за улучшение поста с большим содержанием.
Сонго