DACBuilder – 基于 XML 和 XSL 模板转换的数据访问对象生成工具
DACBuilder 应用程序提供了从多个数据库系统自动生成多种编程语言代码的功能。
引言
当我开始从事数据库编程和复杂的业务实现时,对数据访问框架的需求是明确的。我开始构建自己的访问数据对象的类。当然,从零开始,工作量更大,我认为一个“通用”的数据访问框架可以帮助我和我的团队。短时间不允许我设想这样的基础设施,至少针对一个数据库管理系统和一种编程语言。但当我开始在 Prodigy 公司工作时,这个愿望就实现了。我发现了一个复杂的框架实现,它反映了许多 SQL Server 数据库表的特性,如表和列定义、约束和关系。当然,映射数据库表的数据访问类是使用特定工具自动生成的。但本应用程序的范围不是解释那个复杂的框架。数据访问类生成是一个常用功能,被许多程序员使用,甚至微软也有其 企业库,并且将实现 LINQ,这可能是一个强大的工具。DACBuilder 无意实现另一个数据访问层生成器,而是展示如何在不同的编程语言中轻松生成映射到不同数据源中的表的类。实现此生成的关键是 XML 和 XSL 转换。
示例
这是一个使用 VBScript 生成的代码示例,用于通过 ODBC 驱动程序访问的数据库管理系统。考虑一个名为“Items”的表,其结构如下:
ID – int
PK Name – varchar(255)
Price – decimal (18,2)
CategoryID – int – FK to "Categories" table
Const adUseServer = 2
Const adCmdText = 1
Const adCmdStoredProc = 4
Const adInteger = 3
Const adVarChar = 200
Const adDate = 7
Const adDecimal = 14
Const adBoolean = 11
Const adBinary = 128
Const adVariant = 12
Const adParamOutput = 2
Class DACItems
'BEGIN Private members
Private xID
Private xName
Private xPrice
Private xCategoryID
Private cnnString
Private cnn
'END Private members
Public Sub Initialize()
xID = -1
xName = ""
xPrice = -1
xCategoryID = -1
cnnString = "" 'fill in with the specified value
End Sub
Public Sub Uninitialize()
Set cnn = Nothing
End Sub
'BEGIN Properties
Public Property Let ID(vData)
xID = vData
End Property
Public Property Get ID()
ID = xID
End Property
Public Property Let Name(vData)
xName = vData
End Property
Public Property Get Name()
Name = xName
End Property
Public Property Let Price(vData)
xPrice = vData
End Property
Public Property Get Price()
Price = xPrice
End Property
Public Property Let CategoryID(vData)
xCategoryID = vData
End Property
Public Property Get CategoryID()
CategoryID = xCategoryID
End Property
'END Properties
'BEGIN Connection
Private Sub OpenConnection()
Set cnn = CreateObject("ADODB.Connection")
With cnn
.Provider = "MSDASQL"
.CursorLocation = adUseServer 'Must use Server side cursor.
.ConnectionString = cnnString
.Open
End With
End Sub
Private Sub CloseConnection()
cnn.Close
Set cnn = Nothing
End Sub
Public Property Let ConnectionString(vData)
cnnString = vData
End Property
Public Property Get ConnectionString ()
ConnectionString = cnnString
End Property
'END Connection
'BEGIN Standard methods
Public Function Add()
Dim bRet
OpenConnection
Dim cmd
Set cmd = CreateObject("ADODB.Command")
Set cmd.ActiveConnection = cnn
cmd.CommandType = adCmdText
cmd.CommandText = "INSERT INTO Items VALUES(?, ?, ?, ?)"
Dim param
Set param = cmd.CreateParameter("", adInteger)
param.Value = xID
cmd.Parameters.Append param
Set param = cmd.CreateParameter("", adVarChar, 255)
param.Value = xName
cmd.Parameters.Append param
Set param = cmd.CreateParameter("", adDecimal, 18, 2)
param.Value = xPrice
cmd.Parameters.Append param
Set param = cmd.CreateParameter("", adInteger)
param.Value = xCategoryID
cmd.Parameters.Append param
Dim result
cmd.Execute result
CloseConnection
bRet = result > 0
Set cmd = Nothing
Add = bRet
End Function
Public Function Edit()
Dim bRet
OpenConnection
Dim cmd
Set cmd = CreateObject("ADODB.Command")
Set cmd.ActiveConnection = cnn
cmd.CommandType = adCmdText
cmd.CommandText = "UPDATE Items SET Name = ?," & _
" Price = ?, CategoryID = ? WHERE ID = ?"
Dim param
Set param = cmd.CreateParameter("", adVarChar, 255)
param.Value = xName
cmd.Parameters.Append param
Set param = cmd.CreateParameter("", adDecimal, 18, 2)
param.Value = xPrice
cmd.Parameters.Append param
Set param = cmd.CreateParameter("", adInteger)
param.Value = xCategoryID
cmd.Parameters.Append param
Set param = cmd.CreateParameter("", adInteger)
param.Value = xID
cmd.Parameters.Append param
Dim result
cmd.Execute result
CloseConnection
bRet = result > 0
Set cmd = Nothing
Edit = bRet
End Function
Public Function Delete()
Dim bRet
OpenConnection
Dim cmd
Set cmd = CreateObject("ADODB.Command")
Set cmd.ActiveConnection = cnn
cmd.CommandType = adCmdText
cmd.CommandText = "DELETE FROM Items WHERE ID = ?"
Dim param
Set param = cmd.CreateParameter("@ID", adInteger)
param.Value = xID
cmd.Parameters.Append param
Dim result
cmd.Execute result
CloseConnection
bRet = result > 0
Set cmd = Nothing
Delete = bRet
End Function
Public Function Read()
Dim bRet
Dim rs
Set rs = CreateObject("ADODB.Recordset")
OpenConnection
Dim cmd
Set cmd = CreateObject("ADODB.Command")
cmd.CommandType = adCmdText
cmd.CommandText = "SELECT * FROM Items WHERE ID = ?"
Dim param
Set param = cmd.CreateParameter(", adInteger)
param.Value = xID
cmd.Parameters.Append param
Set rs = cmd.Execute
If Not(rs.EOF) Then
xID = CInt(rs("ID"))
xName = CStr(rs("Name"))
xPrice = CDbl(rs("Price"))
xCategoryID = CInt(rs("CategoryID"))
bRet = True
End If
CloseConnection
bRet = False
Set rs = Nothing
Set cmd = Nothing
Read = bRet
End Function
Public Function ReadCategoriesItems()
Dim rs
Set rs = CreateObject("ADODB.Recordset")
OpenConnection
Dim cmd
Set cmd = CreateObject("ADODB.Command")
Set cmd.ActiveConnection = cnn
cmd.CommandType = adCmdText
cmd.CommandText = "SELECT * FROM Items WHERE CategoryID = ?"
Dim param
Set param = cmd.CreateParameter("", adInteger)
param.Value = xCategoryID
cmd.Parameters.Append param
Set rs = cmd.Execute
Set cmd = Nothing
ReadCategoriesItems = rs
End Function
'END Standard Methods
'BEGIN Other DAC logic
'END Other DAC logic
End Class
您可能会看到,自动生成的类具有一些属性,这些属性映射到属于 *Items* 表的列,以及一些方法(`Add`、`Edit`、`Delete`、`Read`),负责从表中插入、更新、删除和读取记录,以及 `ReadCategoriesItems` 方法,该方法返回一个包含指定类别所有项目的 `RecordSet`。您可以看到使用无名参数格式的方法的 SQL 语句。这是一个选项。如果数据源是 SQL Server,并且您想执行一个带有命名参数的存储过程(这是代码生成的默认设置),您也可以这样做。
数据库系统、表结构和 XML 输出
代码生成仅从表结构开始。如果您定义了适当的字段结构、表以及它们之间的关系,DACBuilder 的工作就简单而强大。所以,起点是数据库。表结构可以反映在对象中。DACBuilder 应用程序会获取指定表或视图的结构,如果适用,还会获取指定存储过程或函数(例程)的结构。它过滤表或视图用于持久对象,过滤存储过程和函数用于例程。代码生成有多种选择:
- .NET 平台不同语言的数据访问类(目前仅限 C# 和 Visual Basic .NET);
- 其他语言(PHP、VB 6.0、VBScript 和 JScript)的数据访问类;
- 用于插入、更新、删除、选择和搜索的 SQL Server 存储过程;
- .NET Windows Forms、WebForms(*aspx* 页面和 *ascx* 控件)和 HTML 的 UI 功能(设计和代码);
- XML 格式资源生成;
- XML 模式生成;
- 通用自定义字段实现;
- 多个数据源的连接字符串生成。
代码生成在一个富文本编辑框中,而不是直接在磁盘上的文件中,但您可以根据其内容(SQL 脚本或代码)保存带有指定扩展名的文件。它具有基于当前生成内容语言的简单颜色语法高亮显示。对于 .NET 语言,它具有编译选项和程序集生成命令(以及所需程序集的运行时引用),当然还有错误管理。根据您选择的数据库系统,应用程序会读取指定数据库中包含的表。目前仅支持 SQL Server、Access 和 MySQL,并且对于每个系统,都有特定的方法来获取元数据信息。在每种情况下,输出都是一个 XML 文档,该文档使用为每个所选操作设计的模板进行转换。XML 结构是简单的两级结构(表信息 – 级别 1,列信息 – 级别 2)。
给定示例中表的 XML 如下:
<table name="Items" friendly_name="Items"
base="" namespace="" abbreviation="">
<column column_name="ID" column_friendly_name="ID"
ordinal_position="1" is_nullable="No" data_type="int"
numeric_precision="10" numeric_precision_radix="10"
numeric_scale="0" identity="0"
constraint_type="PRIMARY KEY" referenced_table="" />
<column column_name="Name" column_friendly_name="Name"
ordinal_position="2" is_nullable="No" data_type="varchar"
character_maximum_length="255" character_octet_length="255"
constraint_type="" referenced_table="" />
<column column_name="Price" column_friendly_name="Price"
ordinal_position="3" is_nullable="No" data_type="decimal"
numeric_precision="18" numeric_precision_radix="10"
numeric_scale="2" constraint_type="" referenced_table="" />
<column column_name="CategoryID" column_friendly_name="CategoryID"
ordinal_position="4" is_nullable="No" data_type="int"
numeric_precision="10" numeric_precision_radix="10"
numeric_scale="0" constraint_type="FOREIGN KEY"
referenced_table="Categories" />
</table>
XML 节点中包含的属性的含义在下表中解释:
级别 1 – 表信息 | |
名称 |
表名。用于数据访问容器 (DAC) 类的友好表名。 |
命名空间 |
用于包含生成的 DAC 的命名空间。 |
缩写 |
|
列_友好_名称 |
用于轻松识别生成的 DAC 的缩写。 |
级别 2 – 列信息 | |
名称 |
列名。 |
列_友好_名称 |
用于创建 DAC 属性的友好名称。 |
序号_位置 |
列在表定义中的位置。 |
列_默认值 |
列的默认值。 |
是否可为空 |
如果列接受 NULL 或不接受。 |
数据类型 |
数据库底层列类型。 |
字符_最大_长度 |
如果数据类型为字符,则为列的最大长度。 |
字符_八位字节_长度 |
如果数据类型为字符,则为列的八位字节长度。 |
数字_精度 |
数值数据类型精度。 |
数字_精度_基数 |
数值数据类型精度基数。 |
数字_刻度 |
数值数据类型刻度。 |
日期时间_精度 |
如果数据类型为日期或日期时间,则为列的日期时间精度。 |
标识 |
列自动增量属性。 |
约束类型 |
列约束类型 (PRIMARY KEY 或 FOREIGN KEY )。 |
引用表 |
如果约束类型是 FOREIGN KEY ,则为列引用的表。 |
列描述 |
列描述,扩展属性。 |
对于 SQL Server,返回元数据信息的存储过程名为 `GetTableColumnsXML`,并且在 SQL 语句中包含 FOR XML EXPLICIT
子句。信息从 `INFORMATION_SCHEMA` 视图中获取:表、列、`KEY_COLUMN_USAGE`、表约束和引用约束。
对于 MS Access 系统,获取元数据信息的方法是使用 OLEDB 功能,例如 `OleDbConnection` 类提供的 `GetOleDbSchemaTable` 方法。这很有用,因为不仅可以分析 MS Access 系统,还可以分析任何其他 OLEDB 提供程序。输出是使用 `DataSet` 对象的 XML 输出获得的 XML 文档。
对于 MySQL 系统,分析以特定方式进行,使用 SHOW
语句:“SHOW FULL COLUMNS FROM tableName
”。需要 MySQL 的 ODBC 驱动程序 3.51,因为应用程序使用 ODBC 对象获取信息。输出是一个 XML 文档,其获取方式与 MS Access 情况相同。
对于 MS Access 和 MySQL 系统,以及任何可能实现的其他新系统,XML 输出必须转换为 DACBuilder XML 格式(前面已解释)。转换使用特定的模板(*msaccess_2_sql.xsl* 或 *mysql_2_sql.xsl*)执行。
负责获取数据库系统中元数据信息的对象称为 `DBHelper`,它包含在 `DACCreator` 命名空间中,位于引用的 DACCreator 程序集中。此程序集中的代码由于时间和空间限制未提供和解释。当然,它可以按需提供。`DBHelper()` 构造函数接收一个连接字符串和一个数据库系统类型。数据库类型使用名为 `DBTypes` 的枚举指定:
public enum DBTypes
{
SQL = 0
, ORACLE = 1
, MSACCESS = 2
, MYSQL = 3
}
`DBTypes.ORACLE` 的 Oracle 系统尚未实现。
在 `BForm` 应用程序主窗体的 `GetStructure` 方法中提供了一个如何获取特定系统数据库中表信息的示例:
DBHelper dbH = new DBHelper(ConnectionString, dbType);
XmlDocument doc = dbH.GetTableStructure(cboTables.SelectedValue.ToString());
switch(dbType)
{
case DBTypes.MYSQL:
CommonGenerator mysql_DACGen =
new CommonGenerator(cboTables.SelectedValue.ToString(), doc);
mysql_DACGen.Generate(templatesPath + "mysql_2_sql.xsl");
doc.LoadXml(mysql_DACGen.CommonCode);
break;
case DBTypes.MSACCESS:
CommonGenerator msaccess_DACGen =
new CommonGenerator(cboTables.SelectedValue.ToString(), doc);
msaccess_DACGen.Generate(templatesPath + "msaccess_2_sql.xsl");
doc.LoadXml(msaccess_DACGen.CommonCode);
break;
default:
break;
}
`CommonGenerator` 类用于转换 XML 文档(可以是磁盘上的文件,也可以是内存中的 `XmlDocument` 对象)。使用的方法是 `Generate`。如果转换没有错误,输出将作为字符串存储在 `CommonCode` 属性中(可以是 XML、HTML 或文本,具体取决于作为 `Generate` 方法参数传入的 XML 文档中 `xsl:output` 元素的 `method` 属性)。转换代码仅包含几行:
XPathNavigator nav = StructureDOM.DocumentElement.CreateNavigator();
XmlDocument xsldoc = new XmlDocument();
xsldoc.Load(xslTemplatePath);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(xsldoc.NameTable);
nsmgr.AddNamespace("xsl", "http://www.w3.org/1999/XSL/Transform");
XmlUrlResolver resolver = new XmlUrlResolver();
XslTransform trans = new XslTransform();
trans.Load(xsldoc, resolver, this.GetType().Assembly.Evidence);
TextWriter writer = new StringWriter();
XmlTextWriter xmlWriter = new XmlTextWriter(writer);
xmlWriter.Formatting = Formatting.Indented;
trans.Transform(nav, null, xmlWriter, null);
string sOutput = writer.ToString();
`StructureDOM` 是要进行转换的 XML 文档,作为参数传递给 `CommonGenerator` 构造函数。
例程处理仅适用于 SQL Server 系统。例程列表使用相同的 `DBHelper` 对象和 `GetRoutines` 方法获取,该方法使用 `INFORMATION_SCHEMA.ROUTINES` 视图。返回元数据信息的存储过程名为 `GetRoutineParametersXML`,并使用 `INFORMATION_SCHEMA.PARAMETERS` 视图。当然,SQL 语句包含 FOR XML EXPLICIT
子句,用于将数据格式化为 DACBuilder XML 格式。基础表中的大多数属性都可用于存储过程参数。
通过收集元数据信息获得的 XML 输出随后被添加到包含两个表的 `DataSet` 对象中。该 `DataSet` 对象作为数据源传递给 `dgColumns` 网格,您可以在其中编辑信息。建议为父对象(表或例程)和子对象(列或参数)提供简单简洁的友好名称,因为这些名称将是您以后在使用生成的 DAC 对象的应用程序中使用的名称。
模板参数
关于对象生成,所有对象的技术都是相同的。从表或例程结构开始,应用不同的模板,生成不同的对象,可以是 SQL 语句(存储过程),也可以是特定编程语言中的类,或者是模式或 HTML 代码。由于 XSL 模板可以使用 `xsl:parameter` 元素进行自定义,因此实现了一个可视化功能,允许更改这些参数。这是一个表单类,在其构造函数中接收 `DataTable` 类型的参数,其中包含 XSL 样式表中的所有参数。应用程序将样式表加载到 `XmlDocument` 对象中,搜索所有现有参数,并创建一个具有三列的 `DataTable` 对象 `dtParameters`:
- `name` – 参数名称;
- `value` – 参数值;
- `select` – 布尔属性,指定参数是从常量值获取其值,还是从 `select` 属性获取其值(如果为
true
,则表示该值是从解析的 `XmlDocument` 中获得的)。
有一个带有此参数集合的模板 (`xsl:param`):
<xsl:param name="auto_increment" select="boolean(/table/column[@identity=1])"/>
<xsl:param name="pk">PRIMARY KEY</xsl:param>
<xsl:param name="fk">FOREIGN KEY</xsl:param>
<xsl:param name="getID">0</xsl:param>
<xsl:param name="insert_sp" select="concat(/table/@friendly_name, '_INSERT')"/>
<xsl:param name="update_sp" select="concat(/table/@friendly_name, '_UPDATE')"/>
<xsl:param name="delete_sp" select="concat(/table/@friendly_name, '_DELETE')"/>
<xsl:param name="select_sp" select="concat(/table/@friendly_name, '_SELECT')"/>
<xsl:param name="use_noname_parameters">0</xsl:param>
<xsl:param name="use_DataAccessComponent">0</xsl:param>
<xsl:param name="use_CustomFields">0</xsl:param>
表单实例的配置将是:
根据参数值的不同,生成的对象可能会有很大差异,这为应用程序提供了强大的可扩展性。该表单名为 `frmParameters`,它在主应用程序表单的 `PrepareXslParameters` 方法中作为模态对话框打开。`XmlDocument` 类型的私有成员 `xslParametersDoc` 填充了有效的 XSL 样式表,其 `xsl:param` 集合转换为 `DataTable` 对象。
xslParametersDoc = new XmlDocument();
xslParametersDoc.Load(xslFileName);
XmlNamespaceManager nsmgr = new
XmlNamespaceManager(xslParametersDoc.NameTable);
nsmgr.AddNamespace("xsl",
"http://www.w3.org/1999/XSL/Transform");
XmlNodeList parameters =
xslParametersDoc.SelectNodes("/xsl:styl" +
"esheet/xsl:param", nsmgr);
DataTable dtParameters = new DataTable();
dtParameters.Columns.Add(new
DataColumn("name", typeof(string)));
dtParameters.Columns.Add(new
DataColumn("value", typeof(string)));
dtParameters.Columns.Add(new
DataColumn("select", typeof(bool)));
该表单使用充当可编辑网格数据源的 `DataTable` 对象实例化。
frmParameters frm = new frmParameters(dtParameters);
...
dtParameters.DefaultView.AllowNew = false;
dgParameters.DataSource = dtParameters.DefaultView;
只有 `value` 属性是可编辑的,修改后的参数通过 `frmParameters` 表单上的一个事件返回到 XSL 文档中。
frm.SelectParameters += new
SelectParametersEventHandler(frm_SelectParameters);
委托 `SelectParametersEventHandler` 定义如下:
public delegate void
SelectParametersEventHandler(object sender,
SelectParametersEventArgs e);
`SelectParametersEventArgs` 类包含带有新值的参数 `DataTable` 对象(在一个名为 `Parameters` 的属性中)。为 `SelectParameters` 事件分配的事件处理程序获取 `Parameters` `DataTable` 并将所有 `xsl:param` 节点返回到 `xslParametersDoc` 中。
XmlNamespaceManager nsmgr = new
XmlNamespaceManager(xslParametersDoc.NameTable);
nsmgr.AddNamespace("xsl",
"http://www.w3.org/1999/XSL/Transform");
XmlNode node;
XmlElement elem;
bool bIsSelect;
foreach(DataRow dr in e.Parameters.Rows)
{
node = xslParametersDoc.SelectSingleNode("/xsl:stylesh" +
"eet/xsl:param[@name='" +
dr["name"].ToString() + "']", nsmgr);
if(node != null)
{
elem = (XmlElement)node;
bIsSelect = (bool)dr["select"];
if(bIsSelect)
elem.SetAttribute("select", dr["value"].ToString());
else
node.InnerText = dr["value"].ToString();
}
}
如果用户在 `frmParameters` 上按下“确定”,则 `xslParametersDoc` 对象将传递给 `CommonGenerator` 对象的 `Generate` 方法,并使用新的参数值呈现输出;否则,输出将使用默认参数值呈现。
DAC 转换
接下来,我将尝试解释 DAC 转换。使用文章开头示例中的表,我们将生成一个简单的 DAC(数据访问容器),它可访问 SQL Server 系统,并使用 C# 作为编程语言。首先,我们从表组合框中选择表 *Items*。然后,在结构菜单中,选择 *Get structure* 命令。这将自动使用关于列的元数据信息填充 `dgColumns` `DataGrid`。该网格将有两张嵌套表:
- 第一个包含有关所选表的信息;
- 第二个(子表)包含有关表列的信息。
我们可以在第一张表中填写 `friendly_name`、`namespace` 和 `abbreviation` 的值(假设此 DAC 将在会计系统中使用,因此我们将其缩写定为 *ACC*)。现在我们可以填写一些关于表列的信息。第二张表的第二列是 `column_friendly_name`,它将表示属性名称。所有其他字段都可以更改。例如,如果数据库中没有定义与 *Categories* 表的约束,我们可以强制在应用程序中存在它;尽管不推荐这种技术(我们可以将 `constraint_type` 值更改为“FOREIGN KEY
”,将 `referenced_table` 值更改为“Categories”)。完成所有修改后,我们可以生成 DAC 类。从 .NET DAC 菜单中,我们可以选择 *Simple DAC (SQL)* 命令。`frmParameters` 窗口会被调用,因为相应的样式表包含参数。让我们解释一下参数:
- `auto_increment` – 指定列是否具有标识属性。
- `pk` - 主键。
- `fk` - 外键。
- `getID` - 如果为 1,则输出插入的主键值。
- `insert_sp` - 插入存储过程名称,通常是 *table_name + "_INSERT"*。
- `update_sp` - 更新存储过程名称,通常是 *table_name + "_UPDATE"*。
- `delete_sp` - 删除存储过程名称,通常是 *table_name + "_DELETE"*。
- `select_sp` - 选择存储过程名称,通常是 *table_name + "_SELECT"*。
- `use_noname_parameters` - 如果为 1,则使用 OLE DB 无名参数格式“?”创建 SQL 语句。
- `use_DataAccessComponents` - 如果为 1,则 DAC 对象将不独立,而是作为数据访问组件框架的一部分,该框架提供连接信息、事务等(此外,.NET Data Access Container 和 .NET Data Access Component 命令提供了与此框架中生成的 DAC 对象结合使用的类)。
- `use_CustomFields` - 如果为 1,则生成自定义字段用法的补充语句。
主样式表模板如下:
<xsl:template match="/">using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
<xsl:if test="$use_CustomFields=1">
using System.Reflection;
using System.IO;
using DotNetScripting;
using CustomFields;
</xsl:if>
namespace <xsl:value-of select="/table/@namespace"/>.DAC
{
public class <xsl:value-of select="/table/@abbreviation"/>
DAC<xsl:value-of select="/table/@friendly_name"/>
<xsl:if test="$use_DataAccessComponent=1">
: DataAccessContainer</xsl:if>
{
public <xsl:value-of
select="/table/@abbreviation"/>DAC
<xsl:value-of select="/table/@friendly_name"/>()
{
<xsl:if test="$use_DataAccessComponent=1">
this.TableName = "<xsl:value-of select="/table/@name"/>";
this.AutoIncrement =
<xsl:choose><xsl:when test="$auto_increment=1">
true</xsl:when><xsl:otherwise>false
</xsl:otherwise></xsl:choose>;
<xsl:if test="$use_CustomFields=1">
LoadCustomFields();
</xsl:if>
</xsl:if>
}
#region Private members
<xsl:apply-templates select="/table/column" mode="member"/>
private DataRow x<xsl:value-of select="/table/@friendly_name"/>Row;
#endregion
#region Properties
<xsl:apply-templates select="/table/column" mode="property"/>
public DataRow <xsl:value-of select="/table/@friendly_name"/>Row
{
get
{
return x<xsl:value-of select="/table/@friendly_name"/>Row;
}
}
#endregion
<xsl:if test="$use_DataAccessComponent!=1">
#region Connection
<xsl:call-template name="connection"/>
#endregion
</xsl:if>
#region Standard methods
<xsl:choose>
<xsl:when test="$use_DataAccessComponent!=1">
<xsl:call-template name="methods"/>
</xsl:when>
<xsl:when test="$use_DataAccessComponent=1">
<xsl:call-template name="methods_DataAccessComponent"/>
</xsl:when>
</xsl:choose>
#endregion
#region Other DAC logic
#endregion
<xsl:if test="$use_CustomFields=1">
<xsl:call-template name="CustomFields"/>
</xsl:if>
}
}
</xsl:template>
您可以看到,通过使用参数值进行过滤,结果是不同的。其他模板也通过调用或应用的方式使用。一个调用模板的示例用于连接,当 `use_DataAccessComponent` 参数值不等于 1 时:
<xsl:if test="$use_DataAccessComponent!=1">
#region Connection
<xsl:call-template name="connection"/>
#endregion
</xsl:if>
名为 `connection` 的模板包含用于打开数据库连接的私有成员、公共属性和方法。应用模板的一个示例用于创建映射到表列的私有成员和公共属性。
<xsl:apply-templates select="/table/column" mode="member"/>
<xsl:apply-templates select="/table/column" mode="property"/>
`mode` 属性用于区分具有相同匹配项的模板(在我们的示例中是列)。根据您想要输出的代码(SQL 语句或编程语言),有不同的模板。根据所选语言,将有额外的模板,专门用于成员或属性数据类型、参数类型、默认值或转换。参数 `use_DataAccessComponent` 很重要,因为它将对象包含在不同的框架中。如果您想使用此框架,则必须为指定的命名空间(`SQL`、`OleDb` 或 `Odbc`)生成 `DataAccessContainer` 和 `DataAccessComponent` 类。`DataAccessContainer` 类是 DAC 对象的基类,包含 `Add`、`Edit`、`Delete` 和 `Read` 等虚拟方法。`DataAccessComponent` 类可以处理 DAC 对象,无论是单个还是多个,无论是否在事务中。这些对象将不再具有连接管理,因为连接可以由 `DataAccessComponent` 类打开。但是,必须向对象提供有效的连接对象和有效的数据访问命令。`DataAccessContainer` 具有一个名为 `RowState` 的公共属性,类型为 `DataRowState`,它告诉 `DataAccessComponent` 对 DAC 对象执行何种操作。派生的 DAC 对象必须重写 `Add`、`Edit` 和 `Delete` 方法。`ExecuteOperation` 方法接收一个 DAC 对象作为参数,并根据 `RowState` 属性的值插入、更新或删除它。`ExecuteOperations` 方法接收一个 `ArrayList` 的 DAC 对象,并对其执行相同的操作,无论是否在事务中。
这些类的代码在 .NET DAC 菜单的“数据访问容器”或“数据访问组件”命令中提供。您必须编译并使用这两个类(.NET DAC->编译和 .NET DAC->创建程序集方法)创建一个程序集。必须引用生成的程序集才能在 `DataAccessComponent` 环境中编译生成的 DAC 对象。
通过这种方式,框架可以通过附加功能进行定制,因为建议在 DAC 框架和数据库功能之间采取平衡的方法,以在应用程序中获得最佳性能。`DataAccessComponent` 类提供了执行存储过程和读取数据的附加方法。
数据库例程执行
DACBuilder 应用程序提供了生成 .NET 代码的功能,该代码允许执行 SQL 例程或查询。它构建了一个名为 `DAC_
编译代码
生成的 .NET DAC 对象代码可以编译并作为二进制文件保存到程序集中。编译和程序集生成由 `DotNetScripting` 命名空间中的对象执行。这项强大功能曾是 CodeProject 文章《DotNetScript》的主题。感谢 jconwell 的精彩文章。它使用 `System.Reflection` 和 `System.CodeDom.Compiler` 来编译、创建程序集、添加资源、实例化对象、执行方法、获取或设置属性值以及处理错误。如果编译的代码需要引用 GAC 或文件中的程序集,DACBuilder 可以通过使用 `BForm` 窗口右角的三个按钮来实现。第一个按钮提供一个带有 GAC 程序集的弹出菜单。应用程序不使用 GAC API 来检索安装在 GAC 中的程序集;这可能是一个很好的主题,可以另写一篇文章。
第二个按钮加载磁盘上的程序集,而第三个按钮将其从引用程序集列表中移除。
资源生成
DACBuilder 应用程序能够以托管的 ResX 格式生成资源。资源生成方式与 VS 相同,使用原生数据网格,其数据源为 `XmlDataDocument`,并带有富文本编辑框。以下是代码示例:
doc = new XmlDataDocument();
doc.DataSet.ReadXmlSchema(templatesPath + "template.resx");
doc.Load(fileName);
doc.DataSet.Tables[0].RowChanged += new
DataRowChangeEventHandler(frmResources_RowChanged);
doc.DataSet.Tables[0].RowDeleted += new
DataRowChangeEventHandler(frmResources_RowDeleted);
dgResources.DataSource = doc.DataSet.Tables[0].DefaultView;
XML Schema 生成
XML Schema 的生成方式类似,使用特定的样式表(*dataset_schema.xsl*)。该模板包含特定的 XML Schema 元素,并通过使用 `xsl:output` 元素的 `xml` 方法完成输出。
<xsl:output method="xml" omit-xml-declaration="yes"/>
SQL 脚本生成
SQL 存储过程以相同的方式生成,使用具有不同参数的 XSL 样式表。可以生成的存储过程用于插入、更新和删除,用于选择单个记录,以及使用默认参数进行搜索。其他功能包括表创建和用于表中所有行的 insert
语句。最后一个命令必须小心使用,因为不建议对记录数量庞大的表进行访问。SQL 语句可以在当前服务器上解析和执行。执行语句时也必须小心,因为大多数是 DDL 语句,可能会影响您的数据库。生成 SQL 语句的一个示例是搜索存储过程。该过程接收表中每一列的参数。参数具有默认值,对于数值为 -1,对于其他数据类型为 NULL
。
<xsl:template match="column" mode="parameter_default">
<xsl:if test="position()>1">,
</xsl:if> @<xsl:value-of select="@column_name"/>
[<xsl:value-of select="@data_type"/>]
<xsl:call-template name="data-length-precision-scale">
<xsl:with-param name="data-type" select="@data_type"/>
<xsl:with-param name="length" select="@character_maximum_length"/>
<xsl:with-param name="precision" select="@numeric_precision"/>
<xsl:with-param name="scale" select="@numeric_scale"/>
</xsl:call-template> = <xsl:call-template
name="select_primitive_type_default_SQL">
<xsl:with-param name="data_type" select="@data_type"/>
</xsl:call-template>
</xsl:template>
该过程构建一个 select
SQL 语句字符串,其中包含对不具有默认值的参数的筛选。这意味着如果用户只想对一个参数进行筛选,则只为该参数提供一个值。参数筛选器的模板如下:
<xsl:template match="column" mode="column_filter">
IF @<xsl:value-of select="@column_name"/>
<xsl:call-template
name="select_primitive_type_default_comparison_SQL">
<xsl:with-param name="data_type" select="@data_type"/>
</xsl:call-template>
BEGIN
IF @filter = N''
SET @filter = N' <xsl:value-of select="@column_name"/>
<xsl:call-template name="select_primitive_type_comparison_SQL">
<xsl:with-param name="data_type" select="@data_type"/>
<xsl:with-param name="object"
select="concat('@', @column_name)"/>
</xsl:call-template>
'
ELSE
SET @filter = N' AND <xsl:value-of select="@column_name"/>
<xsl:call-template
name="select_primitive_type_comparison_SQL">
<xsl:with-param name="data_type" select="@data_type"/>
<xsl:with-param name="object"
select="concat('@', @column_name)"/>
</xsl:call-template>
'
END
</xsl:template>
该过程将构建 SQL 字符串,并在存在筛选条件时(至少一个参数的值与其默认值不同)附加筛选条件,该字符串将作为 `sp_executesql` 语句的参数,连同搜索存储过程的参数和值一起使用。
DECLARE @filter NVARCHAR(4000)
SET @filter = N''
<xsl:apply-templates select="/table/column" mode="column_filter"/>
IF @filter != N''
SET @stmt = @stmt + N'
WHERE
' + @filter
PRINT @stmt
EXEC sp_executesql @stmt
, N'
<xsl:apply-templates
select="/table/column" mode="parameter"/>
'
,
<xsl:apply-templates select="/table/column"
mode="parameter_itself_value"/>
为表 *Items* 生成的输出将是:
IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_NAME = 'Items_SEARCH')
BEGIN
DROP PROCEDURE [Items_SEARCH]
END
GO
CREATE PROCEDURE [Items_SEARCH]
(
@ID [int] = -1,
@Name [varchar](255) = NULL,
@Price [decimal](18, 2) = -1,
@CategoryID [int] = -1
)
AS
DECLARE @stmt NVARCHAR(4000)
SET @stmt = N'
SELECT
[ID] AS [ID]
, [Name] AS [Name]
, [Price] AS [Price]
, [CategoryID] AS [CategoryID]
FROM [Items] AS [Items]
'
DECLARE @filter NVARCHAR(4000)
SET @filter = N''
IF @ID != -1
BEGIN
IF @filter = N''
SET @filter = N' ID = @ID
'
ELSE
SET @filter = N' AND ID = @ID
'
END
IF @Name IS NOT NULL
BEGIN
IF @filter = N''
SET @filter = N' Name LIKE ''%'' + @Name + ''%''
'
ELSE
SET @filter = N' AND Name LIKE ''%'' + @Name + ''%''
'
END
IF @Price != -1
BEGIN
IF @filter = N''
SET @filter = N' Price = @Price
'
ELSE
SET @filter = N' AND Price = @Price
'
END
IF @CategoryID != -1
BEGIN
IF @filter = N''
SET @filter = N' CategoryID = @CategoryID
'
ELSE
SET @filter = N' AND CategoryID = @CategoryID
'
END
IF @filter != N''
SET @stmt = @stmt + N'
WHERE
' + @filter
PRINT @stmt
EXEC sp_executesql @stmt
, N'
@ID [int],
@Name [varchar](255),
@Price [decimal](18, 2),
@CategoryID [int]
'
,
@ID = @ID, @Name = @Name, @Price = @Price, @CategoryID = @CategoryID
GO
其他 SQL 功能包括用于 `fnGetExtendedProperty` 函数、`GetTableColumnsXML` 过程、`GetRoutineParametersXML` 过程、`KeyTables` 表创建语句和 `GET_NEXT_ID` 过程的“元”脚本。最后两个功能在您不希望使用标识属性时,作为生成主键数值的机制非常有用。
UI 生成
UI 功能对于为 DAC 对象生成非常简单的界面很有用。它提供视觉对象(如 Windows Forms、Web Forms 或 HTML 页面)的 UI 特性和使用 DAC 对象的方法的类代码。每个 UI 平台和 .NET 语言(C# 和 VB.NET)都有样式表。命令 *UI->Simple DAC Methods (Web and Windows)* 生成的代码可以保存、编译并包含在程序集中。
自定义字段实现
自定义字段实现是一种旨在增强基础表及其 DAC 对象功能的技术。该实现始于 Prodigy,并根据现有数据访问框架进行了定制。它允许最终用户向数据库中的任何表添加新字段。该实现仅适用于 SQL Server。处理自定义字段所需的“元”对象可在“自定义字段”菜单的“数据库实体”命令(用于 SQL 语句)和“DAC 实体”(用于 C# 或 VB.NET)中获取。DAC 实体必须经过编译并作为引用添加到生成的自定义字段 DAC 对象中。数据库实现很简单,系统管理具有自定义字段属性的表、该表的主键(系统要求该表的主键只能是单个列)以及自定义属性。这些对象定义了自定义字段的结构。表 *CustomFieldsProperty* 包含以下结构:
- `PKProperty` – 属性标识符(主键)。
- `FKPropertyType` – 属性类型(引用 *CustomFieldType* 表的外键)。
- `FKColumn` – 列标识符(引用 *CustomFieldsTableColumn* 的外键)。
- `DefaultValue` – 属性默认值。
- `PropertyName` – 属性名称(除字母、数字和 '_' 外,不得包含任何其他字符)。
- `FKPropertyList` – 属性列表标识符(如果属性支持多值)。
自定义属性的值存储在特定于所选类型的表中。目前已实现的类型包括 `String`、`DateTime`、`Bool`、`Decimal` 和 `Integer`。任何值表都具有以下结构:
- `FKProperty` – 属性标识符(引用 *CustomFieldsProperty* 表的外键)。
- `FKParent` – 基础表中的主键值。
- `PropertyValue` – 属性值(类型不同,取决于表 – `CustomFieldsValuesString`、`CustomFieldsValuesInteger`、`CustomFieldsValuesBool`、`CustomFieldsValuesDateTime`、`CustomFieldsValuesDecimal`)。反映自定义字段数据库实体的 DAC 对象由 DACBuilder 应用程序生成,它们与任何其他 DAC 对象没有什么不同。
非程序员用户可以通过在 `frmCustomFieldsManagement` 表单中添加、修改和删除属性来管理自定义字段。
对于 *Items* 表,我们以这种方式定义了四个自定义字段:
- `MeasureUnit` – `String`;
- `StockLimit` – `Decimal`;
- `IsInStock` – `Bool`;
- `ExpireDate` – `DateTime`。
使用“生成视图”命令,将生成一个包含所有基础列和/或自定义字段的视图。视图名称为 `CustomFields_LEFT JOIN
操作组成。为了区分相同类型的两个属性,子查询接收一个别名,其名称为 `CustomFieldsValues
LEFT JOIN (
SELECT
CustomFieldsValuesString.FKParent AS ID
, CustomFieldsValuesString.PropertyValue AS [MeasureUnit]
FROM CustomFieldsValuesString
WHERE CustomFieldsValuesString.FKProperty = 5
) AS CustomFieldsValuesString_5 ON Items. = CustomFieldsValuesString_5.ID
视图是使用 DACBuilder 应用程序创建的。在此视图上执行了几个 SQL select
语句,结果输出如下:
使用“生成读取 SP”命令,将生成一个 select
存储过程。它与视图非常相似,只是它接收 `PKObjectID@` 参数以仅筛选单个记录。对于 *Items* 表,该语句与之前讨论的视图非常相似,并且 WHERE
子句如下:
WHERE Items.ID = @PKObjectID
“生成类”命令提供了最有趣的自定义字段功能。它为选定的表生成自定义字段 DAC 类。由于此类是在运行时生成的,并且必须由二进制对象处理,因此属性和方法必须同质化。该类将为每个定义的自定义字段拥有成员和属性。属性类型为 `CustomFieldsProperty`,这是一个在代码中反映自定义字段属性的类(`PropertyID`、`PropertyName`、`PropertyTypeID`、`ParentID`、`PropertyValue`)。`Initialize()` 方法为自定义字段设置“元”值(`PropertyID`、`PropertyName`、`PropertyTypeID`)。`ParentID` 属性包含一个整数值,指定主表中记录的键(*Items*)。自定义字段 DAC 对象具有插入、更新和删除值的方法。名为 `Save` 的方法在不存在值时执行插入,在相应值存在时执行更新。使用反射,二进制 DAC 对象将在运行时加载自定义字段 DAC 对象。自定义字段 DAC 对象可以放在程序集文件中,主 DAC 对象必须为此做好准备才能使用它。
在 DAC 对象生成中,如果您在参数窗口中将 `use_CustomFields` 参数设置为 1,则 DAC 对象将知道它具有自定义字段。DAC 对象将包含额外的自定义字段实现:
#region Custom Fields
private bool xHasCustomFields = false;
public bool HasCustomFields
{
get
{
return xHasCustomFields;
}
}
private string execAsmbDir;
private string customFieldsAssemblyLocation;
private Assembly customFieldsAssembly;
private string customFieldsObjectName;
private object xCustomFields = null;
public object CustomFields
{
get
{
return xCustomFields;
}
set
{
xCustomFields = value;
}
}
private void LoadCustomFields()
{
Assembly execAsmb = Assembly.GetExecutingAssembly();
execAsmbDir = execAsmb.Location;
execAsmbDir =
execAsmbDir.Substring(0,
execAsmbDir.LastIndexOf("\\") + 1);
customFieldsObjectName = "DACCustomFields_Items";
customFieldsAssemblyLocation =
execAsmbDir + customFieldsObjectName + ".dll";
if(File.Exists(customFieldsAssemblyLocation))
{
xHasCustomFields = true;
customFieldsAssembly =
Assembly.LoadFile(customFieldsAssemblyLocation);
xCustomFields =
DotNetScriptEngine.CreateInstance(customFieldsAssembly,
customFieldsObjectName, null);
}
}
#endregion
程序集名称必须采用类名称形式:`DACCustomFields_
if(xHasCustomFields)
{
xCustomFields =
DotNetScriptEngine.SetProperty(xCustomFields,
"Connection", cnn);
xCustomFields =
DotNetScriptEngine.SetProperty(xCustomFields,
"Command", cmd);
xCustomFields =
DotNetScriptEngine.SetProperty(xCustomFields,
"ParentID", this.xID);
DotNetScriptEngine.ExecMethod(xCustomFields,
"Save", null);
}
`DotNetScriptEngine` 类(在 `DotNetScripting` 命名空间中)将设置属性,并为 `xCustomFields` 对象调用 `Save` 方法。对于删除,将调用 `Delete` 方法,对于读取,将调用 `Read`。要读取自定义字段值,您可以使用 `DotNetScriptEngine` 类的 `GetProperty` 静态方法。
CustomFieldsProperty oMeasureUnit =
(CustomFieldsProperty)
DotNetScriptEngine.GetProperty(objDAC.CustomFields,
"MeasureUnit");
MessageBox.Show(oMeasureUnit.PropertyValue.ToString());
请注意,主 DAC 对象不以任何方式引用自定义字段属性,而只引用一个名为 `CustomFields` 的对象,因为它不知道它有什么类型的自定义字段。使用反射,您可以为主 DAC 对象的 `CustomFields` 属性(类型为 object
)获取类型为 `CustomFieldsProperty` 的自定义字段属性。
其他功能
最后一个有用的功能是在运行时更改任何数据库系统的连接,以及为各种数据库系统生成连接字符串(更多信息请访问 www.connectionstrings.com)。
谢谢
感谢 Andreea Govorosciuc 在自定义字段管理表单、连接更改和连接字符串生成方面提供的有用帮助。
使用应用程序
首次构建项目时,请确保已激活生成后事件,该事件将在应用程序文件夹中创建模板文件夹并复制所有模板文件。然后,您可以停用生成后事件(一个简单的方法是在所有生成后事件命令之前写入 REM
指令)。
启动应用程序时,您必须设置连接信息(工具->新建连接...)。如果是 SQL Server 连接,您必须运行一些“元”例程才能使用 DACBuilder 应用程序(`fnGetExtendedProperty`、`GetTableColumnsXML`、`GetRoutineParametersXML`)。
代码生成得益于编辑列信息和自定义模板参数。列信息编辑必须小心进行,因为它会影响生成的代码。建议编辑有关友好名称、命名空间和缩写的信息。
在 .NET 语言代码生成方面,我建议使用 `DataAccessComponent` 和 `DataAccessContainer` 类,因为它们提供了读取数据和执行操作的附加功能。
在服务器上执行 SQL 代码时必须谨慎,因为它们是 DDL 语句。在执行 drop
/ create table
语句之前,您必须备份数据库。
当您想要编译 .NET 代码时,请确保您已引用了适当的程序集,这些程序集位于 GAC 中或磁盘上。您可以在 *Templates\Libraries* 文件夹中找到有用的库;即使它们可以由应用程序生成(作为“元”库)。这些程序集包括 *DataAccessComponent.dll*、*DataAccessContainer.dll*(用于 SQL、OLEDB、ODBC)、*DACCreator.dll*、*CustomFields.dll*、*DotNetScripting.dll*、*RJS.Web.WebControl.PopCalendar.dll*(一个 ASP.NET 日历控件,可在此处获取)和 *DataGridCustomColumns.dll*(原生 `DataGrid` 组合框列实现,可在此处获取)。
历史
- 2005-03-01 - 首次 DAC 实现(插入、更新、选择、删除存储过程),适用于 SQL Server 和 .NET 语言。
- 2005-05-01 - 资源生成实现。
- 2005-10-20 - 其他存储过程生成。
- 2005-10-28 - XML Schema 生成。
- 2005-11-10 - 简单 DAC 的 OLEDB 和 ODBC 生成。
- 2005-12-15 - UI 生成。
- 2006-01-26 - 其他语言实现。
- 2006-02-14 - SQL 例程处理。
- 2006-02-20 - `DataAccessComponent` 实现。
- 2006-03-03 - 自定义字段实现。
- 2006-03-15 - 连接字符串创建。