前面定义了模型,下面了解查询的更多细节。本节讨论:
- 基本查询
- 在服务器和客户端上求值
- 原始SQL查询
- 编译过的查询有更好的性能
- 全局查询过滤器
- EF.Functions
1. 基本查询
如前所述,访问上下文的返回类型为DbSet的属性将返回指定表的所有实体的列表。下面详细讨论。
访问Books属性,会从数据库Books表中检索所有Book记录:
private static async Task QueryAllBooksAsync()
{
Console.WriteLine(nameof(QueryAllBooksAsync));
using (var context = new BooksContext())
{
List<Book> books = await context.Books.ToListAsync();
foreach (var book in books)
{
Console.WriteLine(book);
}
}
}
有了异步API,也可以使用从ToAsyncEnumerable方法返回的IAsyncEnumerable接口,使用ForEachAsync方法而不是foreach循环:
await context.Books.ToAsyncEnumerable()
.ForEachAsync(b=>
Console.WriteLine(b));
访问Books属性,会把下面的SQL语句发送到数据库:
SELECT [b].[BookId], [b].[IsDeleted], [b].[LastUpdated], [b].[Publisher], [b].[Title]
FROM [Books] AS [b]
可以使用Find和FindAsync方法查询具有特定键的对象。如果没有找到记录,该方法就返回null:
private static async Task FindSingleById(int id)
{
using (var context = new BooksContext())
{
Book book = await context.Books.FindAsync(id);
if (book != null)
{
Console.WriteLine(book);
}
}
}
这就得到了一个带有TOP(1)和WHERE子句的SELECT SQL语句:
SELECT TOP(1) [e].[BookId], [e].[IsDeleted], [e].[LastUpdated], [e].[Publisher], [e].[Title]
FROM [Books] AS [e]
WHERE [e].[BookId] = @__get_Item_0
与使用Find或FindAsync方法不同,还可以使用同步的Single或SingleOrDefault方法,或者使用异步变体SingleAsync或SingleOrDefaultAsync。Single和SingleOrDefault的区别在于,Single在没有找到记录时抛出异常,而SingleOrDefault会在没有找到记录时返回null。这些两个方法还在找到多个记录时抛出一个异常。
下面的代码片段使用SingleOrDefaultAsync方法来请求书名:
private static async Task FindSingleByTitle(string title)
{
using (var context = new BooksContext())
{
Book book = await context.Books.SingleOrDefaultAsync(book=>book.Title == title);
Console.WriteLine(book);
}
}
生成的SQL语句要求TOP(2)记录,它允许在找到两个记录时抛出异常:
SELECT TOP(2) [book].[BookId], [book].[IsDeleted], [book].[LastUpdated], [book].[Publisher], [book].[Title]
FROM [Books] AS [book]
WHERE [book].[Title] = @__title_0
Where方法允许基于条件进行简单的过滤。还可以在Where表达式中使用Contains方法。Where方法没有可用的异步变体,因为Where方法使用了惰性求值。可以使用foreach语句来迭代查询的所有结果。然而,foreach会触发查询的执行,阻塞线程,直到检索到结果。与使用foreach和Where方法的结果不同,可以使用ToListAsync立即触发执行,但是在异步任务中执行:
private static async Task QueryBooksAsync(string title)
{
using (var context = new BooksContext())
{
List<Book> wroxBooks = await context.Books
.Where(b => b.Title.Contains(title))
.ToListAsync();
}
}
生成的SQL语句使用了SQL子句中的一个简单的WHERE:
SELECT [b].[BookId], [b].[IsDeleted], [b].[LastUpdated], [b].[Publisher], [b].[Title]
FROM [Books] AS [b]
WHERE (CHARINDEX(@__title_0, [b].[Title]) > 0) OR (@__title_0 = N'')
可以在EF Core中使用更多的LINQ方法和LINQ子句。请记住,LINQ to Objects和LINQ to EF Core的实现是不同的。在LINQ to EF Core中,使用表达式树(参数为:Expression<Func<TSource, bool>> predicate)可以在运行时使用完整的LINQ表达式创建SQL查询(此种情况,lambdda表达式中不可以包含函数体表示,即花括号{})。在LINQ to Objects中,大多数LINQ查询都是在Enumerable类中定义的。在LINQ to EF Core中,带有表达式树(Expression)的LINQ在Queryable类中实现,对于EF Core(如异步变体)的许多增强在EntityFrameworkQueryableExtensions类中实现。
2. 客户端和服务器求值
不是查询的每个部分都可以转换为SQL语句,从而在服务器(SQL Server)上运行。有些部分需要在客户端上运行。EF Core允许进行透明的客户端和服务器求值。如果查询SQL Server不能解析,会自动在客户端上运行。这对于使用不同的提供程序有很大的优势。例如,对于一个提供程序,可以在服务器上对查询进行完全的求值。使用不转换所有查询的另一个提供程序,程序仍然运行,但是有些部分现在在客户端上进行求值。
下面看一个n-n关系的示例。Book类型与Author类型通过一个关联实体关联。一本书可以由多名作者写,一个作者也可以写多本书。
下面的代码片段通过Books属性访问Book对象。Where方法用于过滤,OrderBy方法定义顺序。使用Select方法定义结果——包括使用BookAuthors属性与作者关联:
private static void SearchValueByClientOrServer()
{
using (var context = new BooksContext())
{
var books = context.Books
.Where(b => b.Title.StartsWith("Pro"))
.OrderBy(b => b.Title)
.Select(b => new
{
newTitle = b.Title,
newBookAuthors = b.BookAuthors
});
}
}
所有这些都使用EF Core 2.0转换为SQL语句。求值完全在服务器上进行,使用Select、INNER JOIN、Where和ORDEY BY,通过关联转换Where、OrdeyBy和Select:
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.1-rtm-30846 initialized 'BooksContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (37ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [b].[Title], [b].[BookId]
FROM [Books] AS [b]
WHERE [b].[Title] LIKE N'Pro' + N'%' AND (LEFT([b].[Title], LEN(N'Pro')) = N'Pro')
ORDER BY [b].[Title], [b].[BookId]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [b.BookAuthors].[BookAuthorId], [b.BookAuthors].[AuthorId], [b.BookAuthors].[BookId], [t].[Title], [t].[BookId]
FROM [BookAuthors] AS [b.BookAuthors]
INNER JOIN (
SELECT [b0].[Title], [b0].[BookId]
FROM [Books] AS [b0]
WHERE [b0].[Title] LIKE N'Pro' + N'%' AND (LEFT([b0].[Title], LEN(N'Pro')) = N'Pro')
) AS [t] ON [b.BookAuthors].[BookId] = [t].[BookId]
ORDER BY [t].[Title], [t].[BookId]
如果将Select语句修改为返回包含作者的逗号分隔字符串,那么结果将非常不同。这在下面的代码片段中完成:把一个字符串分配给Authors属性。使用关系BookAuthors只选择作者的FirstName和LastName属性,string.Join把列表连接到一个字符串上:
var books = await context.Books
.Where(b => b.Title.StartsWith("Pro"))
.OrderBy(b => b.Title)
.Select(b =>new
{
b.Title,
Authors = string.Join(", ", b.BookAuthors.Select(b =>
$"{b.Author.FirstName} {b.Author.LastName}").ToArray())
}).ToListAsync();
EF Core 2.0无法将此查询转换为SQL语句。来自EF Core的日志信息显示了这个警告:
注意:示例使用的时EF Core 2.1.1,并未出现该警告,运行上述代码片段,日志信息如下:
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.1-rtm-30846 initialized 'BooksContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (41ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [b].[Title], [b].[BookId]
FROM [Books] AS [b]
WHERE [b].[Title] LIKE N'Pro' + N'%' AND (LEFT([b].[Title], LEN(N'Pro')) = N'Pro')
ORDER BY [b].[Title], [b].[BookId]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [t].[Title], [t].[BookId], N'{0} {1}', [b.Author].[FirstName], [b.Author].[LastName], [b.BookAuthors].[BookId]
FROM [BookAuthors] AS [b.BookAuthors]
INNER JOIN [Authors] AS [b.Author] ON [b.BookAuthors].[AuthorId] = [b.Author].[AuthorId]
INNER JOIN (
SELECT [b0].[Title], [b0].[BookId]
FROM [Books] AS [b0]
WHERE [b0].[Title] LIKE N'Pro' + N'%' AND (LEFT([b0].[Title], LEN(N'Pro')) = N'Pro')
) AS [t] ON [b.BookAuthors].[BookId] = [t].[BookId]
ORDER BY [t].[Title], [t].[BookId]
若出现警告情况,说明如下:
现在执行三个查询。这些查询的结果在客户端上连接。应用程序仍然可以工作,但是查询并没有那么有效。三个语句在SQL Server中执行,而不是执行一个语句。分析查询时,可以看到在客户机上执行求值之前,所有的作者都是从服务器中检索的。这可能会导致向客户端的大量转移:
SELECT [b].[Ttile], [b].[BookId]
FROM [Books] AS [b]
WHERE [b].[Title] LIKE N'Pro' + N'%' AND (LEFT([b].[Title], LEN(N'Pro')) = N'Pro')
ORDER BY [b].[Title]
SELECT [b0].[BookId], [b0].[AuthorId]
FROM [BookAuthors] AS [b0]
WHERE @_outer_BookId = [b0].[BookId]
SELECT [a.Author].[AuthorId], [a.Author].[FirstName], [a.Author].[LastName]
FROM [Author] AS [a.Author]
自动进行客户端和服务器的求值是很实用的。与EF Core 1.0不同,用于EF Core 2.0的SQL Server提供程序可以在服务器上进行更多的求值,未来的版本甚至可能在服务器上支持更多的求值。使用其他提供程序可能会有不同的结果。效率是不同的,但至少程序是有效的。
为了避免在服务器(应该是客户端)上进行求值,可以配置上下文,使求值仅在服务器(客户端)上进行时抛出异常。为此,在配置上下文时,可以在optionsBuilder上调用ConfigureWarnings方法:
optionsBuilder.UseSqlServer(ConnectionString)
.ConfigureWarnings(warnings=>
warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
注意:实际运行示例时,即使调用了ConfigureWarnings方法,也没有捕获到相关异常。可能原因是:示例所使用的EF Core版本,求值完全在SQL Server上进行。
警告:
客户端和服务器求值是一个很好的特性,可以使程序在不同的提供程序之间工作。然而,这会导致性能损失。要为了获得最佳性能而定义查询,可以通过配置抛出异常,来发现在客户端进行求值的情况。然后可以相应地更改查询。
3. 原始SQL查询
EF Core 2.0还允许定义原始SQL查询,原始SQL查询返回实体对象并跟踪这些对象。只需要调用DbSet对象的FromSql方法,如下面的代码片段所示:
private static async Task RawSqlQuery(string publisher)
{
Console.WriteLine(nameof(RawSqlQuery));
using (var context = new BooksContext())
{
var books = await context.Books.FromSql(
$"SELECT * FROM Books WHERE Publisher = {publisher}")
.ToListAsync();
foreach (var book in books)
{
Console.WriteLine($"{book.Title} {book.Publisher}");
}
}
}
分配给RawSqlQuery方法的SQL查询需要返回作为模型一部分的实体类型,需要返回模型所有属性的数据。
分配给FromSql方法的SQL字符串可能看起来像SQL注入,因为字符串被定义了。然而,事实并非如此。FromSql要求分配一个FormattableString类型。对于这个ForamttableString,EF Core提取参数并创建SQL参数。
4. 已编译查询
对于需要反复执行的查询,可以创建一个只需要执行的已编译查询。可以使用EF.CompileQuery创建已编译的查询。此方法提供了不同的泛型重载,可以在其中传递不同数量的参数。在下面的代码片段中,创建一个查询来定义一个字符串参数。该方法需要一个委托参数,其中第一个参数是类型BooksContext,第二个参数是字符串——这里使用的是publisher。定义已编译的查询后,可以使用它传递上下文和参数:
private static void CompiledQuery()
{
Console.WriteLine(nameof(CompiledQuery));
Func<BooksContext, string, IEnumerable<Book>> query =
EF.CompileQuery<BooksContext, string, Book>((context,publisher)=>
context.Books.Where(b => b.Publisher == publisher));
using (var context = new BooksContext())
{
IEnumerable<Book> books = query(context,"Wrox Press");
foreach (var book in books)
{
Console.WriteLine($"{book.Title} {book.Publisher}");
}
}
}
可以为成员字段创建一个已编译的查询,以便在需要的时候使用它,并且可以根据需要,传递不同的上下文,调用查询。
5. 全局查询过滤器
本章的前面介绍了使用IsDeleted列的阴影状态。不需要为每个查询定义WHERE子句,以避免返回IsDeleted为真的记录;相反,可以在创建模式时定义全局查询过滤器,这是下一个代码片段所做的——全局检查IsDeleted。因为IsDeleted并没有映射到模型,而只是通过阴影状态来检查,所以可以使用EF.Property检索值:
modelBuilder.Entity<Book>().HasQueryFilter(b => !EF.Property<bool>(b, IsDeleted));
在定义了这个查询过滤器之后,在该上下文使用的每个查询中都添加了对IsDeleted == false的WHERE检查。
注意:
全局查询过滤器也适用于多租户需求。可以为特定的tenant-id筛选上下文的所有查询。在构建上下文时,只需要传递tenant-id。不使用依赖注入,可以将tenant_id传递给构造函数。使用依赖注入,只需要指定一个用构造函数注入的服务,其中,可以在查询过滤器中检索tenant_id。
注意:
可以忽略全局查询过滤器。例如,要获取所有被删除的实体,可以使用带有LINQ表达式的IgnoreQueryFilters方法。
6. EF.Functions
EF Core允许自定义扩展方法可以由提供程序实现。为此,EF类定义了DbFunctions类型的Functions属性,它可以使用扩展方法进行扩展。在撰写本文时,Like方法是关系数据提供程序的这样一种扩展。
下面的代码片段使用EF.Functions.Like,并提供包含参数titleSegment的表达式,增强了Where方法的查询。参数titleSegment嵌入在两个%字符内:
private static async Task UseEFFunctions(string titleSegment)
{
Console.WriteLine(nameof(UseEFFunctions));
using (var context = new BooksContext())
{
string likeExpression = $"%{titleSegment}%";
IList<Book> books = await context.Books
.Where(b => EF.Functions.Like(b.Title,likeExpression)).ToListAsync();
foreach (var book in books)
{
Console.WriteLine($"{book.Title} {book.Publisher}");
}
}
}
运行应用程序时,包含EF.Functions.Like的Where方法转换为带有LIKE的SQL子句WHERE:
SELECT [b].[BookId], [b].[IsDeleted], [b].[LastUpdated], [b].[Publisher], [b].[Title]
FROM [Books] AS [b]
WHERE ([b].[IsDeleted] = 0) AND [b].[Title] LIKE @__likeExpression_1