让我们来看看简单的类示例:
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个类,但是如果我有数百个类,嵌套到非常深的层次,那么工作会非常缓慢和不舒服。
我应该如何组织使用我的数据库,以避免许多包含和冗余查询?
答案 0 :(得分:5)
问题1:我每次都要添加一堆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);
}
结论 - 如果你发现自己多次写同样的东西,那么这是一个很好的机会,可以将你的代码重构为可以重复使用的小方法。
答案 1 :(得分:1)
我不知何故错过了这是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
});
}
答案 2 :(得分:1)
我会建立这样的模型:
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; }
}
由于BookPage
中有Book
的集合,因此在BookPage
中创建了一个外键。在我的模型中,我在BookPage
中明确地公开了这一点。而且不仅使用对象Book
,还使用密钥BookId
。创建的表格完全相同,但现在您可以在不使用BookId
表的情况下访问Book
。
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表)使用了流畅的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
答案 3 :(得分:1)
所给出的示例都不需要任何Include语句。如果您在查询结尾处使用select并且仍在使用诸如DbSet之类的IQueryable,则Entity Framework将执行所谓的“投影”,并将运行包含所有必需字段的查询自动。
例如,您的原始代码:
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如何执行它所做的事情,那么最后一步是至关重要的一步。在您的示例中,当您调用SingleOrDefault
时,您正在指示实体框架执行查询,这就是您需要包含的原因。在您的示例中,您实际上并未在运行查询时告知Entity Framework您需要这些页面,因此您必须使用Include
手动请求它们。
在我发布的示例中,您可以看到,当您运行查询时(ToList
是触发查询执行的内容)Entity Framework从您的Select表达式中知道它将需要页面,以及他们的头衔。更好 - 这意味着实体框架甚至不会在它生成的SELECT
语句中包含未使用的列。
我强烈建议调查预测,这可能是我所知道的最好的方法,可以删除连续手动包含内容的要求。