15

假设我们在数据模型中有四个实体:类别、书籍、作者和 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

4 回答 4

8

编写单个方法 GetCategoryById 并发送要包含的关系列表(也许,但似乎仍然不够优雅)

编写 GetCategoryByIdWithBooks、GetCategoryByIdWithBooksAndBooksPages 和 GetCategoryByIdWithBooksAndAuthors 等方法(不实用)

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

这种分离使您更加仔细地考虑要公开哪些数据集,因为这样的数据访问代码通常隐藏在服务之下。

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

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);
    }
}

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

var bookAccess = new BookAccess();

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

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

您可以按照自己喜欢的方式链接这些方法,因为它们中的每一个(以及实体框架的Include()扩展方法)都会返回另一个IQueryable<T>.

于 2013-07-08T16:15:51.873 回答
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()”的时间是当您创建一个新条目时,您会立即将其保存到数据库中,然后丢弃而不尝试从中获取任何内容。

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

<EDIT>我找到了第三种选择,就是使用类的Create方法DbSet<>。这意味着您将调用myContext.Books.Create()而不是new Book(). 有关更多信息,请参阅此 Q+A:DbSet.Create 与 new Entity() 的影响 </EDIT>

现在,延迟加载可以破坏的另一种方式是关闭它时。(我假设这ModelEntities是您DbContext班级的名称。)要关闭它,您可以设置ModelEntities.Configuration.LazyLoadingEnabled = false;Pretty self explanatory,不是吗?

底线是您不需要在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"));

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

希望有帮助!;)

于 2013-07-12T22:30:52.107 回答
1

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

首先,请注意DbContext(and 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();
}

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

于 2013-07-08T15:55:01.293 回答
0

在 db first 方法中,假设您创建 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而不是使用 static CategoryHelper。所以你可以拥有IQueryable<Category> db.Categories.WithBooks()
  • 您不应包含所有子实体,GetCategoryById因为它不会在方法名称中自我解释,如果此方法的用户不是实体的兄弟,则会导致性能问题Books
  • 即使你没有包括所有,如果你使用延迟加载,你仍然可能有潜在的N+1 性能问题
  • 如果您有 1000 个Books更好的页面,您可以将负载分页类似这样db.Books.Where(b => b.CategoryId = categoryId).Skip(skip).Take(take).ToList(),或者甚至更好,您可以将上面的方法添加为这样db.GetBooksByCategoryId(categoryId, skip, take)

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

于 2013-07-18T07:23:24.773 回答