創建代碼生成器可以很簡單:如何通過T4模板生成代碼?[上篇]
在《基于T4的代碼生成方式》中,我對T4模板的組成結構、語法,以及T4引擎的工作原理進行了大體的介紹,并且編寫了一個T4模板實現了如何將一個XML轉變成C#代碼。為了讓由此需求的讀者對T4有更深的了解,我們通過T4來做一些更加實際的事情——SQL Generator。在這里,我們可以通過SQL Generator為某個數據表自動生成進行插入、修改和刪除的存儲過程。[文中源代碼從這里下載]
一、代碼生成器的最終使用效果
我們首先來看看通過直接適用我們基于T4的SQL生成模板達到的效果。右圖(點擊看大圖)是VS2010的Solution Explorer,在Script目錄下面,我定義了三個后綴名為.tt的T4模板。它們實際上是基于同一個數據表(T_PRODUCT)的三個存儲過程的生成創建的模板文件,其中P_PRODUCT_D.tt、P_PRODUCT_I.tt和P_PRODUCT_D.tt分別用于記錄的刪除、插入和修改。自動生成的擴展名為.sql的同名附屬文件就是相應的存儲過程。
基于三種不同的數據操作(Insert、Update和Delete),我創建了3個重用的、與具體數據表無關的模板: InsertProcedureTemplate、UpdateProcedureTemplate和DeleteProcedureTemplate。這樣做的目的為為了實現最大的重用,如果我們需要為某個數據表創建相應的存儲過程的時候,我們可以直接使用它們傳入相應的數據表名就可以了。實際上,P_PRODUCT_D.tt、P_PRODUCT_I.tt和P_PRODUCT_D.tt這三個T4模板的結構很簡單,它們通過<#@include>指令將定義著相應ProcedureTemplate的T4模板文件包含進來。最終的存儲過程腳本通過調用ProcudureTempalte的Render方法生成。其中構造函數的參數表示的分別是連接字符串名稱(在配置文件中定義)和數據表的名稱。
<#@ template language="C#" hostspecific="True" #>
<#@ output extension="sql" #>
<#@ include file="T4Toolbox.tt" #>
<#@ include file="..\Templates\DeleteProcedureTemplate.tt" #>
<#
new DeleteProcedureTemplate("TestDb","T_PRODUCT").Render();
#>
<#@ template language="C#" hostspecific="True" #>
<#@ output extension="sql" #>
<#@ include file="T4Toolbox.tt" #>
<#@ include file="..\Templates\InsertProcedureTemplate.tt" #>
<#
new InsertProcedureTemplate("TestDb","T_PRODUCT").Render();
#>
<#@ template language="C#" hostspecific="True" #>
<#@ output extension="sql" #>
<#@ include file="T4Toolbox.tt" #>
<#@ include file="..\Templates\UpdateProcedureTemplate.tt" #>
<#
new UpdateProcedureTemplate("TestDb","T_PRODUCT").Render();
#>
二、安裝T4工具箱(ToolBox)和編輯器
VS本身只提供一套基于T4引擎的代碼生成的執行環境,為了利于你的編程你可以安裝一些輔助性的東西。T4 ToolBox是一個CodePlex上開源的工具,它包含一些可以直接使用的代碼生成器,比如Enum SQL View、AzMan wrapper、LINQ to SQL classes、LINQ to SQL schema和Entity Framework DAL等。T4 ToolBox還提供一些基于T4方面的VS的擴展。當你按照之后,在“Add New Item”對話框中就會多出一個命名為“Code Generation”的類別,其中包括若干文件模板。下面提供的T4模板的編輯工作依賴于這個工具。
為了提高編程體驗,比如智能感知以及代碼配色,我們還可以安裝一些第三方的T4編輯器。我使用的是一個叫做Oleg Sych的T4 Editor。它具有免費版本和需要付費的專業版本,當然我使用的免費的那款。成功按裝了,它也會在Add New Item”對話框中提供相應的基于T4 的文件模板。
三、創建數據表
T4模板就是輸入和輸出的一個適配器,這與XSLT的作用比較類似。對于我們將要實現的SQL Generator來說,輸入的是數據表的結構(Schema)輸出的是最終生成的存儲過程的SQL腳本。對于數據表的定義,不同的項目具有不同標準。我采用的是我們自己的數據庫標準定義的數據表:T_PRODUCT(表示產品信息),下面是創建表的腳本。
CREATE TABLE [dbo].[T_PRODUCT](
[ID] [VARCHAR](50) NOT NULL,
[NAME] [NVARCHAR] NOT NULL,
[PRICE] [float] NOT NULL,
[TOTAL_PRICE] [FLOAT] NOT NULL,
[DESC] [NVARCHAR] NULL,
[CREATED_BY] [VARCHAR](50) NULL,
[CREATED_ON] [DATETIME] NULL,
[LAST_UPDATED_BY] [VARCHAR](50) NULL,
[LAST_UPDATED_ON] [DATETIME] NULL,
[VERSION_NO] [TIMESTAMP] NULL,
[TRANSACTION_ID] [VARCHAR](50) NULL,
CONSTRAINT [PK_T_PRODUCT] PRIMARY KEY CLUSTERED( [ID] ASC)ON [PRIMARY])
每一個表中有6個公共的字段:CREATED_BY、CREATED_ON、LAST_UPDATED_BY、LAST_UPDATED_ON、VERSION_NO和TRANSACTION_ID分別表示記錄的創建者、創建時間、最新更新者、最新更新時間、版本號(并發控制)和事務ID。
四、創建抽象的模板:ProcedureTemplate
我們需要為三不同的數據操作得存儲過程定義不同的模板,但是對于這三種存儲過程的SQL結構都是一樣的,基本結果可以通過下面的SQL腳本表示。
IF OBJECT_ID( '<<ProcedureName>>', 'P' ) IS NOT NULL
DROP PROCEDURE <<ProcedureName>>
GO
CREATE PROCEDURE <<ProcedureName>>
(
<<ParameterList>>
)
AS
<<ProcedureBody>>
GO
為此我定義了一個抽象的模板:ProcedureTemplate。為了表示CUD三種不同的操作,我通過T4模板的“類特性塊”(Class Feature Block)定義了如下一個OperationKind的枚舉。
<#+
public enum OperationKind
{
Insert,
Update,
Delete
}
#>
然后下面就是整個ProcedureTemplate的定義了。ProcedureTemplate直接繼承自T4Toolbox.Template(來源于T4 ToolBox,它繼承自TextTransformation)。ProcedureTemplate通過SMO(SQL Server Management Object)獲取數據表的結構(Schema)信息,所以我們需要應用SMO相關的程序集和導入相關命名空間。ProcedureTemplate具有三個屬性DatabaseName(表示連接字符串名稱)、Table(SMO中表示數據表)和OperationKind(表示具體的CUD操作的一種),它們均通過構造函數初始化。
<#@ assembly name="Microsoft.SqlServer.Smo" #>
<#@ assembly name="Microsoft.SqlServer.Management.Sdk.Sfc" #>
<#@ import namespace="System" #>
<#@ import namespace="Microsoft.SqlServer.Management.Smo" #>
<#+
public abstract class ProcedureTemplate : Template
{
public string DatabaseName {get; private set;}
public OperationKind OperationKind {get; private set;}
public Table Table {get; private set;}
public const string VersionNoField = "VERSION_NO";
public const string VersionNoParameterName = "@p_version_no";
public ProcedureTemplate(string databaseName, string tableName,OperationKind operationKind)
{
Guard.ArgumentNotNullOrEmpty(databaseName,"databaseName");
Guard.ArgumentNotNullOrEmpty(tableName,"tableName");
this.DatabaseName = databaseName;
this.OperationKind = operationKind;
Server server = new Server();
Database database = new Database(server, DatabaseName);
this.Table = new Table(database, tableName);
this.Table.Refresh();
}
public virtual string GetProcedureName()
{
switch(this.OperationKind)
{
case OperationKind.Insert: return "P_" +this.Table.Name.Remove(0,2) + "_I";
case OperationKind.Update: return "P_" +this.Table.Name.Remove(0,2) + "_U";
default: return "P_" +this.Table.Name.Remove(0,2) + "_D";
}
}
protected virtual string GetParameterName(string columnName)
{
return "@p_" + columnName.ToLower();
}
protected abstract void RenderParameterList();
protected abstract void RenderProcedureBody();
public override string TransformText()
{
Server server = new Server();
Database database = new Database(server, DatabaseName);
Table table = new Table(database, this.Table.Name);
table.Refresh();
#>
IF OBJECT_ID( '[dbo].[<#= GetProcedureName()#>]', 'P' ) IS NOT NULL
DROP PROCEDURE [dbo].[<#= GetProcedureName()#>]
GO
CREATE PROCEDURE [dbo].[<#= GetProcedureName() #>]
(
<#+
PushIndent("\t");
this.RenderParameterList();
PopIndent();
#>
)
AS
<#+
PushIndent("\t");
this.RenderProcedureBody();
PopIndent();
PopIndent();
WriteLine("\nGO");
return this.GenerationEnvironment.ToString();
}
}
#>
存儲過程的參數我們采用小寫形式,直接在列名前加上一個"p_”(Parameter)前綴,列名到參數名之間的轉化通過方法GetParameterName實現。存儲過程名稱通過表明轉化,轉化規則為:將"T_”(Table)改成"P_”(Procedure)前綴,并添加"_I"、"_U"和"_D"表示相應的操作類型,存儲過程名稱的解析通過GetProcedureName實現。整個存儲過程的輸出通過方法TransformText輸出,并通過PushIndent和PopIndent方法控制縮進。由于CUD存儲只有兩個地方不一致:參數列表和存儲過程的主體,我定義了兩個抽象方法RenderParameterList和RenderProcedureBody讓具體的ProcedureTemplate去實現。
五、為CUD操作創建具體模板
基類ProcedureTemplate已經定義出了主要的轉化規則,我們現在需要做的就是通過T4模板創建3個具體的ProcedureTemplate,分別實現針對CUD存儲過程的生成。為此我創建了三個繼承自ProcedureTemplate的具體類:InsertProcedureTemplate、UpdateProcedureTemplate和DeleteProcedureTemplate,它只需要實現RenderParameterList和RenderProcedureBody這兩個抽象方法既即可,下面是它們的定義。
<#@ include file="ProcedureTemplate.tt" #>
<#+
public class InsertProcedureTemplate : ProcedureTemplate
{
public InsertProcedureTemplate(string databaseName, string tableName): base(databaseName,tableName,OperationKind.Insert){}
protected override void RenderParameterList()
{
for(int i=0; i<this.Table.Columns.Count;i++)
{
Column column = this.Table.Columns[i];
if(column.Name != VersionNoField)
{
if(i<this.Table.Columns.Count -1)
{
WriteLine("{0, -20}[{1}],", GetParameterName(column.Name),column.DataType.Name.ToUpper());
}
else
{
WriteLine("{0, -20}[{1}]", GetParameterName(column.Name),column.DataType.Name.ToUpper());
}
}
}
}
protected override void RenderProcedureBody()
{
WriteLine("INSERT INTO [dbo].[{0}]", this.Table.Name);
WriteLine("(");
PushIndent("\t");
for(int i=0; i<this.Table.Columns.Count;i++)
{
Column column = this.Table.Columns[i];
if(column.Name != VersionNoField)
{
if(i<this.Table.Columns.Count -1)
{
WriteLine("[" +column.Name + "],");
}
else
{
WriteLine("[" +column.Name + "]");
}
}
}
PopIndent();
WriteLine(")");
WriteLine("VALUES");
WriteLine("(");
PushIndent("\t");
for(int i=0; i<this.Table.Columns.Count;i++)
{
Column column = this.Table.Columns[i];
if(column.Name != VersionNoField)
{
if(i<this.Table.Columns.Count -1)
{
WriteLine(GetParameterName(column.Name) + ",");
}
else
{
WriteLine(GetParameterName(column.Name));
}
}
}
PopIndent();
WriteLine(")");
}
}
#>
<#@ include file="ProcedureTemplate.tt" #>
<#+
public class UpdateProcedureTemplate : ProcedureTemplate
{
public UpdateProcedureTemplate(string databaseName, string tableName): base(databaseName,tableName,OperationKind.Update)
{}
protected override void RenderParameterList()
{
for(int i=0; i<this.Table.Columns.Count;i++)
{
Column column = this.Table.Columns[i];
if(i<this.Table.Columns.Count -1)
{
WriteLine("{0, -20}[{1}],", GetParameterName(column.Name),column.DataType.Name.ToUpper());
}
else
{
WriteLine("{0, -20}[{1}]", GetParameterName(column.Name),column.DataType.Name.ToUpper());
}
}
}
protected override void RenderProcedureBody()
{
WriteLine("UPDATE [dbo].[{0}]", this.Table.Name);
WriteLine("SET");
PushIndent("\t");
for(int i=0; i<this.Table.Columns.Count;i++)
{
Column column = this.Table.Columns[i];
if(!column.InPrimaryKey)
{
if(i<this.Table.Columns.Count -1)
{
WriteLine("{0,-20}= {1},", "[" +column.Name + "]", this.GetParameterName(column.Name));
}
else
{
WriteLine("{0,-20}= {1}", "[" +column.Name+"]", this.GetParameterName(column.Name));
}
}
}
PopIndent();
WriteLine("WHERE");
PushIndent("\t");
for(int i=0; i<this.Table.Columns.Count;i++)
{
Column column = this.Table.Columns[i];
if(column.InPrimaryKey)
{
WriteLine("{0, -20}= {1} AND", "[" +column.Name + "]", GetParameterName(column.Name));
}
}
WriteLine("{0, -20}= {1}", "[" + VersionNoField + "]", VersionNoParameterName);
PopIndent();
}
}
#>
<#@ include file="ProcedureTemplate.tt" #>
<#+
public class DeleteProcedureTemplate : ProcedureTemplate
{
public DeleteProcedureTemplate(string databaseName, string tableName): base(databaseName,tableName,OperationKind.Delete){}
protected override void RenderParameterList()
{
foreach (Column column in this.Table.Columns)
{
if (column.InPrimaryKey)
{
WriteLine("{0, -20}[{1}],", GetParameterName(column.Name),column.DataType.Name.ToUpper());
}
}
WriteLine("{0, -20}[{1}]", VersionNoParameterName, "TIMESTAMP");
}
protected override void RenderProcedureBody()
{
WriteLine("DELETE FROM [dbo].[{0}]", this.Table.Name);
WriteLine("WHERE");
PushIndent("\t\t");
foreach (Column column in this.Table.Columns)
{
if (column.InPrimaryKey)
{
WriteLine("{0, -20}= {1} AND", column.Name, GetParameterName(column.Name));
}
}
WriteLine("{0, -20}= {1}", VersionNoField, VersionNoParameterName);
}
}
#>
至于三個具體的ProcedureTemplate如何生成參數列表和主體部分,在這里就不在多做說明了。這里唯一需要強調的是:腳本的輸出是通過TextTransformation的靜態WriteLine方法實現,它和Console的同名方法使用一致。針對我們之前定義的數據表T_PRODUCT的結果,通過在文章開頭定義的三個TT模板,最終將會生成如下的三個存儲過程。
IF OBJECT_ID( '[dbo].[P_PRODUCT_I]', 'P' ) IS NOT NULL
DROP PROCEDURE [dbo].[P_PRODUCT_I]
GO
CREATE PROCEDURE [dbo].[P_PRODUCT_I]
(
@p_id [VARCHAR],
@p_name [NVARCHAR],
@p_price [FLOAT],
@p_total_price [FLOAT],
@p_desc [NVARCHAR],
@p_created_by [VARCHAR],
@p_created_on [DATETIME],
@p_last_updated_by [VARCHAR],
@p_last_updated_on [DATETIME],
@p_transaction_id [VARCHAR]
)
AS
INSERT INTO [dbo].[T_PRODUCT]
(
[ID],
[NAME],
[PRICE],
[TOTAL_PRICE],
[DESC],
[CREATED_BY],
[CREATED_ON],
[LAST_UPDATED_BY],
[LAST_UPDATED_ON],
[TRANSACTION_ID]
)
VALUES
(
@p_id,
@p_name,
@p_price,
@p_total_price,
@p_desc,
@p_created_by,
@p_created_on,
@p_last_updated_by,
@p_last_updated_on,
@p_transaction_id
)
GO
IF OBJECT_ID( '[dbo].[P_PRODUCT_U]', 'P' ) IS NOT NULL
DROP PROCEDURE [dbo].[P_PRODUCT_U]
GO
CREATE PROCEDURE [dbo].[P_PRODUCT_U]
(
@p_id [VARCHAR],
@p_name [NVARCHAR],
@p_price [FLOAT],
@p_total_price [FLOAT],
@p_desc [NVARCHAR],
@p_created_by [VARCHAR],
@p_created_on [DATETIME],
@p_last_updated_by [VARCHAR],
@p_last_updated_on [DATETIME],
@p_version_no [TIMESTAMP],
@p_transaction_id [VARCHAR]
)
AS
UPDATE [dbo].[T_PRODUCT]
SET
[NAME] = @p_name,
[PRICE] = @p_price,
[TOTAL_PRICE] = @p_total_price,
[DESC] = @p_desc,
[CREATED_BY] = @p_created_by,
[CREATED_ON] = @p_created_on,
[LAST_UPDATED_BY] = @p_last_updated_by,
[LAST_UPDATED_ON] = @p_last_updated_on,
[VERSION_NO] = @p_version_no,
[TRANSACTION_ID] = @p_transaction_id
WHERE
[ID] = @p_id AND
[VERSION_NO] = @p_version_no
GO
IF OBJECT_ID( '[dbo].[P_PRODUCT_D]', 'P' ) IS NOT NULL
DROP PROCEDURE [dbo].[P_PRODUCT_D]
GO
CREATE PROCEDURE [dbo].[P_PRODUCT_D]
(
@p_id [VARCHAR],
@p_version_no [TIMESTAMP]
)
AS
DELETE FROM [dbo].[T_PRODUCT]
WHERE
ID = @p_id AND
VERSION_NO = @p_version_no
GO
六、局限性
上面這個例子雖然很好實現了基于數據表的存儲過程的生成,但是使用起來仍然不方便——我們需要為每一個需要生成出來的存儲過程定義T4模板。也就是說在這種代碼生成下,模板文件和生成文件之間是1:1的關系。實際上我們希望的方式是:創建一個基于某個表的TT文件,讓它生成3個CUD三個存儲過程;或者在一個TT文件中設置一個數據表的列表,讓基于這些表的所有存儲過程一并生成;或者直接子指定數據庫,讓所有數據表的存儲過程一并生成出來。到底如何實現基于多文件的代碼生成,請聽下回分解。