Как заставить EF активно загружать свойство навигации по коллекции через GroupJoin? - PullRequest
0 голосов
/ 11 октября 2018

Я пытаюсь GroupJoin некоторых данных с IQueryable и проецировать эти данные в анонимный тип.Исходная сущность, на которую я GroupJoin вхожу, обладает свойством навигации ICollection (т.е. один: много).Я хочу загрузить это свойство, чтобы получить доступ к нему после присоединения группы без возврата EF в БД.Я знаю, что Include() не работает, когда вы используете GroupJoin, но следующий код - единственный найденный мной способ заставить его загружать коллекцию (ContactRoomRoles):

using (var context = new MyDbContext()) {
    var foundRooms = context.Rooms.Include(rm => rm.ContactRoomRoles);
    foundRooms.ToList();  // <-- Required to make EF actually load ContactRoomRoles data!

    var roomsData = foundRooms
        .GroupJoin(
            context.Contacts,
            rm => rm.CreatedBy,
            cont => cont.Id,
            (rm, createdBy) => new {
                ContactRoomRoles = rm.ContactRoomRoles,
                Room = rm,
                CreatedBy = createdBy.FirstOrDefault()
            }
        )
        .ToList();

    var numberOfRoles1 = roomsData.ElementAt(1).Room.ContactRoomRoles.Count();
    var numberOfRoles2 = roomsData.ElementAt(2).Room.ContactRoomRoles.Count();
    var numberOfRoles3 = roomsData.ElementAt(3).Room.ContactRoomRoles.Count();
}

Если я удаляю foundRooms.ToList(), EF отправляется в базу данных 3 раза, чтобы заполнить мои numberOfRoles переменные в конце, но с foundRooms.ToList() этого не происходит - он просто загружает данные в одном запросе заранее.

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

ПРИМЕЧАНИЕ. Я хочу использовать свойство навигации, а не проецировать его в новое свойство анонимного типа, поскольку AutoMapper хочет получить доступ к Room.ContactRoomRolesкогда он отображается на объект DTO.

Ответы [ 3 ]

0 голосов
/ 15 октября 2018

Это называется Утечка абстракции , и это означает, что ваша абстракция раскрывает некоторые детали реализации.

Это происходит, когда вы вызываете .ToList() и вы переключаетесь (мне не нравится слово крест) между Linq на sql и Привязка к объектам .

Я бы порекомендовал вам прочитать Закон Утечки Абстракций , чтобы лучше понять, как это довольно сложно объяснитьодна нога.

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


Изменить, чтобы уточнить:

вызов ToList() заставляет linq-to-entity оценивать и возвращать результаты в виде списка.

Это означает, что, например, из ответа выше:

var blogs = context.Blogs
    .FromSql("EXECUTE dbo.GetMostPopularBlogsForUser {0}", user)
    .ToList();

Будет оценка для соответствующей модели контекста - модель блогов.

Другими словами, он лениво выполняется в тот момент, когда вы вызываете ToList().

До вызова ToList() C # НЕ выполняет SQL-вызовов.Так что на самом деле это НЕ операция в памяти.

Так что да, она помещает эти данные в память как часть контекста и считывает их в том же контексте.

0 голосов
/ 15 октября 2018

Это все о коллекциях, которые помечены как загруженные или нет.

Линия

foundRooms.ToList();

(или foundRooms.Load())

загружает все Rooms и их ContactRoomRoles коллекции в контексте.Поскольку используется оператор Include, эти коллекции помечаются как загруженные EF.Вы можете проверить это, посмотрев на

context.Entry(Rooms.Local.First()).Collection(r => r.ContactRoomRoles).IsLoaded

, который должен вернуть true.

Если вы опустите строку foundRooms.ToList();, каждый раз, когда к коллекции Room.ContactRoomRoles обращаются, EF будетобратите внимание, что он еще не помечен как загруженный и будет загружен ленивым.После этого коллекция помечается как загруженная, но потребовался дополнительный запрос.

Коллекция помечается как загруженная только в том случае, если она является -

  • Include -ed
  • загружается с помощью отложенной загрузки
  • загружается с помощью оператора Load(), как в

    context.Entry(Rooms.Local.First()).Collection(r => r.ContactRoomRoles).Load();
    

Не, когда он является частью проекциив другое свойство (например, часть ContactRoomRoles = rm.ContactRoomRole в вашем запросе).

Однако после оператора var roomsData = foundRooms (...).ToList() все Room.ContactRoomRoles заполнены , поскольку запрос действительно загрузил их вв контексте, а EF всегда выполняет процесс исправления отношений , который автоматически заполняет свойства навигации.

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

Зная это, теперь очевидно, что единственное, что нужно сделать, - это предотвратить отложенную загрузку.

Лучший способ добиться этого - предотвратить создание сущности EFобъекты, способные к отложенной загрузке, известные как прокси .Вы делаете это, добавляя строку

context.Configuration.ProxyCreationEnabled = false;

чуть ниже оператора using.

Теперь вы заметите, что строка

var numberOfRoles1 = roomsData.ElementAt(1).Room.ContactRoomRoles.Count();

невызвать дополнительный запрос, но возвращает правильное количество.

0 голосов
/ 15 октября 2018

Это не хак.Это утечка абстракции.Мы должны быть готовы к утечкам абстракции, используя инструменты ORM (и любые другие внутренние DSL).

После ToList() вы не только выполняете фактический вызов sql (и загружаете данные в память), но также переходите к другим разновидностям Linq- «Линк для объектов».После этого все ваши вызовы Count() не генерируют sql только потому, что вы начинаете работать с коллекциями памяти (а не с деревьями выражений , которые скрыты IQueryable - типом возврата оператора GroupBy, но с List collection - типом возврата ToList).

Без ToList() вы останетесь с «Linq for sql», и EF будет переводить каждый вызов Count() на IQuerybale в sql;Три вызова Conut () = три подчеркнутых оператора Sql.

Невозможно избежать этого, иначе вычислить все значения count(*) на стороне сервера в одном сложном запросе.Если вы попытаетесь написать такой запрос с помощью Linq (построение expression tree) - вы снова столкнетесь с утечкой абстракции.Инструмент ORM предназначен для сопоставления объектов «сущностям RDBS», остающимся с операциями CRUD (Create Read Update Delete) - если оператор становится более сложным - вы не сможете предвидеть сгенерированный sql (и все исключения времени выполнения, такие как ', не могут генерировать sqlдля такого linq ').Поэтому не используйте linq для сложных запросов типа «отчет как» (в некоторых случаях вы можете - это зависит от ваших требований повторного использования и возможностей тестирования).Используйте старый добрый SQL и вызывайте его через ADO или EF ADO «sql extensions», такие как EF Core FromSql:

var blogs = context.Blogs
    .FromSql("EXECUTE dbo.GetMostPopularBlogsForUser {0}", user)
    .ToList();

Обновление: хорошая рекомендация также избегать ленивой загрузки и ручной загрузки сущностей, еслиВы не работаете над многоразовыми инструментами EF.В некотором смысле они противоположны запросам linq - деревьям выражений.Это был важный (если не только один) вариант для достижения загрузки ссылочных объектов на «старых» платформах, где не было «деревьев выражений» в языке, но в .NET / EF, где полные запросы можно было записать «декларативным способом» в виде деревьев выражений без выполнения(но с отложенной интерпретацией) должна быть очень веская причина вернуться к «ручной» загрузке.

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