Возврат доменных объектов из репозиториев на присоединяющихся таблицах - PullRequest
0 голосов
/ 19 января 2020

Я читал, что репозитории должны возвращать только доменные объекты. У меня возникли трудности с реализацией этого. В настоящее время у меня есть API с Service Layer, Repository, и я использую EF Core для доступа к базе данных sql.

Если мы рассматриваем пользователя (идентификатор, имя, адрес, номер телефона, адрес электронной почты, имя пользователя) и заказы (идентификатор, OrderDetails, идентификатор пользователя) как 2 объекта домена. Один клиент может иметь несколько заказов. Я создал навигационное свойство

public virtual User User{ get; set; }

и внешний ключ.

Сервисный уровень должен возвращать DTO с OrderId, OrderDetails, CustomerId, CustomerName. Что должен вернуть репозиторий в этом случае? Вот что я пытался:

public IEnumerable<Orders> GetOrders(int orderId)
        {
            var result = _context.Orders.Where(or=>or.Id=orderId)
                .Include(u => u.User)
                .ToList();
            return result;
        }

У меня проблемы с загрузкой Eager. Я пытался использовать включить. Я использую базу данных в первую очередь. В случае выше, свойства навигации всегда возвращаются с NULL. Единственный способ получить данные в свойствах навигации - включить отложенную загрузку с прокси для контекста. Я думаю, что это будет проблема производительности

Может кто-нибудь помочь с тем, что я должен вернуть и почему .Include не работает?

Ответы [ 2 ]

0 голосов
/ 20 января 2020

Совет, который я даю в отношении шаблона репозитория, заключается в том, что репозитории должны возвращать IQueryable<TEntity>, а не IEnumerable<TEntity>.

Целью репозитория является:

  • Упрощение тестирования кода.
  • Централизация общих бизнес-правил.

цель репозитория должна не быть:

  • Абстрактная EF вне вашего проекта.
  • Скрыть знание вашего домена. (Entities)

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

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

Для данной службы, где вы хотите "вернуть DTO с OrderId, OrderDetails, CustomerId, CustomerName."

Шаг 1: Необработанный пример, без репозитория. ..

Сервисный код:

public OrderDto GetOrderById(int orderId)
{
    using (var context = new AppDbContext())
    {
        var order = context.Orders
            .Select(x => new OrderDto
            {
                OrderId = x.OrderId,
                OrderDetails = x.OrderDetails,
                CustomerId = x.Customer.CustomerId,
                CustomerName = x.Customer.Name
            }).Single(x => x.OrderId == orderId);
        return order;
    }    
}

Этот код может прекрасно работать, но он связан с DbContext, поэтому его сложно выполнить модульным тестированием. У нас может быть дополнительная бизнес-логика c, чтобы учесть, что ее придется применять практически ко всем запросам, например, если у заказов есть состояние «IsActive» (мягкое удаление) или база данных обслуживает несколько клиентов (мультитенант). В наших контроллерах будет много запросов, что повлечет за собой необходимость во многих вещах, таких как .Where(x => x.IsActive), включенных повсюду.

С шаблоном Repository (IQueryable), единица работы:

public OrderDto GetOrderById(int orderId)
{
    using (var context = ContextScopeFactory.CreateReadOnly())
    {
        var order = OrderRepository.GetOrders()
            .Select(x => new OrderDto
            {
                OrderId = x.OrderId,
                OrderDetails = x.OrderDetails,
                CustomerId = x.Customer.CustomerId,
                CustomerName = x.Customer.Name
            }).Single(x => x.OrderId == orderId);
        return order;
    }    
}

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

Код репозитория:

public class OrderRepository : IOrderRepository
{
    private readonly IAmbientContextScopeLocator _contextScopeLocator = null;

    public OrderRepository(IAmbientContextScopeLocator contextScopeLocator)
    {
        _contextScopeLocator = contextScopeLocator ?? throw new ArgumentNullException("contextScopeLocator");
    }

    private AppDbContext Context => return _contextScopeLocator.Get<AppDbContext>();

    IQueryable<Order> IOrderRepository.GetOrders()
    {
        return Context.Orders.Where(x => x.IsActive);
    }
}

В этом примере используется единица DhContextScope от Mehdime для единицы работать, но может быть адаптирован к другим или внедренному DbContext, если это время жизни ограничено запросом. Это также демонстрирует случай с очень распространенными критериями фильтра («IsActive»), который мы могли бы хотеть централизовать по всем запросам.

В приведенном выше примере мы используем репозиторий для возврата заказов в виде IQueryable. Метод репозитория полностью поддается моделированию, когда вызов DbContextScopeFactory.CreateReadOnly может быть заглушен, а вызов репозитория может быть смоделирован для возврата любых данных, которые вы хотите, используя, например, List<Order>().AsQueryable(). Возвращая IQueryable, вызывающий код полностью контролирует, как будут использоваться данные. Обратите внимание, что нет необходимости беспокоиться о том, чтобы загружать данные о клиенте / пользователе. Запрос не будет выполнен, пока вы не выполните вызов Single (или ToList et c.), Что приведет к очень эффективным запросам. Сам класс репозитория очень прост, так как нетрудно сказать ему, какие записи и связанные данные включать. Мы можем настроить наш запрос, добавив сортировку, нумерацию страниц (Skip / Take) или получить Count или просто проверить, существуют ли какие-либо данные (Any) без добавления функций и т. Д. c. в хранилище или с накладными расходами на загрузку данных только для простой проверки.

Наиболее распространенные возражения, которые я слышу о возвращении хранилищ IQueryable:

" Утечка. Вызывающие абоненты должны знать об EF и структуре сущностей. "Да, вызывающие абоненты должны знать об ограничениях EF и структуре сущностей. Однако многие альтернативные подходы, такие как внедрение деревьев выражений для управления фильтрацией, сортировкой и активной загрузкой, требуют одинакового знания ограничений EF и структуры сущностей. Например, введение выражения для выполнения фильтрации по-прежнему не может включать в себя детали, которые EF не может выполнить. Полное абстрагирование EF приведет к лоту аналогичных, но ограниченных методов в репозитории и / или к потере производительности и возможностей, которые дает EF. Если вы внедрите EF в свой проект, он будет работать намного лучше, если ему доверяют как первоклассному гражданину в рамках проекта.

" Как сопровождающий слой домена, я могу оптимизировать код, когда репозитории отвечают за критерии. "Я объяснил это преждевременной оптимизацией. В репозиториях можно применять фильтрацию на уровне ядра, такую ​​как активное состояние или владение, оставляя желаемые запросы и поиск вплоть до кода реализации. Это правда, что вы не можете предсказать или контролировать, как эти итоговые запросы будут выглядеть по отношению к вашему источнику данных, но оптимизация запросов - это то, что лучше всего делать при рассмотрении использования данных в реальном мире. Запросы, которые генерирует EF, отражают необходимые данные, которые могут быть уточнены, и основу для того, какие индексы будут наиболее эффективными. Альтернативой является попытка предсказать, какие запросы будут использоваться, и предоставление этих ограниченных выборок сервисам для использования с намерением запросить дополнительные уточненные «варианты». Это часто приводит к тому, что сервисы, выполняющие менее эффективные запросы, чаще получают свои данные, когда возникает большая проблема с введением новых запросов в репозитории.

0 голосов
/ 19 января 2020

Репозитории могут возвращать другие типы объектов, даже примитивные типы, такие как целые числа, если вы хотите подсчитать некоторое количество объектов на основе критериев.

Это из книги «Конструкция, управляемая доменом»:

Они (хранилища) также могут возвращать символическую информацию, например, количество экземпляров (объекта домена), удовлетворяющих некоторым критериям. Они могут даже возвращать итоговые вычисления, такие как сумма по всем совпадающим объектам какого-либо числового атрибута.

Если вы возвращаете что-то, что не является объектами домена, это потому, что вам нужна некоторая информация о домене Объекты, поэтому вы должны возвращать только неизменяемые объекты и примитивные типы данных, такие как целые числа.

Если вы создаете запрос и объекты с намерением изменить его после его получения, это должен быть Доменный объект.

Если вам нужно сделать это, поместите границы вокруг ваших доменных объектов и организуйте их в агрегаты.

Вот хорошая статья, которая объясняет, как разбить вашу модель на агрегаты: https://dddcommunity.org/library/vernon_2011/

В вашем случае вы можете либо объединить сущности Пользователь и Заказ в одном Агрегате, либо объединить их в отдельные Агрегаты.

РЕДАКТИРОВАТЬ:

Пример:

Здесь мы будем использовать Reference By Id , и все сущности из разных агрегатов будут ссылаться на другие права s из разных агрегатов по Id.

У нас будет три Агрегата : Пользователь , Продукт и Заказ с одним ValueObject OrderLineItem .

public class User {

    public Guid Id{ get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
}

public class Product {

    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public Money Price { get; private set; }
}

public class OrderLineItem {

    public Guid ProductId { get; private set; }
    public Quantity Quantity { get; private set; }
    // Copy the current price of the product here so future changes don't affect old orders
    public Money Price { get; private set; } 
}

public class Order {

    public Guid Id { get; private set; }
    public IEnumerable<OrderLineItem> LineItems { get; private set; }
}

Теперь, если вам нужно выполнить тяжелые запросы в вашем приложении, вы можете создать ReadModel , который будет созданный из модели выше


public class OrderLineItemWithProductDetails {

    public Guid ProductId { get; private set; }
    public string ProductName { get; private set; }

    // other stuff quantity, price etc.
}

public class OrderWithUserDetails {

    public Guid Id { get; private set; }
    public string UserFirstName { get; private set; }
    public string UserLastName { get; private set; }
    public IEnumerable<OrderLineItemWithProductDetails > LineItems { get; private set; }
    // other stuff you will need

}

То, как вы заполняете ReadModel - это целая топика c, поэтому я не могу охватить все это, но вот несколько указателей.

Вы Я сказал, что вы будете делать Join, так что вы, вероятно, используете RDBMS, например Poste SQL или MySQL. Вы можете присоединиться в специальном ReadModel репозитории . Если ваши данные находятся в одной базе данных, вы можете просто использовать репозиторий ReadModel.


// SQL Repository, No ORM here
public class OrderReadModelRepository {

    public OrderWithUserDetails FindForUser(Guid userId) {

        // this is suppose to be an example, my SQL is a bit rusty so...
        string sql = @"SELECT * FROM orders AS o 
                    JOIN orderlineitems AS l
                    JOIN users AS u ON o.UserId = u.Id
                    JOIN products AS p ON p.id = l.ProductId
                    WHERE u.Id = userId";

        var resultSet = DB.Execute(sql);

        return CreateOrderWithDetailsFromResultSet(resultSet);
    }
}

// ORM based repository
public class OrderReadModelRepository {

    public IEnumerable<OrderWithUserDetails> FindForUser(Guid userId) {

        return ctx.Orders.Where(o => o.UserId == userId)
                         .Include("OrderLineItems")
                         .Include("Products")
                         .ToList();
    }
}

Если это не так, вам придется создать их и хранить в отдельной базе данных. Вы можете использовать DomainEvents , чтобы сделать это, но я не буду go так далеко, если у вас есть одна SQL база данных.

...