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();
在您的情况下,您可能希望为集合的每个视图创建单独的IncludePages
和GetBooksWithPages
-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。 这就是为什么你应该做两件事之一:
- 定义一个包含
BookPages = new List
的(); Book()
构造函数BookPages = new List
((); Authors
相同)或 - 确保你的代码中只有“
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 Include
和Where
而不是使用静态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上下文的小范围内使用,否则我无法控制数据库命中和结果有多大。