我不知道大家对CodeDOM的代码生成机制是否熟悉,但是有一点可以确定:如果你使用过Visual Studio,你就应该体验过它带给我们在编程上的便利。随便列举三种典型的代码生成的场景:在创建强类型DataSet的时候,VS会自动根据Schema生成相应的C#或者VB.NET代码;当我们编辑Resource文件的时候,相应的的后台代码也会自动生成;当我们通过添加Web Reference调用Web Service或者WCF Service的时候,VS会自动生成服务代理的代码和相应的配置。总的来说,通过和VS集成的动态代码生成工具使我们可以“强类型”的方式进行编程,进而提供我们的效率并减低错误的几率。
CodeDOM 提供了表示许多常见的源代码元素类型的类型。您可以设计一个生成源代码模型的程序,使用CodeDOM 元素构成一个对象图。而这个对象图包含C#或者VB.NET代码包含的基本元素:命名空间、类型、类型成员(方法、属性、构造函数、事件等),并且包括方法实现的具体语句(Statement)。也就是说它的结构就是对一个具体.vb或者.cs文件代码的反映。在这里我不会具体介绍CodeDOM体系结构……[阅读全文]
在《前篇》中我们已经通过CodeDOM的形式定义了将要生成的代码结构,而这个CodeDOM对象就是CodeCompileUnit。CodeCompileUnit本身是与编程语言无关的,我们可以利用基于某种编程语言(VB.NET或者C#)的CodeDomProvider将CodeCompileUnit对象转换成具体的代码文本。为了让我们的VS自动地为我们生成代码,我们需要以Custom Tool的形式编写相应的代码生成器。我们编写的代码生成器最终通过COM组件的形式对外提供服务,所以其中涉及到COM组件的注册。为了实现同VS的集成,还涉及到相应注册表设置……[阅读全文]
前面两篇介绍了如何通过CodeDOM+Custom Tool的代码生成方式实现了代码的自动生成。实际上,我们最常用的代码生成当时不是CodeDOM,而是T4,这是一个更为强大,并且适用范围更广的代码生成技术。T4是对“Text Template Transformation Toolkit”(4个T)的简称。T4直接包含在VS2008和VS2010中,是一个基于文本文件转换的工具包。T4的核心是一个基于“文本模板”的转换引擎,我们可以通过它生成一切类型的文本型文件,比如我们常用的代码文件类型包括:C#、VB.NET、T-SQL、XML甚至是配置文件等。
对于需要通过T4来进行代码生成工作的我们来说,需要做的仅仅是根据转换源(Transformation Source),比如数据表、XML等(由于例子简单,HelloWord模板没有输入源)和目标文本(比如最终需要的C#或者T-SQL代码等)定义相应的模板。T4模板作用就相当于进行XML转化过程中使用的XSLT。
T4模板的定义非常简单,整个模板的内容包括两种形式:静态形式和动态动态。前者就是直接写在模板中作为原样输出的文本,后者是基于某种语言编写代码,T4引擎会动态执行它们。这和我们通过内联的方式编写的ASP.NET页面很相似:HTML是静态的,以C#或者VB.NET代码便写的动态执行的代码通过相应的标签内嵌其中。[阅读全文]
在《前一篇》中,我对T4模板的组成结构、语法,以及T4引擎的工作原理进行了大体的介绍,并且编写了一个T4模板实现了如何将一个XML转变成C#代码。为了让由此需求的读者对T4有更深的了解,我们通过T4来做一些更加实际的事情——SQL Generator。在这里,我们可以通过SQL Generator为某个数据表自动生成进行插入、修改和删除的存储过程……[阅读全文]
在《前一篇》中我们通过T4模板为我们指定的数据表成功生成了我们需要的用于添加、修改和删除操作的存储过程。但是这是一种基于单个文件的解决方案,即我们必须为每一个生成的存储过程建立一个模板。如果我们提供一种基于多文件的代码生成方式,将会为编程人员带来极大的便利。借助于T4 ToolBox这个开源工具箱,多文件的SQL Generator的实现变得异常简单。[阅读全文]
如果你的T4模板需要调用一个自定义的类型,并且该类型定义在某个非系统程序集中,你就需要通过T4<#@ assembly…#>指令引用该程序集。在VS 2008环境下,你只需要为当前项目添加相应的程序集引用,并且通过<@ assembly…#>指令的name属性指定程序集(Dll)的文件名即可。但是这种方式对于VS 2010则行不通,因为T4引擎在解析和编译模板的时候并不会从项目引用列表中去定位目标程序集。本篇文章为你介绍在VS2010下5种不同的程序集引用的方式……[ 阅读全文]
出于提高性能考虑,T4引擎在进行基于代码生成的模板转换(Template Transformation)的时候,会始终重用同一个AppDomain。由于该AppDomain不会自动卸载,这就会导致该AppDomain始终锁定所有被它加载的程序集。如果我们需要释放程序集,我们不得不重启VS。但是,对于T4模板的开发调试阶段,这种通过重新启动VS的方式去释放程序集以确保我们的项目能够成功编译是不能接受的。那么,是否有一种解决方案既能够确保T4引擎能够进行正常的模板转换,又能避免它强行锁定引用程序集呢?
VS和一些T4编辑器虽然给了基本的智能感知支持,但是在绝大部分我们相当于在编写纯文本的脚本,所以对于一些比较复杂的模板转换逻辑,我们需要通过Debug的方式去发现一些无法避免的问题……[阅读全文]
对于ASP.NET应用的开发者来说,你可能不知道什么是BuildProvider,但是你几乎无时无刻不在使用它所带来的代码生成机制。当你创建一个.aspx文件的时候,为什么会自动创建对应源代码?当你在该.aspx页面中以XML的方式添加一个按钮,源代码中为什么会自动添加一个同名的属性。实际上,ASP.NET就是通过一个特殊的BuildProvider实现了将.aspx文件内容转换成相应的源代码,这个特殊的.aspx文件就是:PageBuildProvider。基于不同的文件类型,ASP.NET会采用不同的BuildProvider进行源代码的生成。比如UserControlBuildProvider和MasterPageBuildProvider分别实现了基于用户控件文件(.ascx)和母板页(.master)的源代码生成……[阅读全文]
在《基于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();
#>
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。
我们需要为三不同的数据操作得存储过程定义不同的模板,但是对于这三种存储过程的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存储只有两个地方不一致:参数列表和存储过程的主体,我定义了两个抽象方法RenderParameterList和RenderProcedureBody让具体的ProcedureTemplate去实现。
基类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文件中设置一个数据表的列表,让基于这些表的所有存储过程一并生成;或者直接子指定数据库,让所有数据表的存储过程一并生成出来。到底如何实现基于多文件的代码生成,请听《下回》分解。
从数据到代码——通过代码生成机制实现强类型编程[上篇]
从数据到代码——通过代码生成机制实现强类型编程[下篇]
从数据到代码——基于T4的代码生成方式
创建代码生成器可以很简单:如何通过T4模板生成代码?[上篇]
创建代码生成器可以很简单:如何通过T4模板生成代码?[下篇]
目录
一、多文件代码生成器会带来多大的便利?
二、创建自定义的Generator
三、ProcedureGenerator如何被使用?
我们先来直观的感受一下较之《上篇》提供的单一文件的代码生成器,基于多文件的代码生成解决方案会为开发人员带来多大的便利。 同样对于《上篇》创建的数据表T_PRODUCT,之前我们为了生成三个不同的存储过程,我们不得已需要创建3个不同的T4模板文件。实际上我们更需要的方式只需要创建一个T4模板,让我们的SQL Generator自动为我们生成3个包含相应存储过程的.sql附属文件,如左图所示(点击看大图)。
有的时候,基于单个数据表的存储过程生成方式我们依然觉得不方便。如果我们能够在T4模板文件中指定的数据表的列表,让我们的SQL Generator为列表的每一个数据表都生成CUD三个存储过程,这样的方式更加具有吸引力。如右图所示(点击看大图),一个订单模块包含两个具有主子关系的两张表(T_ORDER和T_ORDER_DETAIL),现在我们在一个T4模板中指定这两个表明,通过SQL Generator可以帮助我们生成6个包含存储过程的.sql附属文件。
甚至有的时候我们连数据表列表都无需指定,让SQL Generator为所有的表都生成相应的存储过程。我的例子中没有提供这样的功能,但是实现自来不会存在任何问题。
在《上篇》中我创建了一个抽象的ProcedureTemplate类,以及三个基于生成CUD存储过程的具体ProcedureTemplate: InsertProcedureTemplate、UpdateProcedureTemplate和DeleteProcedureTemplate。它们都将直接服务于我们今天将要提供的基于多文件的SQL Generator。
在《上篇》中,这四个Template分别定义在4个不同的TT文件中,3个具体的ProcedureTemplate通过<#@include>指令将抽象ProcedureTemplate模板文件包含过来。由于我们将要创建的T4模板将会使用到这四个类,如果我们用四个<#@include>指令将四个TT文件包含过来,由于T4引擎将会导致对ProcedureTemplate的4次包含,最好将会导致变异问题。个人觉得这应该算是T4引擎解析包含关系的一个局限性,为了解决这个问题我们不得不抽象的ProcedureTemplate和三个具体的ProcedureTemplate都合并成一个TT文件。
T4 ToolBox为类库中为了提供了一个抽象的T4Toolbox.Generator类用于实现多文件的代码生成。为此我们创建一个TT模板文件,定义了如下一个继承自该类的ProcedureGenerator。ProcedureGenerator的核心是通过属性Templates定义的类型为IEnumerable<ProcedureTemplate>的ProcedureTemplate列表,这个列表在存储过程中进行初始化。而对于ProcedureGenerator的构造函数,处理定义了一个表示数据库连接字符串的databaseName的参数外,并以数组参数的形式指定了生成的存储过程基于的数据表名的列表。
<#@ import namespace="System.Collections.Generic" #>
<#@ include file="ProcedureTemplate.tt" #>
<#@ include file="T4Toolbox.tt" #>
<#+
public class ProcedureGenerator : Generator
{
public IEnumerable<ProcedureTemplate> Templates{get; private set;}
public ProcedureGenerator(string databaseName, params string[] tableNames)
{
if(null == tableNames || tableNames.Length == 0)
{
throw new ArgumentNullException("tableNames");
}
this.Templates = InitlizeTemplates(databaseName,tableNames);
}
private IEnumerable<ProcedureTemplate> InitlizeTemplates(string databaseName, string[] tableNames)
{
foreach(string tableName in tableNames)
{
yield return new InsertProcedureTemplate(databaseName, tableName);
yield return new UpdateProcedureTemplate(databaseName, tableName);
yield return new DeleteProcedureTemplate(databaseName, tableName);
}
}
protected override void RunCore()
{
foreach(ProcedureTemplate tempalte in this.Templates)
{
tempalte.RenderToFile(tempalte.GetProcedureName() + ".sql");
}
}
}
#>
真正的存储过程的T-SQL脚本实现在重写的RunCore中。由于具体的文本转化逻辑都定义在了ProcedureTemplate中了,所以在这里我们需要遍历的ProcedureTemplate集合中每一个Template对象,调用RenderToFile方法将相应的存储过程的脚本写入以存储过程命名同名的.sql文件中。
我们最后来看看我们创建的ProcedureGenerator最终如何被应用于具体的代码生成。其实很简单,我们只需要创建相应的模板文件,通过<#@include>将定义ProcedureGenerator类的TT文件包含近来,最后以代码语句调用块(<#StatementCode#>)的形式实力化该对象,并调用Run方法即可。在构造函数中指定数据库连接字符串的名称和数据表名的列表。下面是基于但表的T4模板。
<#@ template language="C#" hostSpecific="true" debug="true" #>
<#@ include file="Templates\ProcedureGenerator.tt" #>
<#
new ProcedureGenerator("TestDb","T_PRODUCT").Run();
#>
下面是基于多表的T4模板:
<#@ template language="C#" hostSpecific="true" debug="true" #>
<#@ include file="Templates\ProcedureGenerator.tt" #>
<#
new ProcedureGenerator("TestDb","T_ORDER","T_ORDER_DETAIL").Run();
#>
当你代码生成工作执行之后,会多出一个与TT文件同名的附属文件,你需要手工删除掉它。
从数据到代码——通过代码生成机制实现强类型编程[上篇]
从数据到代码——通过代码生成机制实现强类型编程[下篇]
从数据到代码——基于T4的代码生成方式
创建代码生成器可以很简单:如何通过T4模板生成代码?[上篇]
创建代码生成器可以很简单:如何通过T4模板生成代码?[下篇]