如何高效地使用Entity Framework Core?

How to efficiently work with Entity Framework Core?

让我们来看看简单的class例子:

public class Book
{
    [Key]
    public string BookId { get; set; }
    public List<BookPage> Pages { get; set; }
    public string Text { get; set; }
} 

public class BookPage
{
    [Key]
    public string BookPageId { get; set; }
    public PageTitle PageTitle { get; set; }
    public int Number { get; set; }
}

public class PageTitle
{
    [Key]
    public string PageTitleId { get; set; }
    public string Title { get; set; }
}

所以,如果我想获取所有的 PageTitiles,如果我只知道 BookId,我需要写一些包含,如下所示:

using (var dbContext = new BookContext())
{
    var bookPages = dbContext
    .Book
    .Include(x => x.Pages)
    .ThenInclude(x => x.PageTitle)//.ThenInclude(x => x.Select(y => y.PageTitle)) Shouldn't use in EF Core
    .SingleOrDefault(x => x.BookId == "some example id")
    .Pages
    .Select(x => x.PageTitle);
}

如果我想将PageTitles与其他书关联起来,我需要重写这个方法,除了BookId之外什么都没有改变!这是使用数据库的非常低效的方式,在这个例子中我有 3 个 classes,但是如果我有数百个 classes,嵌套到很深的层次,那将是非常缓慢和不舒服的上班。

我应该如何组织使用我的数据库,以避免许多包含和冗余查询?

我不知何故错过了这是 EF Core(尽管有标题)。试试这个:

public class BookPage
{
    [Key]
    public string BookPageId { get; set; }
    public int Number { get; set; }
    public PageTitle PageTitle { get; set; }
    public Book Book { get; set; }   // Add FK if desired
}

现在获取一本书的所有页面标题:

// pass the book you want in as a parameter, viewbag, etc.
using (var dbContext = new BookContext())
{
    var bookPages = dbContext.BookPages
        .Include(p => p.Book)
        .Include(p => p.PageTitle)
        .Where(p => p.Book.BookId == myBookId)
        .Select(p => new { 
            Bookid = p.Book.BookId,
            Text = p.Book.Text,
            PageNumber = p.Number,
            PageTitle = p.PageTitle.Title
        });
}

问题一:每次都要加一堆Includes

嗯,没有办法解决这个问题,因为您必须在 EF 中显式包含相关数据,但您可以轻松创建扩展方法以使其更简洁:

public static IQueryable<Book> GetBooksAndPages(this BookContext db)
{
    return db.Book.Include(x => x.Pages);
}

public static IQueryable<Book> GetBooksAndPagesAndTitles(this BookContext db)
{
    return GetBooksAndPages(db).ThenInclude(p => p.PageTitle)

}

那么你可以这样做:

var bookPages = dbContext
    .GetBooksAndPagesAndTitles()
    .SingleOrDefault(x => x.BookId == "some example id")
    .Pages
    .Select(x => x.PageTitle);

问题 2:我必须针对不同的 ID 多次编写此查询。

为什么不将其重构为具有 bookId 参数的方法?

public IEnumerable<PageTitle> GetPageTitlesForBook(BookContext dbContext, int bookId)
{
    return dbContext
        .GetBooksAndPagesAndTitles()
        .SingleOrDefault(x => x.BookId == bookId)
        .Pages
        .Select(x => x.PageTitle);
}

底线 - 如果您发现自己多次编写同一内容,这是将代码重构为可重复使用的更小方法的绝好机会。

我会像这样构建模型:

    public class Book
    {
        // a property "Id" or ClassName + "Id" is treated as primary key. 
        // No annotation needed.
        public int BookId { get; set; }

        // without [StringLenth(123)] it's created as NVARCHAR(MAX)
        [Required]
        public string Text { get; set; }

        // optionally if you need the pages in the book object:
        // Usually I saw ICollections for this usage.
        // Without lazy loading virtual is probably not necessary.
        public virtual ICollection<BookPage> BookPages { get; set; }
    }

    public class BookPage
    {
        public int BookPageId { get; set; }

        // With the following naming convention EF treats those two property as 
        // on single database column. This automatically corresponds
        // to ICollection<BookPage> BookPages of Books.
        // Required is not neccessary if "BookId" is int. If not required use int?
        // A foreign key relationship is created automatically. 
        // With RC2 also an index is created for all foreign key columns.
        [Required]
        public Book Book { get; set; }
        public int BookId { get; set; }

        [Required]
        public PageTitle PageTitle { get; set; }
        public int PageTitleId { get; set; }

        public int Number { get; set; }
    }

    public class PageTitle
    {
        public int PageTitleId { get; set; }

        // without StringLenth it's created as NVARCHAR(MAX)
        [Required]
        [StringLength(100)]
        public string Title { get; set; }
    }

因为您在 Book 中有一个 BookPage 的集合,所以在 BookPage 中创建了一个外键。在我的模型中,我已在 BookPage 中明确公开了这一点。不仅是对象 Book,还有键 BookId。创建的 table 完全相同,但现在您无需使用 Book table.

即可访问 BookId
    using (var dbContext = new BookContext())
    {
        var pageTitles = dbContext.BookPages
            .Include(p => p.PageTitle)
            .Where(p => p.BookId == myBookId)
            .Select(p => p.PageTitle);
    }

我建议激活日志记录或使用探查器检查实际执行了哪些 SQL 语句。

关于@bilpor 的评论: 我发现我不需要很多 DataAnnotations 并且几乎不需要流畅的 API 映射。如果您使用指定的命名约定,则会自动创建主键和外键。对于外键关系,如果我在相同的两个 类 上有两个外键关系,我只需要在集合上使用 [InverseProperty()]。目前,我只对复合主键 (m:n tables) 使用流畅的 API 映射,并在 TPH 结构中定义鉴别器。

提示: 目前 EF Core 中存在导致客户端评估约束的错误。

.Where(p => p.BookId == myBookId)  // OK 
.Where(p => p.BookId == myObject.BookId) // client side 
.Where(p => p.BookId == myBookIdList[0]) // client side 

当您使用 Contains() 并混合可空和不可空数据类型时也是如此。

.Where(p => notNullableBookIdList.Contains(p.NullableBookId)) // client side 

None 给出的示例根本不需要任何 Include 语句。如果您在查询末尾使用 select 并且仍在对 DbSet 等 IQueryable 进行操作,则 Entity Framework 将执行所谓的 'projection' 并将 运行 自动为您包含所有必填字段的查询。

比如你的原代码:

using (var dbContext = new BookContext())
{
    var bookPages = dbContext
        .Book
        .Include(x => x.Pages)
        .ThenInclude(x => x.PageTitle)//.ThenInclude(x => x.Select(y => y.PageTitle)) Shouldn't use in EF Core
        .SingleOrDefault(x => x.BookId == "some example id")
        .Pages
        .Select(x => x.PageTitle);
}

你可以这样改写:

using (var dbContext = new BookContext())
{
    var bookPages = dbContext
        .Book
        .Where(x => x.BookId == "some example id")
        .SelectMany(x => x.Pages.Select(y => y.PageTitle))
        .ToList();
}

Entity Framework 将采取以下措施解决此问题:

  1. 我们告诉 Entity Framework 我们要查看书中的条目 table
  2. 然后我们告诉 Entity Framework 我们只想要具有特定 ID 的书(当然应该只是一条记录)
  3. 从那里开始,对于每本书,我们告诉 Entity Framework 我们需要所有这些书页的列表(同样,由于 Where 语句,这将只是一本书的页)
  4. 然后我们告诉 Entity Framework 我们只需要每个页面的 PageTitle
  5. 最后,我们告诉 Entity Framework 使用我们刚刚提供的所有信息来生成查询并执行它

如果您想了解 Entity Framework 的工作原理,那么最后一步是至关重要的一步。在您的示例中,当您调用 SingleOrDefault 时,您正在指示 Entity Framework 执行查询,这就是您需要包含的原因。在您的示例中,当您 运行 查询时,您实际上并没有告诉 Entity Framework 您需要这些页面,因此您必须使用 Include.

手动请求它们

在我发布的示例中,您可以看到当您 运行 查询时(ToList 触发查询执行)Entity Framework 从您的 Select 表示它将需要页面及其标题。更好的是 - 这意味着 Entity Framework 甚至不会在它生成的 SELECT 语句中包含未使用的列。

我强烈建议调查预测,它们可能是我所知道的消除连续手动包含内容的要求的最佳方法。