摘要:[.NET] Lazy Row Mapping
前言
在做架構設計的時候,資料物件進出系統邊界,可以採用先前的文章介紹的[Architecture Pattern] Repository,來將外部的系統、模組、資料庫…等等,隔離在架構之外。而當系統採用關聯式資料庫來做為儲存資料庫的時候,開發人員必需要在程式內加入ORM(Object Relational Mapping)的功能,才能將資料物件與關聯式資料庫資料做互相的轉換。
但當開發人員要從資料庫查詢大量資料的時候,會驚覺上述ORM的運作模式是:將資料庫查詢到的「所有資料」,轉換為資料物件集合放在「記憶體內」,再交由系統去使用。「所有資料」、「記憶體內」這兩個關鍵字,決定了在大量資料的處理上,這個運作模式有風險。畢竟就算電腦記憶體的售價再便宜,使用者大多還是不會為了軟體,多投資金錢去購買記憶體。
本篇文章介紹一個「Lazy Row Mapping」實做,用來處理對關聯式資料庫查詢大量資料做ORM的需求。為自己做個紀錄,也希望能幫助到有需要的開發人員。
使用
因為實做的內容有點硬,所以先展示使用範例,看能不能減少開發人員按上一頁離開的機率…。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Data;
namespace LazyRowMappingSample
{
class Program
{
static void Main(string[] args)
{
// UserRepositoryProvider
UserRepositoryProvider userRepositoryProvider = new UserRepositoryProvider();
// QueryAll
foreach (User user in userRepositoryProvider.QueryAll())
{
Console.WriteLine("Name : " + user.Name);
Console.WriteLine("Create Time : " + user.CreateTime);
Console.WriteLine("Read Time : " + DateTime.Now);
Console.WriteLine();
System.Threading.Thread.Sleep(2000);
}
// End
Console.ReadLine();
}
}
public class User
{
// Properties
public string Name { get; set; }
public DateTime CreateTime { get; set; }
}
public class UserRepositoryProvider
{
// Methods
public IEnumerable<User> QueryAll()
{
// Arguments
string connectionString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\LazyRowMappingSample.mdf;Integrated Security=True;User Instance=True";
string commandText = @"SELECT * FROM [User]";
SqlParameter[] parameters = new SqlParameter[] { };
Func<IDataRecord, User> rowMappingDelegate = delegate(IDataRecord dataRecord)
{
User user = new User();
user.Name = Convert.ToString(dataRecord["Name"]);
user.CreateTime = DateTime.Now;
return user;
};
// Return
return new SqlLazyRowMappingEnumerable<User>(connectionString, commandText, parameters, rowMappingDelegate);
}
}
}
上面的使用範例,展示如何使用實做章節完成的SqlLazyRowMappingEnumerable。SqlLazyRowMappingEnumerable只需要四個參數,就可以對Sql資料庫做查詢資料以及完成ORM的工作。
範例中每次讀取一個資料物件就暫停2秒,並且輸出資料物件的讀取時間與生成時間。在執行結果中可以看到,每個資料物件讀取的時間與生成的時間是相同的。也就是說:每次透過SqlLazyRowMappingEnumerable取得的資料物件,都是在取得的當下才去生成資料物件。這樣可以避免一次將所有資料物件載入到記憶體內,造成記憶體不足的風險。
可以說SqlLazyRowMappingEnumerable幫助開發人員簡單的處理了,對Sql資料庫查詢大量資料做ORM的需求。
實做
範列下載
實做說明請參照範例程式內容:LazyRowMappingSample點此下載
範例結構
下圖是簡易的範例結構,對照這張圖片說明與範例程式,可以幫助開發人員較快速理解本篇文章的思路。
LazyDataRecordEnumerable
讀取資料可以使用ADO.NET裡的DbDataReader類別,來與關連式資料庫做連接。開發人員去分析ADO.NET裡提供的DbDataReader類別,可以發現DbDataReadr類別是以一次讀取一筆資料的方式來處理資料庫資料,這看起來能滿足查詢大量資料的需求。而這個功能主要是由DbDataReader類別實做的IDataReader介面跟IDataRecord介面所定義;IDataReader介面定義了詢覽下一筆資料的方法、IDataRecord介面則是定義取得資料內容的方式。
初步了解DbDataReader類別的運作模式之後,來看看下列這個LazyDataRecordEnumerable物件,LazyDataRecordEnumerable將DbDataReader類別的運作模式封裝成為IEnumerable<IDataRecord>的實做。系統在使用foreach列舉IEnumerable<IDataRecord>的時候,LazyDataRecordEnumerable才會建立DbDataReader去關連式資料庫查詢資料。然後以一次讀取一筆資料的方式,將資料庫資料透過IDataRecord交由系統使用。
關於LazyDataRecordEnumerable的設計細節,可以參考:
[.NET] LINQ Deferred Execution
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.Common;
namespace LazyRowMappingSample
{
public abstract class LazyDataRecordEnumerable: IEnumerable<IDataRecord>
{
// Methods
protected abstract LazyDataRecordEnumerator CreateEnumerator();
public IEnumerator<IDataRecord> GetEnumerator()
{
return this.CreateEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
public abstract class LazyDataRecordEnumerator : IEnumerator<IDataRecord>
{
// Constructor
public void Dispose()
{
this.EndEnumerate();
}
// Properties
protected abstract DbDataReader DataReader { get; }
public IDataRecord Current
{
get
{
return this.DataReader;
}
}
object System.Collections.IEnumerator.Current
{
get
{
return this.Current;
}
}
// Methods
protected abstract void BeginEnumerate();
protected abstract void EndEnumerate();
public bool MoveNext()
{
if (this.DataReader == null) this.BeginEnumerate();
if (this.DataReader != null)
{
while (this.DataReader.Read() == true)
{
return true;
}
}
return false;
}
public void Reset()
{
this.EndEnumerate();
}
}
}
*LazyDataRecordEnumerable也可以使用.NET提供的yield關鍵字來實做,有興趣的開發人員可以找相關資料來學習。
CastingEnumerable
LazyDataRecordEnumerable實做的IEnumerable<IDataRecord>介面所提供的IDataRecord,並不是開發人員想要得到的資料物件。要將IDataRecord轉換為資料物件,可以在LazyDataRecordEnumerable外面套用一層CastingEnumerable。透過CastingEnumerable提供的功能,以一次一筆的方式,將LazyDataRecordEnumerable提供的IDataRecord轉變成為資料物件交由系統使用。
關於CastingEnumerable的設計細節,可以參考:
[.NET] CastingEnumerable
LazyRowMappingEnumerable
LazyDataRecordEnumerable套用一層CastingEnumerable的方式,已經可以完成Lazy Row Mapping的功能。但為了讓開發人員方便使用,另外建立了一個LazyRowMappingEnumerable,用來封裝了LazyDataRecordEnumerable、CastingEnumerable的生成跟運作。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
namespace LazyRowMappingSample
{
public abstract class LazyRowMappingEnumerable<T> : IEnumerable<T>
where T : class
{
// Fields
private readonly Func<IDataRecord, T> _rowMappingDelegate = null;
private IEnumerable<T> _enumerable = null;
// Constructor
public LazyRowMappingEnumerable(Func<IDataRecord, T> rowMappingDelegate)
{
#region Require
if (rowMappingDelegate == null) throw new ArgumentNullException();
#endregion
_rowMappingDelegate = rowMappingDelegate;
}
// Methods
protected abstract LazyDataRecordEnumerable CreateEnumerable();
private T CreateObject(IDataRecord dataRecord)
{
return _rowMappingDelegate(dataRecord);
}
public IEnumerator<T> GetEnumerator()
{
if (_enumerable == null)
{
LazyDataRecordEnumerable lazyDataRecordEnumerable = this.CreateEnumerable();
CastingEnumerable<T, IDataRecord> castingEnumerable = new CastingEnumerable<T, IDataRecord>(lazyDataRecordEnumerable, this.CreateObject);
_enumerable = castingEnumerable;
}
return _enumerable.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
}
SqlLazyRowMappingEnumerable
一直到建立LazyDataRecordEnumerable為止,整個範例處理目標都是抽象的DbDataReader,而不是連接各種資料庫的DbDataReader實做。最後一個範例,就用來說明如何建立LazyRowMappingEnumerable的Sql版本實做。開發人員使用這個SqlLazyRowMappingEnumerable,就可以簡單處理對Sql資料庫查詢大量資料做ORM的需求。
關於SqlLazyRowMappingEnumerable的設計細節,可以參考:
KB-當心SqlDataReader.Close時的額外資料傳輸量 - 黑暗執行緒
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Data;
namespace LazyRowMappingSample
{
public class SqlLazyRowMappingEnumerable<T> : LazyRowMappingEnumerable<T>
where T : class
{
// Fields
private readonly string _connectionString = null;
private readonly string _commandText = null;
private readonly SqlParameter[] _parameters = null;
// Constructor
public SqlLazyRowMappingEnumerable(string connectionString, string commandText, SqlParameter[] parameters, Func<IDataRecord, T> rowMappingDelegate)
: base(rowMappingDelegate)
{
#region Require
if (string.IsNullOrEmpty(connectionString) == true) throw new ArgumentException();
if (string.IsNullOrEmpty(commandText) == true) throw new ArgumentException();
if (parameters == null) throw new ArgumentNullException();
#endregion
_connectionString = connectionString;
_commandText = commandText;
_parameters = parameters;
}
// Methods
protected override LazyDataRecordEnumerable CreateEnumerable()
{
return new SqlLazyDataRecordEnumerable(_connectionString, _commandText, _parameters);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Data.SqlClient;
namespace LazyRowMappingSample
{
public class SqlLazyDataRecordEnumerable : LazyDataRecordEnumerable
{
// Fields
private readonly string _connectionString = null;
private readonly string _commandText = null;
private readonly SqlParameter[] _parameters = null;
// Constructor
public SqlLazyDataRecordEnumerable(string connectionString, string commandText, SqlParameter[] parameters)
{
#region Require
if (string.IsNullOrEmpty(connectionString) == true) throw new ArgumentException();
if (string.IsNullOrEmpty(commandText) == true) throw new ArgumentException();
if (parameters == null) throw new ArgumentNullException();
#endregion
_connectionString = connectionString;
_commandText = commandText;
_parameters = parameters;
}
// Methods
protected override LazyDataRecordEnumerator CreateEnumerator()
{
return new SqlLazyDataRecordEnumerator(_connectionString, _commandText, _parameters);
}
}
public class SqlLazyDataRecordEnumerator : LazyDataRecordEnumerator
{
// Fields
private readonly string _connectionString = null;
private readonly string _commandText = null;
private readonly SqlParameter[] _parameters = null;
private SqlConnection _connection = null;
private SqlCommand _command = null;
private SqlDataReader _dataReader = null;
// Constructor
public SqlLazyDataRecordEnumerator(string connectionString, string commandText, SqlParameter[] parameters)
{
#region Require
if (string.IsNullOrEmpty(connectionString) == true) throw new ArgumentException();
if (string.IsNullOrEmpty(commandText) == true) throw new ArgumentException();
if (parameters == null) throw new ArgumentNullException();
#endregion
_connectionString = connectionString;
_commandText = commandText;
_parameters = parameters;
}
// Properties
protected override DbDataReader DataReader
{
get { return _dataReader; }
}
// Methods
protected override void BeginEnumerate()
{
// End
this.EndEnumerate();
// Begin
_connection = new SqlConnection(_connectionString);
_command = new SqlCommand(_commandText, _connection);
_command.Parameters.AddRange(_parameters);
_connection.Open();
_dataReader = _command.ExecuteReader();
}
protected override void EndEnumerate()
{
// End
if(_dataReader!=null)
{
_command.Cancel();
_dataReader.Close();
_dataReader.Dispose();
_dataReader = null;
}
if(_command!=null)
{
_command.Dispose();
_command = null;
}
if(_connection!=null)
{
_connection.Close();
_connection.Dispose();
_connection = null;
}
}
}
}
後記
了解整個LazyRowMappingEnumerable的運作之後,開發人員遇到對關聯式資料庫查詢大量資料做ORM的需求。如果目標關聯式資料庫有提供DbDataReadr物件來查詢資料,開發人員可以實做LazyRowMappingEnumerable的子類別來滿足需求。如果沒有提供DbDataReadr物件或是根本不是關聯式資料庫,也可以依照本章的思路建立一個LazyXxxMappingEnumerable來完成這個功能需求。
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。