请选择 进入手机版 | 继续访问电脑版

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

[复制链接]
科达工艺 发表于 2021-1-2 19:42:45 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
文章目次



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

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

在这种理念的指引,使用ADO.NET访问数据库通常会是下面的画风。博主相信,各人在各种各样的DbHelper大概DbUtils中都见过雷同的代码片断,在更复杂的场景中,我们会使用DbParameter来辅助DbCommand,而这就是所谓的SQL参数化查询
  1. 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("Title")}");                Console.WriteLine($"ArtistId={reader.GetInt32("ArtistId")}");            }        }        //套路2:使用DbDataAdapter读取数据        using (var adapter = new SQLiteDataAdapter(command))        {            var dataTable = new DataTable();            adapter.Fill(dataTable);        }    }}
复制代码
这里经常会引发的讨论是,DbDataReader和DbDataAdapter的区别以及各自的使用场景是什么?简朴来说,前者是按需读取/只读,数据库毗连会一直保持;而后者是一次读取,数据全部加载到内存,数据库毗连用完就会关掉。从资源释放的角度,听起来后者更友好一点,可显然效果集越大占用的内存就会越多。而如果从易用性上来思量,后者可以直接填充数据到DataSet大概DataTable,前者则需要费一点周折,你看这段代码是不是有点秀使用的意思:
  1. //各种眼花缭乱的写法:)Console.WriteLine($"AlbumId={reader.GetValue(0)}");Console.WriteLine($"Title={reader.GetFieldValue("Title")}");Console.WriteLine($"ArtistId={reader.GetInt32("ArtistId")}");
复制代码
在这个“遗产项目”中,DbDataReader和DbDataAdapter都有所涉猎,后者在效果集不大的情况下照旧可以的,唯一的遗憾就是DataTable和LINQ的违和感实在太强烈了,虽然可以委曲使用AsEnumerable()拯救一下,而前者就有一点魔幻了,你能看到各种GetValue(1)、GetValue(2)这样的写法,这简直就是成心不想让反面维护的人好过,因为加字段的时候要小心翼翼地,确保字段顺序不会被修改。显着这个世界上有DapperSqlSugarSmartSql这样优秀的ORM存在,为什么就要如此执著地写这种代码呢?是以为MyBatis在XML里写SQL语句很时尚吗?
所以,我开始实验改进这些代码,我希望它可以像Dapper一样,提供Query()和Execute()两个方法足矣!如果要把效果集映射到一个详细的范例上,各人都能想到使用反射,我更想实现的是Dapper里的DapperRow,它可以通过“·”大概字典的形式来访问字段,现在的问题来了,你能实现雷同Dapper里DapperRow的效果吗?因为想偷懒的时候,dynamic不比DataRow更省事儿吗?那玩意儿光转换范例就要烦死人了,更不消说要映射到某个DTO啦!
实现DynamicRow

通过阅读Dapper的源代码,我们知道,Dapper中用DapperTableDapperRow替换掉了DataTable和DataRow,可见这两个玩意儿有多欠好用,果然,英雄所见略同啊,哈哈哈!实在,这背后的一切的功臣是IDynamicMetaObjectProvider,通过这个接口我们就能实现雷同的功能,我们熟悉的ExpendoObject就是最好的例子:
  1. dynamic person = new ExpandoObject(); person.FirstName = "Sherlock"; person.LastName = "Holmes";//等价形式(person as IDctionary)["FirstName"] = "Sherlock";(person as IDctionary)["LastName"] = "Holmes";
复制代码
这里,我们用一种简朴的方式,让DynamicRow继续者DynamicObject,下面一起来看详细的代码:
  1. 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()和Execute()两个方法,我们注意到Query()需要用到DbDataReader大概DbDataAdapter其一,对于DbDataAdapter而言,它的实现完全由详细的子类决定,所以,对于IDbConnection接口而言,它完全不知道对应的子类是什么,此时,我们只能通过判断IDbConnection的范例来返回对应的DbDataAdapter。读过我之前博客的朋侪,应该对Dapper里的数据库范例的字典有印象,不盛情思,这里汗青要再次上演啦!
  1. public static IEnumerable 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 Query(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();}
复制代码
这里的CreateDataReader()和Cast()都是博主自界说的扩展方法:
  1. 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(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()方法则要简朴的多,因为从IDbConnection到IDbCommand的这条线,可以直接通过CreateCommand()来实现:
  1. 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完全没有用上,这是因为IDbCommand的Paraneters属性显然是一个抽象类的聚集。所以,从IDbConnection的角度来看这个问题的时候,它又不知道这个参数要如何来给了,而且像Dapper里的参数,涉及到聚集范例会存在IN和NOT IN以及批量使用的问题,比平凡的字符串替换还要稍微复杂一点。如果我们只思量最简朴的情况,它照旧可以实验一番的:
  1. private static void SetDbParameter(this IDbCommand command, object param = null){    if (param == null) return;    if (param is IDictionary)    {        //使用字典作为参数        foreach (var arg in param as IDictionary)        {              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()和Execute()两个方法中使用参数,我们需要修改相关的方法:
  1. 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中DbConnection、DbCommand、DbDataReader、DbDataAdapter这些关键的组成部分,而为相识决DataTable在使用上的种种稳定,我们想到了鉴戒Dapper中的DapperRow来实现“动态查询”,由此引出了.NET中实现dynamic最重要的一个接口:IDynamicMetaObjectProvide,这使得我们可以在查询数据库的时候返回一个dynamic的聚集。而为了更靠近Dapper一点,我们基于扩展方法的形式为IDbConnection编写了Query()和Execute()方法,在数据库读写层面上彻底终结了DataSet/DataTable的生命。最后,我们实现了一个简化版本的参数化查询,同样是鉴戒Dapper的思路。这说明一件什么事情呢?当你在一个看似公道、了局固定的现状中无法摆脱的时候,“平躺”虽然能让你得到一丝喘气的时机,但与此同时,你永远失去了跳出这个层级去对待事物的时机,就像我以前吐槽同事天天用StringBuider拼接字符串一样,一味地吐槽是没有什么用的,重要的是你会选择怎么做,所以,厥后我向各人推荐了Linquid2021年已经来了,希望你不但是增长了年事和皱纹,晚安!
                                                                    
                                                qinyuanpei                                           
                CSDN认证博客专家                                        .NET                Python                伪·全栈攻城狮                            谢谢你,在这世界的角落,找到我,一个即将进入而立之年的中年大叔,常年以 飞鸿踏雪 的混名混迹江湖。在现实生活中,我是一名 伪·全栈攻城狮,因为我以为,什么都略懂一点,生活会更多彩一些。现在,主要关注.NET、.NET Core、Python、数据分析、微服务、Web 等技能方向。日常行为:读书、写作、影戏、烹调、洞箫等。喜欢看日剧/记录片/科普、刷B站、刷LeetCode等。
来源:https://blog.csdn.net/qinyuanpei/article/details/112060622
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )