65.9K
CodeProject 正在变化。 阅读更多。
Home

DACBuilder – 基于 XML 和 XSL 模板转换的数据访问对象生成工具

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2006 年 3 月 31 日

CPOL

23分钟阅读

viewsIcon

82914

downloadIcon

1879

DACBuilder 应用程序提供了从多个数据库系统自动生成多种编程语言代码的功能。

DACBuilder application

引言

当我开始从事数据库编程和复杂的业务实现时,对数据访问框架的需求是明确的。我开始构建自己的访问数据对象的类。当然,从零开始,工作量更大,我认为一个“通用”的数据访问框架可以帮助我和我的团队。短时间不允许我设想这样的基础设施,至少针对一个数据库管理系统和一种编程语言。但当我开始在 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 KEYFOREIGN 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>

表单实例的配置将是:

Parameters window

根据参数值的不同,生成的对象可能会有很大差异,这为应用程序提供了强大的可扩展性。该表单名为 `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_` 的类,其中包含一个名为 `` 的方法,其中 ** 是过程名称。如果 `use_properties` 参数设置为 1,则它会为此类创建属性,而不是使用方法参数。每个例程参数都将有自己的属性。在其他情况下,方法将为每个例程参数接收一个参数。`query` 参数用于区分执行非查询语句和接收结果集。

编译代码

生成的 .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` 表单中添加、修改和删除属性来管理自定义字段。

Custom fields management

对于 *Items* 表,我们以这种方式定义了四个自定义字段:

  • `MeasureUnit` – `String`;
  • `StockLimit` – `Decimal`;
  • `IsInStock` – `Bool`;
  • `ExpireDate` – `DateTime`。

使用“生成视图”命令,将生成一个包含所有基础列和/或自定义字段的视图。视图名称为 `CustomFields_`。此视图可用于报告,并帮助用户执行自定义字段上允许的任何操作(筛选、聚合等)。该视图是使用 `GetTableCustomFieldsXML` 存储过程生成的,由基础表与每个已定义自定义属性的特定子查询之间的 LEFT JOIN 操作组成。为了区分相同类型的两个属性,子查询接收一个别名,其名称为 `CustomFieldsValues_`。这是 `MeasureList` 自定义字段的连接(`PKProperty` = 5,`type` - `String`)。

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 语句,结果输出如下:

Custom fields view

使用“生成读取 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_`,并且必须位于 DAC 对象程序集目录中。`LoadCustomFields()` 方法将在 DAC 对象构造函数中调用。`Add` 和 `Edit` 方法都将包含自定义字段保存:

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 - 连接字符串创建。
© . All rights reserved.