[.NET]使用Func<T1, T2, …, Tn, TResult>自訂RowMapper邏輯

  • 4740
  • 0
  • 2011-12-28

[.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的集合。參數如下:

  1. SqlStatement:主要的SQL內容
  2. Parameters:搭配SQL內容所需要的parameter
  3. ConnectionString:若傳進來的為connectionString,則代表這次SQL的執行為新起一個connection。(可撰寫overload function,傳入connection與transaction,則代表要使用同一個connection,或包含在同一個transaction中)
  4. Delegate RowMapper Function:這邊輸入參數型別為SqlDataReader與int,其中SqlDataReader代表SqlCommand.ExecuteReader中的reader.Read()結果。int則代表目前讀到第幾筆record。
  5. 其他:包括上面提到的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的執行結果如下圖所示:
image

對應的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.』

測試結果:

image

結論

這篇文章的例子,只是透過委派來讓使用的人決定方法中的某一段邏輯,以達到SQL不需要每次都把所有欄位撈出來,針對需要的欄位撈即可,以降低撈資料的effort。

而委派可以直接使用Func<T1, T2, ….Tn, TResult>來表示,Func這些泛型型別的意義為

  1. Func<T1, T2, …, TResult>,其中T1到Tn,代表的就是input的參數個數以及對應的參數型別,而最後一個TResult則代表return的型別。
  2. Func<T1, TResult>就等同於public delegate TResult Delegate名稱(T1 parameter1),再多T也一樣。
  3. 而在assign值給Func<T1, TResult>時,可以直接使用delegate(T1 parameter1){return some TResult;}即可。
  4. 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
)


有沒有更清楚一點了呢?

不就是

  1. 掛在System.Linq的NameSpace底下,針對IEnumberable<T>的Extension Method
  2. 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/