惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
F
Fortinet All Blogs
Cisco Talos Blog
Cisco Talos Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Secure Thoughts
美团技术团队
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
月光博客
月光博客
T
Tor Project blog
P
Privacy & Cybersecurity Law Blog
Recorded Future
Recorded Future
I
Intezer
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
GbyAI
GbyAI
罗磊的独立博客
V
V2EX
Google DeepMind News
Google DeepMind News
D
DataBreaches.Net
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
A
About on SuperTechFans
Scott Helme
Scott Helme
Vercel News
Vercel News
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
Recent Announcements
Recent Announcements
Hacker News: Ask HN
Hacker News: Ask HN
C
CERT Recently Published Vulnerability Notes
G
Google Developers Blog
B
Blog
博客园 - 叶小钗
WordPress大学
WordPress大学
博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Jina AI
Jina AI
IT之家
IT之家
C
Cybersecurity and Infrastructure Security Agency CISA
P
Palo Alto Networks Blog
小众软件
小众软件
博客园 - Franky
Microsoft Azure Blog
Microsoft Azure Blog
AWS News Blog
AWS News Blog

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
记通过 EF 生成不同数据库 SQL 脚本的一次尝试 - 元视角
飞鸿踏雪 · 2018-09-17 · via 元视角

接触新项目有段时间了,如果让我用一句话来形容此刻的感受,大概就是**“痛并快乐着”。痛苦之一是面对 TFS,因为它的分支管理实在是一言难尽,无时无刻不在体验着人肉合代码的“趣味”。而痛苦之二是同时维护三套数据库的脚本,这让我想到一个梗,在讲到设计模式的时候,一个常常被提到的场景是,怎么样从设计上支持不同数据库的切换。我想,这个问题是非常容易回答的,真正的问题是我们真的需要切换数据库吗?原谅我的年少无知,我们的产品因为要同时支持公有云和私有化部署,所以在数据库的选择上,覆盖到了主流 MySQL、Oracle 和 SQL Server,这直接导致我们要维护三套数据库的脚本,你说这样子能不痛苦吗?而快乐的地方在于,终于有机会在一个有一定用户体量的产品上参与研发,以及从下周开始我们将从 TFS 切换到 Git。好了,今天这篇文章的主题是,通过 EF 来生成不同数据库的 SQL 脚本,这是痛苦中的一次尝试,所谓“痛并快乐着”**。

基本原理

我们知道数据库和面向对象这两者间存在着天然阻抗,这是因为两者在事物的认知上存在差异,数据库关注的是二维表、是集合间的关系,而面向对象关注的是封装、是细节的隐藏,所以,不管到什么时候,这两者都只能以某种尴尬的方式共存,SQL 执行效率高,这是以牺牲可读性为代价的; ORM 迎合了面向对象,这是以牺牲性能为代价的,所以,即使到了今天,关于 SQL 和 ORM 的争论从来没有停止过,甚至写 SQL 的人不知不觉间“造”出了 ORM,而使用 ORM 的人有时需要 SQL。所以,面对这样一个需要同时维护三套数据库脚本的工作,我个人倾向于用工具去生成,或许是出于程序员对“懒”这种美德的极致追求,或许是出于我对 SQL 这种“方言”天生的排斥,总而言之,我不是很喜欢手写 SQL 除非特别必要,因为它和正则一样,只有写得人懂它真正的含义。

那么,说到这里,我们就知道了一件事情,ORM 可以帮助我们生成 SQL,所以,我们为什么不让它帮我们生成不同数据库的 SQL 脚本呢?虽然 ORM 的性能总是为人所诟病,因为它严格遵循某种规则,所以注定做不到像人类一样“灵活”。我们始终认为不“灵活”的就是“笨拙”的,可即便如此 ORM 生成的 SQL 依然比人类写得要好看。故而,我们的思路是,在 ORM 生成 SQL 语句的时候将其记录下来,然后按照一定规则生成不同数据库的脚本。毕竟 SQL 语言更接近“方言”,每一种数据库的 SQL 脚本都存在着细微的差别。所以,后来人们不得不发明 T-SQL,可任何东西归根结底不都是权力和利益带来的附属品吗?人类为了互相竞争而形成差异化,可当一切差异都不甚明显时,最终又不得不花费精力来解决这些差异。可一个只有垄断存在的世界,除了让人想起 1984 里的 Big Brother 以外,还能想起什么呢?

尝试过程

好了,顺着这个思路,我们就会想到在 ORM 中添加拦截器或者是日志的方式,来获得由 ORM 生成的 SQL 语句,这里我们以 Entity Framework(以下简称 EF)为例,这是.NET 中最常见的 ORM,因为目前官方的 Web 开发框架有 ASP.NET 和 ASP.NET Core 两个版本,所以这里我们分别以 ASP.NET 和 ASP.NET Core 为例来说明具体的实现过程,相应地,我们分别使用了 EF6 和 EF Core 作为各自的 ORM。

EF6

对于 EF6,我们可以通过继承DbCommandInterecptor类来编写一个拦截器。而在拦截器中重写相应的方法,就可以对数据库中的常见操作(CURD)进行拦截。所以,根据这个思路,我们会联想到,通常数据库迁移都是针对“写”这个操作,因此,我们的想法是记录 INSERT 和 UPDATE 两种 SQL 语句。这里我们通过下面的示例来验证这个想法,需要说明的是,本文中所有数据库相关的示例,均采用 Code First 的方式来创建。

public class SQLGenInterceptor : DbCommandInterceptor
{
    public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
        var sqlText = FormatSQL(command);
        Log.Info(sqlText);
    }

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        var sqlText = FormatSQL(command);
        Log.Info(sqlText);
    }

    public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        var sqlText = FormatSQL(command);
        Log.Info(sqlText);
    }

    private string FormatSQL(DbCommand command)
    {
        var sqlText = command.CommandText;
        foreach (DbParameter sqlParam in command.Parameters)
        {
            sqlText = sqlText.Replace(sqlParam.ParameterName,            sqlParam.Value.ToString());
        }

        return sqlText;
    }
}

在这个示例中,我们使用 NLog 来记录由 EF 生成的 SQL 语句,可以注意到它比我们想象中的要稍微复杂些,所以,人们说 ORM 性能差并不是没有道理。可当你见过那些由人手写出的天书一般的 SQL 语句后,也许两者在可读性上来说不过是五十步笑百步。实际上 EF 生成的 SQL 是一种叫做 T-SQL 的东西,你可以把它理解为一种标准的 SQL 语言。譬如在 PowerBuilder 这个数据库建模软件中,我们可以通过 T-SQL 转换出主流数据库的 SQL 语句。博主在工作中需要维护三套 SQL 脚本,而这些脚本间细小的语法差异,就变成了这个过程中最难忘的记忆,这里我们不考虑去做语法转换的事情,因为实际上通过传入不同的连接字符串,我们就能得到不同数据库的 SQL 脚本,所以接下来的工作就交给各位了(逃……

//注入SQLGen拦截器
DbInterception.Add(new SQLGenInterceptor());
using (var context = new DataContext())
{
    context.Users.Add(new User()
    {
        UserName = "PayneQin",
        UserRole = "Administrator"
    });

    context.SaveChanges();
}

现在,我们需要将这个拦截器注册到 EF 中,注册过程非常简单,一旦拦截器注册完成,当我们在 EF 中执行相应操作的时候,就可以在日志中看到相对应的 SQ 语句了,这样我们就达到了用 EF 生成 SQL 语句的目的,虽然说这样可能还没手写来快,可它至少让你知道了,这个世界上有一种不需要手写 SQL 的可能性啊,你说对吗?

EF生成SQL语句比想象中更为复杂 EF生成SQL语句比想象中更为复杂

EF Core

对于 EF Core 来说,它并没有提供像 EF6 那样的拦截器,虽然官方曾经说过后续会做这方面的工作[摊手]……不过办法终究是人想出来的,对于 EF Core 我们可以通过注入日志的方式来实现。我们知道,微软在.NET Core 中大力地发展了依赖注入、中间件等一系列特性,所以,这对于我们这种喜欢搞事情的人来说,简直太方便了有木有啊!.NET Core 中日志注入主要集中在 ILogger、ILoggerFactory 和 ILoggerProvider 三个接口,简单来说,ILoggerFactory 是日志工厂,负责返回具体的 Logger;而 ILoggerProvider,则决定在什么情况下应该提供什么样的 Logger。最常见的两种 LoggerProvider 是 Console 和 Debug,它们分别通过 AddConsole()和 AddDebug()来注入。具体到这里,我们通过下面的方式实现:

public class SQLGenLogger : ILogger
{
    private readonly string categoryName;
    public SQLGenLogger(string categoryName) => this.categoryName = categoryName;
    public IDisposable BeginScope<TState>(TState state) => null;
    public bool IsEnabled(LogLevel logLevel) => true;
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        Log.Info(state)
    }
}

首先定义 SQLGenLogger,顾名思义,它是用来记录生成的 SQL 语句的,同样,我们选择了 NLog。这里有一点要说明,平时我们在控制器中使用 ILogger 的时候,通常会在控制器的构造函数中注入 ILogger,一旦我们使用泛型的 ILogger 接口,Log()方法中的参数 state 实际上就是当前类型,这里和 SQL 语句相关的类型 DbCommandData,实际上是博主试出来的,因为如果不限定 ILogger中的参数 T,我们将得到所有的执行日志,显然,这不是我们想要的结果。

public class SQLGenLoggerProvider : ILoggerProvider
{
    public ILogger CreateLogger(string categoryName)
    {
        if(categoryName  == "Microsoft.EntityFrameworkCore.Database.Command")
            return new SQLGenLogger(categoryName);
        return NullLogger.Instance;
    }

    public void Dispose()
    {

    }
}

接下来来看,ILoggerProvider 接口的实现。我们说过,ILoggerProvider 接口决定在什么情况下应该提供什么样的 Logger,我们注意到它提供了一个 CreateLogger()的方法,它会根据 categoryName 来返回不同的 Logger,而参数 categoryName 实际上等价与 nameof(FooController),所以,到这里我们就会明白,为什么这里要判断 categoryName 了,它实际上起一个过滤的作用,因为我们只需要 SQL 相关的日志,它和 SQLGenLogger 中的 state 相对应,我们已经说过,这是博主试出来的。

public class DataContext : DbContext
{
    public virtual DbSet<User> Users { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var loggerFactory = new LoggerFactory();
        loggerFactory.AddProvider(new SQLGenLoggerProvider());
        //在这里注入日志工厂
        optionsBuilder.UseLoggerFactory(loggerFactory)
            .EnableSensitiveDataLogging()
            .UseSqlServer(@"Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=SQLGen.DataContext;Integrated Security=True;MultipleActiveResultSets=True;App=EntityFramework"); 
  }

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
  		modelBuilder.ApplyConfiguration(new UserTypeMap());
  		modelBuilder.Entity<User>().ToTable("Users");
  }
}

好啦,接下来就非常简单啦,我们在 DbContext 里对 EF 的 Logger 进行配置,把我们定义的 SQLGenLoggerProvider 注入到 EF 里,可以注意到,它可以如我们期望得那样,输出由 EF 生成的 SQL 脚本,这实在是有趣,Ok,打完收工!

通过注入日志获取EF生成的SQL 通过注入日志获取EF生成的SQL

本文小结

我一直相信,懒惰是工程师的一种美德,因为为了让自己有机会懒惰,你就必须要先让自己勤奋起来。我一直怕自己在舒适区里温水煮青蛙,明明一直在重复做一件事情,还要安慰自己说:“做好这一件事情一样是成功“,有时候,一味地重复自己并不见得会有太多收获,所以,就像这篇文章一样,我本来像偷懒少写一点 SQL,结果意外地发现了给数据库记录日志的方法。当有了意外收获以后,曾经的初衷到底是什么可能就没那么重要了,如“雨血”中左殇所说,当你赢了的时候,你说曾经有十成把握亦不为过。

这篇文章主要介绍如何利用 EF 来生成不同数据库的 SQL 脚本,对 EF6 来说,需要继承 DbCommandInterecptor 类编写拦截器;对于 EF Core 来说,需要注入 ILogger 来记录日志。本文的延伸之一是记录 SQL 执行日志,这一点在本文已经有所体现。本文更深层次的延伸是,在这个基础上实现数据库的主从复制、读写分离,这一点我会在下一篇博客中讲解,欢迎大家继续关注我的博客,好啦,以上就是这篇文章的全部内容啦,晚安!