Entity Framework: проблема со свойствами навигации

Я работаю с кодом Entity Framework в первую очередь, и у меня есть класс Course, который имеет свойство навигации Students:

public virtual Collection<Student> Students { get; set;}

Это работает нормально, но когда я обращаюсь к этому свойству навигации, все данные извлекаются из базы данных:

var allStudents = course.Students; // Here it retrieves the data
var activeStudents = allStudents.Where(n => n.Active); // Here it filter the data on memory
var listOfActiveStudents = activeStudents.ToList(); // It already has the data on memory.

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

Вы знаете, что я делаю неправильно?


person ascherman    schedule 03.12.2013    source источник


Ответы (4)


Ленивая загрузка загружает весь набор в память. Если вы этого не хотите, отключите ленивую загрузку, удалив ключевое слово virtual, и используйте объект Query для DbEntry:

public GetCourseWithActiveStudentsLoaded(int courseid)
{
   var course= context.Courses.Find(courseid); 

   context.Entry(course)
          .Collection(c => c.Students)
          .Query()
          .Where(s => s.Active)
          .Load();

   return user
}

Является ли флаг «Активно» индикатором того, что вы пытаетесь реализовать обратимое удаление? Если это так, здесь есть решение: Soft Delete в Entity Framework

Вы можете проголосовать за отфильтрованные включения здесь: Разрешить фильтрацию для метода расширения Include

Другой способ сделать это - наследование. У вас может быть свойство ActiveStudent, наследуемое от Student, и свойство навигации ActiveStudents, а также свойство навигации AllStudents в классе Course.

public virtual Collection<Student> AllStudents { get; set;}
public virtual Collection<ActiveStudent> ActiveStudents { get; set;}

Ссылка:

Применение фильтров при явной загрузке связанных объектов:

person Colin    schedule 03.12.2013
comment
что это за тип user.Students, у которого есть метод .Query()? - person Moho; 03.12.2013
comment
Активное поле является примером. Приходится применять более сложные фильтры. Я попытался удалить виртуальный файл, и он не запускает запрос... но ничего не приносит, когда выполняется ToList() (он не выполняет никакого запроса) - person ascherman; 03.12.2013
comment
@ Мохо а. Мой плохой ;-( Я здесь не на том уровне. Мне нужен DbCollectionEntry. - person Colin; 03.12.2013
comment
@ArielScherman, когда вы отключаете ленивую загрузку, вы должны явно загружать. Посмотрите, как быстро загрузить ссылку, которую я включил в свой ответ. - person Colin; 03.12.2013
comment
Чтобы не загружать все элементы, вы можете использовать метод .Query, как описано в ответе @Moho, моем отредактированном ответе и ссылке, которую я указал в своем ответе. - person Colin; 04.12.2013
comment
Спасибо @Colin, это здорово! Но знаете ли вы, как я могу сделать это для нескольких коллекций? Вот вопрос: stackoverflow.com/questions/20524311/ - person ascherman; 11.12.2013

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

//user is an instance of the class User referenced by DbSet<User>
//when you lazy load a navigation property in that set, it loads the data
ICollection<Student> allStudents = user.Students;

//At this point, all of the data was lazy loaded
//But the Where creates an IEnumerable of the in memory set
IEnumerable<Student> activeStudents = allStudents.Where(n => n.Active);

//At this point, the IEnumerable is iterated, and a List is returned
List<Student> listOfActiveStudents = activeStudents.ToList();
person Travis J    schedule 03.12.2013
comment
Спасибо, я знаю, что... проблема в том, что Студенты извлекаются в первой строке при доступе к свойству навигации. Как я могу получить только активных студентов? - person ascherman; 03.12.2013
comment
Я имею в виду... запрос выполняется в первой строке, а затем применяет фильтр к памяти. Но я хочу, чтобы запрос выполнялся, когда я делаю ToList() - person ascherman; 03.12.2013
comment
@ArielScherman - для этого вам нужен IQueryable. Вы должны выполнить запрос к таблице студентов, используя отношение внешнего ключа. Что-то вроде db.Students.Where( s => s.Active && s.UserId == user.UserId ). Это даст вам IQueryable, способный воспользоваться преимуществом отложенного выполнения, которое вы ищете. - person Travis J; 03.12.2013
comment
@Ariel - Тогда я бы посоветовал пойти с предложением Мохо и использовать: db.Entry(user).Collection( u => u.Students ).Query().Where( s => s.Active ).ToList();. Это загрузит только активных учеников пользователя. .Collection возвращает DbCollectionEntry, а Query возвращает IQueryable ни один из них не загрузит набор в память. Использование Where также вернет здесь IQueryable, а последний вызов ToList — это место, где набор загружается в память. - person Travis J; 03.12.2013

используйте dbContext.Entry( user ).Collection( u => u.Students ).Query(), чтобы получить IQueryable<Student> для свойства навигации коллекции студентов, после чего вы можете добавить свой фильтр и перечислить, когда будете готовы к данным

person Moho    schedule 03.12.2013
comment
Итак... нет решения для свойств навигации? Как это возможно? Я работаю с шаблоном репозитория, так что делать то, что вы говорите, — заноза в заднице. - person ascherman; 03.12.2013
comment
Отложенная загрузка свойств навигации — это операция «все или ничего», которая по сути dbContext.Entry( user ).Collection( u => u.Students ).Load(). Используя .Query(), вы можете фильтровать результаты перед их загрузкой из БД. Я думаю, что шаблон репо сделает это менее громоздким, поскольку вы уже абстрагируете загрузку объектов от разработчика. - person Moho; 03.12.2013

Один из способов обхода этого — перевернуть ваш запрос, хотя это означает отказ от использования свойств навигации в целом. (Фактическая реализация зависит от вашей модели.)

var allStudents =
   context
   .Students
   .Where(s => s.CourseID == course.ID) // depends on your model
   .Where(s => s.Active)
   .ToList();

Я предпочитаю это использованию метода Entry, так как я использую общий интерфейс со своими моделями и не хочу раскрывать типы EF6.

Другой способ избежать раскрытия типов EF6 — написать такой метод:

public IQueryable<TChild> Nav<TParent, TChild>(
   TParent pParent,
   Expression<Func<TParent, ICollection<TChild>>> pNavigationExpression
) where TParent : class
where TChild : class =>
   Entry(pParent)
   .Collection(pNavigationExpression)
   .Query();

Использовал что-то вроде этого:

var allStudents =
   context
   .Nav(course, c => c.Students)
   .Where(s => s.Active)
   .ToList()
person Dave Cousineau    schedule 01.10.2019