[.NET]使用Func<T1, T2, …, Tn, TResult>自訂RowMapper邏輯
前言
在欣賞小朱大的ORM系列文時,剛好和同事討論到Linq裡面delegate:Func<TResult>的使用,加上之前有使用Spring.Net RowMapper with Delegate的經驗,所以就衍生出這一篇Sample Code,透過泛型、委派,自己來撰寫仿Spring.Net中的RowMapper with Delegate function。
需求
撰寫一個封裝ADO.NET的底層,只需要傳入幾個參數,就可以直接得到Entity的集合。參數如下:
- SqlStatement:主要的SQL內容
- Parameters:搭配SQL內容所需要的parameter
- ConnectionString:若傳進來的為connectionString,則代表這次SQL的執行為新起一個connection。(可撰寫overload function,傳入connection與transaction,則代表要使用同一個connection,或包含在同一個transaction中)
- Delegate RowMapper Function:這邊輸入參數型別為SqlDataReader與int,其中SqlDataReader代表SqlCommand.ExecuteReader中的reader.Read()結果。int則代表目前讀到第幾筆record。
- 其他:包括上面提到的connection, transaction, 以及CommandType等等,都可以自訂overload function來使用。
範例
這邊只舉一個簡單的範例來說明,如何傳入sqlStatement, connectionString, parameters,以及自訂的Func<SqlDataReader, int, T>,來得到這次SQL執行結果的Entity集合。其他的需求,皆可依此類推。
JoeySqlModule.cs
public class JoeySqlModule
{
/// <summary>
/// 供每次都是新起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>
/// <history>
/// 1. Joey Chen, 2011/12/10, 下午 01:40, Created
/// </history>
public static IEnumerable<T> GetEntityCollection<T>(string sqlStatemnet, string connectionString, SqlParameter[] parameters, Func<SqlDataReader, int, T> rowMapperDelegate)
{
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;
}
}
}
}
說明:
1. 把每次ADO.NET execute要做的事,封裝成靜態方法,每一個DAO要執行,只需要傳入對應參數即可。
2. 透過Func<SqlDataReader, int, T>這個參數,就可以讓使用這個靜態方法的『人』,來自己決定,要怎麼拿SqlDataReader與rowIndex來mapping自己的T。
3. 一樣,最後rowMapperDelegate回來的T,透過yield return來得到IEumerable<T>,得到資料結構的彈性。
對應的整合測試程式
1. 使用到的Entity為JoeyEmployee:
public class JoeyEmployee
{
public int Id { get; set; }
public string FullName { get; set; }
public string Title { get; set; }
public string TitleOfCourtesy { get; set; }
}
一樣用到上次Converter裡面的DBNullToNull的Extension Method:
public static class ConverterExtension
{
public static object DbNullToNull(this object original)
{
return original == DBNull.Value ? null : original;
}
}
2. 測試程式
預期SQL的執行結果如下圖所示:
對應的Test Case:
/// <summary>
///GetEntityCollection 的測試
///</summary>
public void GetEntityCollectionTestHelper<T>()
{
//arrange
string sqlStatemnet = @"SELECT * FROM Employees where TitleOfCourtesy =@TitleOfCourtesy";
string connectionString = @"Data Source=localhost\sqlexpress;Initial Catalog=Northwind;Integrated Security=True";
SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("TitleOfCourtesy", "Mr.") };
Func<SqlDataReader, int, JoeyEmployee> rowMapperDelegate =
(reader, rowIndex) =>
{
var result = new JoeyEmployee
{
Id = Convert.ToInt32(reader["EmployeeID"].DbNullToNull()),
Title = Convert.ToString(reader["Title"].DbNullToNull()),
TitleOfCourtesy = Convert.ToString(reader["TitleOfCourtesy"].DbNullToNull()),
FullName = string.Format("{0} {1}", Convert.ToString(reader["FirstName"].DbNullToNull()), Convert.ToString(reader["LastName"].DbNullToNull()))
};
return result;
};
List<JoeyEmployee> expected = new List<JoeyEmployee>
{
new JoeyEmployee{ Id=5, Title="Sales Manager", TitleOfCourtesy="Mr.", FullName="Steven Buchanan" },
new JoeyEmployee{ Id=6, Title="Sales Representative", TitleOfCourtesy="Mr.", FullName="Michael Suyama" },
new JoeyEmployee{ Id=7, Title="Sales Representative", TitleOfCourtesy="Mr.", FullName="Robert King" }
};
//act
IEnumerable<JoeyEmployee> actual;
actual = JoeySqlModule.GetEntityCollection<JoeyEmployee>(sqlStatemnet, connectionString, parameters, rowMapperDelegate);
var actualToDictionary = actual.ToDictionary(x => x.Id, x => x);
//assert
Assert.AreEqual(expected.Count, actualToDictionary.Count);
Assert.AreEqual(expected[0].FullName, actualToDictionary[5].FullName);
Assert.AreEqual(expected[1].FullName, actualToDictionary[6].FullName);
Assert.AreEqual(expected[2].FullName, actualToDictionary[7].FullName);
foreach (var item in actualToDictionary)
{
Assert.AreEqual("Mr.", item.Value.TitleOfCourtesy);
}
}
[TestMethod()]
public void GetEntityCollectionTest()
{
GetEntityCollectionTestHelper<GenericParameterHelper>();
}
說明:
1. Arrange中,初始化Func<T1, T2, TResult>,這邊模擬的是即使SQL回傳了許多欄位,JoeyEmployee的mapping,只需要Id => EmployeeId, Title => Title, TitleOfCourtesy => TitleOfCourtesy, 而FullName則是mapping到 FirstName +空白 + LastName。
2.Assert則是驗證筆數是否相等,是否FullName符合期望,是否回傳的每一筆資料的TitleOfCourtesy都是『Mr.』
測試結果:
結論
這篇文章的例子,只是透過委派來讓使用的人決定方法中的某一段邏輯,以達到SQL不需要每次都把所有欄位撈出來,針對需要的欄位撈即可,以降低撈資料的effort。
而委派可以直接使用Func<T1, T2, ….Tn, TResult>來表示,Func這些泛型型別的意義為
- Func<T1, T2, …, TResult>,其中T1到Tn,代表的就是input的參數個數以及對應的參數型別,而最後一個TResult則代表return的型別。
- Func<T1, TResult>就等同於public delegate TResult Delegate名稱(T1 parameter1),再多T也一樣。
- 而在assign值給Func<T1, TResult>時,可以直接使用delegate(T1 parameter1){return some TResult;}即可。
- assign時,也可以直接使用Lambda運算式指派給Func<T1,T2, TResult>,例如測試程式中的(p1, p2)=>{return some TResult; }
有了以上的範例以及說明,回過頭來看Linq裡面的Select方法:
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector
)
有沒有更清楚一點了呢?
不就是
- 掛在System.Linq的NameSpace底下,針對IEnumberable<T>的Extension Method
-
TSource可以跟TResult不一樣,以測試程式裡面來說,例如:
var result = actual.Select(x=>x.Id);
Select的參數型別為Func<JoeyEmployee, TResult>,而參數值為一段Lambda運算式,Lambda運算式為『x=>x.Id』,x的型別為JoeyEmployee,但Select裡面的Func<JoeyEmployee, TResult>最後TResult是回傳JoeyEmployee.Id,Id為int型別。所以最後的result型別為IEnumerable<int>。
Linq真的是一堆很基礎的features所堆砌起來很美的設計,在使用之餘,也應該要去瞭解背後基礎的features與原理,這樣更能去欣賞一些framework的美妙之處。
Source Code : RowMapper.zip
blog 與課程更新內容,請前往新站位置:http://tdd.best/