[.NET]Entity Generator by T4 with Table Description and Column Description
前言
在之前的文章:[.NET]RowMapper with Entity Generated by T4 and Mapping Nested Entity 中,有針對原本的 RowMapper 與使用 T4 來當做 Entity Generator 進行改善。
最近筆者開發系統時,又碰到了一個讓人覺得繁瑣的動作,也就是在設計 Entity 時,需要針對 class 與 property 補上 summary 說明。(也就是 C# document )
在面對過去前人所設計的欄位時,往往欄位命名不容易透過直覺就能了解, 就像下圖這樣的情況,實在不容易理解:
而部門內其實也有開發一個內部網頁,只需輸入 DB 與要查詢的 table 名稱,即可查到對應的欄位說明。如下圖所示:
由於一些欄位命名實在很像,加上這個複製貼上的動作實在很囉嗦兼容易出錯,正所謂「科技始終來自於人性」,內部網頁做的到,那 T4 也做的到,為什麼不乾脆在自動產生 Entity 的時候,就把 Column 的描述,直接產生在 class 與 property 的 summary 中呢?
所以,針對原本的 T4 ,又進行了一次改版。
Table Description 與 Column Description 相關的 SQL Statement
想要讓 T4 自動產生 table/column description 到 class/property 的 summary 中,那麼要先知道該怎麼查詢出 table description 與 column description 。
要查詢出 column 的相關資訊,請參考下面這段 SQL statement :
SELECT c.name AS [column],
cd.value AS [column_desc],
c.isnullable AS [isNullable]
FROM sysobjects t WITH(nolock)
INNER JOIN syscolumns c WITH(nolock)
ON c.id = t.id
LEFT OUTER JOIN sys.extended_properties cd WITH(nolock)
ON cd.major_id = c.id
AND cd.minor_id = c.colid
AND cd.name = 'MS_Description'
WHERE t.type = 'u'
and t.name='@tableName'
ORDER BY t.name, c.colorder;
查詢出來的結果,如下圖所示:
而要查詢 table description 則透過下面這段 SQL statement :
SELECT top 1
t.name AS [table_name],
td.value AS [table_desc]
FROM sysobjects t WITH(nolock)
INNER JOIN sys.extended_properties td WITH(nolock)
ON td.major_id = t.id
AND td.minor_id = 0
AND td.name = 'MS_Description'
WHERE t.type = 'u'
and t.name='@tableName';
查詢出來的結果,如下圖所示:
T4 內容
查到 table description 與 column description 的資料後,要放進去 T4 也就不是什麼難事了。改版後的 T4 如下:
<#@ template language="C#" debug="True" hostspecific="True" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Data.DataSetExtensions" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.xml" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.Linq" #>
using System;
namespace YourNamespace
{
<# //修改connection string
//todo, 請修改為你的connectionstring
string connectionString = "你的connectionstring";
SqlConnection conn = new SqlConnection(connectionString);
conn.Open();
//todo, 請修改為你的tableName
var tableName = "你的table名稱";
//如果需要database中全部table,則使用conn.GetSchema("Tables")即可
string[] restrictions = new string[4];
restrictions[1] = "dbo";
//修改table名稱
restrictions[2] = tableName;
DataTable schema = conn.GetSchema("Tables", restrictions);
string selectQuery = @"
SELECT top 1 * from @tableName WITH(nolock);
SELECT c.name AS [column],
cd.value AS [column_desc],
c.isnullable AS [isNullable]
FROM sysobjects t WITH(nolock)
INNER JOIN syscolumns c WITH(nolock)
ON c.id = t.id
LEFT OUTER JOIN sys.extended_properties cd WITH(nolock)
ON cd.major_id = c.id
AND cd.minor_id = c.colid
AND cd.name = 'MS_Description'
WHERE t.type = 'u'
and t.name='@tableName'
ORDER BY t.name, c.colorder;
SELECT top 1
t.name AS [table_name],
td.value AS [table_desc]
FROM sysobjects t WITH(nolock)
INNER JOIN sys.extended_properties td WITH(nolock)
ON td.major_id = t.id
AND td.minor_id = 0
AND td.name = 'MS_Description'
WHERE t.type = 'u'
and t.name='@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)
{
command.CommandText = selectQuery.Replace("@tableName",row["TABLE_NAME"].ToString());
ad.Fill(ds);
var isExistData = ds.Tables[2].Rows.Count > 0 ;
var tableDescription = isExistData ? ds.Tables[2].Rows[0]["table_desc"].ToString() : "";
#>
/// <summary>
/// <#= tableDescription #>
/// mapping table name: <#= row["TABLE_NAME"].ToString() #>
/// </summary>
public class <#= row["TABLE_NAME"].ToString() #>
{
<#
foreach (DataColumn dc in ds.Tables[0].Columns)
{
var columnDefinition = ds.Tables[1].AsEnumerable().Where(x => x["column"].ToString() == dc.ColumnName).FirstOrDefault();
var columnDescription = columnDefinition["column_desc"].ToString();
var isAllowNull = columnDefinition["isNullable"].ToString() == "1";
#>
/// <summary>
/// <#= columnDescription #>
/// </summary>
[DBColumnMapping("<#= dc.ColumnName #>")]
<# if(isAllowNull && dc.DataType.Name != "String"){ #>
public Nullable<<#= dc.DataType.Name #>> <#= dc.ColumnName #> { get; set; }
<# }
else
{ #>
public <#= dc.DataType.Name #> <#= dc.ColumnName #> { get; set; }
<#}
#>
<# } #>
}
<#
}
conn.Close();
#>
}
說明如下:
- 由於要在 T4 中使用 LINQ 語法,所以要讓 T4 參考 LINQ 相關的 DLL 以及 using 對應的 namespace。在 T4 中,要參考某一顆 DLL ,要透過 assembly 來加入參考,要 using namespace ,則是透過 import 。
- 這三段 SQL 以及查詢相關的 description 其實可以 tuning 的更有效率,不過這邊就留給讀者們自行改善囉,因為 T4 執行的次數不多,基本上不會對 DB 造成太多負擔。
這次有改善先前 T4 設計幾個問題:
- 先前查詢是直接使用 Select * From @tableName ,其實只是要 DataTable 的結構,根本不需要撈出所有資料,所以這一版加上 top 1 ,避免 table 資料過多時,花費過多不必要的資源。
- 先前 Query table 時沒有加上 with(Nolock) ,這一版把 nolock 加上去了。
- 最要命的是這一點,先前 connection open 之後,沒有呼叫 Close() ,這一版也補上去了。
最後產出的 Entity ,就會類似下圖(這邊 product table 沒有設定 table description ):
結論
就像筆者從之前到現在,一直提到的一句話:「每一次的複製貼上,都是扼殺自己成長的機會」,原本只是用來說明 DRY 原則,在設計物件內容時,一定要盡量避免相同的意義擁有重複的程式碼。
相同的,在一般開發流程中,太多重複性質的手動作業,只會消耗 developer 的 flow 能量,善用工具,了解工具,就可以客製化一些簡單的程式碼產生器,來幫助自己把時間花在更美好的事物上。
希望這一篇文章可以讓大家更了解 T4 可以幫助我們做哪些事,也希望這個 T4 範本可以協助更多 developer 擺脫弱型別的資料結構。即使不使用 ORM ,仍然可以使用強型別來當作溝通媒介。
補充
在 SQL Server 中,要設定 table description 與 column description ,要記得使用較高權限的帳號才能修改。
Column description 的設定,請在 table 上點選 [design] ,接著選取某一個欄位,在 Column Properties 中,編輯 [Description] 即可。請見下圖:
Table description 則在同一個畫面,開啟 Properties 視窗,裡面放的其實是 table 的 properties,一樣編輯 [Description] 即可。如下圖所示:
blog 與課程更新內容,請前往新站位置:http://tdd.best/