5. 查询

前面定义了模型,下面了解查询的更多细节。本节讨论:

  • 基本查询
  • 在服务器和客户端上求值
  • 原始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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值