[.NET]RowMapper with Entity Generated by T4 and Mapping Nested Entity
前言
之前寫了兩篇文章,分別是透過 T4 來產生 DB table 對應的 entity class,以及自訂 rowmapper 模組來方便 DAL 回來的資料為強型別,請參考下列連結:
最近在用的時候,發現了一些些不足的地方,例如:
- 當 DB table schema 的欄位設計為 AllowNull 時,希望 entity 對應的 property type,也可以宣告成 Nullable 的形態。而原本的 T4 沒有這一段判斷。
- Rowmapper module 針對 Nullable 型態的 property type 在 mapping 時,也需要額外的處理。例如當 type 為 Nullable,且為 ValueType 時,且資料庫的值為 DBNull 時,則應該給予該型別預設值。例如當值為 DBNull 時,property type 為 int? 時,則值給予 null。若為 int 時,則值給 0。如果是 string,則給 null。如果是 DateTime,則給 new DateTime()。
- 當查詢語法是 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):
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妹的提醒。
流程基本上也很簡單:
- 判斷 entity property type 是否為 Enum
- 若為 Enum,則透過 Enum.ToObject 來轉換值
- 若不是 Enum,判斷 property type 是否為 Nullable,並取得 Nullable<T> 中的 T 是什麼型別。
-
取得該型別的預設值:若為Nullable 或 reference type,則回傳 null。若為 value type,則回傳該 type 的預設值。
使用 Activator.CreateInstance(type) 的原因,不用 default(T) 的原因,則是T只能為靜態,不能自己給 default( type的instance ),所以透過 Activator.CreateInstance(type) 來產生型別初始值。 - 最後,當資料來源的值為 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/