Альтернативи схемі сховища для інкапсуляції логіки ORM?


24

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

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

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

public static class PersonExtensions
{
    public static IEnumerable<Person> GetRetiredPeople(this IRepository<Person> personRep)
    {
        // logic
    }
}

5
Що саме стосується шаблону репозиторію, який вам важко кодувати та підтримувати?
Меттью Флінн

1
Там є альтернативи. Одним із таких є використання шаблону «Об’єкт запитів», який, хоча я не використовував, здається хорошим підходом. ІМХО для сховища - це хороший зразок, якщо використовувати його правильно, але не бути всім і закінчити все
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. Це нещільна абстракція. Спробуйте реалізувати цей інтерфейс самостійно або користуйтеся ним, не знаючи про базового постачальника даних. Наприклад, кожен постачальник db має свій власний API для охочих завантажень об'єктів. Ось чому це нещільна абстракція.

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

В якості альтернативи можна використовувати запити (наприклад, як це визначено шаблоном розділення команд / запитів). CQS описаний у wikipedia.

Ви в основному створюєте класи запитів, до яких викликаєте:

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/


Чи не був би альтернативний шаблон, який ви запропонували створити ще більше безладу на великих проектах, бачачи, яким методом у шаблоні репозиторію є цілий клас?
Данте

3
Якщо ваше визначення малих класів є непохитним, так, так. У такому випадку вам не слід намагатися слідувати принципам SOLID, оскільки застосування їх завжди створюватиме більше (менших) класів.
jgauffin

2
Менші класи є кращими, але чи не навігація по існуючих класах запитів була б більш клопіткою, ніж сказати, перегляд інтелігенції сховища (домену), щоб побачити, чи потрібна функціональність лежить там, а якщо ні, то застосувати її.
Данте

4
Структура проекту стає важливішою. Якщо ви покладете всі запити в простір імен, так, як YourProject.YourDomainModelName.Queriesце буде легко переміщуватися.
jgauffin

Одне, що мені важко зробити гарним чином із CQS - це звичайні запити. Наприклад, даючи користувачеві, один запит отримує всі асоційовані студенти (різні класи, власні діти тощо). Тепер ще один запит повинен отримати дітей для користувача, а потім зробити деяку операцію над ними - тому запит 2 може використовувати загальну логіку в запиті 1, але немає простого способу зробити це. Мені не вдалося знайти жодної реалізації CQS, яка б це полегшило. У цьому плані багато допомагають сховища. Не шанувальник жодного зразків, а просто ділиться моєю думкою.
Mrchief

8

Спочатку подумайте, що я кажу, що ORM вже досить велика абстракція над вашою базою даних. Деякі ORM забезпечують прив'язку до всіх загальних реляційних баз даних (NHibernate має прив'язки до MSSQL, Oracle, MySQL, Postgress тощо). Тому створення нового абстрагування поверх цього мені не видається вигідним. Крім того, міркувати про те, що вам потрібно "абстрагуватися" від цієї ОРМ, безглуздо.

Якщо ви все ще хочете побудувати цю абстракцію, я б перейшов до шаблону сховища. Це було чудово у вік чистого SQL, але це досить клопітно перед сучасними ORM. Головним чином через те, що ви знову реалізуєте більшість функцій ORM, як-от операції CRUD та запити.

Якби я будував такі абстракції, я використовував би ці правила / зразки

  • CQRS
  • Використовуйте якомога більше існуючих функцій ORM
  • Інкапсулюйте лише складну логіку та запити
  • Постарайтеся, щоб "сантехніка" ORM була захована всередині архітектури

У конкретному виконанні я б використовував CRUD-операції ORM безпосередньо, без будь-якого обгортання. Я б також робив прості запити безпосередньо в коді. Але складні запити будуть інкапсульовані у власні об’єкти. Щоб "приховати" ORM, я б спробував прозоро ввести контекст даних в об'єкти сервісу / інтерфейсу і зробив це так само, щоб запитувати об'єкти.

Останнє, що я хотів би сказати, - це те, що багато людей використовують ORM, не знаючи, як ним користуватися та як отримати найкращий «прибуток» від цього. Люди, які рекомендують сховища, зазвичай є подібними.

Як рекомендоване читання, я б сказав блог Айенде , особливо ця стаття .


+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);

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 постачальників 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
Хоча це посилання може відповісти на питання, краще включити сюди суттєві частини відповіді та надати посилання для довідки. Відповіді лише на посилання можуть стати недійсними, якщо пов’язана сторінка зміниться.
Ден Пішельман

Вибачте, я поспішав. Відповідь оновлено, щоб показати приклад коду.
Shayne

1
+1 для покращення публікації з більшою кількістю вмісту.
Сонго
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.