如何高效地使用Entity Framework Core?

3
让我们来看一些简单的类示例:
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; }
}

所以,如果我想获取所有页面标题,只知道书籍编号,我需要写几个包含语句,就像这样:

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

如果我想获取与其他书籍相关联的页面标题,我需要再次重写此方法,除了BookId之外没有任何变化!这是一种非常低效的处理数据库的方式,在此示例中,我有3个类,但如果我有数百个类,嵌套到非常深的级别,那么处理起来将会非常缓慢和不舒适。

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


类书中的页面需要声明为虚拟集合。bookPage中的PageTitle应该声明为int PageTitleId,并且应该具有ForeignKey的数据注释。PageTitle中的PageTitleId应该声明为int而不是字符串...这只是一个入门级别的内容。 - bilpor
是的,你不需要定义外键,但这并不意味着你不应该这样做。从数据库的角度来看,你可能会按照自己的方式编写应用程序,没有外键等,这可能是可以的,因为你可以查看代码和继承/对象图来查看关系。一段时间后,DBA出现了,想要进行一些SSRS工作,但没有访问你或应用程序代码的权限。现在他们将很难确定关系。没有外键,你开始失去SQL Server等关系型数据库的意义。这也意味着当你删除时,记录可能会变成孤立的。 - bilpor
@bilpor 但是 EF 会自动生成外键,而不是由我手动创建。 - Yurii N.
@bilpor 这是另一个问题,我不想在一个简单的类中定义数百个外键。我们有其他方法吗? - Yurii N.
@bilpor,你能否提供一些示例链接? - Yurii N.
显示剩余8条评论
4个回答

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

归根结底,如果你发现自己多次编写相同的代码,那么这是将代码重构为可以重复使用的较小方法的绝佳机会。


扩展方法是个好主意,我们能否在IQueryable中使用它们而不是DbContext? - Yurii N.
@YuriyN。当然,您可以在几乎任何类型上创建扩展方法。 - D Stanley
如果你正在使用扩展方法,那么显然你没有测试你的代码。它们很麻烦。 - Chris Paton

2

所有给出的示例都不需要任何Include语句。如果您在查询末尾使用select,并且仍在使用IQueryable,例如DbSet,则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解决此问题的步骤:
1. 我们告诉Entity Framework我们要查看books表中的条目 2. 然后我们告诉Entity Framework我们只想要特定ID的书(当然应该只有一条记录) 3. 对于每本书,我们告诉Entity Framework我们想要该书的所有页面的列表(由于Where语句,这将只是一本书的页面) 4. 然后我们告诉Entity Framework我们只想要每个页面的PageTitle 5. 最后,我们告诉Entity Framework使用我们刚刚提供的所有信息来生成查询并执行它
如果您想了解Entity Framework如何实现其功能,最后一步是关键。在您的示例中,当您调用SingleOrDefault时,您正在指示Entity Framework执行查询,这就是为什么您需要includes的原因。在您的示例中,您实际上没有告诉Entity Framework在运行查询时需要页面,因此必须使用Include手动请求它们。
在我发布的示例中,您可以看到当您运行查询(ToList是触发查询执行的内容)时,Entity Framework从您的选择表达式中知道它将需要页面及其标题。更好的是 - 这意味着Entity Framework甚至不会在生成的SELECT语句中包括未使用的列。
我强烈建议调查投影,它们可能是我所知道的消除持续手动包含内容的最佳方法。

非常有趣,这是在 EF Core 1.0 中吗? - Yurii N.
是的,这种行为一直存在。这是我最喜欢的 EF 特性之一。 - Nick Coad
两年前你去哪了...谢谢,我也会尝试那个功能! - Yurii N.
@YuriyN。自从进一步了解这个问题后,似乎嵌套投影在EF Core 1.0中有些不可靠(或者根本不存在)。我相信现在这个问题已经或多或少得到解决,上述方法应该可以使用,但我需要进行检查。在.NET Framework的EF中它肯定是有效的,但我没有意识到他们忽略了在EF Core中包含嵌套投影。 - Nick Coad

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

我们在Entity Framework Core中没有惰性加载,这就是问题所在,只能使用Include。因此,你的例子不起作用。 - Yurii N.
好的,那就试试Includes()吧。这就是为什么我现在还坚持使用EF6的原因 :) - Steve Greene
是的,它可以工作,但与问题的示例没有区别 :) - Yurii N.
我正在寻找处理深度嵌套数据的正确方法,这些数据位于表格链的末端。例如,Book是基本类,而PageTitle则是深度嵌套的。 - Yurii N.
我不会称之为深度嵌套,但这都是相对的。如果你要讨论5或6层深度,那么你可能需要考虑存储过程、视图或多个获取操作。Include()是处理嵌套数据的首选工具。文档展示了常见的场景:https://msdn.microsoft.com/en-us/library/gg671236(v=vs.103).aspx - Steve Greene
显示剩余3条评论

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

由于您在Book中有一个BookPage的集合,因此在BookPage中创建了一个外键。在我的模型中,我已经明确地在BookPage中公开了这一点。不仅如此,我还使用了BookId关键字而不是只使用Book对象。创建的表几乎相同,但现在您可以在不使用Book表的情况下访问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表)和在TPH结构中定义鉴别器时使用流畅API映射。
提示: 目前,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 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接