[.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来完成这个功能需求。
期許自己~
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。