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

推荐订阅源

量子位
小众软件
小众软件
S
SegmentFault 最新的问题
人人都是产品经理
人人都是产品经理
博客园 - 【当耐特】
博客园 - 三生石上(FineUI控件)
C
Check Point Blog
S
Schneier on Security
Microsoft Azure Blog
Microsoft Azure Blog
N
Netflix TechBlog - Medium
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI
罗磊的独立博客
有赞技术团队
有赞技术团队
V
V2EX
Y
Y Combinator Blog
博客园 - 叶小钗
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
F
Fortinet All Blogs
W
WeLiveSecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Stack Overflow Blog
Stack Overflow Blog
The Cloudflare Blog
S
Security @ Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
PCI Perspectives
PCI Perspectives
H
Heimdal Security Blog
Schneier on Security
Schneier on Security
Security Latest
Security Latest
AWS News Blog
AWS News Blog
月光博客
月光博客
Security Archives - TechRepublic
Security Archives - TechRepublic
Recent Announcements
Recent Announcements
Google DeepMind News
Google DeepMind News
博客园 - Franky
Cisco Talos Blog
Cisco Talos Blog
T
Threat Research - Cisco Blogs
M
MIT News - Artificial intelligence
T
Troy Hunt's Blog
N
News and Events Feed by Topic
Cloudbric
Cloudbric
Scott Helme
Scott Helme
云风的 BLOG
云风的 BLOG
Attack and Defense Labs
Attack and Defense Labs

元视角

.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 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 视频是不能 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 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角
飞鸿踏雪 · 2020-12-30 · via 元视角

这段时间在维护一个“遗产项目”,体验可以说是相当地难受,因为它的数据持久化层完全由 ADO.NET 纯手工打造,所以,你可以在项目中看到无所不在的 DataTable,不论是读操作还是写操作。这个 DataTable 让我这个习惯了 Entity Framework 的人感到非常别扭,我并不排斥写手写 SQL 语句,我只是拥有某种自觉并且清醒地知道,自己写的 SQL 语句未必就比 ORM 生成的 SQL 语句要好。可至少应该是像 Dapper 这种程度的封装啊,因为关系型数据库天生就和面向对象编程存在隔离,所以,频繁地使用 DataTable 无疑意味着你要写很多的转换的代码,当我看到DbConnectionDbCommandDbDataReaderDbDataAdapter这些熟悉的“底层”的时候,我意识到我可以结合着 Dapper 的实现,从中梳理出一点改善的思路,所以,这篇博客想聊一聊ADO.NETDapperDynamic这三者间交叉的部分,希望能给大家带来新的启发。

重温 ADO.NET

相信大家都知道,我这里提到的DbConnectionDbCommandDbDataReaderDbDataAdapte以及DataTableDataSet,实际上就是 ADO.NET 中核心的组成部分,譬如DbConnection负责管理数据库连接,DbCommand负责 SQL 语句的执行,DbDataReaderDbDataAdapter负责数据库结果集的读取。需要注意的是,这些类型都是抽象类,而各个数据库的具体实现,则是由对应的厂商来完成,即我们称之为“驱动”的部分,它们都遵循同一套接口规范,而DataTableDataSet则是“装”数据库结果集的容器。关于 ADO.NET 的设计理念,可以从下图中得到更清晰的答案:

ADO.NET架构 ADO.NET架构

在这种理念的指引,使用 ADO.NET 访问数据库通常会是下面的画风。博主相信,大家在各种各样的DbHelper或者DbUtils中都见过类似的代码片段,在更复杂的场景中,我们会使用DbParameter来辅助DbCommand,而这就是所谓的SQL 参数化查询

var fileName = Path.Combine(Directory.GetCurrentDirectory(), "Chinook.db");
using (var connection = new SQLiteConnection($"Data Source={fileName}"))
{
    if (connection.State != ConnectionState.Open) connection.Open();
    using (var command = connection.CreateCommand())
    {
        command.CommandText = "SELECT AlbumId, Title, ArtistId FROM [Album]";
        command.CommandType = CommandType.Text;

        //套路1:使用DbDataReader读取数据
        using (var reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                //各种眼花缭乱的写法:)
                Console.WriteLine($"AlbumId={reader.GetValue(0)}");
                Console.WriteLine($"Title={reader.GetFieldValue<string>("Title")}");
                Console.WriteLine($"ArtistId={reader.GetInt32("ArtistId")}");
            }
        }

        //套路2:使用DbDataAdapter读取数据
        using (var adapter = new SQLiteDataAdapter(command))
        {
            var dataTable = new DataTable();
            adapter.Fill(dataTable);
        }
    }
}

这里经常会引发的讨论是,DbDataReaderDbDataAdapter的区别以及各自的使用场景是什么?简单来说,前者是按需读取/只读,数据库连接会一直保持;而后者是一次读取,数据全部加载到内存,数据库连接用完就会关掉。从资源释放的角度,听起来后者更友好一点,可显然结果集越大占用的内存就会越多。而如果从易用性上来考虑,后者可以直接填充数据到DataSet或者DataTable,前者则需要费一点周折,你看这段代码是不是有点秀操作的意思:

//各种眼花缭乱的写法:)
Console.WriteLine($"AlbumId={reader.GetValue(0)}");
Console.WriteLine($"Title={reader.GetFieldValue<string>("Title")}");
Console.WriteLine($"ArtistId={reader.GetInt32("ArtistId")}");

在这个“遗产项目”中,DbDataReaderDbDataAdapter都有所涉猎,后者在结果集不大的情况下还是可以的,唯一的遗憾就是DataTableLINQ的违和感实在太强烈了,虽然可以勉强使用AsEnumerable()拯救一下,而前者就有一点魔幻了,你能看到各种GetValue(1)GetValue(2)这样的写法,这简直就是成心不想让后面维护的人好过,因为加字段的时候要小心翼翼地,确保字段顺序不会被修改。明明这个世界上有DapperSqlSugarSmartSql这样优秀的 ORM 存在,为什么就要如此执著地写这种代码呢?是觉得 MyBatis 在 XML 里写 SQL 语句很时尚吗?

所以,我开始尝试改进这些代码,我希望它可以像 Dapper 一样,提供Query<T>()Execute()两个方法足矣!如果要把结果集映射到一个具体的类型上,大家都能想到使用反射,我更想实现的是 Dapper 里的DapperRow,它可以通过“·”或者字典的形式来访问字段,现在的问题来了,你能实现类似 Dapper 里 DapperRow 的效果吗?因为想偷懒的时候,dynamic 不比 DataRow 更省事儿吗?那玩意儿光转换类型就要烦死人了,更不用说要映射到某个 DTO 啦!

实现 DynamicRow

通过阅读 Dapper 的源代码,我们知道,Dapper 中用DapperTableDapperRow替换掉了 DataTable 和 DataRow,可见这两个玩意儿有多不好用,果然,英雄所见略同啊,哈哈哈!其实,这背后的一切的功臣是IDynamicMetaObjectProvider,通过这个接口我们就能实现类似的功能,我们熟悉的ExpendoObject就是最好的例子:

dynamic person = new ExpandoObject(); 
person.FirstName = "Sherlock"; 
person.LastName = "Holmes";

//等价形式
(person as IDctionary<string, object>)["FirstName"] = "Sherlock";
(person as IDctionary<string, object>)["LastName"] = "Holmes";

这里,我们用一种简单的方式,让 DynamicRow 继承者 DynamicObject,下面一起来看具体的代码:

public class DynamicRow : DynamicObject
{
    private readonly IDataRecord _record;
    public DynamicRow(IDataRecord record)
    {
        _record = record;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var index = _record.GetOrdinal(binder.Name);
        result = index > 0 ? _record[binder.Name] : null;
        return index > 0;
    }
        
    //支持像字典一样使用
    public object this[string field] =>
       _record.GetOrdinal(field) > 0 ? _record[field] : null;
}

对于DynamicObject这个类型而言,里面最重要的两个方法其实是TryGetMember()TrySetMember(),因为这决定了这个动态对象的读和写两个操作。因为我们这里不需要反向地去操作数据库,所以,我们只需要关注TryGetMember()即可,一旦实现这个方法,我们就可以使用类似foo.bar这种形式访问字段,而提供一个索引器,则是为了提供类似foo["bar"]的访问方式,这一点同样是为了像 Dapper 看齐,无非是 Dapper 的 DynamicRow 本来就是一个字典!

现在,我们来着手实现一个简化版的 Dapper,给IDbConnection这个接口扩展出Query<T>()Execute()两个方法,我们注意到Query<T>()需要用到DbDataReader或者DbDataAdapter其一,对于DbDataAdapter而言,它的实现完全由具体的子类决定,所以,对于IDbConnection接口而言,它完全不知道对应的子类是什么,此时,我们只能通过判断IDbConnection的类型来返回对应的 DbDataAdapter。读过我之前博客的朋友,应该对 Dapper 里的数据库类型的字典有印象,不好意思,这里历史要再次上演啦!

public static IEnumerable<dynamic> Query(this IDbConnection connection, string sql, 
  object param = null, IDbTransaction trans = null)
{
    var reader = connection.CreateDataReader(sql);
    while (reader.Read())
        yield return new DynamicRow(reader as IDataRecord);
}

public static IEnumerable<T> Query<T>(this IDbConnection connection, string sql,
  object param = null, IDbTransaction trans = null) 
  where T : class, new()
{
    var reader = connection.CreateDataReader(sql);
    while (reader.Read())
        yield return (reader as IDataRecord).Cast<T>();
}

这里的CreateDataReader()Cast()都是博主自定义的扩展方法:

private static IDataReader CreateDataReader(this IDbConnection connection, string sql)
{
    var command = connection.CreateCommand();
    command.CommandText = sql;
    command.CommandType = CommandType.Text;
    return command.ExecuteReader();
}

private static T Cast<T>(this IDataRecord record) where T:class, new()
{
    var instance = new T();
    foreach(var property in typeof(T).GetProperties())
    {
        var index = record.GetOrdinal(property.Name);
        if (index < 0) continue;
        var propertyType = property.PropertyType;
        if (propertyType.IsGenericType && 
          propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            propertyType = Nullable.GetUnderlyingType(propertyType);
        property.SetValue(instance, 
          Convert.ChangeType(record[property.Name], propertyType));
    } 

    return instance;  
}

Execute()方法则要简单的多,因为从IDbConnectionIDbCommand的这条线,可以直接通过CreateCommand()来实现:

public static int Execute(this IDbConnection connection, string sql, 
  object param = null, IDbTransaction trans = null)
{
    var command = connection.CreateCommand();
    command.CommandText = sql;
    command.CommandType = CommandType.Text;
    return command.ExecuteNonQuery();
}

实现参数化查询

大家可以注意到,我这里的参数 param 完全没有用上,这是因为IDbCommandParaneters属性显然是一个抽象类的集合。所以,从IDbConnection的角度来看这个问题的时候,它又不知道这个参数要如何来给了,而且像 Dapper 里的参数,涉及到集合类型会存在INNOT IN以及批量操作的问题,比普通的字符串替换还要稍微复杂一点。如果我们只考虑最简单的情况,它还是可以尝试一番的:

private static void SetDbParameter(this IDbCommand command, object param = null)
{
    if (param == null) return;
    if (param is IDictionary<string, object>)
    {
        //使用字典作为参数
        foreach (var arg in param as IDictionary<string, object>)
        {
              var newParam = command.CreateParameter();
              newParam.ParameterName = $"@{arg.Key}";
              newParam.Value = arg.Value;
              command.Parameters.Add(newParam);
        }
    }
    else 
    {
        //使用匿名对象作为参数
        foreach (var property in param.GetType().GetProperties())
        {
              var propVal = property.GetValue(param);
              if (propVal == null) continue;
              var newParam = command.CreateParameter();
              newParam.ParameterName = $"@{property.Name}";
              newParam.Value = propVal;
              command.Parameters.Add(newParam);
        }
    }
}

相应地,为了能在Query<T>()Execute()两个方法中使用参数,我们需要修改相关的方法:

public static int Execute(this IDbConnection connection, string sql, 
  object param = null, IDbTransaction trans = null)
{
    var command = connection.CreateCommand();
    command.CommandText = sql;
    command.CommandType = CommandType.Text;
    command.SetDbParameter(param);
    return command.ExecuteNonQuery();
}

private static IDataReader CreateDataReader(this IDbConnection connection, string sql, 
  object param = null)
{
    var command = connection.CreateCommand();
    command.CommandText = sql;
    command.CommandType = CommandType.Text;
    command.SetDbParameter(param);
    return command.ExecuteReader();
}

现在,唯一的问题就剩下DbType@啦,前者在不同的数据库中可能对应不同的类型,后者则要面临 Oracle 这朵奇葩的兼容性问题,相关内容可以参考在这篇博客:Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决。到这一步,我们基本上可以实现类似 Dapper 的效果。当然,我并不是为了重复制造轮子,只是像从 Dapper 这样一个结果反推出相关的技术细节,从而可以串联起整个 ASO.NET 甚至是 Entity Framework 的知识体系,工作中解决类似的问题非常简单,直接通过 NuGet 安装 Dapper 即可,可如果你想深入了解某一个事物,最好的方法就是亲自去探寻其中的原理。现在基础设施越来越完善了,可有时候我们再找不回编程的那种快乐,大概是我们内心深处放弃了什么……

考虑到,从微软的角度,它鼓励我们为每一家数据库去实现数据库驱动,所以,它定义了很多的抽象类。而从 ORM 的角度来考虑,它要抹平不同数据库的差异,Dapper 的做法是给IDbConnection写扩展方法,而针对每个数据库的“方言”,实际上不管什么 ORM 都要去做这部分“脏活儿”,以前是分给数据库厂商去做,现在是交给 ORM 设计者去做,我觉得 ADO.NET 里似乎缺少了一部分东西,它需要提供一个 IDbAdapterProvider 的接口,返回 IDbAdapter 接口,这样就可以不用关心它是被如何创建出来的。你看,同样是设计接口,可微软和 ServiceStack 俨然是两种不同的思路,这其中的差异,足可窥见一斑矣!实际上,Entity Framework 就是在以 ADO.NET 为基础发展而来的,在这个过程中,还是由厂商来实现对应的 Provider。此时此刻,你悟到了我所说的“温故而知新”了嘛?

本文小结

本文实则由针对 DataSet/DataTable 的吐槽而引出,在这个过程中,我们重新温习了 ADO.NET 中DbConnectionDbCommandDbDataReaderDbDataAdapter这些关键的组成部分,而为了解决 DataTable 在使用上的种种不变,我们想到了借鉴 Dapper 中的 DapperRow 来实现“动态查询”,由此引出了.NET 中实现 dynamic 最重要的一个接口:IDynamicMetaObjectProvide,这使得我们可以在查询数据库的时候返回一个 dynamic 的集合。而为了更接近 Dapper 一点,我们基于扩展方法的形式为IDbConnection编写了Query<T>()Execute()方法,在数据库读写层面上彻底终结了 DataSet/DataTable 的生命。最后,我们实现了一个简化版本的参数化查询,同样是借鉴 Dapper 的思路。这说明一件什么事情呢?当你在一个看似合理、结局固定的现状中无法摆脱的时候,“平躺”虽然能让你获得一丝喘息的机会,但与此同时,你永远失去了跳出这个层级去看待事物的机会,就像我以前吐槽同事天天用StringBuider拼接字符串一样,一味地吐槽是没有什么用的,重要的是你会选择怎么做,所以,后来我向大家推荐了Linquid2021 年已经来了,希望你不只是增长了年龄和皱纹,晚安!