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