EF Core — почему явная загрузка ужасно медленная?

Я работаю над микросервисами в .Net Core с дизайном, управляемым доменом. Уровень инфраструктуры имеет EF Core DbContext для доступа к базе данных, а в моих репозиториях у меня есть асинхронные методы для извлечения данных.

Поскольку Include/ThenInclude не поддерживает фильтрацию (по крайней мере, до Ef Core 2.1), я перепробовал все возможные подходы, которые нашел, когда искал в Google, как заменить Include. Я также смотрел видео Pluralsight об Ef Core, и когда я увидел опцию Explicit Loading, я был очень счастлив из-за ее способности фильтровать связанные объекты, но когда я переписал один из методов в Explicit версию, запрос, который выполнялся в течение нескольких миллисекунды увеличились до пары минут!

В конфигурациях моих сущностей я настроил все переходы и внешние ключи, но я не уверен, требует ли явная загрузка какой-либо дополнительной настройки или нет? Прежде чем рекомендовать использовать глобальные фильтры, обратите внимание, что предложение Where обычно длиннее, поэтому приведенный ниже пример — это просто укороченная версия реальной фильтрации!

Вот как выглядят мои методы (TransferService служит агрегатом, TransferServiceDetail и любые другие классы — это просто сущности в домене TransferService):

public async Task<IEnumerable<TransferService>> GetAllAsync(
    TransferServiceFilter transferServiceFilter)
    {
        int? pageIndex = null;
        int? itemsPerPage = null;

        IEnumerable<TransferService> filteredList = DBContext.TransferServices.Where(
           ts => !ts.IsDeleted); //This one itself is quick.

        //This is just our filtering, it does not affect performance.
        if (transferServiceFilter != null)
        {
            pageIndex = transferServiceFilter.PageIndex;
            itemsPerPage = transferServiceFilter.ItemsPerPage;

            filteredList = filteredList.Where(f =>
                (transferServiceFilter.TransferSupplierId == null ||
                f.TransferSupplierId == transferServiceFilter.TransferSupplierId) &&
                (transferServiceFilter.TransferDestinationId == null || 
              f.TransferDestinationId == transferServiceFilter.TransferDestinationId) &&
                (transferServiceFilter.TransferSupplierId == null ||
              f.TransferSupplierId == transferServiceFilter.TransferSupplierId) &&
                (string.IsNullOrEmpty(transferServiceFilter.TransportHubRef) ||
              f.NormalizeReference(f.TransportHubRef) == 
                 f.NormalizeReference(transferServiceFilter.TransportHubRef)));
        }

        //This is just for paging and again, this is quick.
        return await FilterList(filteredList.AsQueryable(), pageIndex, itemsPerPage);
    }

    public async Task<IEnumerable<TransferService>> GetAllWithServiceDetailsAsync(
      TransferServiceFilter transferServiceFilter)
    {
        IEnumerable<TransferService> returnList = await GetAllAsync(
           transferServiceFilter);

        //This might be the problem as I need to iterate through my TransferServices
        //to be able to load all TransferServiceDetails that belong to each individual
        //Service.
        foreach (TransferService service in returnList)
        {
            await DBContext.Entry<TransferService>(service)
              .Collection(ts => ts.TransferServiceDetails.Where(
                 tsd => !tsd.IsDeleted)).LoadAsync();
        }

        return returnList;
    }

В моем репозитории есть и другие методы, аналогичные предыдущему методу GetAllXY... (у TransferServiceDetails есть Rates, Rates have Periods и т. д.).

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

Итак, мой вопрос снова: есть ли какие-либо дополнительные настройки, которые я мог пропустить в конфигурациях объектов, требующих явной загрузки, или просто мои запросы неверны? Или, может быть, явная загрузка хороша только тогда, когда в качестве родителя есть только один TransferService вместо списка TransferServices (50-100 в моем случае), а также есть всего несколько дочерних связанных сущностей (в моем случае у меня обычно есть 5- 10 деталей, каждая деталь имеет 2-3 ставки, каждая ставка имеет ровно 1 период и т.д.)?


person Daniel    schedule 17.08.2018    source источник
comment
Посмотрите Фильтры запросов на уровне модели.   -  person Gert Arnold    schedule 19.08.2018
comment
Спасибо, но, как я уже упоминал в своей статье, я не могу использовать эти фильтры. Иногда мне нужны поля IsDeleted, иногда нет. Иногда я применяю фильтры x, y, z, иногда только их части.   -  person Daniel    schedule 20.08.2018


Ответы (1)


Я предполагаю, что ваша фильтрация не может быть преобразована в SQL, где и вся фильтрация происходит на стороне клиента (EF загружает ВСЕ TransferServices объекты в память, фильтрует в памяти и удаляет несоответствующие).

Я могу проверить это, включив подробное (отладочное) ведение журнала — EF будет выгружать SQL-запросы в журнал.

После подтверждения вы должны внести улучшения:

Во-первых, поместите ifs из Where. Вместо:

filteredList = filteredList.Where(f => transferServiceFilter.TransferSupplierId == null ||
     f.TransferSupplierId == transferServiceFilter.TransferSupplierId)

использовать

if (transferServiceFilter.TransferSupplierId != null)
{
  filteredList = filteredList.Where(f => f.TransferSupplierId == transferServiceFilter.TransferSupplierId)
}

Во-вторых, вы должны переосмыслить NormalizeReference. Это не может быть выполнено на стороне сервера, потому что SQL-сервер не знает об этой реализации. Вы должны предварительно нормализовать TransportHubRef, сохранить его в БД (скажем, NormalizedTransportHubRef) и использовать Where с простым равенством. (Кроме того, не забывайте об индексах).

person Dmitry    schedule 17.08.2018
comment
Спасибо, но, как я уже упоминал, исходный запрос TransferService выполняется молниеносно. На данный момент возвращается 220 сервисов, если я не указываю фильтры. Потеря производительности происходит, когда я явно загружаю коллекцию сведений. Даже загрузка сведений о первом сервисе занимает почти секунду и загружает существующие в настоящее время 4 записи сведений о первом сервисе. - person Daniel; 20.08.2018
comment
Включите ведение журнала и проверьте SQL. У вас есть фильтрация на стороне клиента, и вы не можете гарантировать молниеносную скорость при объединении выбрать все с JOIN. Я думаю, вас очень удивят тексты SQL... - person Dmitry; 20.08.2018