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

DLinqEntityGenerator – 使用 XSL 模板的类 SqlMetal 的 DLinq 实体生成器

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.30/5 (10投票s)

2007年1月2日

CPOL

12分钟阅读

viewsIcon

63492

downloadIcon

1161

本文介绍如何使用 VS.NET 上的插件应用程序,生成映射到 SQL Server 数据库对象的 DLinq 实体。

引言

SqlMetal 工具可用于生成映射到 SQL Server 数据库的 C# 或 VB.NET 对象。它提供具有映射到表和视图列的成员和属性的类。它反映了表之间的关系。它还提供了一个派生自基类 DataContext 的类,该类可以按需映射函数和存储过程,区分表函数和标量函数,以及区分返回行集的存储过程和仅执行插入、更新、删除等操作的存储过程。另一项功能是创建用于单独存储数据库对象定义的 XML 映射。

DLinqEntityGenerator 应用程序生成对象的方式与 SqlMetal 工具类似,但它构建为 VS.NET 的插件,并提供更多选项,例如为每个数据库对象生成输出文件,以及创建附加到当前解决方案的 C# 或 VB.NET 项目。它不是取代 SqlMetal 的工具,而是一个用于生成 DLinq 类的向导式选项。

DLinq Entities generator add-in application

这个想法源于之前的文章 DACBuilder – 基于 XML 和 XSL 模板转换的数据访问对象生成工具,该工具使用 XSL 转换来生成映射到不同数据源的简单对象。使用 XML 和 XSL 转换要容易得多,因为在修改和后续功能时,您只需要修改模板而无需更改代码并重新编译应用程序。另一个原因是您可以使用 XSL 参数(使用 xsl:param 节点)来自定义模板。

元数据

数据库对象元数据定义使用 SQL Server 中提供的 INFORMATION_SCHEMA 视图和 FOR XML 功能。调用的对象是:

  • EXCLUDE_DB_OBJECTS – 一个包含不应映射的数据库实体的表;
  • vwGetTables – 用于表的视图;
  • vwGetViews – 用于视图的视图;
  • vwGetProcedures – 用于存储过程的视图;
  • vwGetFunctions – 用于函数的视图;
  • GetTableColumnsXML – 一个存储过程,用于获取有关表(基表或视图)中列的元数据信息;
  • GetRoutineParametersXML – 一个存储过程,用于获取有关例程(存储过程或函数)参数的元数据信息;
  • GetFunctionColumnsXML – 一个存储过程,用于获取有关函数返回的列的元数据信息;
  • fnGetExtendedProperty – 一个函数,用于获取表中指定列的扩展属性;
  • fnCreateFriendlyName – 一个函数,用于从字符串创建友好名称(有助于提供有效的类名和标识符);

vwGetTablesvwGetViews 使用 INFORMATION_SCHEMA.TABLES 视图,通过对 TABLE_TYPE 列进行特定过滤,并排除 EXCLUDE_DB_OBJECTS 表中的相应对象来获取表或视图。

vwGetProceduresvwGetFunctions 视图使用 INFORMATION_SCHEMA.ROUTINES,通过对 ROUTINE_TYPE 列进行特定过滤,并排除 EXCLUDE_DB_OBJECTS 表中的相应对象来获取存储过程或函数。

GetTableColumnsXML 存储过程使用 FOR XML EXPLICIT 子句来分层存储有关指定表、其列以及与其他表的关联信息。FOR XML EXPLICIT 子句在存储过程中的逻辑可能看起来很复杂,但它是创建嵌套层次结构的标准技术。嵌套是通过使用 TAGPARENT 别名以及应用于具有 IDREFHIDE 指令的属性的排序来确保的。结构与 DACBuilder 应用程序使用的 GetTableColumnsXML 存储过程相似,但略有改进,以包含更多关于参照完整性的信息。

GetRoutineParametersXML 存储过程从 INFORMATION_SCHEMA.PARAMETERS 视图读取有关指定存储过程或函数的信息。

GetFunctionColumnsXML 存储过程使用 INFORMATION_SCHEMA.ROUTINE_COLUMNS 视图读取有关指定函数的信息。

向导窗体 (frmWizard) 和 DLinqEntitiesEventArgs

用于收集数据库对象和将映射它们的实体的相关信息的窗体称为 frmWizard。由于我有点懒,我使用了一个小技巧来模拟向导。我在设计时添加了一个 TabControl 对象,以便在相应的步骤上创建所有必需的控件。TabControl 控件的 Visible 属性设置为 false。每个步骤都有自己的 TabPage 对象,其中包含一个 Panel 控件,该控件包含相应的 TexBoxLabelPictureBoxComboboxButton 控件。当用户执行下一步或上一步操作时,相应的 Panel 对象会从其 TabPage 父级分离并添加到窗体。

当用户单击下一步上一步按钮时,将调用窗体的 NextActionBackAction 方法。提供了验证子句,因此如果当前步骤设置无效,用户将无法进入向导的下一步。但是,用户可以返回以更改上一步的设置。

第一步是连接信息。必须提供服务器名称或 IP 地址、用户名和密码以及数据库名称才能进入下一步。如果服务器名称、用户名和密码正确,当您“下拉”数据库组合框时,它将填充指定服务器上的所有数据库。

ServerConnection cnn = new ServerConnection(txtServer.Text, txtUser.Text, 
                                            txtPassword.Text);
cnn.Connect();
Server srvr = new Server(cnn);
DatabaseCollection oColl = srvr.Databases;

ServerConnection 类属于 Microsoft.SqlServer.Management.Common 命名空间,而 ServerDatabaseCollection 类属于 Microsoft.SqlServer.Management.Smo 命名空间。

First step - connection information

第二步是数据库对象选择步骤。这些对象存储在 TreeView 控件的四个独立分支中:表、视图、存储过程和函数。每个节点都是派生自 TreeNode 基类的类实例,因为有必要存储有关数据库对象的补充信息(对象类型、名称、友好名称,如果函数是返回类型)。此信息是 DBObject 结构的一部分。

public struct DBObject
{
    public DBObjectTypes ObjectType;
    public string ObjectName;
    public string ObjectFriendlyName;
    public string RoutineDataType; // null for tables, views, stored  
                                   // procedures; sql data type for scalar 
                                   // function; TABLE for inline and multi-
                                   // statement table-valued functions
}

如果未选中至少一个数据库对象,则无法进入下一步。

Second step - database objects selection

第三步是补充选项,例如项目名称、要存储结果文件的文件夹、生成的代码语言、XML 映射文件以及在生成的类上进行映射的选项。默认情况下,应用程序会以所选语言创建项目,但这可以禁用。在创建项目的情况下,选定的文件夹必须为空。项目将添加到当前解决方案。如果没有可用的解决方案,将创建一个新的解决方案。可选地,您可以将所有类生成到一个文件中。

Third step - project options

第三步之后的摘要回顾了收集到的信息,并为每个设置提供了简短的描述。

Summary

frmWizard 窗体有一个公共事件 WizardFinished,遵循委托的定义:

public delegate void DLinqEntitiesEventHandler(object sender,
                                               DLinqEntitiesEventArgs e);
...
public event DLinqEntitiesEventHandler WizardFinished;

当向导完成其工作后,所有收集到的信息都存储在 DLinqEntitiesEventArgs 类型的对象中。该事件可以在 OnWizardFinished 方法中消耗此创建的对象。

private void OnWizardFinished()
{
    ArrayList dbObjects = new ArrayList();
    foreach(TreeNode root in tvDBObjects.Nodes)
    {
        foreach(DBObjectTreeNode tn in root.Nodes)
        {
            if(tn.Checked)
            {
                dbObjects.Add(tn.DBObj);
            }
        }
    }
    string dbName = cboDatabases.SelectedIndex >= 0 ? 
                     DBHelper.CreateFriendlyName(
                                 cboDatabases.SelectedItem.ToString()) : "";
    LanguageTypes lt = rbCSharp.Checked ? LanguageTypes.CSharp : 
                      LanguageTypes.VisualBasic;
    string projectName = chkDontCreateProject.Checked ? null : 
                      DBHelper.CreateFriendlyName(txtProjectName.Text);
    string path = txtFolder.Text;
    DLinqEntitiesEventArgs e = new DLinqEntitiesEventArgs(cnnString, dbName, 
                                   dbObjects, chkUseOneOnlyFile.Checked, lt, 
                                   path, projectName, txtXMLFile.Text,
                                   chkUseMapping.Enabled && 
                                   chkUseMapping.Checked);
    if(WizardFinished != null)
    {
        WizardFinished(this, e);
    }
}

DLinqEntitiesEventArgs 类具有以下属性:

  • ConnectionString – 使用第一步中的连接信息构建;
  • DBName – 选定的数据库名称;
  • DBObjects – 一个 ArrayList,仅包含选定的数据库对象;
  • UseOneOnlyFile – 布尔属性,指定生成的类是输出到一个文件中,还是为每个对象生成一个文件;
  • LanguageTypeLanguageTypes 枚举属性(两个值:CSharpVisualBasic),指定生成代码的语言;
  • Path – 输出将保存到的完整目录名称;
  • ProjectName – 项目的名称;
  • CreateProject – 布尔属性,如果 ProjectName 不为 null 且不为空,则返回 true,否则返回 false;
  • ProjectExtension – 根据 LanguageType,为 "csproj" 或 "vbproj";
  • FileExtension – 根据 LanguageType,为 "cs" 或 "vb";
  • GenerateXML – 布尔属性,如果 XML 映射文件名不为 null 且不为空,则返回 true,否则返回 false;
  • XmlMapFileName – XML 映射文件的完整名称;
  • UseMapping – 布尔属性,指定是否使用映射源生成类(不使用映射的类在其定义中包含数据库对象元数据,作为自定义属性)。

DBHelper

DBHelper 类提供用于以下方面的函数和属性:

  • 连接到 SQL Server:OpenConnectionCloseConnectionIsConnectionOpen
  • 调用元数据视图和存储过程:GetTablesGetViewsGetProceduresGetFunctionsGetTableColumnsGetRoutineParametersGetFunctionColumnsGetProcedureType
  • 使用 XSL 样式表转换 XML 文档或读取器(可以是磁盘上的文件,也可以是内存中的 XML 文档):Transform(有三个重载定义);
  • 其他实用程序:SetXslDocumentParameters(允许使用 XSL 参数自定义 XSL 文档)、CreateFriendlyName(静态方法,用于生成有效的类名或标识符)。

除了 TransformGetProcedureType 之外,所有方法都易于理解。其中大部分是用于收集元数据的视图和存储过程的简单包装器。

Transform(XmlReader xmlRdr, XmlDocument xslTemplateDoc) 方法使用 .NET Framework 2.0 的新 XslCompiledTransform 类。

public string Transform(XmlReader xmlRdr, XmlDocument xslTemplateDoc)
{
    string strXML = string.Empty;
    try
    {
        XmlNamespaceManager nsmgr
                      = new XmlNamespaceManager(xslTemplateDoc.NameTable);
        nsmgr.AddNamespace("xsl", "http://www.w3.org/1999/XSL/Transform");

        XmlUrlResolver resolver = new XmlUrlResolver();
        XslCompiledTransform trans = new XslCompiledTransform();
        trans.Load(xslTemplateDoc, null, resolver);

        TextWriter writer = new StringWriter();

        XmlTextWriter xmlWriter = new XmlTextWriter(writer);
        xmlWriter.Formatting = Formatting.Indented;

        trans.Transform(xmlRdr, xmlWriter);

        strXML = writer.ToString();
    }
    catch(Exception ex)
    {
        strXML = "<errors>";
        while(ex != null)
        {
            strXML += "<error>";
            strXML += ex.Message;
            strXML += "</error>";
            ex = ex.InnerException;
        }
        strXML += "</errors>";
    }
    return strXML;
}

GetProcedureType(string procedureName, XmlDocument parametersDoc, string xslExecuteProcTemplatePath, out XmlDocument rowsetMetaDataDoc, out string strErrors) 方法用于区分返回行集的存储过程和返回插入、更新、删除等操作结果的存储过程。由于我没有找到 SQL Server 提供的方法来检索存储过程的行集元数据,因此我使用 SqlCommand 对象的 ExecuteReader 方法,并将参数设置为 CommandBehavior.SchemaOnly。一个问题是 SQL 字符串,因为我必须为参数提供默认值。这可能是一个问题,因为 NULL 值赋给参数可能会改变存储过程的行为和返回类型。此外,当我使用 SqlMetal 工具生成 DLinq 实体时,我看到对于 T-SQL 角度来说没问题的存储过程,出现了一些架构错误。如果有人有关于如何检索行集信息的有用信息并愿意分享,我将不胜感激。

首先,GetProcedureType 方法生成一个有效的 SQL 语句用于 EXECUTE 存储过程,并为参数提供默认值(parametersDoc 参数是使用 GetRoutineParametersXML 存储过程创建的 XML 文档):数字类型为 -1,位类型为 0,所有其他类型为 NULLExecuteReader 方法的结果是 SqlDataReader 对象。我假设如果此对象的 FieldCount 属性大于 0,则该过程返回一个行集,否则它返回一个值或为空。

SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.SchemaOnly);
if(rdr.FieldCount > 0)
{
    ...
    return ProcedureTypes.Rowset;
}
else
{
    ...
    return ProcedureTypes.ReturnValue;
}

如果该过程返回一个行集,元数据信息将在通过 SqlDataReader 对象的 GetSchemaTable() 方法获得的架构表中找到。通过遍历 DataTable 对象中的列集合,创建包含与 GetFunctionsColumnsXML 收集的结构相似的 XML 文档。

如果发生任何 SQL 错误,返回类型是 UnableToHandleSQL,如果发生其他未知错误,返回类型是 UnableToHandleOther,正如 ProcedureTypes 枚举中定义的。

public enum ProcedureTypes
{
    UnableToHandleSQL = 0
    , UnableToHandleOther = 1
    , Rowset = 2
    , ReturnValue = 3
}

Add-In Connect

Connect 类是用于将插件实例附加到 VS.NET 的主类。帮助我们执行操作的方法称为 Exec,其定义如下:

public void Exec(string commandName, vsCommandExecOption executeOption, 
                 ref object varIn, ref object varOut, ref bool handled)

此方法实例化一个 frmWizard 类型的对象,并为 WizardFinished 事件附加一个处理程序。

frmWizard frm = new frmWizard();
frm.WizardFinished += new DLinqEntitiesEventHandler(frm_WizardFinished);
DialogResult dr = frm.ShowDialog();

处理程序 frm_WizardFinished 使用向导收集并存储在 DLinqEntitiesEventArgs e 参数中的设置来生成 DLinq 类。它遍历 DBObjects 集合,并根据 ObjectType 属性执行某些操作。要访问 VS.NET 对象,Add-In 应用程序使用类型为 DTE2_applicationObject 对象。

private DTE2 _applicationObject;

如果选择了项目创建,它将使用特定方法以所选语言创建项目,并添加对 System.Data.DLinqSystem.Query 程序集的引用。为了反映代码生成的当前状态,它使用 VS.NET 状态栏来显示文本和操作进度。

if(e.CreateProject)
{
    prjPath = e.Path;

    prjTemplatePath = sln.GetProjectTemplate("ClassLibrary.zip", 
                                             e.LanguageType.ToString());
    sln.AddFromTemplate(prjTemplatePath, prjPath, e.ProjectName, false);
    prj = sln.Projects.Item(sln.Projects.Count);
    ProjectItem item = sln.FindProjectItem(prjPath + "\\Class1." + 
                       e.FileExtension);
    item.Remove();
    itemTemplatePath = sln.GetProjectItemTemplate("Class.zip", 
                                                  e.LanguageType.ToString());
    VSProject thisPrj = (VSProject)prj.Object;
    thisPrj.References.Add("System.Data.DLinq");
    thisPrj.References.Add("System.Query");
    _applicationObject.StatusBar.Progress(true, "Creating project " + 
                       e.ProjectName + "." + e.ProjectExtension + "...", 
                       2, 100);
}

对于每个数据库对象,根据设置,应用程序获取数据库对象的结构,执行代码生成,如果需要将其保存在其文件中,则将其添加到 StreamWriter 对象,如果选择了项目创建,则将项添加到之前创建的项目中。例如,对于表和视图,代码是:

xmlDoc = dbH.GetTableColumns(dbObj.ObjectName);
if(e.UseOneOnlyFile)
{
    sCode += dbH.Transform(xmlDoc, xslDoc);
}
else
{
    sCode = dbH.Transform(xmlDoc, xslDoc);
    sw = new StreamWriter(e.Path + @"\" + dbObj.ObjectFriendlyName + "." + 
                          e.FileExtension);
    sw.Write(sCode);
    sw.Close();
    if(e.CreateProject)
    {
        prj = sln.Projects.Item(sln.Projects.Count);
        prj.ProjectItems.AddFromFile(e.Path + @"\" + dbObj.ObjectFriendlyName
                                     + "." + e.FileExtension);
    }
}

在转换完所有选定的数据库对象后,应用程序生成派生自 DataContext 的主类,其中包含表(包括视图)、存储过程和函数的定义。

如果用户请求创建 XML 映射,将在指定位置创建一个包含数据库对象定义信息的 XML 文件。此外,如果用户请求生成的类使用映射,则包含数据库对象定义的自定义属性将不会添加到类中。

XSL 样式表和 DLinq 类

表和视图类、表函数和行集存储过程类使用 table_CSharp.xsltable_VisualBasic.xsl 样式表进行映射。它们包含特定的语言语法和模板,用于生成类定义、构造函数、映射到列的私有成员和公共属性、用于关系的私有成员和公共属性(EntityRefEntitySet 对象),以及反映列值更改的事件。

派生自 DataContext 的主类使用 data_context_CSharp.xsldata_context_VisualBasic.xsl 生成。它是映射到所选数据库的类,具有映射到 Table 对象的构造函数和公共属性,用于返回值的存储过程的 StoredProcedureResult,用于返回值的存储过程的 int 数据类型,用于标量函数的返回类型,以及用于表函数的 IQueryable 对象。

所有样式表都包含名为 type.xsl 的样式表,用于将 SQL 类型映射到 C# 或 VB.NET 的基本类型,并包含这些语言的默认值。

execute_procedure.xsl 样式表用于生成有效的 T-SQL 语句,用于执行带有参数默认值的存储过程,这有助于区分返回行集的存储过程和返回值的存储过程。

map.xsl 用于获取包含数据库对象元数据定义的 XML 映射文件。

输出与 SqlMetal 工具创建的非常相似,除了 EntityRefEntitySet 属性。类定义如下:

[Table(Name="Customers")]
public partial class Customer
{
    private int xCustomerID;
    [Column(Storage="xCustomerID", DBType="int NOT NULL IDENTITY", 
                                   Id=true, AutoGen=true)]
    public CustomerID
    {
        get
        {
            return xCustomerID;
        }
        set
        {
            if(this.xCustomerID != value)
            {
                this.OnPropertyChanging("CustomerID");
                this.xCustomerID = value;
                this.OnPropertyChanged("CusotmerID");
            }
        }
    }
}

EntitySet 类用于父类中,以拥有子实体的“集合”。

[Table(Name="Customers")]
public class Customer
{
    [Column(Id=true)]
    public string CustomerID;
    ...
    private EntitySet<Order> _Orders;
    [Association(Storage="_Orders", OtherKey="CustomerID")]
    public EntitySet<Order> Orders
    {
        get { return this._Orders; }
        set { this._Orders.Assign(value); }
    }
}

子实体必须具有指向父级的引用,使用 EntityRef 类。

[Table(Name="Orders")]
public class Order
{
    [Column]
    public string CustomerID;
    private EntityRef<Customer> _Customer;    
    [Association(Storage="_Customer", ThisKey="CustomerID")]
    public Customer Customer 
    {
        get { return this._Customer.Entity; }
        set { this._Customer.Entity = value; }
    }
}

派生自 DataContext 的类(主数据库类)如下所示:

public partial class Northwind : DataContext
{
    public Table<Customer> Customers;
    public Table<Order> Orders;
    
    public Northwind(string connection): base(connection) {}

    [StoredProcedure(Name="CustOrderHist")]
    public StoredProcedureResult<CustOrderHistResult>
    CustOrderHist([Parameter(Name="CustomerID")] string customerID)
    {
        return this.ExecuteStoredProcedure<CustOrderHistResult>

            (((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);
    }

    [StoredProcedure(Name="InsertRegion")]
    public int InsertRegion([Parameter(Name="RegionID", DBType="int")]
                               System.Nullable RegionID, 
                               [Parameter(Name="RegionDescription",
                               DBType="nchar(50)")] string RegionDescription)
    {
        StoredProcedureResult result = 
                            this.ExecuteStoredProcedure(((MethodInfo)
                              (MethodInfo.GetCurrentMethod())), RegionID, 
                               RegionDescription);
        return result.ReturnValue.Value;
    }

    [Function(Name="[dbo].[ProductsCostingMoreThan]")]
    public IQueryable<ProductsCostingMoreThanResult>
    ProductsCostingMoreThan(System.Nullable<decimal> cost)
    {
        MethodCallExpression mc = Expression.Call(           

((MethodInfo)(MethodInfo.GetCurrentMethod())),
                Expression.Constant(this),
                new Expression[] {
                  Expression.Constant(cost, typeof(System.Nullable<decimal>))
                }
            );
        return this.CreateQuery<ProductsCostingMoreThanResult>(mc);
    }

    [Function(Name="[dbo].[fnGetCustomers]")]
    public IQueryable<FnGetCustomersRowset> FnGetCustomers()
    {
        MethodCallExpression mc = Expression.Call(((MethodInfo)
                                  (MethodInfo.GetCurrentMethod())), 
                                  Expression.Constant(this), null);
        return this.CreateQuery<FnGetCustomersRowsetRowset>(mc);
    }
}

生成的类的简单用法,如 LINQ 文档所示:

NorthwindDataContext db
                   = new NorthwindDataContext("c:\\northwind\\northwnd.mdf");
var q =
    from c in db.Customers
    where c.City == "London"
    select c;
    foreach (var cust in q)
        Console.WriteLine("id = {0}, City = {1}",cust.CustomerID, cust.City);

使用应用程序

首先,您必须运行用于选择元信息的表、视图、函数和存储过程的脚本。要映射到 DLinq 实体的数据库必须具有所有这些对象(在 scripts 文件夹中可用)。

为了使 DLinqEntityGenerator 插件应用程序可用于 VS.NET,必须将程序集和 .AddIn 文件复制到“Add-in File Paths”选项指定的目录之一(工具 - > 选项 -> Add-In/宏安全性)。此外,必须选中“Allow Add-in components to load”选项。包含用于生成类的 XSL 样式表的 Templates 文件夹必须复制到选定的目录。如果您在运行 DLinqEntityGenerator Add-in 时遇到困难,请将随 DlinqEntityGenerator.dll 文件提供的其他程序集复制到同一目录。

文件复制完成后,当您启动 VS.NET 时,将在“工具”菜单的开头找到一个名为“DLinqEntityGenerator”的新命令。DLinqEntityGenerator 应用程序现在应该在 VS.NET 环境中可用,作为一个新工具。

© . All rights reserved.