当前位置: 首页 > news >正文

【EF Core】两种方法记录生成的 SQL 语句

原本计划 N 天前写的内容,无奈拖到今天。大伙伴们可能都了解,年近岁末,风干物燥,bug 特多,改需求的精力特旺盛。有几个工厂的项目需要不同程度的修改或修复。这些项目都是老周个人名义与他们长期合作的(有些项目已断尾了,他们觉得不用再改了),所以不一定都是新项目,有两三个都维护好几年了。

今天咱们的主题是记录 SQL 语句。用过 EF 的都知道,它可以将 LINQ 表达式树翻译成 SQL 语句,然后发送到数据库执行。这个框架从 Framework 时代走到 Core 时代,虽说不是什么新鲜技术,但这活真的是好活,以面向对象的方式与数据库交互是真的爽。

将 LINQ 转译为 SQL 是框架内部功能,官方团队或许也没考虑让我们做太多的扩展(实际开发中也的确很少),因此,框架并没有提供独立的服务让我们去做表达式树的翻译。在执行查询时,EF Core 是经过几个步骤的,这个可以看看 QueryCompilationContext 类的源代码。其实处理查询转译的代码是写在这个类里面的,不是 Database 类。上次在某公司有位妹子程序员问过老周,她想看看 LINQ 翻译 SQL 的大致过程,可在源代码中找不到。你不要惊讶,这个公司的团队绝对少见,七个成员,四个是女的,恐怕你都找不出第二个这样的团队。

老周告诉她,源代码庞大,直接拿着看很多东西不好找的,你可以用调试进入源码,一步步跟进去,才比较好找。不废话了,咱们看代码。

    public virtual Expression<Func<QueryContext, TResult>> CreateQueryExecutorExpression<TResult>(Expression query){var queryAndEventData = Logger.QueryCompilationStarting(Dependencies.Context, _expressionPrinter, query);var interceptedQuery = queryAndEventData.Query;var preprocessedQuery = _queryTranslationPreprocessorFactory.Create(this).Process(interceptedQuery);var translatedQuery = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Translate(preprocessedQuery);var postprocessedQuery = _queryTranslationPostprocessorFactory.Create(this).Process(translatedQuery);var compiledQuery = _shapedQueryCompilingExpressionVisitorFactory.Create(this).Visit(postprocessedQuery);// If any additional parameters were added during the compilation phase (e.g. entity equality ID expression),// wrap the query with code adding those parameters to the query contextvar compiledQueryWithRuntimeParameters = InsertRuntimeParameters(compiledQuery);return Expression.Lambda<Func<QueryContext, TResult>>(compiledQueryWithRuntimeParameters,QueryContextParameter);}

这代码一旦展开是非常复杂的,你不仅要有使用 LINQ 表达式树的知识,还得看懂其思路,所以,没兴趣的话就不用看了。而且看不懂也不影响写代码。大体过程是这样的:

1、先执行拦截器。拦截器这东西老周以后会介绍,拦截器可以拦截你执行的 LINQ 表达式树,并且你可以在翻译之前修改它。

2、预处理。这里面又是一堆处理,如参数命名规整化、把如 long.Max 这样的方法调用标准化为 Math.Max 调用、表达式简化等等。

3、特殊方法调用转换,如调用 Where、All、FirstOrDefault 这样标准查询方法,还有 ExecuteUpdate、ExecuteDelete 这些专用方法的调用转换等。

4、转换扫尾工作,这个主要是不同数据库的特殊处理,比如,Sqlite 和 SQL Server 的处理不同。

5、正式转译为 SQL 语句。

6、生成 Lambda 表达式树。这个委托接收 QueryContext 类型的参数(可以用 IQueryContextFactory 服务创建),返回的结果一般是 IEnumerable<T>。

想想,调用这些代码获取 SQL 太麻烦,这等同于把人家源代码抄一遍了。其实,单纯的把 LINQ 转 SQL 意义不大的,许多场景下,可能最需要的是日志功能——记录发送到数据库的 SQL 语句。

 

好了,上面的只是理论铺设,接下来咱们聊主题。咱们有两种方法可以记录SQL语句,不废话,老周直接说答案:

1、通过日志 + 事件过滤功能。这个最简单;

2、通过拦截器拦截 DbCommand 对象,从而获取 SQL 语句。

 

-----------------------------------------------------------------------------------------------------------------------------------

先说第一种,先写个实体类,随便写就行。

public class Song
{public int ID { get; set; }public string Name { get; set; } = null!;public string? Artist { get; set; }public long Duration { get; set; }
}

然后写数据库上下文。

public class MyDbContext : DbContext
{protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlite("data source=demo.db");}protected override void OnModelCreating(ModelBuilder modelBuilder){modelBuilder.Entity<Song>(et =>{et.ToTable("tb_songs");et.HasKey(x => x.ID).HasName("PK_Song");et.Property(a => a.Name).HasMaxLength(20);});}// 公开数据集合public DbSet<Song> Songs { get; set; }
}

写好后回过头看 OnConfiguring 方法,现在咱们要配置日志。

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlite("data source=demo.db");optionsBuilder.LogTo(// 第一个委托:过滤事件(eventid, loglevel) =>{if(eventid.Id == RelationalEventId.CommandExecuting){return true;}return false;},// 第二个委托:记录SQLeventData =>{// 转换事件数据if(eventData is CommandEventData data){// 记录SQLConsole.Write("命令源:{0}", data.CommandSource);Console.Write(",SQL 语句:{0}", data.LogCommandText);Console.Write("\n\n");}});}

这里,LogTo 调用的是以下重载:

public virtual DbContextOptionsBuilder LogTo(Func<EventId, LogLevel, bool> filter,Action<EventData> logger)

filter 是个过滤器,EventId 表示相关事件,LogLevel 表示日志级别,如 Information、Warning、Error 等。第三个是返回值,布尔类型。所以,这个委托的用法很明显,如果返回 false,表示不记录该事件的日志,第二个委托logger就不会调用;如果过滤器返回 true,表明要接收此事件的日志,此时,logger 委托会调用。

咱们的代码只关心 CommandExecuting 事件,这是 DbCommand 执行之前触发的,如果是命令执行之后,会触发 CommandExecuted 事件。咱们的目标明确——获取生成的 SQL 语句,其实这里也可以用 CommandInitialized 事件。其实 CommandInitialized、CommandExecuting、CommandExecuted 三个事件都能得到 SQL 语句,任意抓取一个用即可。

在第二个委托中,它有一个输入参数—— EventData,它是所有事件数据的基类,所以,在委托内部需要进行类型分析。

if(eventData is CommandEventData data)……

不过这里我们不必关心其他类型,毕竟 filter 只选出一个事件,其他事件都返回 false,不会调用 logger 委托。

从 LogCommandText 属性上就能得到 SQL 语句。另外,CommandSource 是一个枚举,它表示这个 SQL 语句是由哪个操作引发的。如

  • SaveChanges:你调用 DbContext.SaveChanges 方法后保存数据时触发的。
  • Migrations:迁移数据库时触发,包括在运行阶段执行迁移,或者调用 Database.EnsureCreate 或 EnsureDelete 等方法也会触发。
  • LinqQuery:这个熟悉,就是你常规操作,使用 LINQ 查询转 SQL 后执行。
  • ExecuteDelete 与 ExecuteUpdate:就是调用 ExecuteUpdate、ExecuteDelete 方法时触发。

好,咱们试一下,先用 EnsureCreate 创建数据库,然后执行一个查询。

using var ctx = new MyDbContext();
ctx.Database.EnsureCreated();
// 查询
var res = ctx.Songs.Where(s => s.ID > 2).ToArray();

运行一下看看。结果如下:

命令源:Migrations,SQL 语句:PRAGMA journal_mode = 'wal';命令源:Migrations,SQL 语句:CREATE TABLE "tb_songs" ("ID" INTEGER NOT NULL CONSTRAINT "PK_Song" PRIMARY KEY AUTOINCREMENT,"Name" TEXT NOT NULL,"Artist" TEXT NULL,"Duration" INTEGER NOT NULL
);命令源:LinqQuery,SQL 语句:SELECT "t"."ID", "t"."Artist", "t"."Duration", "t"."Name"
FROM "tb_songs" AS "t"
WHERE "t"."ID" > 2

前两个语句的命令源都是 Migrations,这是创建数据库和表时的语句(SQLite 不需要 CREATE DATABASE 语句,直接建表)。第三个就是咱们执行查询生成的 SQL 语句,可以看到命令源是 LinqQuery。

 

----------------------------------------------------------------------------------------------------------------------------------------------------------------

现在看一下第二种方案,咱们先把数据库上下文的 OnConfiguring 方法中的日志配置注释掉。

现在,咱们实现自己的命令拦截器。

拦截器的基础接口是 IInterceptor,它是个空接口,没有任何成员,仅作为标志。咱们一般不会直接实现它。

拦截命令,框架提供的是 IDbCommandInterceptor 接口,它要求你实现以下成员:

public interface IDbCommandInterceptor : IInterceptor
{// 当 DbCommand 对象(不同数据库有具体的类)被创建前触发// 这个时候是获取不到 SQL 语句的,命令对象原则上还没创建// 但是,你可以自己创建一个,并用 InterceptorResult 返回// 你要么原样返回,要么用 SuppressWithResult 静态方法自己创建一个命令对象// 这时候 EF Core 会用你创建的命令对象代替内部代码所创建的命令对象// 注意这里只是抑制内部创建命令对象而已,并不能阻止命令的执行InterceptionResult<DbCommand> CommandCreating(CommandCorrelatedEventData eventData, InterceptionResult<DbCommand> result){return result;}// 命令对象创建后,这里是 EF Core 负责创建命令对象,你负责修改// 不修改就原样返回。此时,你不能自己 new 命令对象了,只能修属性
    DbCommand CommandCreated(CommandEndEventData eventData, DbCommand result){return result;}// 这里可以获取到 SQL 了
    DbCommand CommandInitialized(CommandEndEventData eventData, DbCommand result)=> result;// 和前面的命令对象一样,这里你可以用自己创建的 DataReader 代替框架内部创建的// 这是有查询结果的 reader,比如 SELECT 语句// 此时还没有执行 SQLInterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)=> result;// 查询单个标量值之前调用此方法,你可以自己分配一个值来代替     // 此时还没执行 SQL 语句InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)=> result;// 执行无结果查询前触发,你可以自己弄一个结果值覆盖真实查询的结果// 此时还没有执行 SQL 语句InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)=> result;// 异步版本ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command,CommandEventData eventData,InterceptionResult<DbDataReader> result,CancellationToken cancellationToken = default)=> new(result);// 异步版本ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command,CommandEventData eventData,InterceptionResult<object> result,CancellationToken cancellationToken = default)=> new(result);// 异步版本ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command,CommandEventData eventData,InterceptionResult<int> result,CancellationToken cancellationToken = default)=> new(result);// 以下是 SQL 语句执行完毕,且从数据库返回结果,但你仍可以处理这些结果
    DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)=> result;object? ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object? result)=> result;int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result)=> result;// 以下是异步版本ValueTask<DbDataReader> ReaderExecutedAsync(DbCommand command,CommandExecutedEventData eventData,DbDataReader result,CancellationToken cancellationToken = default)=> new(result);ValueTask<object?> ScalarExecutedAsync(DbCommand command,CommandExecutedEventData eventData,object? result,CancellationToken cancellationToken = default)=> new(result);ValueTask<int> NonQueryExecutedAsync(DbCommand command,CommandExecutedEventData eventData,int result,CancellationToken cancellationToken = default)=> new(result);// 以下是命令被取消或执行失败后调用void CommandCanceled(DbCommand command, CommandEndEventData eventData){}Task CommandCanceledAsync(DbCommand command, CommandEndEventData eventData, CancellationToken cancellationToken = default)=> Task.CompletedTask;void CommandFailed(DbCommand command, CommandErrorEventData eventData){}Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = default)=> Task.CompletedTask;// 以下是当 dataReader 被关闭前触发
    InterceptionResult DataReaderClosing(DbCommand command, DataReaderClosingEventData eventData, InterceptionResult result)=> result;ValueTask<InterceptionResult> DataReaderClosingAsync(DbCommand command, DataReaderClosingEventData eventData, InterceptionResult result)=> new(result);// dataReader 被释放前触发// 这个最好原样返回,就算你 Suppressed 它,阻止不了连象、命令、阅读器被设为 null// Suppressed 它纯粹只是在设为 null 前不调用 Dispose 方法罢了
    InterceptionResult DataReaderDisposing(DbCommand command, DataReaderDisposingEventData eventData, InterceptionResult result)=> result;
}

你看看,我只是想拦截某个操作,却要实现这么多方法,这太不像话了。为了你觉得像话,EF Core 提供了一个抽象类,叫 DbCommandInterceptor,它会以默认行为实现 IDbCommandInterceptor 接口。这样你就轻松了,想修改哪个操作,只要重写某个方法成员就好了。

这里,咱们要获取 SQL 语句,只有在 CommandInitialized 时 SQL 语句才被设置,所以重写 CommandInitialized 方法。

public class MyCommandInterceptor : DbCommandInterceptor
{public override DbCommand CommandInitialized(CommandEndEventData eventData, DbCommand result){// 只获取 LINQ 查询生成的 SQL 语句if (eventData.CommandSource == CommandSource.LinqQuery){// 第一种方法Console.WriteLine($"\nLog Command:\n{eventData.LogCommandText}");// 第二种方法Console.WriteLine($"DB Command:\n{result.CommandText}\n");// 第三种方法//Console.WriteLine($"From Event Data:\n{eventData.Command.CommandText}\n");
        }return base.CommandInitialized(eventData, result);}
}

其实传入方法的参数里面有些对象是重复的,所以你有多个方法来获取 SQL。eventData.Command 其实就是 result 参数所引用的对象,所以随便哪个的 CommandText 属性都能获取 SQL 语句;另外,eventData 的 LogCommandText 属性也是 SQL 语句。这些方法你随便选一个。

上述代码中,老周用 CommandSource.LinqQuery 进行判断,咱们只记下由 LINQ 查询生成的 SQL 语句。

现在回到数据库上下文类,在 OnConfiguring 方法中添加刚刚弄好的拦截器。

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlite("data source=demo.db");optionsBuilder.AddInterceptors(new MyCommandInterceptor());}

调用 AddInterceptors 方法,把你想要添加的拦截器实例扔进去就完事了。

再次运行程序,控制台输出以下内容:

Log Command:
SELECT "t"."ID", "t"."Artist", "t"."Duration", "t"."Name"
FROM "tb_songs" AS "t"
WHERE "t"."ID" > 2
DB Command:
SELECT "t"."ID", "t"."Artist", "t"."Duration", "t"."Name"
FROM "tb_songs" AS "t"
WHERE "t"."ID" > 2

 

好了,今天的内容就到这里完毕了,下次老周继续聊 EF Core。这是老周的习惯,抓住一个主题聊他个天荒地老。

 

http://www.jsqmd.com/news/56842/

相关文章:

  • ubuntu_12.04_tftp的安装与设置
  • 2025年仓储货架厂家综合实力排行榜:三阳货架领跑行业
  • 2025年知名的企业网站建设价格与服务对照表
  • 组装成树
  • AI生成代码系列:在不干扰开发者体验的情况下集成开源代码片段检测
  • 2025 武汉文化课集训学校权威推荐榜单
  • 2025年国标mpp电力管直销厂家权威推荐榜单:mpp电力管/mpp电缆保护管/mpp电力管穿线管源头厂家精选
  • 2025年密封垫片生产厂家联系方式完整汇总:全国重点企业官方联系方式与高效采购指引
  • 标题:2025年密封垫片生产厂家联系方式完整汇总:全国重点企业官方联系方式与高效采购指引
  • 2025年评价高的多功能移动餐车十大热门品牌推荐榜
  • 波束赋形MATLAB代码实现
  • 2025年真空袋厂家联系电话完整汇总:全国重点产区企业联系方式及高效采购指引
  • 2025年知名的南通家纺/设计感家纺高品质好评榜单
  • 2025年聚氨酯发泡保温厂家联系电话完整汇总:全国重点企业和官方联系方式高效合作指引
  • 2025年真空袋厂家联系电话完整汇总:全国重点企业官方联系方式与高效采购指南
  • 2025年线上1对1记单词课程电话联系方式汇总:全国主流品牌官方联系方式与高效选课指引
  • 2025年聚氨酯发泡保温厂家联系电话完整汇总:全国重点企业官方联系方式与高效合作指引
  • 2025年知名的卷制轴承/复合轴承行业内口碑厂家排行榜
  • 2025年湖南省长沙打井管厂家权威推荐榜单:打井管生产厂家联系方式‌/打井管厂家有哪些‌/打井管厂家‌源头厂家精选
  • 2025年江苏钢球厂家哪家好?十大钢球制造企业价格合理排行榜
  • Asp.net 后端Api和SPA(单页面应用)整合
  • 2025国内战略咨询公司哪家好?优质品牌咨询公司推荐
  • 2025年12月北京陪诊公司推荐榜:专业机构对比与选择指南
  • 2025年比较好的铸石板衬板/铸石板耐磨用户口碑最好的厂家榜
  • 2025年11月绩效管理咨询机构排行:专业服务深度评测与选择指南
  • 2025年比较好的被动边坡防护网行业内知名厂家排行榜
  • BepInEx:强大的Unity游戏插件框架
  • 2025年北京助听器验配机构联系电话完整汇总: 北京主要区域官方联系方式与专业验配指引
  • 2025年北京助听器验配机构联系电话汇总: 覆盖全市重点区域官方联系方式与专业验配指引
  • 2025年靠谱的干法选煤设备/环保选煤设备厂家推荐及选择指南