文章目录

  • 重温ADO.NET
  • 实现DynamicRow
  • 实现参数化查询
  • 本文小结

这段时间在维护一个“遗产项目”,体验可以说是相当地难受,因为它的数据持久化层完全由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访问数据库通常会是下面的画风。博主相信,大家在各种各样的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)这样的写法,这简直就是成心不想让后面维护的人好过,因为加字段的时候要小心翼翼地,确保字段顺序不会被修改。明明这个世界上有Dapper、SqlSugar、SmartSql这样优秀的ORM存在,为什么就要如此执著地写这种代码呢?是觉得MyBatis在XML里写SQL语句很时尚吗?

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

实现DynamicRow

通过阅读Dapper的源代码,我们知道,Dapper中用DapperTable和DapperRow替换掉了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拼接字符串一样,一味地吐槽是没有什么用的,重要的是你会选择怎么做,所以,后来我向大家推荐了Linquid,2021年已经来了,希望你不只是增长了年龄和皱纹,晚安!

温故而知新,由ADO.NET与Dapper所联想到的相关推荐

  1. 由QQ文件中转站超快速上传联想到

    今天向QQ文件中转站上传了两个文件,一个30M,一个60M,都发现上传时间仅为1s, 猜想了一下,这里应该是有算法的,如果服务器上以及同样的文件了,就不需要用户再上传,而是直接给出文件链接,这样既减轻 ...

  2. 由梅西控球助攻联想到的......

    2022年卡塔尔世界杯第一场半决赛中,梅西再次展现超强能力,在中场边路得球后,突破本届杯赛表现最好的中卫之一格瓦迪奥尔的防守,禁区内底线回传阿尔瓦雷斯破门. 控制球,假动作,灵活转弯,速度快,看准时机 ...

  3. mybatis框架中的queryWrapper的or查询,联想到MySQL中and 和or的关系

    统计的原生SQL应该是这样的: SELECTCOUNT( 1 ) FROMtable_name WHERE( id = '679135XXXXXXX1212' AND ( STATUS = 1 OR ...

  4. 由铁路订票系统联想到的

    作为一个互联网初哥这样级别飙升的流量,居然还能让大把的人定到票,可想而知:要么是所谓的高流量网站技术太简单:要么是铁道部信息技术中心太NB.真相到底是什么呢?我个人以为,两边都沾着那么一点. 中国铁路 ...

  5. 由return联想到的

    大多数函数都包含一条return 语句,return 语句导致函数停止执行.使函数停止执行的还有break,throw,他们叫强制跳转语句. return语句使函数停止执行, 如果代码块里为 retu ...

  6. 由胡润百富榜联想到的

    国庆之前中国2012胡润百富榜出炉了,前四甲分别是宗庆后(饮料)800亿元,王健林(地产)650亿元,李彦宏(搜索引擎)510亿元,严彬(地产)500亿元,但是中国大陆的前四远远比不上中国香港的前四甲 ...

  7. 实现暂停一秒输出的效果_从暂停游戏联想到的

    你希望玩家按下暂停键时就可以暂停游戏,如果你使用 Unity,最关键的的操作就是把 Time.timeScale 设置为 0.后来,你又希望应用程序失去焦点时也能暂停游戏,这时聪明的你想到一个问题:那 ...

  8. 从“梁漱溟:思考问题有八层境界”所联想到的

    最近一段时间以来写的文章比较少了,这固然是有一些客观原因,但确实有我不可说的一些自我反省和认识等主观因素.记得8月初有一次友人聚餐,席间有朋友聊到公众号的运营心得体会,其中有一条是:避免粉丝减少的黄金 ...

  9. 由项目中的一个小问题所联想到的。

    项目经理安排把项目中涉及到人民币的全部保留小数点后两位,自己就用 select sum(Convert(decimal(19,2),oop)) as total from test 去做,功能是实现了 ...

最新文章

  1. php 事件调度,mysql数据库事件调度(Event)
  2. 孙正义:互联网流量将转化为智能AI流量,我的时代终于来了
  3. Word提供的【样式和格式】设计!
  4. ylb:表的结构的修改和基本约束
  5. JDK 12新闻(2018年9月13日)
  6. 总结mysql的基础语法_mysql 基础sql语法总结 (二)DML
  7. TCP、UDP(网络协议:传输层协议)
  8. HTML5 绘制动画
  9. 初学者python笔记(map()函数、reduce()函数、filter()函数、匿名函数)
  10. CentOS 6.5 最小化安装zabbix
  11. .git文件夹_如何使用git把本地代码上传(更新)到github上
  12. TCP,UDP,IP数据包格式详解
  13. 把“友商”装进芯里威联通运行黑群晖最新DSM系统
  14. 网站漏洞测试 关于webshell木马后门检测
  15. 蓝牙技术|智能蓝牙芯片助力元宇宙发展
  16. 服务器迁移域名和证书要改什么用,服务器数据迁移方案介绍 怎样更换网站域名?...
  17. 深度丨银行零售客群策略与标签体系搭建指南
  18. 米老师解惑----1
  19. 图文:Linux-DNS主备服务器搭建(高可用)
  20. 采用Cartographer、LIO-SAM构建三维点云地图,采用Octomap构建八叉树地图(三维栅格地图)

热门文章

  1. getline函数解析
  2. 伸出你的仙人掌,接电话吧铃声 伸出你的仙人掌,接电话吧手机...
  3. ELK本地(win10)搭建
  4. 现在做什么行业好一点?
  5. Qt-FFmpeg开发-视频播放(5)
  6. 」卢俊义见了越怒 水浒传
  7. PCADV 117期、4月1日出刊:拒绝移动炸弹,14款移动电池拆解实测
  8. ETHBMC: A Bounded Model Checker for Smart Contracts
  9. 易买网更多新闻代码_新闻 | 0528李东健赵伦熙离婚等更多资讯
  10. python怎么恢复默认设置_centos 下怎么恢复为默认的python版本