Entity Framework6、预加载嵌套关系returns空集合
Entity Framework 6, eager loading nested relationship returns empty collection
我有一个看起来像这样的领域模型(去掉了一些不重要的属性)
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public virtual int UserId { get; set; }
private ICollection<Recipe> _recipes;
public virtual ICollection<Recipe> Recipes
{
get { return _recipes ?? new List<Recipe>(); }
set { _recipes = value; }
}
}
public class Recipe
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int RecipeId { get; set; }
private ICollection<Ingredient> _ingredients;
public virtual ICollection<Ingredient> Ingredients
{
get { return _ingredients ?? new List<Ingredient>(); }
set { _ingredients = value; }
}
public User User { get; set; }
}
public class Ingredient
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int IngredientId { get; set; }
public Recipe Recipe { get; set; }
}
所以简而言之,User
与 Recipe
具有一对多关系,而 Recipe
又与 Ingredient
具有一对多关系。我正在使用代码优先方法,在 protected override void OnModelCreating(DbModelBuilder modelBuilder)
中使用此映射:
modelBuilder.Entity<User>()
.HasMany(u => u.Recipes)
.WithRequired(u => u.User)
.WillCascadeOnDelete(true);
modelBuilder.Entity<Recipe>()
.HasMany(u => u.Ingredients)
.WithRequired(u => u.Recipe)
.WillCascadeOnDelete(true);
我正在使用存储库,它具有将数据存储到 MySQL 数据库中的代码:
public void Add(User entity)
{
using (var context = new UserContext())
{
context.Users.Add(entity);
context.SaveChanges();
}
}
并获取:
public User GetById(int id)
{
using (var context = new UserContext())
{
var user = context.Users
.Include(u => u.Recipes)
.Include(u => u.Recipes.Select(x => x.Ingredients))
.FirstOrDefault(u => u.UserId == id);
return user;
}
}
我有集成测试,它创建了一个 User
对象,List<Recipe>
有两个食谱,每个食谱有 List<Ingredient>
和 2 个项目。每个食谱都不同,所以总共有 4 种成分。当我将它添加到存储库时,我可以在数据库中看到它具有正确的 PK 和 FK。
然而,当从数据库中检索时,返回的 User
对象有食谱,但这些食谱中都没有成分。由于我设置吸气剂的方式,当我尝试访问它们时,我收到一个空集合。
例如这样做:
/* create recipes collection and seed it */
User user = new User {Recipes = recipes /* plus some omitted properites*/};
_repository.Add(user);
var returnedUser = _repository.GetById(user.UserId);
var userIngredients = user.Recipes.First(u => u.RecipeName == "Test").Ingredients.ToList();
var returnedIngredients = returnedUser.Recipes.First(u => u.RecipeName == "Test").Ingredients.ToList();
returnedIngredients
是空的,而 userIngredients
有两个元素。这将导致断言失败并导致集成测试失败。
有人能告诉我如何正确地对嵌套的一对多进行预加载吗?
几个想法:
public class Ingredient
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int IngredientId { get; set; }
public virtual Recipe Recipe { get; set; } // <-- make this virtual
}
...和...
public class Recipe
{
public Recipe()
{
// set up a default collection during construction
Ingredients = new List<Ingredient>();
// could also be Ingredients = new Collection<Ingredient>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int RecipeId { get; set; }
// private ICollection<Ingredient> _ingredients; <-- don't back
public virtual ICollection<Ingredient> Ingredients
{
get; // <-- should never return null because of constructor
protected set; // and because externals cannot set to null
}
public virtual User User { get; set; } // <-- make this virtual too
}
可能还值得向依赖实体(例如 Ingredient 中的 RecipeId)添加一些外键属性并更新 modelBuilder 定义。添加 fk 属性 后类似这样的内容:
modelBuilder.Entity<Recipe>()
.HasMany(u => u.Ingredients)
.WithRequired(u => u.Recipe)
.HasForeignKey(u => u.RecipeId) // <-- change is here
.WillCascadeOnDelete(true);
这个:
get { return _recipes ?? new List<Recipe>(); }
应该读作:
get { return _recipes ?? (_recipes = new List<Recipe>()); }
成分相同。
否则你每次都会返回一个新的空列表(因为它没有存储在任何地方)。这仅在整个列表在某个点设置(从数据库读取并覆盖)时才有效,但如果您通过代码创建实体则无效,因为它们将被添加到未存储在支持字段中的列表中。
您看到 Recipes
已满,因为您在此处设置它:User user = new User {Recipes = recipes /* plus some omitted properites*/};
,因此它存储在支持字段中。
但是成分不是,当你添加它们时:
var recipe = new Recipe();
recipe.Ingredients.Add(myIngredient); // <-- this will be stored in a
// new List<Ingredient>,
// but not on the
// _ingredients backing field
// since the get accesor doesn't
// set it
recipe.Ingredients.Add(myIngredient); // <-- this will be stored in a
// new List<Ingredient>, not in
// the expected created one
至于预加载,你不需要这个:
var user = context.Users
.Include(u => u.Recipes)
.Include(u => u.Recipes.Select(x => x.Ingredients))
.FirstOrDefault(u => u.UserId == id);
只要:
就可以了
var user = context.Users
.Include(u => u.Recipes.Select(x => x.Ingredients))
.FirstOrDefault(u => u.UserId == id);
它将加载两个级别
我有一个看起来像这样的领域模型(去掉了一些不重要的属性)
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public virtual int UserId { get; set; }
private ICollection<Recipe> _recipes;
public virtual ICollection<Recipe> Recipes
{
get { return _recipes ?? new List<Recipe>(); }
set { _recipes = value; }
}
}
public class Recipe
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int RecipeId { get; set; }
private ICollection<Ingredient> _ingredients;
public virtual ICollection<Ingredient> Ingredients
{
get { return _ingredients ?? new List<Ingredient>(); }
set { _ingredients = value; }
}
public User User { get; set; }
}
public class Ingredient
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int IngredientId { get; set; }
public Recipe Recipe { get; set; }
}
所以简而言之,User
与 Recipe
具有一对多关系,而 Recipe
又与 Ingredient
具有一对多关系。我正在使用代码优先方法,在 protected override void OnModelCreating(DbModelBuilder modelBuilder)
中使用此映射:
modelBuilder.Entity<User>()
.HasMany(u => u.Recipes)
.WithRequired(u => u.User)
.WillCascadeOnDelete(true);
modelBuilder.Entity<Recipe>()
.HasMany(u => u.Ingredients)
.WithRequired(u => u.Recipe)
.WillCascadeOnDelete(true);
我正在使用存储库,它具有将数据存储到 MySQL 数据库中的代码:
public void Add(User entity)
{
using (var context = new UserContext())
{
context.Users.Add(entity);
context.SaveChanges();
}
}
并获取:
public User GetById(int id)
{
using (var context = new UserContext())
{
var user = context.Users
.Include(u => u.Recipes)
.Include(u => u.Recipes.Select(x => x.Ingredients))
.FirstOrDefault(u => u.UserId == id);
return user;
}
}
我有集成测试,它创建了一个 User
对象,List<Recipe>
有两个食谱,每个食谱有 List<Ingredient>
和 2 个项目。每个食谱都不同,所以总共有 4 种成分。当我将它添加到存储库时,我可以在数据库中看到它具有正确的 PK 和 FK。
然而,当从数据库中检索时,返回的 User
对象有食谱,但这些食谱中都没有成分。由于我设置吸气剂的方式,当我尝试访问它们时,我收到一个空集合。
例如这样做:
/* create recipes collection and seed it */
User user = new User {Recipes = recipes /* plus some omitted properites*/};
_repository.Add(user);
var returnedUser = _repository.GetById(user.UserId);
var userIngredients = user.Recipes.First(u => u.RecipeName == "Test").Ingredients.ToList();
var returnedIngredients = returnedUser.Recipes.First(u => u.RecipeName == "Test").Ingredients.ToList();
returnedIngredients
是空的,而 userIngredients
有两个元素。这将导致断言失败并导致集成测试失败。
有人能告诉我如何正确地对嵌套的一对多进行预加载吗?
几个想法:
public class Ingredient
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int IngredientId { get; set; }
public virtual Recipe Recipe { get; set; } // <-- make this virtual
}
...和...
public class Recipe
{
public Recipe()
{
// set up a default collection during construction
Ingredients = new List<Ingredient>();
// could also be Ingredients = new Collection<Ingredient>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int RecipeId { get; set; }
// private ICollection<Ingredient> _ingredients; <-- don't back
public virtual ICollection<Ingredient> Ingredients
{
get; // <-- should never return null because of constructor
protected set; // and because externals cannot set to null
}
public virtual User User { get; set; } // <-- make this virtual too
}
可能还值得向依赖实体(例如 Ingredient 中的 RecipeId)添加一些外键属性并更新 modelBuilder 定义。添加 fk 属性 后类似这样的内容:
modelBuilder.Entity<Recipe>()
.HasMany(u => u.Ingredients)
.WithRequired(u => u.Recipe)
.HasForeignKey(u => u.RecipeId) // <-- change is here
.WillCascadeOnDelete(true);
这个:
get { return _recipes ?? new List<Recipe>(); }
应该读作:
get { return _recipes ?? (_recipes = new List<Recipe>()); }
成分相同。
否则你每次都会返回一个新的空列表(因为它没有存储在任何地方)。这仅在整个列表在某个点设置(从数据库读取并覆盖)时才有效,但如果您通过代码创建实体则无效,因为它们将被添加到未存储在支持字段中的列表中。
您看到 Recipes
已满,因为您在此处设置它:User user = new User {Recipes = recipes /* plus some omitted properites*/};
,因此它存储在支持字段中。
但是成分不是,当你添加它们时:
var recipe = new Recipe();
recipe.Ingredients.Add(myIngredient); // <-- this will be stored in a
// new List<Ingredient>,
// but not on the
// _ingredients backing field
// since the get accesor doesn't
// set it
recipe.Ingredients.Add(myIngredient); // <-- this will be stored in a
// new List<Ingredient>, not in
// the expected created one
至于预加载,你不需要这个:
var user = context.Users
.Include(u => u.Recipes)
.Include(u => u.Recipes.Select(x => x.Ingredients))
.FirstOrDefault(u => u.UserId == id);
只要:
就可以了 var user = context.Users
.Include(u => u.Recipes.Select(x => x.Ingredients))
.FirstOrDefault(u => u.UserId == id);
它将加载两个级别