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

完全数据库抽象层生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (26投票s)

2003年10月23日

8分钟阅读

viewsIcon

281867

又一个DAL生成器,具有SQL生成、多数据库支持、C#代码生成等功能...

引言

本文介绍了另一个数据抽象层生成器(热门话题)。DAL结构直接借鉴了DotNetNukeDAL文档,SQL生成使用SQLpp完成。

该生成器为您处理几乎所有事情

  • SQL代码生成:数据库创建和存储过程。此外,SQL经过专门化处理,以考虑每个提供程序的SQL语法差异,
  • Web.Config生成:用于配置DAL的XML条目,
  • 业务逻辑层,用于每个表和过程(C#),
  • 抽象数据提供程序,用于每个表和过程(C#),
  • 专用数据提供程序,为每个数据提供程序定制(C#),
  • IDataReader包装器类,用于表和过程(C#)

一些有趣的特性是

  • 无需SQL操作:使用生成器,您无需编写一行SQL,
  • 多数据库系统支持:MSSQL,MySQL。其他数据库可以轻松实现,
  • 通过将查询存储在web.config中(如果需要)来处理存储过程问题,
  • 支持复杂的表连接:生成器在后台使用Boost.Graph.Library来生成查询
  • C#代码已文档化!
  • 非商业和商业用途完全免费!!!

所有下载、文档和跟踪器均可从SourceForge页面获取。

要求

以下提供程序需要一些特定的(免费)库

此外,为了充分理解DAL结构,您应该查看DNN AL文档

说明性示例

生成过程将通过一个简单的模块创建示例进行说明。假设您想要创建一个由以下结构定义的Users-Products-Orders数据库

由于SqlppNet仍然是一个无GUI工具,所以您需要手动编写。

数据库提供程序

第一步是创建一个数据库适配器。例如,MSSQL提供程序构造如下

MsSqlAdaptor msSql = new MsSqlAdaptor(
    "MyCatalog",
    "providerpath",
    "dbo",
    "connectionstring"
    );

其中

  • MyCatalog是数据库名称
  • providerpath是提供程序路径,
  • dbo是数据库所有者,
  • connectionString是此提供程序的连接字符串,

当然,您以后可以创建其他适配器并为其生成代码。

数据库创建

一旦创建了适配器,就会创建一个数据库并将适配器附加到它

Database db = new Database( msSql );

添加表

使用以下函数调用添加表

DbTable users = db.AddTable(
    "User", // data name
    "dnn_", // object qualifier
    "User" // field prefix
    );

其中

  • 数据名称用于组成表名并创建包装器类
    • Users将是表名,
    • UserData, UserDataProvider等将是与Users关联的类名
  • 如DAL文档中提到,对象限定符用于“修饰”表名:dnn_Users是真实的表名,
  • 字段前缀用作自动字段前缀,附加到表的每个字段(预期外键字段):如果您添加一个名为ID的字段,它将被重命名为“UserID”。

其他表也类似添加

DbTable products = db.AddTable("Product","mytables_","Product");
DbTable orders = db.AddTable("Order","mytables_","Order");

添加主键

添加主键很简单

users.AddPrimaryKey("ID","int"); // adds UserID INT as primary key

注释

  • 此方法自动在主键上定义自动递增
  • 可以定义更复杂的主键,但这超出了本文的范围
  • int字符串由框架内部解析并转换为SQL类型。解析器包含大部分可接受的SQL类型。

同样,其他表也类似处理

orders.AddPrimaryKey("ID","int"); 
products.AddPrimaryKey("ID","int");

链接表

现在是时候设置表之间的关系了。通过向表添加外键(引用约束)或唯一约束来强制执行关系/约束。

Users外键添加到Orders表如下所示

orders.AddForeignKey( users );

请注意,此方法使用许多默认设置,例如ON DELETEON UPDATE行为。Orders表现在包含一个引用Users表的UserID字段。

products表的外键也类似添加

orders.AddForeignKey( products );

添加字段

遵循相同的逻辑,使用Table.AddField添加字段

products.AddField(
    "Name", // field name (remember that field prefix is added
    "nvarchar(50)", // type
    true // not null
    );
products.AddField("Price","decimal",true);

orders.AddField("Quantity","int",true);
orders.AddField("Timestamp","timestamp",true);

此时,您的数据库结构已准备就绪。

创建一些查询

当然,您需要对上面定义的数据库进行一些查询。这也由SQLppNet处理。假设我们想要创建以下查询

Select user id, product name, quantity, price and order id 
from users, products and orders.
  1. 创建一个查询,
    SelectQuery q = db.CreateQuery();
  2. 添加带别名的表(可选)并链接它们,
    QueryTable qusers = q.AddTable(
        users, // table to add 
        "U" // alias
        );

    其他表也类似添加。表需要连接

    q.Join( 
        qorders, // table 
        qusers // reference table
        );
    q.Join(qorders,qproducts);
    

    SQLpp将处理Join过程的其余部分。

  3. 添加字段
    q.AddField( 
        FieldExpression.Link(    // links qusers and userID field
            qusers, //
            users.get_Field("ID") // retreiving UserID field
            ),
        "UserID"  // giving the AS name
        );

    其他字段也可以用相同的方式添加。在C++版本中,您可以添加更复杂的SQL表达式,例如聚合函数、算术表达式等...请参阅下面的注释。

  4. 添加Where子句(尚未封装,请参阅下面的注释)

注意:SQLppNet是SQLpp的一个薄型托管C++包装器。目前,该包装器不完整。要获得更多功能,请返回C++。

设置生成器

需要创建一个C#生成器对象并进行配置

CsGenerator cs = new CsGenerator(
    db,              // the database to generator
    ".",             // output file path
    "MyNamespace",   // created namespace
    "StoredProc"     // name of the stored procedure class(explained later)
    );

C#生成器默认不为所有数据库表生成包装器,您必须将要“生成”的表添加到生成器中(由于Users表已存在于数据库中,我们不需要生成它)

cs.AddTable(orders);
cs.AddTable(products);

您还需要添加您生成的特定查询

cs.AddProcedure( StoredProcedure.Wrap(q, "GetOrders") );

生成DAL

这无疑是最简单的部分

cs.Generate();

代码摘要

这是您需要编写的代码的简短摘要,以生成简单的数据库结构

using namespace SQLppNet;
using namespace SQLppNet.Adaptors;
using namespace SQLppNet.Generators;
using namespace SQLppNet.Queries;

try 
{
    // creating MsSql adaptor
    MsSqlDatabaseAdaptor msSql = new MsSqlDatabaseAdaptor(
        "MyCatalog",
        "",
        "dbo",
        "connectionstring"
        );

    // creating db
    Database db = new Database( msSql );

    // add tables
    DbTable users = db.AddTable("User","","User");
    DbTable products = db.AddTable("Product","ex_","Product");
    DbTable orders = db.AddTable("Order","ex_","Order");

    // add pk
    users.AddPrimaryKey("ID","int");
    orders.AddPrimaryKey("ID","int");
    products.AddPrimaryKey("ID","int");

    // link tables
    orders.AddForeignKey( users );
    orders.AddForeignKey( products );

    // add fields
    products.AddField("Name","nvarchar(50)",true);
    products.AddField("Price","decimal",true);
    orders.AddField("Quantity","int",true);
    orders.AddField("Timestamp","timestamp",true);

    // create generator
    CsGenerator cs = new CsGenerator(
        db,
        "c:\\Inetpub\\wwwroot\\DnnFramework\\DotNetNuke\\DesktopModules",
        "UserQuotes",
        "StoredProc"
        );
    // adding tables to generate
    cs.AddTable(orders);
    cs.AddTable(products); 

    // a query
    SelectQuery q = db.CreateQuery();
    // adding tables
    QueryTable qusers = q.AddTable(users,"U");
    QueryTable qorders = q.AddTable(orders,"O");
    QueryTable qproducts = q.AddTable(products,"P");

    // joining tables
    q.Join(qorders, qusers);
    q.Join(qorders,qproducts);

    q.AddField( FieldExpression.Link(qusers, 
             users.get_Field("ID")),"UserID"  );
    q.AddField( FieldExpression.Link(qproducts,
             products.get_Field("Name")),"ProductName"  );
    q.AddField( FieldExpression.Link(qorders,
             products.get_Field("Price")),"Price"  );
    q.AddField( FieldExpression.Link(qorders, 
             orders.get_Field("Quantity")),"Quantity"  );

    cs.AddProcedure( StoredProcedure.Wrap(q, "GetOrders") );

    // generation
    cs.Generate();
}
catch(Exception ex)
{
   Console.WriteLine(ex.ToString());
}

所以这不到100行,但它将生成比这更多的行。

生成的代码

本节描述了上述应用程序生成的内容。

生成文件结构

文件/类结构布局如下,并“C#就绪”(准备好包含在C#项目中)

文件/路径 description
. 根路径
/Config 配置类目录(Config命名空间)
/Config/webconfig.xml 要包含在应用程序的web.config中的XML
/Config/DataProviderConfigurationHandler.cs 处理web.config文件中数据的类
/Data DAL类目录(Data命名空间)
/Data/DbSql.sql 用于创建OrdersProducts表的SQL代码。
/Data/OrderAbstractDataProvider.cs Orders表的抽象数据提供程序类
/Data/OrderSqlDataProvider.cs Orders表的MSSQL数据提供程序类
/Data/OrderStoreProcSql.sql OrderSqlDataProvider数据提供程序使用的存储过程
/Data/OrderDB.cs Orders表的业务逻辑层(OrderDB类)
/Data/OrderData.cs Orders表的DataRow包装器
products表和存储过程也一样

SQL文件已准备好由OSQL工具运行。

配置项目:web.config条目

.cs文件添加到项目并执行.sql文件后,您需要使用生成的webconfig.xml文件更新web.config文件,该文件如下所示

<web.config>
  <!--add this before system.web-->
  <configSections>
   <section name="mynamespace" 
    type="MyNamespace.Config.DataProviderConfigurationHandler, MyNamespace"/>
  </configSections>
  <system.web/>
  <!--add this after system.web-->
  <mynamespace>
    <data defaultProvider="Sql">
      <providers>
        <clear/>
        <add type="Sql" name="MyDatabase" 
          connectionString ="connectionstring" 
          providerPath ="" databaseOwner="dbo"/>
      </providers>
      <dataProviders>
        <clear/>
        <add name="Order" objectQualifier="ex_">
          <providers>
            <provider name="Sql" 
              type="MyNamespace.Data.OrderSqlDataProvider"/>
          </providers>
        </add>
        <add name="Product" objectQualifier="ex_">
          <providers>
            <provider name="Sql" 
               type="MyNamespace.Data.ProductSqlDataProvider"/>
          </providers>
        </add>
        <add name="StoredProc" objectQualifier="">
          <providers>
            <provider name="Sql" 
               type="MyNamespace.Data.StoredProcSqlDataProvider"/>
          </providers>
        </add>
      </dataProviders>
      <procedures/>
    </data>
  </mynamespace>
</web.config>

通常,您只需按照指定将此文件集成到您的web.config中。

数据行包装器类

此类不属于DNN DAL文档。该包装器具有以下优点

  • 通过包装器属性访问字段
    • 避免在检索字段名时出现拼写错误
    • 通过Intellisense简化编程。
  • Dispose时关闭读取器,以避免未关闭的数据库连接

ProductData类定义如下(长代码)

///<summary>A wrapper class for the ex_Products table.</summary>
///<summary>A wrapper class for the ex_Products table.</summary>
public class ProductData : IDisposable
{
    private IDataReader m_dr;
    private int m_ProductID;
    private String m_ProductName;
    private decimal m_ProductPrice;

     ///<summary>Create a data wrapper</summary>
     ///<param name="dr">a opened data reader.</param>
     ///<exception cref="ArgumentNullException">if dr is null</param>
     public ProductData(IDataReader dr)
     {
         if (dr == null)
             throw new ArgumentNullException("datareader");
         m_dr = dr;
     }

     ///<summary>Create a data wrapper</summary>
     ///<param name="dr">a opened data reader.</param>
     ///<exception cref="ArgumentNullException">if dr is null</param>
     public ProductData()
     {
         m_dr = null;
     }

    ///<summary>Reads the row data from the data reader</summary>
    ///<returns>true if data was read, false otherwize</returns>
    ///<exception cref="System.ArgumentNullException">
    ///Thrown if dr is null</exception>
    public bool Read()
    {
        if (m_dr == null)
            throw new Exception("data reader is null");
        if (!m_dr.Read())
        {
            m_dr.Close();
            m_dr = null;
            return false;
        }

        m_ProductID=(int)m_dr["ProductID"];
        m_ProductName=(String)m_dr["ProductName"];
        m_ProductPrice=(decimal)m_dr["ProductPrice"];
        return true;
    }
    ///<summary>Closes the reader, if any</summary>
    public void Close()
    {
        if (m_dr != null)
        {
            m_dr.Close();
            m_dr = null;
        }
    }

    ///<summary>Release and close the reader</summary>
    public void Dispose()
    {
        Close();
    }

    ///<summary>ProductID set/get property</summary>
    public int ProductID
    {
        get{ return m_ProductID;}
        set{ m_ProductID=value;}
    }

    ///<summary>ProductName set/get property</summary>
    public String ProductName
    {
        get{ return m_ProductName;}
        set{ m_ProductName=value;}
    }

    ///<summary>ProductPrice set/get property</summary>
    public decimal ProductPrice
    {
        get{ return m_ProductPrice;}
        set{ m_ProductPrice=value;}
    }

} // ProductData

如您所见,数据检索只执行一次,并对用户隐藏。

使用生成的类操作表

生成的类完全模拟了DNN DAL提案(参见下面的方案)。让我们从一个简单的示例开始,其中操作Products表。

DAL结构,摘自DNN DAL文档。

  • 创建业务逻辑对象:
    // creating the products table business logic
    ProductDB productDB = new ProductDB();
  • 添加新产品。您可以使用包装器或直接将参数传递给AddProduct方法。如果表包含自动递增字段,则将返回此字段或更新包装器。
    // creating a product row wrapper
    ProductData pd = new ProductData();
    // setting fields, each field is a property
    pd.ProductName = "a product name";
    pd.ProductPrice = (decimal)49.99;
    
    // add the product
    productDB.AddProduct(ref pd);

    如您所见,pd作为引用传递给AddProduct方法。实际上,ProductID字段是从数据库中检索(是自动递增字段)并更新到pd中。您也可以通过直接调用来避免使用ProductData

    int pid = productID.AddProduct("name", (decimal)49.99);
  • 按ID检索产品ProductData类在创建时存储一个IDataReader对象,并在释放时关闭它。该包装器有一个Read方法,其行为类似于IDataReader.Read
    using(ProductData pd = productDB.GetProduct(pid))
    {
        // try to reade the Datareader
        if (!pd.Read())
            throw new Exception("...");
        // pd contains the data.
        string name = pd.ProductName;
    }
    // the reader is automatically closed from this loop (exception safe)
  • 更新产品:
    productDB.UpdateProduct( pd );
    productDB.UpdateProduct(pid, name, price); // equivalent
  • 按ID删除产品:
    productDB.DeleteProduct( pd ); 
    productDB.DeleteProduct( pid );

这些是操作表的主要四个生成方法。

使用生成的类执行查询

查询也有自己的业务逻辑对象。在这种情况下,它被称为StoredProcDB,其中方法以存储过程名称命名(记住上面的StoredProcedure.Wrap)。实际上,要调用GetOrders存储过程,我们这样做

  • 使用IDataReader
    StoredProcDC spDB = new StoredProcDB();
    using (GetOrdersData orderData = new GetOrdersData( spDB.GetOrders() ))
    {
        while(orderData.Read())
        {
           // orderData contains the current row data
        }
    }
  • 使用DataSet
    DataSet ds = spDB.GetOrdersDataSet();

至此,生成的代码可见部分的描述结束。让我们看看幕后发生了什么。

幕后

幕后发生的事情与您在DNN DAL描述中发现的非常相似。例如,方法ProductDB.GetProduct的布局如下

public IDataReader GetProduct(int ProductID)
{
    return ProductDataProvider.Instance().GetProduct(ProductID);
}

ProductDataProvider.InstanceProductDataProvider.cs中定义的静态方法:它在缓存中查找数据提供程序构造函数,如果找不到。它从Web.config获取数据提供程序类型并将其插入缓存。

static public ProductDataProvider Instance()
{
    // Use the cache because the reflection used later is expensive
    System.Web.Caching.Cache cache = System.Web.HttpContext.Current.Cache;
    string providerKey = m_ProviderName + m_ProviderType + "provider";
    if ( cache[providerKey] == null)
    {
        // Get the name of the provider
        DataProviderConfiguration providerConfiguration  = 
         DataProviderConfiguration.GetProviderConfiguration(m_ProviderType);
        //  The assembly should be in \bin or GAC,
        // so we simply need to get an instance of the type
        Provider provider = 
            providerConfiguration.Providers
            (providerConfiguration.DefaultProvider);
        DataProvider dataProvider = 
            providerConfiguration.DataProviders("Product");
        String type = dataProvider.Type( provider.Name );
        // Use reflection to store the constructor of
        // the class that implements DataProvider
        Type t = Type.GetType(type, true);

        // Insert the type into the cache
        cache.Insert(providerKey, 
          t.GetConstructor(System.Type.EmptyTypes));
    }
    return (ProductDataProvider) 
      ((ConstructorInfo)cache[providerKey]).Invoke(null);
}

提供者

目前,有两个可用的提供程序:MSSQL和MySQL。但是,这可以扩展到其他数据库系统:OleDB、PostgreSQL、Oracle等...

MSSQL 提供程序

此提供程序使用Microsoft应用程序块,如DNN DAL描述中所示。

MySQL 提供程序

此提供程序使用可在SourceForge站点获取的ByteFX库。由于MySQL不支持存储过程,查询存储在web.config中。例如,让我们为前面的示例添加一个MySQL提供程序

MySqlAdaptor mysql = new MySqlDatabaseAdaptor(
    "MysqlDB",
    "",
    "",
    "connectionstring");
cs.AddAdaptor(mysql);

以下元素添加到web.config

<web.config>
  ...
  <mynamespace>
    <data defaultProvider="Sql">
      <providers>
        <clear/>
        <add name="Sql" catalog="MyDatabase" 
           connectionString ="connectionstring" 
           providerPath ="" databaseOwner="dbo"/>
        <add name="MySql" catalog="MysqlDB" 
              connectionString ="connectionstring" 
              providerPath ="" databaseOwner=""/>
      </providers>
      ...
      <procedures>
        <clear/>
        <add name="GetOrders">
          <versions>
            <version provider="MySql">
              <![CDATA[SELECT 
U.UserID AS 'UserID',
P.ProductName AS 'ProductName',
O.ProductPrice AS 'Price',
O.OrderQuantity AS 'Quantity'
FROM (ex_Orders AS O 
INNER JOIN Users AS U
ON U.UserID = O.UserID)
INNER JOIN ex_Products AS P
ON P.ProductID = O.ProductID
;]]>
            </version>
          </versions>
        </add>
      </procedures>
    </data>
  </mynamespace>

想贡献吗?

SQLppNet是一个开源项目,始终需要新人新思想来发展。如果您有兴趣,请在SourceForge页面留言。

参考文献

© . All rights reserved.