一:背景 1. 講故事 最近因為各方面原因換了一份工作,去了一家主營物聯櫃的公司,有意思的是物聯柜上的終端是用 wpf 寫的,代碼也算是年久失修,感覺技術債還是蠻重的,前幾天在調試一個bug的時候,看到了一段類似這樣的代碼: var dt = new DataTable(); SqlDataAdap ...
一:背景
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()
是只讀向前的,然後一起看一下源碼底層是怎麼實現的。
從上圖中可以看出,還是很簡單的哈,踢掉 startRecord 個 reader(),然後再只讀向前獲取最多 maxRecords 條記錄。
2. dataSet 和 srcTable
這裡的 srcTable 是什麼意思呢? 從 vs 中看是這樣的: The name of the source table to use for table mapping.
乍一看也不是特別清楚,沒關係,我們直接看源碼就好啦,反正我也沒測試,嘿嘿。
從上圖中你應該明白大概意思就是給你 dataset 中的 datatable 取名字,比如:name= 學生表
, 那麼database中的的 tablename依次是: 學生表,學生表1,學生表2 ...
, 這樣你就可以索引獲取表的名字了哈,如下代碼所示:
DataSet dataSet = new DataSet();
dataSet.Tables.Add(new DataTable("學生表"));
var tb = dataSet.Tables["學生表"];
四:總結
本篇就聊這麼多吧,算是解了多年之前我的一個好奇心,希望本篇對您有幫助。