Частичная инициализация доменных сущностей - PullRequest
0 голосов
/ 20 января 2020

Далее автор рекомендует не инициализировать частично доменные сущности.

Как мы указывали ранее, у каждого клиента должно быть не более 5 контактов. Не возвращая контакты вместе с самими клиентами, мы оставляем дыру в нашей доменной модели, которая позволяет нам добавить 6-й контакт и, таким образом, нарушить этот инвариант.

Из-за этого следует выполнить частичную инициализацию избегать. Если ваш репозиторий возвращает список объектов домена (или только одного объекта домена), убедитесь, что объекты полностью инициализированы, что означает, что все их свойства заполнены. https://enterprisecraftsmanship.com/posts/partially-initialized-entities-anti-pattern/

Итак, должны ли мы загрузить весь граф объектов? Клиент со всеми контактами и всеми связанными вещами или структурой сущности ленивая загрузка поможет?

Ответы [ 2 ]

1 голос
/ 21 января 2020

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

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

Пример частично загруженного примера и почему он выходит из строя:

Кто-то отправляет написать следующий метод, который вызовет контроллер MVC, чтобы получить клиент и вернуть его в представление ...

public IEnumerable<Customer> GetCustomers(string criteria)
{
   using (var context = new MyDbContext())
   {
       return context.Customers.Where(x => x.IsActive && x.CustomerName.StartsWith(criteria)).ToList();
   }
}

Код, подобный этому, возможно, работал раньше с более простыми сущностями, но у Клиента были связанные данные, такие как Заказы, и когда MVC пошел на его сериализацию, он получил ошибка из-за того, что прокси-серверу Orders не удалось выполнить отложенную загрузку из-за удаления DbContext. Варианты заключались в том, чтобы каким-то образом загружать все связанные детали с помощью этого вызова, чтобы вернуть полного клиента, полностью отключить ленивые загрузочные прокси или вернуть неполного клиента. Поскольку этот метод будет использоваться для отображения сводного списка только сведений о клиенте, автор может выбрать что-то вроде:

public IEnumerable<Customer> GetCustomers(string criteria)
{
   using (var context = new MyDbContext())
   {
       return context.Customers.Where(x => x.IsActive && x.CustomerName.StartsWith(criteria))
           .Select(x => new Customer
           {
               CustomerId = x.CustomerId,
               CustomerName = x.CustomerName,
               // ... Any other fields that we want to display...
           }).ToList();
   }
}

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

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

Теперь, в первые дни EF и NHibernate, вам советуют всегда возвращать полные сущности или писать свои хранилища. никогда не возвращать сущности, вместо этого возвращать DTO. Например:

public IEnumerable<CustomerDTO> GetCustomers(string criteria)
{
   using (var context = new MyDbContext())
   {
       return context.Customers.Where(x => x.IsActive && x.CustomerName.StartsWith(criteria))
           .Select(x => new CustomerDTO
           {
               CustomerId = x.CustomerId,
               CustomerName = x.CustomerName,
               // ... Any other fields that we want to display...
           }).ToList();
   }
}

Это лучший подход, чем описанный выше, потому что при возврате и использовании CustomerDTO нет абсолютно никакой путаницы между этим частичным объектом и сущностью Customer. Однако это решение имеет свои недостатки. Во-первых, у вас может быть несколько похожих, но разных представлений, которые требуют данных клиента, а некоторым может потребоваться немного больше или некоторые связанные данные. Другие методы будут иметь другие поисковые требования. Некоторые будут хотеть нумерацию страниц или сортировку. Использование этого подхода будет аналогично примеру статьи, где вы получите хранилище, возвращающее несколько похожих, но разных DTO с большим количеством вариантов методов для разных критериев, включений и т. Д. c. (CustomerDTO, CustomerWithAddressDTO, et c. Et c.)

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

public IEnumerable<Customer> GetCustomers()
{
    return Context.Customers.Where(x => x.IsActive)
}

Затем, когда ваш MVC контроллер перейдет к списку клиентов с его критериями:

using (var contextScope = ContextScopeFactory.Create())
{
    return CustomerRepository.GetCustomers()
         .Where(x => x.CustomerName.Contains(criteria)
         .Select(x => new CustomerViewModel
         {
             CustomerId = x.CustomerId,
             CustomerName = x.CustomerName,
             // ... Details from customer and related entities as needed.
         }).ToList();
}

By возвращая IQueryable хранилище не нужно беспокоиться о полном или неполном представлении сущностей. Он может применять основные правила, такие как активная проверка состояния, но предоставить потребителям возможность фильтровать, сортировать, разбивать на страницы или иным образом использовать данные по своему усмотрению. Это делает хранилища очень легкими и простыми в работе, а контроллеры и сервисы, которые их потребляют, могут подвергаться модульному тестированию с макетами вместо хранилищ. Контроллеры должны использовать сущности, возвращаемые хранилищем, но стараться не возвращать эти сущности сами. Вместо этого они могут заполнить модели представлений (или DTO) для передачи веб-клиенту или потребителю API, чтобы избежать частичной передачи сущностей и их путаницы для реальных сущностей.

Это относится к случаям, даже когда ожидается, что хранилище вернуть только 1 сущность, возвращая IQueryable имеет свои преимущества. Например,

, сравнивая:

public Customer GetCustomerById(int customerId)
{
     return Context.Customers.SingleOrDefault(x => x.CustomerId == customerId);
}

против

public IQueryable<Customer> QGetCustomerById(int customerId)
{
     return Context.Customers.Where(x => x.CustomerId == customerId);
}

Они выглядят очень похожими, но для потребителя (контроллера / службы) все будет немного по-другому.

var customer = CustomerRepository.GetCustomerById(customerId);

против

var customer = CustomerRepository.QGetCustomerById(customerId).Single();

Немного отличается, но 2-й гораздо более гибкий. Если бы мы просто хотели проверить, существует ли клиент?

var customerExists = CustomerRepository.GetCustomerById(customerId) != null;

против

var customerExists = CustomerRepository.QGetCustomerById(customerId).Any();

Первый будет выполнять запрос, который загружает всю сущность клиента. Второй просто выполняет запрос проверки Exists. Когда дело доходит до загрузки связанных данных? Первый метод должен опираться на ленивую загрузку или просто не иметь связанных подробностей, где метод IQueryable может:

var customer = CustomerRepository.QGetCustomerById(customerId).Include(x => x.Related).Single();

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

var customerViewModel = CustomerRepository.QGetCustomerById(customerId)
    .Select(x => new CustomerViewModel
    {
        CustomerId = x.CustomerId,
        CustomerName = x.CustomerName,
        RelatedName = x.Related.Name,
        // ... etc.
    }).Single();

Отказ от ответственности: Фактический пробег может варьироваться в зависимости от вашей версии EF. EF Core претерпел ряд изменений по сравнению с EF6, связанных с отложенной загрузкой и построением запросов.

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

Случай, когда использование частично инициализированной сущности является совершенно допустимым, будет при выполнении удаления без предварительного получение объекта. Например, в случаях, когда вы уверены, что конкретный идентификатор или диапазон идентификаторов необходимо удалить, вместо загрузки этих сущностей для удаления вы можете создать новый класс только с заполненным PK этой сущности и указать DbContext удалить его. Ключевым моментом при рассмотрении использования неполных сущностей может быть то, что только в тех случаях, когда сущность живет только в рамках операции и не возвращается вызывающим.

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

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

Поскольку кто-то написал в комментариях к этому посту, проблема с производительностью вполне может возникнуть при наличии тысяч разрешенные контакты. Примером чего-то такого может быть то, что Customer может иметь, скажем, только 5 активных Order экземпляров. Должны ли быть загружены все экземпляры заказа, связанные с клиентом? Конечно, нет. Фактически, Order является другим агрегатом, и экземпляр одного агрегата не должен содержаться в другом агрегате. Вы могли бы использовать объект значения , содержащий идентификатор другого агрегата, но для многих из них такая же проблема производительности может проявиться.

Альтернативой может быть просто оставить ContactCount или, в моем примере, ActiveOrderCount, который остается согласованным. Если фактические отношения должны быть сохранены / удалены, то они могут быть присоединены к соответствующему агрегату при добавлении / удалении, чтобы сохранить изменение, но это временное представление.

Итак, должны ли мы иметь загрузить весь граф объекта? Клиент со всеми контактами и всеми связанными вещами или структурой сущности ленивая загрузка поможет?

Ответ, на самом деле, звучит "да" . Однако ваша объектная модель не должна быть глубокой. Вы должны сделать каждую попытку создать небольшие агрегаты. Я пытаюсь смоделировать свои агрегаты с одной сущностью root и затем содержать объекты значений. Весь агрегат загружен. Lazy-loading - это, вероятно, признак того, что вы запрашиваете свой домен, что я советую сделать , а не . Скорее создайте простой запрос механизм, который использует некоторую модель чтения , чтобы возвратить соответствующие данные для вашего интерфейса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...