• 创建代码生成器可以很简单:如何通过T4模板生成代码?[上篇]


    image在《基于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 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。

    四、创建抽象的模板: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具有两个属性Table(SMO中表示数据表)和OperationKind(表示具体的CUD操作的一种),它们均通过构造函数初始化。简单起见,我们没有指定Server,而默认采用本机指定的数据库。

       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)前缀,列名到参数名之间的转化通过方法GetParameterName实现。存储过程名称通过表明转化,转化规则为:将"T_”(Table)改成"P_”(Procedure)前缀,并添加"_I"、"_U"和"_D"表示相应的操作类型,存储过程名称的解析通过GetProcedureName实现。整个存储过程的输出通过方法TransformText输出,并通过PushIndent和PopIndent方法控制缩进。由于CUD存储只有两个地方不一致:参数列表和存储过程的主体,我定义了两个抽象方法RenderParameterListRenderProcedureBody让具体的ProcedureTemplate去实现。

    五、为CUD操作创建具体模板

    基类ProcedureTemplate已经定义出了主要的转化规则,我们现在需要做的就是通过T4模板创建3个具体的ProcedureTemplate,分别实现针对CUD存储过程的生成。为此我创建了三个继承自ProcedureTemplate的具体类:InsertProcedureTemplate、UpdateProcedureTemplate和DeleteProcedureTemplate,它只需要实现RenderParameterListRenderProcedureBody这两个抽象方法既即可,下面是它们的定义。

    <#@ 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文件中设置一个数据表的列表,让基于这些表的所有存储过程一并生成;或者直接子指定数据库,让所有数据表的存储过程一并生成出来。到底如何实现基于多文件的代码生成,请听《下回》分解。

    从数据到代码——通过代码生成机制实现强类型编程[上篇]
    从数据到代码——通过代码生成机制实现强类型编程[下篇]
    从数据到代码——基于T4的代码生成方式
    创建代码生成器可以很简单:如何通过T4模板生成代码?[上篇]
    创建代码生成器可以很简单:如何通过T4模板生成代码?[下篇]

  • 相关阅读:
    庖丁解牛看委托和事件(续)
    两天完成一个小型工程报价系统(三层架构)
    原生态Ajax无刷新评论和顶踩代码(记事本打造,无验证)
    浪客剑心:位图法Bitmap算法分析
    Entity Framework 4 in Action读书笔记——第一章:数据访问重载:Entity Framework(3)
    Entity Framework 4 in Action读书笔记——第一章:数据访问重载:Entity Framework(1)
    Entity Framework 4 in Action读书笔记——第四章:使用LINQ to Entities查询:筛选数据
    easyUI这样获取Json的内嵌数据
    DWZUI(1.3)框架中遇到的两个问题
    Entity Framework 4 in Action读书笔记——第二章:开始Entity Framework之旅(2)
  • 原文地址:https://www.cnblogs.com/artech/p/T4_01.html
Copyright © 2020-2023  润新知