EFCore中將IQueryable泛型對象提前轉換成SQL語句


背景

  在EFCore中有些時候我們不可避免需要將EFCore中使用Linq寫的查詢語句提前轉換成SQL語句,特別是在寫一些報表應用的時候特別適用,在我們的應用中我們可以將部分查詢操作的語句通過Linq來寫,然后再將其轉換成SQL語句,將轉換的SQL語句嵌入到其它SQL語句中,我們先來看看我們的是如何將IQueryable泛型對象直接轉換成SQL語句的。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Sunlight.Domain.Models;
using Sunlight.EFCore.Repositories;
#if NETCOREAPP2_2
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Storage;
#else
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
#endif

namespace Sunlight.EFCore.Extensions {
    /// <summary>
    /// IQueryable類型的擴展方法
    /// </summary>
    public static class QueryableExtensions {
#if NETCOREAPP2_2
        /// <summary>
        /// 將查詢語句轉換成Sql, 便於進一步的Sql拼接
        /// <seealso href="https://github.com/yangzhongke/ZackData.Net/blob/master/Tests.NetCore/IQueryableExtensions.cs" />
        /// </summary>
        /// <param name="query"></param>
        /// <param name="dbCtx"></param>
        /// <typeparam name="TEntity"></typeparam>
        /// <returns></returns>
        public static string ToSql<TEntity>(this IQueryable<TEntity> query, DbContext dbCtx) {
            var modelGenerator = dbCtx.GetService<IQueryModelGenerator>();
            var queryModel = modelGenerator.ParseQuery(query.Expression);
            var databaseDependencies = dbCtx.GetService<DatabaseDependencies>();
            var queryCompilationContext = databaseDependencies.QueryCompilationContextFactory.Create(false);
            var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor();
            modelVisitor.CreateQueryExecutor<TEntity>(queryModel);
            var sql = modelVisitor.Queries.First().ToString();
            return sql;
        }
#else

        /// <summary>
        /// 將查詢語句轉換成Sql, 便於進一步的Sql拼接
        /// <seealso href="https://gist.github.com/rionmonster/2c59f449e67edf8cd6164e9fe66c545a#gistcomment-3109335" />
        /// </summary>
        /// <param name="query"></param>
        /// <param name="dbCtx">數據庫上下文</param>
        /// <typeparam name="TEntity"></typeparam>
        /// <returns></returns>
        public static string ToSql<TEntity>(this IQueryable<TEntity> query, DbContext dbCtx = null) where TEntity : class {
            return ToSql(query);
        }

        /// <summary>
        /// 將查詢語句轉換成Sql, 便於進一步的Sql拼接
        /// <seealso href="https://gist.github.com/rionmonster/2c59f449e67edf8cd6164e9fe66c545a#gistcomment-3109335" />
        /// </summary>
        /// <param name="query"></param>
        /// <typeparam name="TEntity"></typeparam>
        /// <returns></returns>
        private static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class {
            using var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
            var relationalCommandCache = enumerator.Private("_relationalCommandCache");
            var selectExpression = relationalCommandCache.Private<SelectExpression>("_selectExpression");
            var factory = relationalCommandCache.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");

            var sqlGenerator = factory.Create();
            var command = sqlGenerator.GetCommand(selectExpression);

            var sql = command.CommandText;
            return sql;
        }

        private static object Private(this object obj, string privateField) => obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
        private static T Private<T>(this object obj, string privateField) => (T)obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);

        /// <summary>
        /// 增加
        /// </summary>
        /// <param name="query"></param>
        /// <typeparam name="TEntity"></typeparam>
        /// <returns></returns>
        public static (string, IReadOnlyDictionary<string, object>) ToSqlWithParams<TEntity>(this IQueryable<TEntity> query) {
            using var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
            var relationalCommandCache = enumerator.Private("_relationalCommandCache");
            var selectExpression = relationalCommandCache.Private<SelectExpression>("_selectExpression");
            var factory = relationalCommandCache.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");
            var queryContext = enumerator.Private<RelationalQueryContext>("_relationalQueryContext");

            var sqlGenerator = factory.Create();
            var command = sqlGenerator.GetCommand(selectExpression);

            var parametersDict = queryContext.ParameterValues;
            var sql = command.CommandText;
            return (sql, parametersDict);
        }
#endif
    }
}

  在這個里面有個NETCOREAPP2_2的編譯開關便於我們在EFCore2.2版本和EFCore3.1版本中分別使用不同的方法,我們首先來看在EFCore2.2版本中的這段用法,任何IQueryable<T>類型的查詢表達式都可以使用ToSql方法將我們的查詢表達式轉換成最終的SQL語句,這個方法還必須傳入當前的DbContext對象從而正確的轉換,另外一種是EFCore3.1版本中的兩種方法,其中一種是查詢的時候不帶變量的ToSql方法另外一種是帶參數的ToSqlWithParams,下面我們着重來說明在Asp.Net Core中我們到底該怎么使用這幾個方法。

使用

  無論是在EFCore2.2和EFCore3.1 版本中不帶參數的方法都很好理解,下面的例子主要來講述EFCore3.1中如何執行帶參數的ToSqlWithParams方法,我們來看下面的Linq方法

public async Task<int> PartConsumeStatisticAsync(DateTime? statisticDateTime) {
            // 每個服務站,每個倉庫 + 備件生成一條結轉數據
            var lastMonth = statisticDateTime ?? DateTime.Now;
            lastMonth = new DateTime(lastMonth.AddMonths(-1).Year, lastMonth.AddMonths(-1).Month, 1);
            await _partConsumeRepository.BatchDeleteAsync(c => c.Month == lastMonth);
            var outTypes = new[] {
                PartOutType.維修領料出庫,
                PartOutType.零售出庫,
                PartOutType.保養套餐銷售出庫,
                PartOutType.延保銷售出庫,
                PartOutType.二網調撥出庫,
                PartOutType.領用出庫
            };

            var inTypes = new[] {
                PartInType.維修退料入庫,
                PartInType.零售退貨入庫,
                PartInType.保養套餐退貨入庫,
                PartInType.延保銷售退貨入庫
            };
            var partOuts = (from partOut in _partOutRepository.GetAll()
                    .Where(p => p.CreateTime.HasValue && p.CreateTime.Value.Year == lastMonth.Year && p.CreateTime.Value.Month == lastMonth.Month
                                //這里在EFCore 3.1 版本使用 outTypes.Contains(p.OutType) 會報錯:Unable to cast object of type 'Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlParameterExpression'
                                //to type 'Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlConstantExpression'
                                // chery/home#4597
                                && (p.OutType == PartOutType.維修領料出庫 || p.OutType == PartOutType.零售出庫 || p.OutType == PartOutType.保養套餐銷售出庫
                                    || p.OutType == PartOutType.延保銷售出庫 || p.OutType == PartOutType.二網調撥出庫 || p.OutType == PartOutType.領用出庫))
                            join detail in _partOutDetailRepository.GetAll() on partOut.Id equals detail.PartOutId
                            select new {
                                partOut.DealerId,
                                partOut.WarehouseId,
                                partOut.OutType,
                                detail.PartId,
                                detail.OutQuantity
                            }).GroupBy(p => new { p.DealerId, p.WarehouseId, p.PartId },
                (k, g) => new {
                    k.DealerId,
                    k.WarehouseId,
                    k.PartId,
                    WXOutQuantity = g.Sum(s => s.OutType == PartOutType.維修領料出庫 ? s.OutQuantity : 0),
                    LSOutQuantity = g.Sum(s => s.OutType == PartOutType.零售出庫 ? s.OutQuantity : 0),
                    BYOutQuantity = g.Sum(s => s.OutType == PartOutType.保養套餐銷售出庫 ? s.OutQuantity : 0),
                    YBOutQuantity = g.Sum(s => s.OutType == PartOutType.延保銷售出庫 ? s.OutQuantity : 0),
                    EWOutQuantity = g.Sum(s => s.OutType == PartOutType.二網調撥出庫 ? s.OutQuantity : 0),
                    LYOutQuantity = g.Sum(s => s.OutType == PartOutType.領用出庫 ? s.OutQuantity : 0)
                });

            var partIns = (from partIn in _partInRepository.GetAll().Where(p => p.CreateTime.HasValue && p.CreateTime.Value.Year == lastMonth.Year && p.CreateTime.Value.Month == lastMonth.Month
                                          //這里在EFCore 3.1 版本使用 inTypes.Contains(p.InType) 會報錯 同上
                                          && (p.InType == PartInType.維修退料入庫 || p.InType == PartInType.零售退貨入庫 || p.InType == PartInType.保養套餐退貨入庫 || p.InType == PartInType.延保銷售退貨入庫))
                           join detail in _partInDetailRepository.GetAll() on partIn.Id equals detail.PartInId
                           select new {
                               partIn.DealerId,
                               partIn.WarehouseId,
                               partIn.InType,
                               detail.PartId,
                               detail.InQuantity
                           }).GroupBy(p => new {
                               p.DealerId,
                               p.WarehouseId,
                               p.PartId
                           }, (k, g) => new {
                               k.DealerId,
                               k.WarehouseId,
                               k.PartId,
                               WXInQuantity = g.Sum(s => s.InType == PartInType.維修退料入庫 ? s.InQuantity : 0),
                               LSInQuantity = g.Sum(s => s.InType == PartInType.零售退貨入庫 ? s.InQuantity : 0),
                               BYInQuantity = g.Sum(s => s.InType == PartInType.保養套餐退貨入庫 ? s.InQuantity : 0),
                               YBInQuantity = g.Sum(s => s.InType == PartInType.延保銷售退貨入庫 ? s.InQuantity : 0),
                           });
            return await _partConsumeStatsRepository.GeneratePartConsume(partOuts, partIns, lastMonth);
        }

  這里面partIns和partOuts是兩個IQueryable的匿名對象的集合,這里我們先來看看使用 var partInsSql=partIns.ToSql()方法,我們來看看最終轉換成的sql到底長成啥樣子。

SELECT
             [p].[DealerId],
             [p].[WarehouseId],
             [p0].[PartId],
             SUM(CASE
                 WHEN [p].[InType] = 6
                   THEN [p0].[InQuantity]
                 ELSE 0.0
                 END) AS [WXInQuantity],
             SUM(CASE
                 WHEN [p].[InType] = 4
                   THEN [p0].[InQuantity]
                 ELSE 0.0
                 END) AS [LSInQuantity],
             SUM(CASE
                 WHEN [p].[InType] = 5
                   THEN [p0].[InQuantity]
                 ELSE 0.0
                 END) AS [BYInQuantity],
             SUM(CASE
                 WHEN [p].[InType] = 8
                   THEN [p0].[InQuantity]
                 ELSE 0.0
                 END) AS [YBInQuantity]
           FROM [PartIn] AS [p]
             INNER JOIN [PartInDetail] AS [p0] ON [p].[Id] = [p0].[PartInId]
           WHERE (((DATEPART(year, [p].[CreateTime]) = @__lastMonth_Year_0) OR
                   (DATEPART(year, [p].[CreateTime]) IS NULL AND @__lastMonth_Year_0 IS NULL)) AND
                  ((DATEPART(month, [p].[CreateTime]) = @__lastMonth_Month_1) OR
                   (DATEPART(month, [p].[CreateTime]) IS NULL AND @__lastMonth_Month_1 IS NULL))) AND
                 (((([p].[InType] = 6) OR ([p].[InType] = 4)) OR ([p].[InType] = 5)) OR ([p].[InType] = 8))
           GROUP BY [p].[DealerId], [p].[WarehouseId], [p0].[PartId]

  這里我們發現我們定義的lastMonth變量傳遞到Linq中去,最后我們發現轉換成的SQL中是以變量@__lastMonth_Year_0、@__lastMonth_Month_1的形式呈現的,那么我們怎樣將最終的變量傳遞到這兩個參數中去呢?這里我們肯定想到了使用ToSqlWithParams方法,那么我們來看看這個_partConsumeStatsRepository.GeneratePartConsume(partOuts, partIns, lastMonth)這個子方法我們最終是怎么實現的?

public async Task<int> GeneratePartConsume<T1, T2>(IQueryable<T1> outQuery, IQueryable<T2> inQuery, DateTime theMonth)
            where T1 : class where T2 : class {
            var (outQuerySql, outParams) = outQuery.ToSqlWithParams();
            var (inQuerySql, inParams) = inQuery.ToSqlWithParams();
            var sql = $@"Insert into PartConsume
(Id, DealerId, WarehouseId, PartId,
WXOutQuantity,LSOutQuantity,BYOutQuantity,YBOutQuantity,EWOutQuantity, LYOutQuantity,
WXInQuantity,LSInQuantity,BYInQuantity,YBInQuantity,TotalQuantity,
PartName, PartCode, DealerName, DealerCode, WarehouseName, WarehouseCode, Month, TheDate, IsExternalPart)
select newid(), a.*, Part.Name PartName, Part.Code PartCode,
(select Name From Company WHERE Id = a.DealerId) DealerName,
(select Code From Company WHERE Id = a.DealerId) DealerCode,
(select Name From DealerPartWarehouse WHERE Id = a.WarehouseId) WarehouseName,
(select Code From DealerPartWarehouse WHERE Id = a.WarehouseId) WarehouseCode, '{theMonth:u}', GetDate(), Part.IsExternalPart
FROM (select isnull(outQ.DealerId, inQ.DealerId) DealerId,
isnull(outQ.WarehouseId, inQ.WarehouseId) WarehouseId,
isnull(outQ.PartId, inQ.PartId) PartId,
ISNULL(WXOutQuantity,0) WXOutQuantity,ISNULL(LSOutQuantity,0) LSOutQuantity,ISNULL(BYOutQuantity,0) BYOutQuantity,
ISNULL(YBOutQuantity,0) YBOutQuantity,ISNULL(EWOutQuantity,0) EWOutQuantity, ISNULL(LYOutQuantity,0) LYOutQuantity,
ISNULL(WXInQuantity,0) WXInQuantity,ISNULL(LSInQuantity,0) LSInQuantity,ISNULL(BYInQuantity,0) BYInQuantity,ISNULL(YBInQuantity,0) YBInQuantity,
(ISNULL(WXOutQuantity,0)+ISNULL(LSOutQuantity,0)+ISNULL(BYOutQuantity,0)
+ISNULL(YBOutQuantity,0)+ISNULL(EWOutQuantity,0)+ISNULL(LYOutQuantity,0)
-ISNULL(WXInQuantity,0)-ISNULL(LSInQuantity,0)-ISNULL(BYInQuantity,0)-ISNULL(YBInQuantity,0)) TotalQuantity
From ({outQuerySql}) outQ
    full join
    ({inQuerySql}) inQ
on outQ.DealerId = inQ.DealerId and outQ.WarehouseId = inQ.WarehouseId and outQ.PartId = inQ.PartId) a
inner join Part on Part.Id = a.PartId";
            var parameters = new List<SqlParameter>();
            outParams.ForEach(outParam => {
                parameters.Add(new SqlParameter(outParam.Key, outParam.Value));
            });
            inParams.ForEach(inParam => {
                if (parameters.Any(p => p.ParameterName == inParam.Key && p.Value.ToString() != inParam.Value.ToString())) {
                    throw new ValidationException("轉換出的SQL語句中參數存在參數名稱相同但是值不同的對象");
                }

                if (parameters.All(p => p.ParameterName != inParam.Key && p.Value != inParam.Value)) {
                    parameters.Add(new SqlParameter(inParam.Key, inParam.Value));
                }
            });

            return await Context.Database.ExecuteSqlRawAsync(sql, parameters.ToArray());
        }

  在ToSqlWithParams返回值除了當前的sql之外還有當前sql中的參數信息,我們后面需要將當前的參數信息轉換成SqlParameter集合,然后通過ExecuteSqlRawAsync帶參數的方法將參數傳遞進去,這樣才能夠真正將最終的參數值傳遞到sql中的@__lastMonth_Year_0、@__lastMonth_Month_1中去,從而最終實現數據庫中的sql的執行和應用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM