您的位置:首页 > 编程语言

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

2012-03-11 22:11 681 查看

在《基于T4的代码生成方式》中,我对T4模板的组成结构、语法,以及T4引擎的工作原理进行了大体的介绍,并且编写了一个T4模板实现了如何将一个XML转变成C#代码。为了让由此需求的读者对T4有更深的了解,我们通过T4来做一些更加实际的事情——SQL
Generator。在这里,我们可以通过SQLGenerator为某个数据表自动生成进行插入、修改和删除的存储过程。[文中源代码从这里下载]

一、代码生成器的最终使用效果

我们首先来看看通过直接适用我们基于T4的SQL生成模板达到的效果。右图(点击看大图)是VS2010的SolutionExplorer,在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方法生成。其中构造函数的参数表示的分别是连接字符串名称(在配置文件中定义)和数据表的名称。

<#@templatelanguage="C#"hostspecific="True"#>

<#@outputextension="sql"#>

<#@includefile="T4Toolbox.tt"#>

<#@includefile="..\Templates\DeleteProcedureTemplate.tt"#>

<#

newDeleteProcedureTemplate("TestDb","T_PRODUCT").Render();

#>


<#@templatelanguage="C#"hostspecific="True"#>

<#@outputextension="sql"#>

<#@includefile="T4Toolbox.tt"#>

<#@includefile="..\Templates\InsertProcedureTemplate.tt"#>

<#

newInsertProcedureTemplate("TestDb","T_PRODUCT").Render();

#>


<#@templatelanguage="C#"hostspecific="True"#>

<#@outputextension="sql"#>

<#@includefile="T4Toolbox.tt"#>

<#@includefile="..\Templates\UpdateProcedureTemplate.tt"#>

<#

newUpdateProcedureTemplate("TestDb","T_PRODUCT").Render();

#>


二、安装T4工具箱(ToolBox)和编辑器

VS本身只提供一套基于T4引擎的代码生成的执行环境,为了利于你的编程你可以安装一些辅助性的东西。T4ToolBox是一个CodePlex上开源的工具,它包含一些可以直接使用的代码生成器,比如EnumSQLView、AzManwrapper、LINQtoSQLclasses、LINQtoSQLschema和EntityFrameworkDAL等。T4ToolBox还提供一些基于T4方面的VS的扩展。当你按照之后,在“AddNewItem”对话框中就会多出一个命名为“Code
Generation”的类别,其中包括若干文件模板。下面提供的T4模板的编辑工作依赖于这个工具。



为了提高编程体验,比如智能感知以及代码配色,我们还可以安装一些第三方的T4编辑器。我使用的是一个叫做OlegSych的T4Editor。它具有免费版本和需要付费的专业版本,当然我使用的免费的那款。成功按装了,它也会在AddNewItem”对话框中提供相应的基于T4的文件模板。

三、创建数据表

T4模板就是输入和输出的一个适配器,这与XSLT的作用比较类似。对于我们将要实现的SQLGenerator来说,输入的是数据表的结构(Schema)输出的是最终生成的存储过程的SQL脚本。对于数据表的定义,不同的项目具有不同标准。我采用的是我们自己的数据库标准定义的数据表:T_PRODUCT(表示产品信息),下面是创建表的脚本。

CREATETABLE[dbo].[T_PRODUCT](

[ID][VARCHAR](50)NOTNULL,

[NAME][NVARCHAR]NOTNULL,

[PRICE][float]NOTNULL,

[TOTAL_PRICE][FLOAT]NOTNULL,

[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]PRIMARYKEYCLUSTERED([ID]ASC)ON[PRIMARY])


每一个表中有6个公共的字段:CREATED_BY、CREATED_ON、LAST_UPDATED_BY、LAST_UPDATED_ON、VERSION_NO和TRANSACTION_ID分别表示记录的创建者、创建时间、最新更新者、最新更新时间、版本号(并发控制)和事务ID。

四、创建抽象的模板:ProcedureTemplate

我们需要为三不同的数据操作得存储过程定义不同的模板,但是对于这三种存储过程的SQL结构都是一样的,基本结果可以通过下面的SQL脚本表示。

IFOBJECT_ID('<<ProcedureName>>','P')ISNOTNULL

DROPPROCEDURE<<ProcedureName>>

GO


CREATEPROCEDURE<<ProcedureName>>

(

<<ParameterList>>

)

AS


<<ProcedureBody>>


GO


为此我定义了一个抽象的模板:ProcedureTemplate。为了表示CUD三种不同的操作,我通过T4模板的“类特性块”(ClassFeatureBlock)定义了如下一个OperationKind的枚举。

<#+

publicenumOperationKind

{

Insert,

Update,

Delete

}

#>


然后下面就是整个ProcedureTemplate的定义了。ProcedureTemplate直接继承自T4Toolbox.Template(来源于T4ToolBox,它继承自TextTransformation)。ProcedureTemplate通过SMO(SQLServerManagementObject)获取数据表的结构(Schema)信息,所以我们需要应用SMO相关的程序集和导入相关命名空间。ProcedureTemplate具有两个属性Table(SMO中表示数据表)和OperationKind(表示具体的CUD操作的一种),它们均通过构造函数初始化。简单起见,我们没有指定Server,而默认采用本机指定的数据库。

1:<#@assemblyname="Microsoft.SqlServer.ConnectionInfo"#>

2:<#@assemblyname="Microsoft.SqlServer.Smo"#>

3:<#@assemblyname="Microsoft.SqlServer.Management.Sdk.Sfc"#>

4:<#@importnamespace="System"#>

5:<#@importnamespace="Microsoft.SqlServer.Management.Smo"#>

6:<#+

7:publicabstractclassProcedureTemplate:Template

8:{

9:publicOperationKindOperationKind{get;privateset;}

10:publicTableTable{get;privateset;}

11:

12:publicconststringVersionNoField="VERSION_NO";

13:publicconststringVersionNoParameterName="@p_version_no";

14:

15:publicProcedureTemplate(stringdatabaseName,stringtableName,OperationKindoperationKind)

16:{

17:this.OperationKind=operationKind;

18:Serverserver=newServer();

19:Databasedatabase=newDatabase(server,databaseName);

20:this.Table=newTable(database,tableName);

21:this.Table.Refresh();

22:}

23:

24:publicvirtualstringGetProcedureName()

25:{

26:switch(this.OperationKind)

27:{

28:caseOperationKind.Insert:return"P_"+this.Table.Name.Remove(0,2)+"_I";

29:caseOperationKind.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:protectedvirtualstringGetParameterName(stringcolumnName)

35:{

36:return"@p_"+columnName.ToLower();

37:}

38:

39:protectedabstractvoidRenderParameterList();

40:

41:protectedabstractvoidRenderProcedureBody();

42:

43:publicoverridestringTransformText()

44:{

45:#>

46:IFOBJECT_ID('[dbo].[<#=GetProcedureName()#>]','P')ISNOTNULL

47:DROPPROCEDURE[dbo].[<#=GetProcedureName()#>]

48:GO

49:

50:CREATEPROCEDURE[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:returnthis.GenerationEnvironment.ToString();

67:}

68:}

69:#>


存储过程的参数我们采用小写形式,直接在列名前加上一个"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这两个抽象方法既即可,下面是它们的定义。

<#@includefile="ProcedureTemplate.tt"#>

<#+

publicclassInsertProcedureTemplate:ProcedureTemplate

{

publicInsertProcedureTemplate(stringdatabaseName,stringtableName):base(databaseName,tableName,OperationKind.Insert){}


protectedoverridevoidRenderParameterList()

{

for(inti=0;i<this.Table.Columns.Count;i++)

{

Columncolumn=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());

}

}

}

}


protectedoverridevoidRenderProcedureBody()

{

WriteLine("INSERTINTO[dbo].[{0}]",this.Table.Name);

WriteLine("(");

PushIndent("\t");

for(inti=0;i<this.Table.Columns.Count;i++)

{

Columncolumn=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(inti=0;i<this.Table.Columns.Count;i++)

{

Columncolumn=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(")");

}

}

#>


<#@includefile="ProcedureTemplate.tt"#>

<#+

publicclassUpdateProcedureTemplate:ProcedureTemplate

{

publicUpdateProcedureTemplate(stringdatabaseName,stringtableName):base(databaseName,tableName,OperationKind.Update)

{}


protectedoverridevoidRenderParameterList()

{

for(inti=0;i<this.Table.Columns.Count;i++)

{

Columncolumn=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());

}

}

}


protectedoverridevoidRenderProcedureBody()

{

WriteLine("UPDATE[dbo].[{0}]",this.Table.Name);

WriteLine("SET");

PushIndent("\t");

for(inti=0;i<this.Table.Columns.Count;i++)

{

Columncolumn=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(inti=0;i<this.Table.Columns.Count;i++)

{

Columncolumn=this.Table.Columns[i];

if(column.InPrimaryKey)

{

WriteLine("{0,-20}={1}AND","["+column.Name+"]",GetParameterName(column.Name));

}

}

WriteLine("{0,-20}={1}","["+VersionNoField+"]",VersionNoParameterName);

PopIndent();

}

}

#>


<#@includefile="ProcedureTemplate.tt"#>

<#+

publicclassDeleteProcedureTemplate:ProcedureTemplate

{

publicDeleteProcedureTemplate(stringdatabaseName,stringtableName):base(databaseName,tableName,OperationKind.Delete){}


protectedoverridevoidRenderParameterList()

{

foreach(Columncolumninthis.Table.Columns)

{

if(column.InPrimaryKey)

{

WriteLine("{0,-20}[{1}],",GetParameterName(column.Name),column.DataType.Name.ToUpper());

}

}

WriteLine("{0,-20}[{1}]",VersionNoParameterName,"TIMESTAMP");

}


protectedoverridevoidRenderProcedureBody()

{

WriteLine("DELETEFROM[dbo].[{0}]",this.Table.Name);

WriteLine("WHERE");

PushIndent("\t\t");

foreach(Columncolumninthis.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模板,最终将会生成如下的三个存储过程。

IFOBJECT_ID('[dbo].[P_PRODUCT_I]','P')ISNOTNULL

DROPPROCEDURE[dbo].[P_PRODUCT_I]

GO


CREATEPROCEDURE[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


INSERTINTO[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


IFOBJECT_ID('[dbo].[P_PRODUCT_U]','P')ISNOTNULL

DROPPROCEDURE[dbo].[P_PRODUCT_U]

GO


CREATEPROCEDURE[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_idAND

[VERSION_NO]=@p_version_no


GO


IFOBJECT_ID('[dbo].[P_PRODUCT_D]','P')ISNOTNULL

DROPPROCEDURE[dbo].[P_PRODUCT_D]

GO


CREATEPROCEDURE[dbo].[P_PRODUCT_D]

(

@p_id[VARCHAR],

@p_version_no[TIMESTAMP]

)

AS


DELETEFROM[dbo].[T_PRODUCT]

WHERE

ID=@p_idAND

VERSION_NO=@p_version_no


GO


六、局限性

上面这个例子虽然很好实现了基于数据表的存储过程的生成,但是使用起来仍然不方便——我们需要为每一个需要生成出来的存储过程定义T4模板。也就是说在这种代码生成下,模板文件和生成文件之间是1:1的关系。实际上我们希望的方式是:创建一个基于某个表的TT文件,让它生成3个CUD三个存储过程;或者在一个TT文件中设置一个数据表的列表,让基于这些表的所有存储过程一并生成;或者直接子指定数据库,让所有数据表的存储过程一并生成出来。到底如何实现基于多文件的代码生成,请听《下回》分解。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: