entity framework中多个“包含”的最佳实践是什么?

假设我们在数据模型中有四个实体:Categories,Books,Authors和BookPages。 还假设Categories-Books,Books-Authors和Books-BookPages关系是一对多的。

如果从数据库中检索类别实体实例 – 包括“Books”,“Books.BookPages”和“Books.Authors” – 这将成为一个严重的性能问题。 而且,不包括它们将导致“对象引用未设置为对象的实例”exception。

使用多个Include方法调用的最佳做法是什么?

  • 写一个方法GetCategoryById并包含所有项目(性能问题)
  • 写一个方法GetCategoryById并发送一个包含的关系列表(可能,但似乎还不够优雅)
  • 编写方法,如GetCategoryByIdWithBooks,GetCategoryByIdWithBooksAndBooksPages和GetCategoryByIdWithBooksAndAuthors(不实用)

编辑 :通过第二个选项我的意思是这样的:

public static Category GetCategoryById(ModelEntities db, int categoryId, params string[] includeFields) { var categories = db.Categories; foreach (string includeField in includeFields) { categories = categories.Include(includeField); } return categories.SingleOrDefault(i => i.CategoryId == categoryId); } 

在调用时我们需要这样的代码:

 Category theCategory1 = CategoryHelper.GetCategoryById(db, 5, "Books"); Category theCategory2 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Pages"); Category theCategory3 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Authors"); Category theCategory4 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Pages", "Books.Authors"); 

这种方法有任何明显的缺点吗?

写一个方法GetCategoryById并发送一个包含的关系列表(可能,但似乎还不够优雅)

编写方法,如GetCategoryByIdWithBooks,GetCategoryByIdWithBooksAndBooksPages和GetCategoryByIdWithBooksAndAuthors(不实用)

目前我的方法是这两者的结合。 知道我想要为每个上下文包含哪些属性,所以我宁愿手工编写它们(正如你自己说的那样,延迟加载并不总是一个选项,如果是,你将重复相同的重复Include()从数据模型映射到DTO时的类似语法。

这种分离会让您更加思考要公开的数据集 ,因为数据访问代码通常隐藏在服务之下。

通过使用包含虚方法的基类,您可以覆盖以运行所需的Include()

 using System.Data.Entity; public class DataAccessBase { // For example redirect this to a DbContext.Set(). public IQueryable DataSet { get; private set; } public IQueryable Include(Func, IQueryable> include = null) { if (include == null) { // If omitted, apply the default Include() method // (will call overridden Include() when it exists) include = Include; } return include(DataSet); } public virtual IQueryable Include(IQueryable entities) { // provide optional entities.Include(f => f.Foo) that must be included for all entities return entities; } } 

然后,您可以按原样实例化和使用此类,或者扩展它:

 using System.Data.Entity; public class BookAccess : DataAccessBase { // Overridden to specify Include()s to be run for each book public override IQueryable Include(IQueryable entities) { return base.Include(entities) .Include(e => e.Author); } // A separate Include()-method private IQueryable IncludePages(IQueryable entities) { return entities.Include(e => e.Pages); } // Access this method from the outside to retrieve all pages from each book public IEnumerable GetBooksWithPages() { var books = Include(IncludePages); } } 

现在,您可以实例化BookAccess并在其上调用方法:

 var bookAccess = new BookAccess(); var allBooksWithoutNavigationProperties = bookAccess.DataSet; var allBooksWithAuthors = bookAccess.Include(); var allBooksWithAuthorsAndPages = bookAccess.GetBooksWithPages(); 

在您的情况下,您可能希望为集合的每个视图创建单独的IncludePagesGetBooksWithPages -alike方法对。 或者只是将其作为一种方法编写, IncludePages方法存在可重用性。

您可以按照自己喜欢的方式链接这些方法,因为每个方法(以及Entity Framework的Include()扩展方法)都返回另一个IQueryable

正如@Colin在评论中提到的那样,您需要在定义导航属性时使用virtual关键字,以便它们能够使用延迟加载。 假设您使用的是Code-First,您的Book类看起来应该是这样的:

 public class Book { public int BookID { get; set; } //Whatever other information about the Book... public virtual Category Category { get; set; } public virtual List Authors { get; set; } public virtual List BookPages { get; set; } } 

如果未使用virtual关键字,则EF创建的代理类将无法延迟加载相关实体/实体。

当然,如果您正在创建一个新书,它将无法进行延迟加载,如果您尝试迭代BookPages,则只会抛出NullReferenceException。 这就是为什么你应该做两件事之一:

  1. 定义一个包含BookPages = new List();Book()构造函数BookPages = new List();Authors相同)或
  2. 确保你的代码中只有“ new Book() ”的时间是你创建一个新的条目,你立即保存到数据库然后丢弃而不试图从中获取任何东西。

我个人更喜欢第二种选择,但我知道其他许多人更喜欢第一种选择。

我找到了第三个选项,即使用DbSet<>类的Create方法。 这意味着你可以调用myContext.Books.Create()而不是new Book() 。 有关详细信息,请参阅此Q + A: DbSet.Create与新实体的分歧()

现在,延迟加载可能会破坏的另一种方式是关闭它。 (我假设ModelEntities是您的DbContext类的名称。)要关闭它,您可以设置ModelEntities.Configuration.LazyLoadingEnabled = false; 很自我解释,不是吗?

最重要的是,您不应该在任何地方使用Include() 。 它实际上意味着更多的是优化手段而不是代码运行的要求。 使用Include()过度导致性能非常差,因为您最终会从数据库中获得远远超过您真正需要的内容,因为Include()将始终引入所有相关记录。 假设您正在加载一个类别,并且有1000个属于该类别的图书。 在使用Include()函数时,您无法将其过滤为仅包括获取John Smith编写的书籍。 但是,您可以(启用延迟加载)执行以下操作:

 Category cat = ModelEntities.Categorys.Find(1); var books = cat.Books.Where(b => b.Authors.Any(a => a.Name == "John Smith")); 

这实际上会导致从数据库返回的记录更少,并且更容易理解。

希望有所帮助! ;)

某些性能注意事项是ADO.Net连接器特定的。 如果您没有获得所需的性能,我会记住数据库视图或存储过程作为备份。

首先,请注意DbContext (和ObjectContext )对象不是线程安全的。

如果你担心对性能的苛刻,那么第一个选项是最简单的。

另一方面,如果您担心性能 – 并且在获取数据后愿意处置上下文对象 – 那么您可以使用多个同时任务(线程)使用自己的上下文对象查询数据。

如果需要上下文来跟踪数据的更改,您可以使用单个查询的直接方式将所有项添加到上下文中,或者可以使用Attach方法“重建”原始状态,然后更改和保存。

后者类似于:

 using(var dbContext = new DbContext()) { var categoryToChange = new Categories() { // set properties to original data }; dbContext.Categories.Attach(categoryToChange); // set changed properties dbContext.SaveChanges(); } 

不幸的是,没有一种最佳实践可以满足所有情况。

在db第一种方法中,假设你创建了BookStore.edmx并添加了Category和Book实体,并且它生成了像public partial class BookStoreContext : DbContext这样的上下文,那么如果你可以像这样添加部分类,这是一个简单的好习惯:

 public partial class BookStoreContext { public IQueryable GetCategoriesWithBooks() { return Categories.Include(c => c.Books); } public IQueryable GetCategoriesWith(params string[] includeFields) { var categories = Categories.AsQueryable(); foreach (string includeField in includeFields) { categories = categories.Include(includeField); } return categories; } // Just another example public IQueryable GetBooksWithAllDetails() { return Books .Include(c => c.Books.Authors) .Include(c => c.Books.Pages); } // yet another complex example public IQueryable GetNewBooks(/*...*/) { // probably you can pass sort by, tags filter etc in the parameter. } } 

然后你可以像这样使用它:

 var category1 = db.CategoriesWithBooks() .Where(c => c.Id = 5).SingleOrDefault(); var category2 = db.CategoriesWith("Books.Pages", "Books.Authors") .Where(c => c.Id = 5).SingleOrDefault(); // custom include 

注意:

  • 你可以阅读一些简单的(那里有那么多复杂的)存储库模式只是为了将IDbSet Categories扩展到组通用IncludeWhere而不是使用静态CategoryHelper 。 所以你可以拥有IQueryable db.Categories.WithBooks()
  • 您不应该在GetCategoryById包含所有子实体,因为它不会在方法名称中自我解释,如果此方法的用户不是关于Books entites的兄弟,则会导致性能问题。
  • 即使您不包括所有内容,如果您使用延迟加载,您仍然可能存在潜在的N + 1性能问题
  • 如果你有1000 Books更好,你可以像你这样的db.Books.Where(b => b.CategoryId = categoryId).Skip(skip).Take(take).ToList()甚至更好的你添加方法以上就像这样db.GetBooksByCategoryId(categoryId, skip, take)

我自己更喜欢显式加载实体,因为我会“知道”当前加载的内容但延迟加载仅在条件加载子实体时才有用,并且应该在db上下文的小范围内使用,否则我无法控制数据库命中和结果有多大。