閱讀895 返回首頁    go 技術社區[雲棲]


創建代碼生成器可以很簡單:如何通過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分別用於記錄的刪除、插入和修改。。

基於三種不同的數據操作(Insert、Update和Delete),我創建了3個重用的、與具體數據表無關的模板: InsertProcedureTemplate、UpdateProcedureTemplate和DeleteProcedureTemplate。這樣做的目的為為了實現最大的重用,如果我們需要為某個數據表創建相應的存儲過程的時候,我們可以直接使用它們傳入相應的數據表名就可以了。實際上,P_PRODUCT_D.tt、P_PRODUCT_I.tt和P_PRODUCT_D.tt這三個T4模板的結構很簡單,它們通過指令將定義著相應ProcedureTemplate的T4模板文件包含進來。最終的存儲過程腳本通過調用ProcudureTempalte的Render方法生成。其中構造函數的參數表示的分別是(在配置文件中定義)和。

<#@ template language="C#" hostspecific="True" #>
<#@ output extension="sql" #>
<#@ include  #>
<#@ include  #>
<#
    new DeleteProcedureTemplate("TestDb","T_PRODUCT").Render();
#>
<#@ template language="C#" hostspecific="True" #>
<#@ output extension="sql" #>
<#@ include  #>
<#@ include  #>
<#
    new InsertProcedureTemplate("TestDb","T_PRODUCT").Render();
#>
<#@ template language="C#" hostspecific="True" #>
<#@ output extension="sql" #>
<#@ include  #>
<#@ include  #>
<#
    new UpdateProcedureTemplate("TestDb","T_PRODUCT").Render();
#>

VS本身隻提供一套基於T4引擎的代碼生成的執行環境,為了利於你的編程你可以安裝一些輔助性的東西。T4 ToolBox是一個CodePlex上開源的工具,它包含一些可以直接使用的代碼生成器,比如Enum SQL ViewAzMan wrapperLINQ to SQL classesLINQ to SQL schemaEntity Framework DAL等。T4 ToolBox還提供一些基於T4方麵的VS的擴展。當你按照之後,在“Add New Item”對話框中就會多出一個命名為“Code Generation”的類別,其中包括若幹文件模板。下麵提供的T4模板的編輯工作依賴於這個工具。

image

為了提高編程體驗,比如智能感知以及代碼配色,我們還可以安裝一些第三方的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。

我們需要為三不同的數據操作得存儲過程定義不同的模板,但是對於這三種存儲過程的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直接繼承自(來源於T4 ToolBox,它繼承自TextTransformation)。ProcedureTemplate通過SMO(SQL Server Management Object)獲取數據表的結構(Schema)信息,所以我們需要應用SMO相關的程序集和導入相關命名空間。ProcedureTemplate具有兩個屬性

   1: <#@ assembly name="Microsoft.SqlServer.ConnectionInfo" #>
   2: <#@ assembly name="Microsoft.SqlServer.Smo" #>
   3: <#@ assembly name="Microsoft.SqlServer.Management.Sdk.Sfc" #>
   4: <#@ import namespace="System" #>
   5: <#@ import namespace="Microsoft.SqlServer.Management.Smo" #>
   6: <#+
   7: public abstract class ProcedureTemplate : Template
   8: {
   9:     public OperationKind OperationKind {get; private set;}
  10:     public Table Table {get; private set;}
  11:     
  12:     public const string VersionNoField             = "VERSION_NO";
  13:     public const string VersionNoParameterName     = "@p_version_no";
  14:     
  15:     public ProcedureTemplate(string databaseName, string tableName,OperationKind operationKind)
  16:     {
  17:         this.OperationKind     = operationKind;
  18:         Server server = new Server();
  19:         Database database = new Database(server,databaseName);
  20:         this.Table = new Table(database, tableName);
  21:         this.Table.Refresh();
  22:     }
  23:     
  24:     public virtual string GetProcedureName()
  25:     {
  26:         switch(this.OperationKind)
  27:         {
  28:             case OperationKind.Insert:    return "P_" +this.Table.Name.Remove(0,2) + "_I";
  29:             case OperationKind.Update:    return "P_" +this.Table.Name.Remove(0,2) + "_U";
  30:             default:                    return "P_" +this.Table.Name.Remove(0,2) + "_D";
  31:         }        
  32:     }
  33:     
  34:     protected virtual string GetParameterName(string columnName)
  35:     {
  36:         return "@p_" + columnName.ToLower();
  37:     }
  38:     
  39:     protected abstract void RenderParameterList();
  40:     
  41:     protected abstract void RenderProcedureBody();        
  42:  
  43:     public override string TransformText()
  44:     {
  45: #>
  46: IF OBJECT_ID( '[dbo].[<#=  GetProcedureName()#>]', 'P' ) IS NOT NULL
  47:     DROP  PROCEDURE  [dbo].[<#=  GetProcedureName()#>]
  48: GO
  49:  
  50: CREATE PROCEDURE [dbo].[<#= GetProcedureName() #>]
  51: (
  52: <#+
  53:         PushIndent("\t");
  54:         this.RenderParameterList();
  55:         PopIndent();
  56: #>
  57: )
  58: AS
  59:    
  60: <#+
  61:         PushIndent("\t");
  62:         this.RenderProcedureBody();
  63:         PopIndent(); 
  64:         PopIndent(); 
  65:         WriteLine("\nGO");
  66:         return this.GenerationEnvironment.ToString();
  67:     }
  68: }
  69: #>

存儲過程的參數我們采用小寫形式,直接在列名前加上一個"p_”(Parameter)前綴,列名到參數名之間的轉化通過方法實現。存儲過程名稱通過表明轉化,轉化規則為:將"T_”(Table)改成"P_”(Procedure)前綴,並添加"_I"、"_U"和"_D"表示相應的操作類型,存儲過程名稱的解析通過實現。整個存儲過程的輸出通過方法TransformText輸出,並通過PushIndent和PopIndent方法控製縮進。由於CUD存儲隻有兩個地方不一致:參數列表和存儲過程的主體,我定義了兩個抽象方法和讓具體的ProcedureTemplate去實現。

基類ProcedureTemplate已經定義出了主要的轉化規則,我們現在需要做的就是通過T4模板創建3個具體的ProcedureTemplate,分別實現針對CUD存儲過程的生成。為此我創建了三個繼承自ProcedureTemplate的具體類:InsertProcedureTemplate、UpdateProcedureTemplate和DeleteProcedureTemplate,它隻需要實現和這兩個抽象方法既即可,下麵是它們的定義。

<#@ 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的靜態方法實現,它和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文件中設置一個數據表的列表,讓基於這些表的所有存儲過程一並生成;或者直接子指定數據庫,讓所有數據表的存儲過程一並生成出來。到底如何實現,請聽《》分解。

從數據到代碼——通過代碼生成機製實現強類型編程[上篇]
從數據到代碼——通過代碼生成機製實現強類型編程[下篇]
從數據到代碼——基於T4的代碼生成方式
創建代碼生成器可以很簡單:如何通過T4模板生成代碼?[上篇]
創建代碼生成器可以很簡單:如何通過T4模板生成代碼?[下篇]


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 14:04:35

  上一篇:go  從數據到代碼——基於T4的代碼生成方式
  下一篇:go  創建代碼生成器可以很簡單:如何通過T4模板生成代碼?[下篇]