EF Core:在相关的一对多实体中深入查询数据的正确方法

问题描述 投票:0回答:1

当必须基于多个联接(包括通过联结表进行多对多)获取数据时,我正在尝试学习编写高效的实体框架查询。在下面的示例中,我想获取包含特定书籍的所有州。

让我们使用具有以下表/实体的模型,所有表/实体均通过导航属性链接: State、City、Library、Book、LibraryBook(图书馆和书籍之间多对多关系的联结表。)

  • 每个州有 1 个或多个城市
  • 每个城市有 1 个或多个图书馆
  • 每个图书馆都有很多书,每本书可能存在于多个图书馆。

如何才能最好地返回包含特定图书的所有状态?我倾向于认为单独的查询可能比 1 个大型查询更好,但我不确定最好的实现是什么。我认为首先在单独的查询中从多对多关系获取 LibraryId 可能是一个很好的开始方式。

因此:

var bookId = 12;
var libraryIds = _context.LibraryBook.Where(l => l.BookId == bookId).Select(s => s.LibraryId);

如果这是第一位的,我不确定如何最好地查询下一个数据以获得包含每个 LibraryId 的城市。我可以使用 foreach:

var cities = new List<City>;
foreach(var libraryId in libraryIds)
{
    var city = _context.City.Where(c => c.Library = libraryId)
    cities.Add(city);
}

但是接下来我还必须对包含该城市的州进行另一个 foreach,这一切加起来会产生大量单独的 SQL 查询!

这真的是解决这个问题的唯一方法吗?如果没有,有什么更好的选择?

提前致谢!

c# .net entity-framework linq entity-framework-core
1个回答
5
投票

数据库管理系统在组合表和从结果中选择列方面进行了极其优化。所选数据的传输是较慢的部分。

因此,通常最好限制需要传输的数据:让 DBMS 完成所有连接和选择。

为此,您不需要将所有内容都放在一个难以理解的大型 LINQ 语句中(因此难以测试、重用和维护)。只要您的 LINQ 语句保持

IQuerayble<...>
,查询就不会执行。连接多个 LINQ 语句的成本并不高。

回到你的问题

如果您遵循实体框架约定,您的一对多关系和多对多关系将产生类似于以下内容的类:

class State
{
    public int Id {get; set;}
    public string Name {get; set;}
    ...

    // every State has zero or more Cities (one-to-many)
    public virtual ICollection<City> Cities {get; set;}
}

class City
{
    public int Id {get; set;}
    public string Name {get; set;}
    ...

    // Every City is a City in exactly one State, using foreign key:
    public int StateId {get; set;}
    public virtual State State {get; set;}

    // every City has zero or more Libraries (one-to-many)
    public virtual ICollection<Library> Libraries {get; set;}
}

图书馆和书籍:多对多:

class Library
{
    public int Id {get; set;}
    public string Name {get; set;}
    ...

    // Every Library is a Library in exactly one City, using foreign key:
    public int CityId {get; set;}
    public virtual City City {get; set;}

    // every Library has zero or more Books (many-to-many)
    public virtual ICollection<Book> Books {get; set;}
}

class Book
{
    public int Id {get; set;}
    public string Title {get; set;}
    ...

    // Every Book is a Book in zero or more Libraries (many-to-many)
    public virtual ICollection<Book> Books {get; set;}
}

这就是实体框架识别表、表中的列以及表之间的关系所需的全部信息。

如果您想偏离约定,则只需要属性或流畅的 API:列或表的不同标识符、小数的非默认类型、删除时级联的非默认行为等。

在实体框架中,表中的列由非虚拟属性表示;虚拟属性代表表之间的关系。

外键是表中的实际列,因此它是非虚拟的。一对多在“一”侧有

virtual ICollection<Type>
,在“多”侧有
virtual Type
。多对多两边都有
virtual ICollection<...>

无需指定联结表。实体框架识别多对多并为您创建联结表。如果您首先使用数据库,您可能需要使用 Fluent API 来指定联结表。

但是如果没有连接表,我该如何进行连接呢?

答案:不要自己进行(组)连接,使用虚拟 ICollections!

如何最好地返回包含特定书籍的所有状态?

int bookId = ...
var statesWithThisBookId = dbContext.States
    .Where(state => state.Cities.SelectMany(city => city.Libraries)
                                .SelectMany(library => library.Books)
                                .Select(book => book.Id)
                                .Contains(bookId);

用语言来说:你有很多州。从每个州获取该州所有城市的所有图书馆中的所有书籍。使用 SelectMany 制作这一大册书籍。从每本书中选择 ID。结果是一大堆 BookId 序列(位于该州城市的图书馆中的书籍)。仅保留至少有一本 Id 等于 BookId 的 Book 的州。

优化空间

如果您经常需要做类似的问题,例如:“给我所有拥有某个作者的书的州”,或“给我所有拥有某个书名的书的图书馆”,请考虑为此创建扩展方法。这样您就可以将它们作为任何 LINQ 方法连接起来。扩展方法创建查询,但不会执行它们,因此这不会造成性能损失。

扩展方法的优点:更容易理解、可重用、更容易测试、更容易更改。

如果您不熟悉扩展方法,请阅读扩展方法揭秘

// you need to convert them to IQueryable with the AsQueryable() method, if not
// you get an error since the receiver asks for an IQueryable
// and a ICollection was given
public static IQueryable<Book> GetBooks(this IQueryable<Library> libraries)
{
    return libraries.SelectMany(library => library.AsQueryable().Books);
}

public static IQueryable<Book> GetBooks(this IQueryable<City> cities)
{
    return cities.SelectMany(city => city.Libraries.AsQueryable().GetBooks());
}

用途:

获取所有拥有卡尔·马克思的书的州:

string author = "Karl Marx";
var statesWithCommunistBooks = dbContext.States.
    .Where(state => state.GetBooks()
                         .Select(book => book.Author)
                         .Contains(author));

获取所有没有圣经的城市:

string title = "Bible";
var citiesWithoutBibles = dbContext.Cities
    .Where(city => !city.GetBooks()
                       .Select(book => book.Title)
                       .Contains(title));

因为您使用方法

GetBooks()
扩展了您的类,就好像州和城市都有书籍一样。您已经看到了上面的可重用性。更改可能很容易,例如,如果您扩展数据库,使得城市拥有书店。 GetBooks 可以检查图书馆和书店。您的更改将在一处。
GetBooks()
的用户无需更改。

© www.soinside.com 2019 - 2024. All rights reserved.