[.NET]RowMapper with Entity Generated by T4 and Mapping Nested Entity

  • 6251
  • 0
  • 2012-09-17

[.NET]RowMapper with Entity Generated by T4 and Mapping Nested Entity

前言

之前寫了兩篇文章,分別是透過 T4 來產生 DB table 對應的 entity class,以及自訂 rowmapper 模組來方便 DAL 回來的資料為強型別,請參考下列連結:

  1. [.NET]透過 T4 產生對應 DB table 的 entity
  2. [.NET]RowMapper模組

最近在用的時候,發現了一些些不足的地方,例如:

  1. 當 DB table schema 的欄位設計為 AllowNull 時,希望 entity 對應的 property type,也可以宣告成 Nullable 的形態。而原本的 T4 沒有這一段判斷。
  2. Rowmapper module 針對 Nullable 型態的 property type 在 mapping 時,也需要額外的處理。例如當 type 為 Nullable,且為 ValueType 時,且資料庫的值為 DBNull 時,則應該給予該型別預設值。例如當值為 DBNull 時,property type 為 int? 時,則值給予 null。若為 int 時,則值給 0。如果是 string,則給 null。如果是 DateTime,則給 new DateTime()。
  3. 當查詢語法是 join 多張表,需要 mapping 到複合/巢狀的 entity,則原本的 rowmapper 沒有比較簡潔的作法。

這篇文章,就針對 T4 的 template 以及 rowmapper 的補充,進行說明與 memo。

 

Generate Table Entity with Nullable Type by T4

T4 程式碼如下:


<#@ template language="C#" debug="True" hostspecific="True" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.xml" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="System.Data" #>

using System;
using Monday.DataAccess.Common;

namespace MyProject.Entities 
{        
		<#
			//修改connection string
			string connectionString = "你的ConnectionString"; 
			SqlConnection conn = new SqlConnection(connectionString); 
			conn.Open(); 
			
			//如果需要database中全部table,則使用conn.GetSchema("Tables")即可
			string[] restrictions = new string[4];
			restrictions[1] = "dbo";
			//修改table名稱
			restrictions[2] = "你的Table名稱";
			DataTable schema = conn.GetSchema("Tables", restrictions);
			
			string selectQuery = "select * from  @tableName"; 
			SqlCommand command = new SqlCommand(selectQuery,conn); 
			SqlDataAdapter ad = new SqlDataAdapter(command); 
			System.Data.DataSet ds = new DataSet(); 
			
			foreach(System.Data.DataRow row in schema.Rows) 
			{ 				
			#>             
			//mapping table name: <#= row["TABLE_NAME"].ToString().Trim('s') #>
			public class <#= row["TABLE_NAME"].ToString().Trim('s') #>                            
			{
				<#                 
					command.CommandText = selectQuery.Replace("@tableName",row["TABLE_NAME"].ToString()); 
					ad.FillSchema(ds, SchemaType.Mapped, row["TABLE_NAME"].ToString());

					foreach (DataColumn dc in ds.Tables[0].Columns)
					{
						#>
				[ColumnMappingAttribute("<#= dc.ColumnName #>")]                        
				<# if(dc.AllowDBNull && dc.DataType.Name != "String"){ #>
					public Nullable<<#= dc.DataType.Name #>> <#= dc.ColumnName #>  { get; set; }                                        
				<#     }                
				else
				{ #>
					public <#= dc.DataType.Name #> <#= dc.ColumnName #>  { get; set; }                                        
				<#}
				#>                                	

				<#    }                 #>                                
			}                            
			<#    
			}  #>
}

跟之前的程式碼,基本上差異點只在額外判斷了 DataColumn 是否 AllowDBNull,另外偷用鋸箭法把 String 排除。如果是 AllowDBNull,則加上 Nullable<T>,如果不是,則使用原本的 DataType 。

範例就會類似下圖所示(在我的專案裡面,attribute 是命成 DBColumnMapping):

image

 

RowMapper Enhancement

有了上面的 T4 可根據 DB column 的 type 來決定 property type 是否為 Nullable,接下來就是在做 row mapping 的時候,要針對 Nullable 與 DBNull 來處理。

這邊只列出之前 RowMapper module 骨子裡 mapping 每一筆記錄的 function,並且 開放出來給外部使用,供 mapping 複合式的 entity。


    /// <summary>
    /// RowMapper模組
    /// </summary>
    internal static class ColumnMappingHelper
    {
        /// <summary>
        /// Mappings the entity.
        /// </summary>
        /// <typeparam name="T">entity</typeparam>
        /// <param name="row">The row.</param>
        /// <returns>
        /// mapping完資料的entity
        /// </returns>
        internal static T MappingEntity<T>(DataRow row) where T : new()
        {
            var result = new T();

            ////取得DataRow上所有Column名稱
            var columns = row.Table.Columns.Cast<DataColumn>().Select(x => x.ColumnName);

            PropertyInfo[] properties = typeof(T).GetProperties();

            foreach (var p in properties)
            {
                ////取得property對應要mapping的column name
                var mappingAttribute = p.GetCustomAttributes(typeof(DBColumnMappingAttribute), false).FirstOrDefault() as DBColumnMappingAttribute;
                var columnName = mappingAttribute == null ? p.Name : mappingAttribute.ColumnName;

                if (columns.Contains(columnName))
                {
                    if (!p.PropertyType.IsEnum)
                    {
                        MappingValueToPropertyType<T>(row, result, p, columnName);
                    }
                    else
                    {
                        MappingValueToEnum<T>(row, result, p, columnName);
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// 取得該type的預設值,若為Nullable,則回傳null
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="isNullable">if set to <c>true</c> [is nullable].</param>
        /// <returns>
        /// 該type預設值
        /// </returns>
        private static object GetDefaultValueByType(Type type, bool isNullable)
        {
            if (isNullable || !type.IsValueType)
            {
                return null;
            }
            else
            {
                return Activator.CreateInstance(type);
            }
        }

        /// <summary>
        /// Mappings the value to enum.
        /// </summary>
        /// <typeparam name="T">entity</typeparam>
        /// <param name="row">The row.</param>
        /// <param name="result">The result.</param>
        /// <param name="p">The p.</param>
        /// <param name="columnName">Name of the column.</param>
        private static void MappingValueToEnum<T>(DataRow row, T result, PropertyInfo p, string columnName) where T : new()
        {
            p.SetValue(result, Enum.ToObject(p.PropertyType, row[columnName]), null);
        }

        /// <summary>
        /// Mappings the type of the value to property.
        /// </summary>
        /// <typeparam name="T">entity</typeparam>
        /// <param name="row">The row.</param>
        /// <param name="result">The result.</param>
        /// <param name="p">The p.</param>
        /// <param name="columnName">Name of the column.</param>
        private static void MappingValueToPropertyType<T>(DataRow row, T result, PropertyInfo p, string columnName) where T : new()
        {
            bool isNullable = Nullable.GetUnderlyingType(p.PropertyType) != null;
            Type type = Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType;

            var typeDefaultValue = GetDefaultValueByType(type, isNullable);
            var safeValue = row[columnName] == DBNull.Value ? typeDefaultValue : Convert.ChangeType(row[columnName], type);

            p.SetValue(result, safeValue, null);
        }
    }

 

註:之前判斷 property type 是否為 Nullable<T> 的判斷式寫錯了,應該改成:
bool isNullable = Nullable.GetUnderlyingType(p.PropertyType) != null;
感謝J妹的提醒。

流程基本上也很簡單:

  1. 判斷 entity property type 是否為 Enum
  2. 若為 Enum,則透過 Enum.ToObject 來轉換值
  3. 若不是 Enum,判斷 property type 是否為 Nullable,並取得 Nullable<T> 中的 T 是什麼型別。
  4. 取得該型別的預設值:若為Nullable 或 reference type,則回傳 null。若為 value type,則回傳該 type 的預設值。
    使用 Activator.CreateInstance(type) 的原因,不用 default(T) 的原因,則是T只能為靜態,不能自己給 default( type的instance ),所以透過 Activator.CreateInstance(type) 來產生型別初始值。
  5. 最後,當資料來源的值為 DBNull.Value 時,則給予此 property 剛剛的預設值,若不是 DBNull.Value,則將值透過 Convert.ChangeType 進行轉型,assign 給此 property。

透過這樣的方式,就可以 mapping entity 上,型別為 Nullable<T> 的 property。

 

Row Mapping Nested Entity

假設,我們的 SQL statement 如下:


SELECT MyOrder.*, MyOrderItem.Column1, MyOrderItem.Column2 
FROM MyOrder WITH(nolock)
INNER JOIN MyOrderItem WITH(nolock) ON MyOrder.OrderId = MyOrderItem.OrderId
WHERE MyOrder.OrderId = @OrderId

回傳的資料欄位,是跨多張 table 的,該怎麼來 mapping 成 Entity 呢?

我們可以設計一個 OrderModel 的 entity 如下:


public class OrderModel
{
	public Order Order { get; set; }
  
	public OrderItem OrderItem { get; set; }
}

接著,透過之前的 RowMapper module 所提供的 RowMapperDelegateFunction 參數,來 mapping 這樣的複合型 entity,範例如下:


            var result = RowMapperService.GetEntityList<OrderModel>(
                CommandType.Text,
                sql,
                myConnectionString,
                x =>
                {
                    var entity = new OrderModel
                    {
                        Order = ColumnMappingHelper.MappingEntity<Order>(x),
                        OrderItem = ColumnMappingHelper.MappingEntity<OrderItem>(x)
                    };

                    return entity;
                },
                sqlParameters.ToArray()).ToList();

每一筆 OrderModel 的 Order,透過上面的 MappingEntity<T> 來 mapping 回一個 Order Entity,assign 給 OrderModel.Order。同理,OrderItem 也是如此。

透過這樣的方式,針對 SQL statement 有 JOIN 的情境,就會方便很多,因為不需要再自己把每個 property 攤開,自己在 delegate function 中一一 mapping。

 

結論

基於希望系統設計時,都是透過強型別,且擔心處理大量資料的場景,使用 ORM framework 可能造成的效率問題,所以才會有這種輕量型的 row mapper 模組。

這邊做個記錄,也希望可以給大家參考或一些幫助。

另外,感謝小朱提供 Activator.CreateInstance(type) 的方式,來取代 default(T) 的作法,受益良多。


blog 與課程更新內容,請前往新站位置:http://tdd.best/