【问题标题】:What is the best practice for multiple "Include"-s in Entity Framework?实体框架中多个“包含”的最佳实践是什么?
【发布时间】:2013-07-05 23:19:27
【问题描述】:

假设我们在数据模型中有四个实体: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");

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

【问题讨论】:

  • 您能否详细说明为什么会出现这种异常?延迟加载对您不起作用吗?
  • 如您所知,当他不使用 Include 方法时会出现此异常。延迟加载当然可以,但是使用 Include 的目的是什么? Entity Framework 可能默认包含所有关系,并且由于延迟加载,相关实体在调用时会被加载。在这种情况下,使用 Include 没有任何意义。我可能遗漏了什么,我很好奇那是什么。
  • 你不应该得到一个空引用。你用过 virtual 关键字吗?您还应该在构造函数中将导航集合设置为空列表。 Include 的目的是告诉实体框架您想要预先加载实体而不是延迟加载。启用延迟加载时,EF 默认不包含所有引用
  • @Colin 我应该在哪里使用 virtual 关键字?您写了“启用延迟加载时...”。我们可以启用/禁用此功能吗?如果可以,如何启用?
  • 你可以从这里开始:msdn.microsoft.com/en-us/data/jj574232.aspx

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


【解决方案1】:

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

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

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

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

通过使用包含虚拟方法的基类,您可以重写以运行所需的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);
    }
}

现在您可以实例化 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&lt;T&gt;。

【讨论】:

  • 聪明。在这些情况下,我对 EF 的性能仍然很矛盾。
  • Entity Framework (EF) 类是从 EntityObject 派生的,据我所知,您不能添加第二个派生类,因为它将在下一个“从数据库更新模型...”操作中被覆盖.您可以扩展 EF 类,因为它们是部分类,但在部分类中,您只能在一处指定基类;在这种情况下,该位置是 Designer.cs,它将被覆盖。如果我错了,请纠正我。
  • @ChristopherStevenson EF 的 Include() 在调用枚举器之前“什么都不做”,然后只编写正确的连接。您始终可以使用 SQL Profiler 检查正在执行的查询。 - Anar:我说的是编写一个单独的数据访问类,你可以将它放在你的上下文和需要访问数据库的代码之间。
  • @CodeCaster 使用 Entity Framework 5 和 Oracle 的 MySql ADO.Net 连接器在这些类型的查询上表现不佳(对我来说)。我的工作是为需要连接多个表的数据库的每种使用类型创建数据库视图。
  • @Anar 我也很好奇,让我们看看赏金会带来什么。 :)
【解决方案2】:

正如@Colin 在 cmets 中提到的,您需要在定义导航属性时使用 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. 定义一个包含BookPages = new List&lt;BookPage&gt;(); 的Book() 构造函数(Authors 相同)或
  2. 确保仅您的代码中有“new Book()”的时间是当您创建一个新条目时立即保存到数据库然后丢弃而不尝试获取任何东西。

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

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

现在,延迟加载可能破坏的另一种方式是在它被关闭时。 (我假设 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"));

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

希望对您有所帮助! ;)

【讨论】:

  • 感谢您的全面回答,但我使用的是我尝试在标签中指定的数据库优先方法。
  • 性能问题取决于您如何编码查询。延迟加载不排除在性能问题之外,例如如果您加载所有List&lt;Category&gt; 然后循环foreach 以访问Category.Books 那么您将严重陷入N+1 performance issue。如果用户有 5 个孩子,与懒惰的 5+1 db 请求相比,显式(渴望)加载所有孩子以显示孩子的详细信息是否有意义?所以这取决于,但大多数时候我使用 Include 是因为我小心地加载了我想要的内容
  • @CallMeLaNN - 我完全同意。您提到的类别和书籍的 foreach 循环是一个教科书示例,说明了我所说的 Include()“实际上更多的是一种优化手段,而不是您的代码正常运行的要求”。 Include() 在这种情况下肯定是合适的。但是,如果您不打算遍历每条 Book 记录,那么 Include() 将是不可取的。正如你所说,重点是小心装载你需要的东西。谢谢你的评论。 ;)
  • @Anar - 我对 DB-First 不像对 Code-First 那样熟悉,但如果它使用 DbContext 创建代码,那么关于启用/禁用惰性的部分加载应该仍然适用。设计器中的元素上可能还有一些附加属性,这些属性还可以控制特定导航属性是否延迟加载。当我有机会时,我会看看是否可以找到更多帮助您的情况。 ;)
  • @Anar - 当您在设计器中打开 edmx 模型时,右键单击空白区域并点击属性。然后应该有一个“启用延迟加载”选项。如果这是真的,那么延迟加载应该可以工作。此外,如果您使用“EF5 DbContext Generator”,它会创建 T4 模板 (*.tt),然后从模型生成代码,您只需打开生成的 .cs 文件并确保导航属性是虚拟的。 (如果您必须更改 edmx 模型中的属性,您可能需要重新构建它们才能重新生成。)希望这会有所帮助。 :)
【解决方案3】:

一些性能注意事项是特定于 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();
}

很遗憾,没有一种最佳做法可以满足所有情况。

【讨论】:

    【解决方案4】:

    在 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&lt;Category&gt; Categories 扩展为普通Include 和Where,而不是使用静态CategoryHelper。所以你可以有IQueryable&lt;Category&gt; db.Categories.WithBooks()
    • 您不应在 GetCategoryById 中包含所有子实体,因为它不会在方法名称中自我解释,如果此方法的用户不是 Books 实体的兄弟,则会导致性能问题。
    • 即使你不包括所有,如果你使用延迟加载,你仍然可以有潜力N+1 performance issue
    • 如果您有 1000 个 Books 更好地分页加载类似 db.Books.Where(b =&gt; b.CategoryId = categoryId).Skip(skip).Take(take).ToList() 的内容,或者甚至更好地添加上面的方法,就像这样 db.GetBooksByCategoryId(categoryId, skip, take)

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-10-15
      • 1970-01-01
      • 1970-01-01
      • 2010-09-14
      • 1970-01-01
      • 2023-03-28
      • 1970-01-01
      相关资源
      最近更新 更多