[.NET]RowMapper模組
前言
很常用需要到一些RowMapper模組,這邊做個記錄,沒有優化、也沒有太多防呆,但已經可以符合我的需求了。
RowMapper模組的角色,請見下圖:
模組
2012/04/15補充,發現之前忘了把Entity放上來,這樣會完全看不出來,整合測試所解釋的使用方式。
{
[ColumnMapping("LogID")]
public int Id { get; set; }
[ColumnMapping("Title")]
public string Title { get; set; }
public JoeyEmployee GetMappingEntity(SqlDataReader reader, int index)
{
var result = new JoeyEmployee();
result.Id = Convert.ToInt32(reader["LogID"]);
result.Title = Convert.ToString(reader["Title"].DbNullToNull());
return result;
}
}
1. 使用Delegate來做RowMapper:
==針對回傳集合的module==
整合測試程式
///GetEntityCollection 的測試
///</summary>
public void GetEntityCollectionTest_DelegateMappingFunction<T>()
{
//arrange
string sqlStatemnet = @"SELECT TOP 100 LogID, EventID, Title FROM Log(NOLOCK) where LogID BETWEEN @low and @upper";
string connectionString = @"你的connection string";
SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("low", 47097), new SqlParameter("upper", 47098) };
Func<SqlDataReader, int, JoeyEmployee> rowMapperDelegate =
(reader, rowIndex) =>
{
var result = new JoeyEmployee
{
Id = Convert.ToInt32(reader["LogID"].DbNullToNull()),
Title = Convert.ToString(reader["Title"].DbNullToNull()),
};
return result;
};
List<JoeyEmployee> expected = new List<JoeyEmployee>
{
new JoeyEmployee{ Id=47097, Title="Exception Log Manager Handling"},
new JoeyEmployee{ Id=47098, Title="Exception Log Manager Handling"},
};
//act
IEnumerable<JoeyEmployee> actual;
actual = Joey.RowMapper.RowMapperService.GetEntityCollection<JoeyEmployee>(sqlStatemnet, connectionString, parameters, rowMapperDelegate);
var actualToDictionary = actual.ToDictionary(x => x.Id, x => x);
Assert.AreEqual(expected[0].Id, actualToDictionary[47097].Id);
Assert.AreEqual(expected[0].Title, actualToDictionary[47097].Title);
Assert.AreEqual(expected[1].Id, actualToDictionary[47098].Id);
Assert.AreEqual(expected[1].Title, actualToDictionary[47098].Title);
}
[TestMethod()]
public void GetEntityCollectionTest_DelegateMappingFunction()
{
GetEntityCollectionTest_DelegateMappingFunction<GenericParameterHelper>();
}
實際程式
/// 供每次都是新起connection的sql statement使用
/// 可自訂委派rowmapper function來決定O/R mapping邏輯,其中rowIndex可供Entity結合資料筆數序號
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sqlStatemnet">The SQL statemnet.</param>
/// <param name="connectionString">The connection string.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="rowMapperDelegate">The row mapper delegate.</param>
/// <returns>透過自訂的delegate方法,所回傳的IEnumerable T</returns>
public static IEnumerable<T> GetEntityCollection<T>(string sqlStatemnet, string connectionString, SqlParameter[] parameters, Func<SqlDataReader, int, T> rowMapperDelegate) where T : class, new()
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand sqlCommand = new SqlCommand(sqlStatemnet, connection);
sqlCommand.Parameters.AddRange(parameters);
SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
int rowIndex = 0;
while (reader.Read())
{
var result = rowMapperDelegate(reader, rowIndex);
rowIndex++;
yield return result;
}
}
}
==針對回傳單筆的module==
整合測試程式
///GetEntity 的測試
///</summary>
public void GetEntityTestHelper_透過delegateMappingFunction取得第一筆<T>()
where T : new()
{
//arrange
string sqlStatemnet = @"SELECT TOP 100 LogID, EventID, Title FROM Log(NOLOCK) where LogID BETWEEN @low and @upper";
string connectionString = @"你的connection string";
SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("low", 47097), new SqlParameter("upper", 47098) };
Func<SqlDataReader, JoeyEmployee> rowMapperDelegate =
(reader) =>
{
var result = new JoeyEmployee
{
Id = Convert.ToInt32(reader["LogID"].DbNullToNull()),
Title = Convert.ToString(reader["Title"].DbNullToNull()),
};
return result;
};
JoeyEmployee expected = new JoeyEmployee { Id = 47097, Title = "Exception Log Manager Handling" };
//act
JoeyEmployee actual;
actual = RowMapperService.GetEntity<JoeyEmployee>(sqlStatemnet, connectionString, parameters, rowMapperDelegate);
Assert.AreEqual(expected.Id, actual.Id);
Assert.AreEqual(expected.Title, actual.Title);
}
[TestMethod()]
public void GetEntityTest_透過delegateMappingFunction取得第一筆()
{
GetEntityTestHelper_透過delegateMappingFunction取得第一筆<GenericParameterHelper>();
}
實際程式
/// 取得查詢結果,僅取一筆,若不存在資料則回傳null。若存在多筆資料,則回傳第一筆。
/// 透過rowMapperDelegate function決定如何回傳Entity
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sqlStatemnet">The SQL statemnet.</param>
/// <param name="connectionString">The connection string.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="rowMapperDelegate">The row mapper delegate.</param>
/// <returns></returns>
public static T GetEntity<T>(string sqlStatemnet, string connectionString, SqlParameter[] parameters, Func<SqlDataReader, T> rowMapperDelegate) where T : class, new()
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand sqlCommand = new SqlCommand(sqlStatemnet, connection);
sqlCommand.Parameters.AddRange(parameters);
SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
if (reader.Read())
{
var result = rowMapperDelegate(reader);
reader.Close();
return result;
}
else
{
reader.Close();
return null;
}
}
}
2. 使用自訂屬性來做RowMapping:
==針對回傳集合的module==
整合測試程式
///GetEntityCollection 的測試
///</summary>
public void GetEntityCollectionTestHelper_測試ColumnMappingAttribute<T>()
where T : new()
{
//arrange
string sqlStatemnet = @"SELECT TOP 100 LogID, EventID, Title FROM Log(NOLOCK) where LogID BETWEEN @low and @upper";
string connectionString = @"你的connection string";
SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("low", 47097), new SqlParameter("upper", 47098) };
List<JoeyEmployee> expected = new List<JoeyEmployee>
{
new JoeyEmployee{ Id=47097, Title="Exception Log Manager Handling"},
new JoeyEmployee{ Id=47098, Title="Exception Log Manager Handling"},
};
//act
IEnumerable<JoeyEmployee> actual;
actual = Joey.RowMapper.RowMapperService.GetEntityCollection<JoeyEmployee>(sqlStatemnet, connectionString, parameters);
var actualToDictionary = actual.ToDictionary(x => x.Id, x => x);
Assert.AreEqual(expected[0].Id, actualToDictionary[47097].Id);
Assert.AreEqual(expected[0].Title, actualToDictionary[47097].Title);
Assert.AreEqual(expected[1].Id, actualToDictionary[47098].Id);
Assert.AreEqual(expected[1].Title, actualToDictionary[47098].Title);
}
[TestMethod()]
public void GetEntityCollectionTest_測試ColumnMappingAttribute()
{
GetEntityCollectionTestHelper_測試ColumnMappingAttribute<GenericParameterHelper>();
}
實際程式
自訂的Attribute
public sealed class ColumnMappingAttribute : Attribute
{
public string ColumnName { get; private set; }
public ColumnMappingAttribute(string columnName)
{
this.ColumnName = columnName;
}
}
Mapping的程式
/// 取得查詢結果,透過ColumnMappingAttribute轉換成Entity,回傳IEnumberable泛型結果
/// </summary>
/// <typeparam name="T">回傳Entity型別</typeparam>
/// <param name="sqlStatemnet">The SQL statemnet.</param>
/// <param name="connectionString">The connection string.</param>
/// <param name="parameters">The parameters.</param>
/// <returns>SQL指令回傳的查詢結果</returns>
public static IEnumerable<T> GetEntityCollection<T>(string sqlStatemnet, string connectionString, SqlParameter[] parameters) where T : class, new()
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand sqlCommand = new SqlCommand(sqlStatemnet, connection);
sqlCommand.Parameters.AddRange(parameters);
SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
int rowIndex = 0;
while (reader.Read())
{
var result = GetMappingEntity<T>(reader);
rowIndex++;
yield return result;
}
reader.Close();
}
}
/// <summary>
/// 透過Entity上的ColumnMappingAttribute,取得從SqlReader對應的Entity
/// </summary>
/// <typeparam name="T">Entity型別</typeparam>
/// <param name="reader">The reader.</param>
/// <returns>SqlReader對應的Entity</returns>
private static T GetMappingEntity<T>(SqlDataReader reader) where T : new()
{
var result = new T();
var columns = GetAllColumns(reader);
PropertyInfo[] properties = typeof(T).GetProperties();
foreach (var p in properties)
{
var mappingAttribute = p.GetCustomAttributes(typeof(ColumnMappingAttribute), false).FirstOrDefault() as ColumnMappingAttribute;
var columnName = mappingAttribute == null ? p.Name : mappingAttribute.ColumnName;
if (columns.Contains(columnName))
{
if (!p.PropertyType.IsEnum)
{
p.SetValue(result, Convert.ChangeType(reader[columnName], p.PropertyType), null);
}
else
{
p.SetValue(result, Enum.ToObject(p.PropertyType, reader[columnName]), null);
}
}
}
return result;
}
/// <summary>
/// 取得SqlReader上所有欄位名稱
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>SqlReader上所有欄位名稱</returns>
private static IEnumerable<string> GetAllColumns(SqlDataReader reader)
{
if (reader.FieldCount == 0)
{
yield return null;
}
for (int i = 0; i < reader.FieldCount; i++)
{
var result = reader.GetName(i);
yield return result;
}
}
==針對回傳單筆的module==
整合測試程式
///GetEntity 的測試
///</summary>string connectionString = @"你的connection string";
public void GetEntityTestHelper_透過AttributeMapping取得第一筆<T>()
where T : new()
{
//arrange
string sqlStatemnet = @"SELECT TOP 100 LogID, EventID, Title FROM Log(NOLOCK) where LogID BETWEEN @low and @upper";
string connectionString = @"你的connection string";
SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("low", 47097), new SqlParameter("upper", 47098) };
JoeyEmployee expected = new JoeyEmployee { Id = 47097, Title = "Exception Log Manager Handling" };
//act
JoeyEmployee actual;
actual = RowMapperService.GetEntity<JoeyEmployee>(sqlStatemnet, connectionString, parameters);
Assert.AreEqual(expected.Id, actual.Id);
Assert.AreEqual(expected.Title, actual.Title);
}
[TestMethod()]
public void GetEntityTest_取得第一筆()
{
GetEntityTestHelper_透過AttributeMapping取得第一筆<GenericParameterHelper>();
}
實際程式
/// 取得查詢結果,僅取一筆,若不存在資料則回傳null。若存在多筆資料,則回傳第一筆。
/// 透過ColumnMappingAttribute來決定回傳的Entity
/// </summary>
/// <typeparam name="T">回傳Entity型別</typeparam>
/// <param name="sqlStatemnet">The SQL statemnet.</param>
/// <param name="connectionString">The connection string.</param>
/// <param name="parameters">The parameters.</param>
/// <returns>查詢單筆結果</returns>
public static T GetEntity<T>(string sqlStatemnet, string connectionString, SqlParameter[] parameters) where T : class, new()
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand sqlCommand = new SqlCommand(sqlStatemnet, connection);
sqlCommand.Parameters.AddRange(parameters);
SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
if (reader.Read())
{
var result = GetMappingEntity<T>(reader);
reader.Close();
return result;
}
else
{
reader.Close();
return null;
}
}
}
3.使用RowMapper介面來定義RowMapper的方式
==針對回傳集合的module==
整合測試程式
///GetEntityCollection 的測試
///</summary>
public void GetEntityCollectionTest_使用IRowMapper<T>()
where T : new()
{
//arrange
string sqlStatemnet = @"SELECT TOP 100 LogID, EventID, Title FROM Log(NOLOCK) where LogID BETWEEN @low and @upper";
string connectionString = @"你的connection string";
SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("low", 47097), new SqlParameter("upper", 47098) };
List<JoeyEmployee> expected = new List<JoeyEmployee>
{
new JoeyEmployee{ Id=47097, Title="Exception Log Manager Handling"},
new JoeyEmployee{ Id=47098, Title="Exception Log Manager Handling"},
};
IRowMapper<JoeyEmployee> rowmapper = new JoeyEmployee();
//act
IEnumerable<JoeyEmployee> actual;
actual = RowMapperService.GetEntityCollection<JoeyEmployee>(sqlStatemnet, connectionString, parameters, rowmapper);
var actualToDictionary = actual.ToDictionary(x => x.Id, x => x);
Assert.AreEqual(expected[0].Id, actualToDictionary[47097].Id);
Assert.AreEqual(expected[0].Title, actualToDictionary[47097].Title);
Assert.AreEqual(expected[1].Id, actualToDictionary[47098].Id);
Assert.AreEqual(expected[1].Title, actualToDictionary[47098].Title);
}
[TestMethod()]
public void GetEntityCollectionTest_使用IRowMapper()
{
GetEntityCollectionTest_使用IRowMapper<GenericParameterHelper>();
}
實際程式
IRowMapper介面定義
{
T GetMappingEntity(SqlDataReader reader, int index);
}
使用IRowMapper的方法
/// 取得查詢結果,透過IRowMapper取得對應的Entity,回傳IEnumberable泛型結果
/// </summary>
/// <typeparam name="T">回傳Entity型別</typeparam>
/// <param name="sqlStatemnet">The SQL statemnet.</param>
/// <param name="connectionString">The connection string.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="rowmapper">RowMapper介面</param>
/// <returns>
/// SQL指令回傳的查詢結果
/// </returns>
public static IEnumerable<T> GetEntityCollection<T>(string sqlStatemnet, string connectionString, SqlParameter[] parameters, IRowMapper<T> rowmapper) where T : class, new()
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand sqlCommand = new SqlCommand(sqlStatemnet, connection);
sqlCommand.Parameters.AddRange(parameters);
SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
int rowIndex = 0;
while (reader.Read())
{
var result = rowmapper.GetMappingEntity(reader, rowIndex);
rowIndex++;
yield return result;
}
reader.Close();
}
}
==針對回傳單筆的module==
整合測試程式
///GetEntity 的測試
///</summary>
public void GetEntityTestHelper_透過IRowMapper取得第一筆<T>()
where T : class , new()
{
//arrange
string sqlStatemnet = @"SELECT TOP 100 LogID, EventID, Title FROM Log(NOLOCK) where LogID BETWEEN @low and @upper";
string connectionString = @"你的connection string";
SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("low", 47097), new SqlParameter("upper", 47098) };
JoeyEmployee expected = new JoeyEmployee { Id = 47097, Title = "Exception Log Manager Handling" };
IRowMapper<JoeyEmployee> rowmapper = new JoeyEmployee();
JoeyEmployee actual;
actual = RowMapperService.GetEntity<JoeyEmployee>(sqlStatemnet, connectionString, parameters, rowmapper);
Assert.AreEqual(expected.Id, actual.Id);
Assert.AreEqual(expected.Title, actual.Title);
}
[TestMethod()]
public void GetEntityTest_透過IRowMapper取得第一筆()
{
GetEntityTestHelper_透過IRowMapper取得第一筆<GenericParameterHelper>();
}
實際程式
/// 取得查詢結果,僅取一筆,若不存在資料則回傳null。若存在多筆資料,則回傳第一筆。
/// 透過IRowMapper的GetMappingEntity,來決定如何回傳Entity
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sqlStatemnet">The SQL statemnet.</param>
/// <param name="connectionString">The connection string.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="rowmapper">The rowmapper.</param>
/// <returns></returns>
public static T GetEntity<T>(string sqlStatemnet, string connectionString, SqlParameter[] parameters, IRowMapper<T> rowmapper) where T : class, new()
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand sqlCommand = new SqlCommand(sqlStatemnet, connection);
sqlCommand.Parameters.AddRange(parameters);
SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
if (reader.Read())
{
var result = rowmapper.GetMappingEntity(reader, 0);
reader.Close();
return result;
}
else
{
reader.Close();
return null;
}
}
}
其他補充
Extension Method
{
/// <summary>
/// 若original為DBNull,則轉為null
/// </summary>
/// <param name="original">The original.</param>
/// <returns>轉換結果</returns>
public static object DbNullToNull(this object original)
{
return original == DBNull.Value ? null : original;
}
/// <summary>
/// 將DataTable資料轉換成對應的Entity集合
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dt">The dt.</param>
/// <returns></returns>
public static IEnumerable<T> GetEntityCollection<T>(this DataTable dt) where T : class, new()
{
foreach (DataRow row in dt.Rows)
{
T result = new T();
foreach (DataColumn column in dt.Columns)
{
typeof(T).GetProperty(column.ColumnName).SetValue(result, row[column.ColumnName].DbNullToNull(), null);
}
yield return result;
}
}
}
對應單元測試
///GetEntityCollection 的測試
///</summary>
public void GetEntityCollectionTestHelper_DataTable轉Entity集合<T>()
{
//arrange
DataTable dt = new DataTable();
dt.Columns.Add(new DataColumn("Name", typeof(string)));
dt.Columns.Add(new DataColumn("Age", typeof(int)));
dt.Columns.Add(new DataColumn("Birthday", typeof(DateTime)));
dt.Rows.Add("91", 31, new DateTime(2011, 11, 11));
dt.Rows.Add("Ruffy");
dt.Rows.Add("Bill", 40);
dt.Rows.Add("林志玲", null, new DateTime(1977, 11, 29));
IList<Person> expected = new List<Person>
{
new Person{ Name="91", Age=31, Birthday=new DateTime(2011,11,11)},
new Person{ Name="Ruffy"},
new Person{ Name="Bill", Age=40},
new Person{ Name="林志玲", Birthday=new DateTime(1977,11,29)}
};
//act
IEnumerable<Person> actual;
actual = dt.GetEntityCollection<Person>();
var result = actual.ToList<Person>();
//assert
Assert.AreEqual(expected.Count, result.Count);
for (int i = 0; i < expected.Count; i++)
{
Assert.AreEqual(expected[i].Age, result[i].Age);
Assert.AreEqual(expected[i].Birthday, result[i].Birthday);
Assert.AreEqual(expected[i].Name, result[i].Name);
}
}
[TestMethod()]
public void GetEntityCollectionTest_DataTable轉Entity集合()
{
GetEntityCollectionTestHelper_DataTable轉Entity集合<GenericParameterHelper>();
}
//測試用的Entity class
public class Person
{
public string Name { get; set; }
public int? Age { get; set; }
public DateTime? Birthday { get; set; }
}
結論
主要就是三種方式來實作DA層如何以Entity回傳,而非DataSet, DataTable等形式。
- 透過委派方法,讓使用者自行決定怎麼mapping到自己想要的Entity上。
- 自訂Attribute,透過Attribute來宣告,這個Entity的property要自動mapping到哪一個column Name,讓使用的人無感。只要定義好Entity的property與資料庫columnName的mapping即可。
- 自訂IRowMapper,好處是把mapping的邏輯封到介面裡面。可以重複使用,也可以有彈性的使用。例如一個Entity可以實作多種type的RowMapper。
Source code很短,但可以用的很爽。Developer學習進入門檻也低,鼓勵大家在系統中,盡量透過Entity來操作,不管是可讀性、可擴充性,都會提升許多。
有問題或有建議,麻煩再跟我說一聲,謝謝。另外,上面的絕大部分的測試是整合測試(除了最後一個),因為是在撰寫DAL與DB之間的RowMapper,要再拆IDataReader出來應該也沒啥問題,不過有需求的朋友再自己拆囉。這樣就也可以做單元測試,也可以做DB的抽換。
Sample Code: RowMapper.zip
blog 與課程更新內容,請前往新站位置:http://tdd.best/