[.NET] Lazy Row Mapping

摘要:[.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來完成這個功能需求。

期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。