我们知道,EFCore 将我们的 C# 对象查询和修改语句在后台自动翻译为 SQL 语句然后交给数据库系统执行,那么如何查看 EFCore 生成的 SQL 语句呢?下面介绍几种方法。
Migration SQL
在 Update-Database 时,EFCore 会将当前数据库更新或回滚到程序员指定的 Migration,这也是通过生成 SQL 语句并执行而完成的。使用 Script-Migration 命令可以获取背后的 SQL 脚本。
在无参数的情况下,Script-Migration 生成从零开始到最新 Migration 的 SQL 语句,在控制台执行下面命令:
Script-Migration
EFCore 会生成一个脚本文件,类似这样:
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL BEGIN CREATE TABLE [__EFMigrationsHistory] ( [MigrationId] nvarchar(150) NOT NULL, [ProductVersion] nvarchar(32) NOT NULL, CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) ); END; GO BEGIN TRANSACTION; GO CREATE TABLE [T_Authors] ( [Id] int NOT NULL IDENTITY, [Name] nvarchar(max) NULL, CONSTRAINT [PK_T_Authors] PRIMARY KEY ([Id]) ); GO CREATE TABLE [T_Books] ( [Id] bigint NOT NULL IDENTITY, [Title] nvarchar(50) NOT NULL, [AuthorId] int NOT NULL, [Time] datetime2 NOT NULL, [Price] float NOT NULL, CONSTRAINT [PK_T_Books] PRIMARY KEY ([Id]), CONSTRAINT [FK_T_Books_T_Authors_AuthorId] FOREIGN KEY ([AuthorId]) REFERENCES [T_Authors] ([Id]) ON DELETE CASCADE ); GO CREATE INDEX [IX_T_Books_AuthorId] ON [T_Books] ([AuthorId]); GO INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'20220804153349_init', N'5.0.17'); GO COMMIT; GO BEGIN TRANSACTION; GO CREATE TABLE [T_Stores] ( [Id] uniqueidentifier NOT NULL, [Name] nvarchar(max) NULL, CONSTRAINT [PK_T_Stores] PRIMARY KEY ([Id]) ); GO INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'20220805071641_AddStore', N'5.0.17'); GO COMMIT; GO
由于我用的是 SQLServer,所以这里生成的是 SQLServer 的脚本,从脚本可以看出它先创建了 __EFMigrationsHistory 这个表(用于储存当前数据库的 Migration 状态)以及各个实体的表,所有的实体配置都被翻译为了 SQL 语句。这个 SQL 脚本可以拿到 SQLServer 里执行,和我们执行 Update-Database 的效果是一样的。
为什么有了 Update-Database 还要生成 SQL 脚本呢?在实际项目中,程序员可能是在开发服务器而非生产服务器中开发的,这个时候 Update-Database 命令可以在开发服务器中使用,但到最终提交到生成服务器前,可能需要交给 DBA 审核,而程序员也不一定拥有生产服务器的权限,那么自然不可能在生产服务器中执行 Update-Database。所以需要 Script-Migration 命令来生成 SQL 脚本,在生产服务器中执行,这样才能保证安全性。
但生产服务器中自然不需要从零开始创建数据库,我们需要从当前数据库状态到最新的 Migration 的 SQL 脚本,这个时候就需要给命令增加参数了。
Script-Migration <OldMigrationName> <NewMigrationName>
这个命令从生成一个从名为 <OleMigrationName> 的 Migration 到 名为<NewMigrationName> 的 Migration 的 SQL 脚本。也可以省略后面的 <NewMigrationName> 参数,这样默认生成到最新 Migration 的 SQL 脚本。
注意,Script-Migraion 只是生成脚本,并不会执行它。
对象操作 SQL
在对数据库进行增删查改时,EFCore 也会将我们的 C# 代码翻译为 SQL 语句,如何获取呢?有几种方法,如果是 SQLServer,可以使用 SQLServer Profiler 工具,这个工具会监视该数据库所有执行的脚本,EFCore 生成的自然可以查看了。例如下面的 C# 代码:
var book = ctx.Books.Single(e => e.Title == "西游记"); book.Price *= 0.8; ctx.SaveChanges();
在 SQLServer Profiler 里查看 SQL 语句:
SELECT TOP(2) [t].[Id], [t].[AuthorId], [t].[Price], [t].[Time], [t].[Title] FROM [T_Books] AS [t] WHERE [t].[Title] = N'西游记' exec sp_executesql N'SET NOCOUNT ON; UPDATE [T_Books] SET [Price] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT; ',N'@p1 bigint,@p0 float',@p1=1,@p0=160
这里 EFCore 生成了两句 SQL,第一句是 SELECT 语句,对应 C# 代码里的 Single 方法,而第二句是一个 UPDATE 语句,对应 C# 代码里修改 Price 的那一行。
当然,EFCore 也并不一定要使用 SQLServer,如果是别的数据库,比如 MySQL 的话,没有工具的话怎么查看 SQL 语句呢?这个时候就要使用代码来获取了。在 EFCore 执行过程中,会顺带输出日志,在这个日志里会显示出生成了什么 SQL 语句,但默认情况下这个日志我们是看不到的。如果需要,那么可以让 EFCore 增加日志输出,可以使用 LoggerFactory。在DbContext 作如下设置:
public static readonly ILoggerFactory logger = LoggerFactory.Create(builder => builder.AddConsole()); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string connStr = @"Data source=localhost;database=Test;Integrated Security=SSPI;"; base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer(connStr);
//此处使用 LoggerFactory optionsBuilder.UseLoggerFactory(logger); }
这个时候再执行刚刚的 C# 代码就能看到 SQL 语句了:
使用 LoggerFactory 的方法是比较推荐的,毕竟 Logger 可以通过依赖注入来获取,而且这样做也不会受限于数据库。还有一种方法来输出日志的,就是通过 optionsBuilder.LogTo 方法,它接受一个委托参数,这个委托有一个字符串参数,EFCore 会将生成的日志传入该委托。例如:
optionsBuilder.UseSqlServer(connStr); //optionsBuilder.UseLoggerFactory(logger); optionsBuilder.LogTo(s => Console.WriteLine(s));
这个时候会通过 Console.WriteLine 输出日志,但是这里得到的日志是完整日志,很多细节都输出了,其中一部分:
dbug: 2022/8/5 18:12:08.096 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT TOP(2) [t].[Id], [t].[AuthorId], [t].[Price], [t].[Time], [t].[Title] FROM [T_Books] AS [t] WHERE [t].[Title] = N'西游记' info: 2022/8/5 18:12:08.140 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) Executed DbCommand (48ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT TOP(2) [t].[Id], [t].[AuthorId], [t].[Price], [t].[Time], [t].[Title] FROM [T_Books] AS [t] WHERE [t].[Title] = N'西游记'
除了使用日志来查看 SQL 语句,还可以通过 IQueryable 接口,因为 IQueryable 接口有一个叫 ToQueryString 的扩展方法,可以输出 SQL 语句,Where 语句就是返回 IQueryable 的。比如:
IQueryable<Book> book = ctx.Books.Where(e => e.Title == "西游记"); Console.WriteLine(book.ToQueryString());
这样的方法只能用于查看查询操作的 SQL 语句,而修改、删除是没有办法的。在开发中,使用这个方法可以便捷地获取一个查询操作的 SQL 语句,而且并没有在数据库中执行它。