2013-05-22 3 views
7

Я использую Code First в EF. Скажем, у меня есть две сущности:Фильтрация навигационных свойств в EF Code First

public class Farm 
{ 
    .... 
    public virtual ICollection<Fruit> Fruits {get; set;} 
} 

public class Fruit 
{ 
    ... 

} 

Мои DbContext что-то вроде этого:

public class MyDbContext : DbSet 
{ 
    .... 
    private DbSet<Farm> FarmSet{get; set;} 

    public IQueryable<Farm> Farms 
    { 
     get 
     { 
      return (from farm in FarmSet where farm.owner == myowner select farm); 
     } 
    } 
} 

Я делаю это так, что каждый пользователь может видеть только свои фермы, и я не должен называть Где по каждому запросу в db.

Теперь я хочу, чтобы фильтровать все фрукты из одной фермы, я попробовал это (в классе Farm):

from fruit in Fruits where fruit .... select fruit 

но запрос генерируется не включает в себя положение, где, что очень важно, потому что У меня есть десятки тысяч строк и неэффективно загружать их все и фильтровать, когда они являются объектами.

Я читал, что ленивые загруженные свойства заполняются в первый раз они обращались, но они читают ВСЕ данные, никакие фильтры не могут быть применены, если вы делаете что-то вроде этого:

from fruits in db.Fruits where fruit .... select fruit 

Но я не могу сделайте это, потому что Farm не знает DbContext (я не думаю, что это должно (?)), но и для меня он просто теряет всю цель использования свойств навигации, если мне нужно работать со всеми данными, а не с одним что принадлежит моей ферме.

Так,

  1. я делаю что-то неправильно/что делает неправильные предположения?
  2. Есть ли способ применить фильтр к свойству навигации, которое генерируется для реального запроса? (Я работаю с большим количеством данных)

Благодарим за понимание!

ответ

8

К сожалению, я думаю, что любой подход, который вы могли бы предпринять, должен был включать в себя борьбу с контекстом, а не только с сущностью. Как вы видели, вы не можете фильтровать свойство навигации напрямую, так как это ICollection<T>, а не IQueryable<T>, поэтому он загружается сразу, прежде чем у вас будет возможность применить любые фильтры.

Одна вещь, которую вы могли бы сделать, это создать неотображённые свойство в вашем Farm лица провести отфильтрованный список фруктов:

public class Farm 
{ 
    .... 
    public virtual ICollection<Fruit> Fruits { get; set; } 

    [NotMapped] 
    public IList<Fruit> FilteredFruits { get; set; } 
} 

И затем, в вашем контексте/хранилище, добавить метод для загрузки Farm организация и заполнить FilteredFruits с данными вы хотите:

public class MyDbContext : DbContext 
{ 
    ....  

    public Farm LoadFarmById(int id) 
    { 
    Farm farm = this.Farms.Where(f => f.Id == id).Single(); // or whatever 

    farm.FilteredFruits = this.Entry(farm) 
           .Collection(f => f.Fruits) 
           .Query() 
           .Where(....) 
           .ToList(); 

    return farm; 
    } 
} 

... 

var myFarm = myContext.LoadFarmById(1234); 

Это должно заполнить myFarm.FilteredFruits только отфильтрованную коллекцию, так что вы можете использовать его так, как вы хотите в вашей организации. Тем не менее, я никогда не пробовал этот подход сам, поэтому могут быть подводные камни, о которых я не думаю. Одним из главных недостатков является то, что он будет работать только с Farm с вашей загрузкой с использованием этого метода, а не с любыми общими запросами LINQ, которые вы выполняете в наборе данных MyDbContext.Farms.

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

Очень распространенный подход, если вы решите перенести вещи Farm сущность заключается в использовании проекции:

var results = (from farm in myContext.Farms 
       where .... 
       select new { 
       Farm = farm, 
       FilteredFruits = myContext.Fruits.Where(f => f.FarmId == farm.Id && ...).ToList() 
       }).ToList(); 

... а затем использовать сгенерированные анонимные объекты для все, что вы хотите сделать, а не пытаться для добавления дополнительных данных в объекты Farm.

+0

Спасибо Джереми, я решил следовать вашему совету и оставить обязанности фильтрации/обработки в моем контексте класса , Это имеет смысл, потому что мне нужна только фильтрация для одного из моих сущностей, но это было бы громоздко, если бы мне было нужно это для нескольких объектов, не так ли? Контекст будет заполнен методами запросов и заполнения объектов. Я не думаю, что это нарушает принцип единой ответственности, но это звучит довольно странно, не так ли? – edpaez

1

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

public class FruitFarmContext : DbContext 
{ 
    public DbSet<Farm> Farms { get; set; } 

    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
     modelBuilder.Entity<Farm>().HasMany(Farm.FruitsExpression).WithMany(); 
    } 
} 

public class Farm 
{ 
    public int Id { get; set; } 
    protected virtual ICollection<Fruit> Fruits { get; set; } 
    public static Expression<Func<Farm, ICollection<Fruit>>> FruitsExpression = x => x.Fruits; 

    public IEnumerable<Fruit> FilteredFruits 
    { 
     get 
     { 
      //Apply any filter you want here on the fruits collection 
      return Fruits.Where(x => true); 
     } 
    } 
} 

public class Fruit 
{ 
    public int Id { get; set; } 

} 

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

+0

Есть ли какие-либо последствия для этого решения? – Oswin

2

Lazy loading не поддерживает фильтрацию; использовать filtered explicit loading вместо:

Farm farm = dbContext.Farms.Where(farm => farm.Owner == someOwner).Single(); 

dbContext.Entry(farm).Collection(farm => farm.Fruits).Query() 
    .Where(fruit => fruit.IsRipe).Load(); 

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

Farm farm = (
    from farm in dbContext.Farms 
    where farm.Owner == someOwner 
    select new { 
     Farm = farm, 
     Fruit = dbContext.Fruit.Where(fruit => fruit.IsRipe) // Causes Farm.Fruit to be eager loaded 
    }).Single().Farm; 

EF всегда связывает навигационные свойства их нагруженных лиц. Это означает, что farm.Fruit будет содержать те же фильтрованные коллекции, что и свойство Fruit в анонимном типе. (Просто убедитесь, что вы не загрузили в контекст любые объекты Fruit, которые должны быть отфильтрованы, как описано в Use Projections and a Repository to Fake a Filtered Eager Load.)