当必须基于多个联接(包括通过联结表进行多对多)获取数据时,我正在尝试学习编写高效的实体框架查询。在下面的示例中,我想获取包含特定书籍的所有州。
让我们使用具有以下表/实体的模型,所有表/实体均通过导航属性链接: State、City、Library、Book、LibraryBook(图书馆和书籍之间多对多关系的联结表。)
如何才能最好地返回包含特定图书的所有状态?我倾向于认为单独的查询可能比 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 查询!
这真的是解决这个问题的唯一方法吗?如果没有,有什么更好的选择?
提前致谢!
数据库管理系统在组合表和从结果中选择列方面进行了极其优化。所选数据的传输是较慢的部分。
因此,通常最好限制需要传输的数据:让 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()
的用户无需更改。