实体框架中多个“包含”的最佳实践是什么?

时间:2013-07-08 14:55:27

标签: c# entity-framework architecture data-access-layer ef-database-first

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

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

使用多个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");

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

4 个答案:

答案 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();

在您的情况下,您可能希望为集合的每个视图创建单独的IncludePagesGetBooksWithPages - 类似的方法对。或者只是将其写为一种方法,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。这就是为什么你应该做以下两件事之一:

  1. 定义Book()构造函数,其中包含BookPages = new List<BookPage>();Authors相同)或
  2. 确保您的代码中只有“new Book()”的 ONLY 时间是您创建一个新条目,然后立即保存到数据库然后丢弃而不尝试从中得到任何东西。
  3. 我个人更喜欢第二种选择,但我知道其他许多人更喜欢第一种选择。

    <EDIT>我找到了第三个选项,即使用Create类的DbSet<>方法。这意味着您可以拨打myContext.Books.Create()而不是new Book()。有关详细信息,请参阅此Q + A:Ramifications of DbSet.Create versus new Entity() </EDIT>

    现在,延迟加载可能会破坏的另一种方式是关闭它。 (我假设ModelEntitiesDbContext类的名称。)要关闭它,你会设置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扩展到公共IncludeWhere组,而不是使用静态CategoryHelper 。所以你可以IQueryable<Category> db.Categories.WithBooks()
  • 您不应在GetCategoryById中包含所有子实体,因为它不会在方法名称中自我解释,并且如果此方法的用户不是Books的兄弟,则会导致性能问题。
  • 即使你不包括所有内容,如果你使用延迟加载,你仍然可能有N+1 performance issue
  • 如果你有1000 Books更好的页面加载,就像这样db.Books.Where(b => b.CategoryId = categoryId).Skip(skip).Take(take).ToList(),或者你更好地添加上面的方法db.GetBooksByCategoryId(categoryId, skip, take)

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