一个有趣的问题, 你知道SqlDataAdapter中的Fill是怎么实现的吗

云长
• 阅读 1875

一:背景

1. 讲故事

最近因为各方面原因换了一份工作,去了一家主营物联柜的公司,有意思的是物联柜上的终端是用 wpf 写的,代码也算是年久失修,感觉技术债还是蛮重的,前几天在调试一个bug的时候,看到了一段类似这样的代码:


    var dt = new DataTable();
    SqlDataAdapter adapter = new SqlDataAdapter(new SqlCommand());
    adapter.Fill(dt);

是不是很眼熟哈,或许你也已经多年不见了,犹记得那时候为了能从数据库获取数据,第一种方法就是采用 SqlDataReader 一行一行从数据库读取,而且还要操心 Reader 的 close 问题,第二种方法为了避免麻烦,就直接使用了本篇说到的 SqlDataAdapter ,简单粗暴,啥也不用操心,对了,不知道您是否和我一样对这个 Fill 方法很好奇呢?,它是如何将数据塞入到 DataTable 中的呢? 也是用的 SqlDataReader 吗? 而且 Fill 还有好几个扩展方法,哈哈,本篇就逐个聊一聊,就当回顾经典啦!

二:对Fill方法的探究

1. 使用 dnspy 查看Fill源码

dnspy小工具大家可以到GitHub上面去下载一下,这里就不具体说啦,接下来追一下Fill的最上层实现,如下代码:


        public int Fill(DataTable dataTable)
        {
            IntPtr intPtr;
            Bid.ScopeEnter(out intPtr, "<comm.DbDataAdapter.Fill|API> %d#, dataTable\n", base.ObjectID);
            int result;
            try
            {
                DataTable[] dataTables = new DataTable[]
                {
                    dataTable
                };
                IDbCommand selectCommand = this._IDbDataAdapter.SelectCommand;
                CommandBehavior fillCommandBehavior = this.FillCommandBehavior;
                result = this.Fill(dataTables, 0, 0, selectCommand, fillCommandBehavior);
            }
            finally
            {
                Bid.ScopeLeave(ref intPtr);
            }
            return result;
        }

上面的代码比较关键的一个地方就是 IDbCommand selectCommand = this._IDbDataAdapter.SelectCommand; 这里的 SelectCommand 来自于哪里呢? 来自于你 new SqlDataAdapter 的时候塞入的构造函数 SqlCommand,如下代码:


        public SqlDataAdapter(SqlCommand selectCommand) : this()
        {
            this.SelectCommand = selectCommand;
        }

然后继续往下看 this.Fill 方法,代码简化后如下:


        protected virtual int Fill(DataTable[] dataTables, int startRecord, int maxRecords, IDbCommand command, CommandBehavior behavior)
        {
            result = this.FillInternal(null, dataTables, startRecord, maxRecords, null, command, behavior);
            
            return result;
        }

上面这段代码没啥好说的,继续往下追踪 this.FillInternal 方法,简化后如下:


        private int FillInternal(DataSet dataset, DataTable[] datatables, int startRecord, int maxRecords, string srcTable, IDbCommand command, CommandBehavior behavior)
        {
            int result = 0;
            try
            {
                IDbConnection connection = DbDataAdapter.GetConnection3(this, command, "Fill");
                try
                {
                    IDataReader dataReader = null;
                    try
                    {
                        dataReader = command.ExecuteReader(behavior);
                        result = this.Fill(datatables, dataReader, startRecord, maxRecords);
                    }
                    finally
                    {
                        if (dataReader != null)    dataReader.Dispose();
                    }
                }
                finally
                {
                    DbDataAdapter.QuietClose(connection, originalState);
                }
            }
            finally
            {
                if (flag)
                {
                    command.Transaction = null;
                    command.Connection = null;
                }
            }
            return result;
        }

大家可以仔细研读一下上面的代码,挺有意思的,至少你可以获取以下两点信息:

  • 从各个 finally 中可以看到,当数据 fill 到 datatable 中之后,操作数据库的几大对象 Connection,Transaction,DataReader 都会进行关闭,你根本不需要操心。
  • this.Fill(datatables, dataReader, startRecord, maxRecords) 中可以看到,底层不出意外也是通过 dataReader.read() 一行一行读取然后塞到 DataTable中去的,不然它拿这个 dataReader 干嘛呢? 不信的话可以继续往下追。

        protected virtual int Fill(DataTable[] dataTables, IDataReader dataReader, int startRecord, int maxRecords)
        {
            try
            {
                int num = 0;
                bool flag = false;
                DataSet dataSet = dataTables[0].DataSet;
                int num2 = 0;
                while (num2 < dataTables.Length && !dataReader.IsClosed)
                {
                    DataReaderContainer dataReaderContainer = DataReaderContainer.Create(dataReader, this.ReturnProviderSpecificTypes);
                    if (num2 == 0)
                    {
                        bool flag2;
                        do
                        {
                            flag2 = this.FillNextResult(dataReaderContainer);
                        }
                        while (flag2 && dataReaderContainer.FieldCount <= 0);    
                        }
                    }
                result = num;
            }
            return result;
        }

从上面代码可以看到, dataReader 被封装到了 DataReaderContainer 中,用 FillNextResult 判断是否还有批语句sql,从而方便生成多个 datatable 对象,最后就是填充 DataTable ,当然就是用 dataReader.Read()啦,不信你可以一直往里面追嘛,如下代码:


        private int FillLoadDataRow(SchemaMapping mapping)
        {
            int num = 0;
            DataReaderContainer dataReader = mapping.DataReader;
            
            while (dataReader.Read())
            {
                mapping.LoadDataRow();
                num++;
            }
            return num;
        }

到这里你应该意识到: DataReader 的性能肯定比 Fill 到 DataTable 要高的太多,所以它和灵活性两者之间看您取舍了哈。

二:Fill 的其他重载方法

刚才给大家介绍的是带有 DataTable 参数的重载,其实除了这个还有另外四种重载方法,如下图:


    public override int Fill(DataSet dataSet);
    public int Fill(DataSet dataSet, string srcTable);
    public int Fill(DataSet dataSet, int startRecord, int maxRecords, string srcTable);
    public int Fill(int startRecord, int maxRecords, params DataTable[] dataTables);

1. startRecord 和 maxRecords

从字面意思看就是想从指定的位置 (startRecord) 开始读,然后最多读取 maxRecords 条记录,很好理解,我们知道 reader() 是只读向前的,然后一起看一下源码底层是怎么实现的。

一个有趣的问题, 你知道SqlDataAdapter中的Fill是怎么实现的吗

从上图中可以看出,还是很简单的哈,踢掉 startRecord 个 reader(),然后再只读向前获取最多 maxRecords 条记录。

2. dataSet 和 srcTable

这里的 srcTable 是什么意思呢? 从 vs 中看是这样的: The name of the source table to use for table mapping. 乍一看也不是特别清楚,没关系,我们直接看源码就好啦,反正我也没测试,嘿嘿。

一个有趣的问题, 你知道SqlDataAdapter中的Fill是怎么实现的吗

从上图中你应该明白大概意思就是给你 dataset 中的 datatable 取名字,比如:name= 学生表, 那么database中的的 tablename依次是: 学生表,学生表1,学生表2 ..., 这样你就可以索引获取表的名字了哈,如下代码所示:

        DataSet dataSet = new DataSet();
        dataSet.Tables.Add(new DataTable("学生表"));
        var tb = dataSet.Tables["学生表"];

四:总结

本篇就聊这么多吧,算是解了多年之前我的一个好奇心,希望本篇对您有帮助。

点赞
收藏
评论区
推荐文章
liam liam
3年前
一个比Postman好用的工具,不试一下?
作为一名前端,日常开发过程中除了写业务代码,前后端联调更是重要的一环。但这一环却往往是整个开发中最繁琐也最累人的一环。任谁都想早点下班,然而提升联调效率并不是个纯技术相关的问题,而是需要有良好工作流程的问题。友情提示:(阅读前情简介仅需1分钟,干货部分大约5分钟)前情简介:亲身经历节选Code:403将我踢飞彼时正处公司切换中台系统的技术栈,以半个
捉虫大师 捉虫大师
4年前
Sentinel-Go 源码系列(一)|开篇
大家好呀,打算写一个Go语言组件源码分析系列,一是为了能学习下Go语言,看下别人是怎么写Go的,二是也掌握一个组件。本次选择了SentinelGo,一是对Java版本的Sentinel算是有一些了解,也在生产上落地过,二是感觉他的代码应该不会太复杂(仅仅是感觉),三是在云原生越来越热的趋势下,用Go实现的限流降级容错应该是比较通用的
Stella981 Stella981
4年前
Python 使用VS Code进行调试
VSCode是一款非常好用的编辑器,现在我基本上所有的开发任务都在VSCode上完成。它的代码调试工具其实也非常强大,但是许多人都不知道该怎么用,今天就来学习怎么用它调试Python代码吧。1.准备既然是用VSCode调试Python代码,那当然你得先安装好Python啦,如果你还没有安装,可以看这篇文章:超详细Pyt
Stella981 Stella981
4年前
Fish Redux中的Dispatch是怎么实现的?
零.前言我们在使用fishredux构建应用的时候,界面代码(view)和事件的处理逻辑(reducer,effect)是完全解耦的,界面需要处理事件的时候将action分发给对应的事件处理逻辑去进行处理,而这个分发的过程就是下面要讲的dispatch,通过本篇的内容,你可以更深刻的理解一个action是如何一步步去进行分发的。一.从e
Stella981 Stella981
4年前
R语言的xtabs函数
今天在做一个列联表独立性检验的时候,总是无法处理好要求的数据类型,偶然的机会,看到了xtabs()函数,感觉很适合用来做列联表,适合将一列数据转换成列联表。shifou<c("yes","yes","no","no")xinbie<c("nan","nv","nan","nv")freq<c(34,38,28
Easter79 Easter79
4年前
Spring中的bean是线程安全的吗?
结论:不是线程安全的Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究。Spring的bean作用域(scope)类型:singleton:单例,默认作用域。p
Easter79 Easter79
4年前
TiDB 在新乐视云联“月光宝盒”项目中的应用与实践
公司介绍2018年,乐视云计算有限公司品牌升级为新乐视云联,新乐视云联是新乐视上市体系中核心业务版块之一,负责新乐视体系所有基础设施服务和云计算服务。新乐视云联围绕视频云和物联云两大方向开展业务,致力成为领先的家庭互联智能娱乐云技术提供者,以物联云为核心创造更智能的家居社区解决方案。新乐视云联在视频行业有强大的技术储备,在视频领域中的点播
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
敏捷开发 敏捷开发
1年前
你的代码是干的还是湿的?
DRY代码是一种软件原则,代表不要重复自己(Don’trepeatyourself),其目标是减少代码的重复。
敏捷开发 敏捷开发
1年前
你的代码是干的还是湿的?
DRY代码是一种软件原则,代表不要重复自己(Don’trepeatyourself),其目标是减少代码的重复。