如何高效地使用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 将采取以下措施解决此问题:
- 我们告诉 Entity Framework 我们要查看书中的条目 table
- 然后我们告诉 Entity Framework 我们只想要具有特定 ID 的书(当然应该只是一条记录)
- 从那里开始,对于每本书,我们告诉 Entity Framework 我们需要所有这些书页的列表(同样,由于 Where 语句,这将只是一本书的页)
- 然后我们告诉 Entity Framework 我们只需要每个页面的 PageTitle
- 最后,我们告诉 Entity Framework 使用我们刚刚提供的所有信息来生成查询并执行它
如果您想了解 Entity Framework 的工作原理,那么最后一步是至关重要的一步。在您的示例中,当您调用 SingleOrDefault
时,您正在指示 Entity Framework 执行查询,这就是您需要包含的原因。在您的示例中,当您 运行 查询时,您实际上并没有告诉 Entity Framework 您需要这些页面,因此您必须使用 Include
.
手动请求它们
在我发布的示例中,您可以看到当您 运行 查询时(ToList
触发查询执行)Entity Framework 从您的 Select 表示它将需要页面及其标题。更好的是 - 这意味着 Entity Framework 甚至不会在它生成的 SELECT
语句中包含未使用的列。
我强烈建议调查预测,它们可能是我所知道的消除连续手动包含内容的要求的最佳方法。
让我们来看看简单的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 将采取以下措施解决此问题:
- 我们告诉 Entity Framework 我们要查看书中的条目 table
- 然后我们告诉 Entity Framework 我们只想要具有特定 ID 的书(当然应该只是一条记录)
- 从那里开始,对于每本书,我们告诉 Entity Framework 我们需要所有这些书页的列表(同样,由于 Where 语句,这将只是一本书的页)
- 然后我们告诉 Entity Framework 我们只需要每个页面的 PageTitle
- 最后,我们告诉 Entity Framework 使用我们刚刚提供的所有信息来生成查询并执行它
如果您想了解 Entity Framework 的工作原理,那么最后一步是至关重要的一步。在您的示例中,当您调用 SingleOrDefault
时,您正在指示 Entity Framework 执行查询,这就是您需要包含的原因。在您的示例中,当您 运行 查询时,您实际上并没有告诉 Entity Framework 您需要这些页面,因此您必须使用 Include
.
在我发布的示例中,您可以看到当您 运行 查询时(ToList
触发查询执行)Entity Framework 从您的 Select 表示它将需要页面及其标题。更好的是 - 这意味着 Entity Framework 甚至不会在它生成的 SELECT
语句中包含未使用的列。
我强烈建议调查预测,它们可能是我所知道的消除连续手动包含内容的要求的最佳方法。