假设我们在数据模型中有四个实体:Categories,Books,Authors和BookPages。还假设Categories-Books,Books-Authors和Books-BookPages关系是一对多的。
如果从数据库中检索类别实体实例 - 包括“Books”,“Books.BookPages”和“Books.Authors” - 这将成为一个严重的性能问题。此外,不包括它们将导致“对象引用未设置为对象的实例”异常。
使用多个Include方法调用的最佳做法是什么?
编辑:通过第二个选项我的意思是这样的:
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");
这种方法有明显的缺点吗?
答案 0 :(得分:8)
编写一个方法GetCategoryById并发送一个包含的关系列表(可能,但似乎还不够优雅)
编写方法,如GetCategoryByIdWithBooks,GetCategoryByIdWithBooksAndBooksPages和GetCategoryByIdWithBooksAndAuthors(不实用)
目前我的方法是这两者的结合。 我知道我想要为每个上下文包含哪些属性,所以我宁愿手工编写它们(就像你自己说的那样,延迟加载并不总是一个选项,如果是,你会当从数据模型映射到DTO时,重复相同的重复Include()
- 类似语法。
这种分离会让您更加思考要公开的数据集,因为数据访问代码通常隐藏在服务之下。
通过使用包含虚方法的基类,您可以覆盖以运行所需的Include()
:
using System.Data.Entity;
public class DataAccessBase<T>
{
// For example redirect this to a DbContext.Set<T>().
public IQueryable<T> DataSet { get; private set; }
public IQueryable<T> Include(Func<IQueryable<T>, IQueryable<T>> 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<T> Include(IQueryable<T> 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<Book>
{
// Overridden to specify Include()s to be run for each book
public override IQueryable<Book> Include(IQueryable<Book> entities)
{
return base.Include(entities)
.Include(e => e.Author);
}
// A separate Include()-method
private IQueryable<Book> IncludePages(IQueryable<Book> entities)
{
return entities.Include(e => e.Pages);
}
// Access this method from the outside to retrieve all pages from each book
public IEnumerable<Book> GetBooksWithPages()
{
var books = Include(IncludePages);
}
}
现在您可以实例化BookAccess
并在其上调用方法:
var bookAccess = new BookAccess();
var allBooksWithoutNavigationProperties = bookAccess.DataSet;
var allBooksWithAuthors = bookAccess.Include();
var allBooksWithAuthorsAndPages = bookAccess.GetBooksWithPages();
在您的情况下,您可能希望为集合的每个视图创建单独的IncludePages
和GetBooksWithPages
- 类似的方法对。或者只是将其写为一种方法,IncludePages
方法存在可重用性。
您可以按照自己喜欢的方式链接这些方法,因为每个方法(以及实体框架的Include()
扩展方法)都返回另一个IQueryable<T>
。
答案 1 :(得分:4)
正如评论中提到的@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<Author> Authors { get; set; }
public virtual List<BookPage> BookPages { get; set; }
}
如果未使用virtual关键字,则EF创建的代理类将无法延迟加载相关实体/实体。
当然,如果您正在创建一个新书,它将无法进行延迟加载,如果您尝试迭代BookPages,则只会抛出NullReferenceException。这就是为什么你应该做以下两件事之一:
Book()
构造函数,其中包含BookPages = new List<BookPage>();
(Authors
相同)或new Book()
”的 ONLY 时间是您创建一个新条目,然后立即保存到数据库然后丢弃而不尝试从中得到任何东西。我个人更喜欢第二种选择,但我知道其他许多人更喜欢第一种选择。
<EDIT>
我找到了第三个选项,即使用Create
类的DbSet<>
方法。这意味着您可以拨打myContext.Books.Create()
而不是new Book()
。有关详细信息,请参阅此Q + A:Ramifications of DbSet.Create versus new Entity() </EDIT>
现在,延迟加载可能会破坏的另一种方式是关闭它。 (我假设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"));
这实际上会导致从数据库返回的记录更少,并且更容易理解。
希望有所帮助! ;)
答案 2 :(得分:1)
某些性能注意事项是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();
}
不幸的是,没有一种最佳做法可以满足所有情况。
答案 3 :(得分:0)
在db第一种方法中,假设您创建BookStore.edmx并添加Category和Book实体并生成类似public partial class BookStoreContext : DbContext
的上下文,那么如果您可以添加这样的部分类,这是一个简单的好习惯:
public partial class BookStoreContext
{
public IQueryable<Category> GetCategoriesWithBooks()
{
return Categories.Include(c => c.Books);
}
public IQueryable<Category> GetCategoriesWith(params string[] includeFields)
{
var categories = Categories.AsQueryable();
foreach (string includeField in includeFields)
{
categories = categories.Include(includeField);
}
return categories;
}
// Just another example
public IQueryable<Category> GetBooksWithAllDetails()
{
return Books
.Include(c => c.Books.Authors)
.Include(c => c.Books.Pages);
}
// yet another complex example
public IQueryable<Category> 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<Category> Categories
扩展到公共Include
和Where
组,而不是使用静态CategoryHelper
。所以你可以IQueryable<Category> db.Categories.WithBooks()
GetCategoryById
中包含所有子实体,因为它不会在方法名称中自我解释,并且如果此方法的用户不是Books
的兄弟,则会导致性能问题。Books
更好的页面加载,就像这样db.Books.Where(b => b.CategoryId = categoryId).Skip(skip).Take(take).ToList()
,或者你更好地添加上面的方法db.GetBooksByCategoryId(categoryId, skip, take)
我自己更喜欢显式加载实体,因为我会“知道”当前加载的内容但延迟加载仅在条件加载子实体时才有用,并且应该在db上下文的小范围内使用,否则我无法控制数据库命中率和结果有多大。