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

LWDbComponents - 另一个数据库组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.13/5 (4投票s)

2006年8月6日

8分钟阅读

viewsIcon

44929

downloadIcon

130

一个功能齐全且易于使用的数据库组件。

引言

在本文中,我将尝试解释如何使用我多年来一直在创建和扩展的数据库组件。我将尝试解释,当然,为什么我认为它们有用且更好,至少对我的需求而言,比其他现有的数据库组件更好……甚至比 MS Enterprise Library 2.0 更好。当然,只谈论数据库访问。Enterprise Library 将许多良好的功能和实践集中在一个地方,而本库未涵盖。

实际上,您会注意到,数据库提供程序概念在某种程度上与您在 Enterprise Library 中找到的概念相同。

这里是本库涵盖的功能的简要摘要。在接下来的章节中,我将详细解释每一个功能。

  • 数据库引擎独立性:您可以使用相同的源代码,无需任何更改,即可针对任何数据库引擎运行查询。
  • 连接自动关闭:您不再需要担心未关闭的数据库连接。
  • 上下文内和嵌套事务:您可以在不指定 DB 命令执行之前的情况下打开嵌套事务或在已打开的事务中执行查询。此功能与 Enterprise Services 中实现的功能相同。
  • DBCommandAdapters:您可以定义自己的 DBCommand 类,以控制查询执行过程。
  • 运行时可配置:所有这些都在配置文件中定义,因此您无需重新编译应用程序即可更改所有内容。

数据库引擎独立性

这些组件要强调的第一个功能是它们独立于您需要使用的数据库引擎。

当然,这对于任何好的数据库组件都是必不可少的。并且您也可以在 Enterprise Library 中找到这一点。唯一的区别在于解决此问题的方法。本库提供两个提供程序,一个用于 SQL Server,另一个用于 Oracle,但您可以定义任何其他所需的提供程序,而无需更改您的应用程序代码。

为此,您必须定义一个实现 IDbProvider 的类,该类由以下两个方法定义:

  • GetDBConnection:此方法返回与数据库引擎关联的 IDbConnection 的新实例。在提供的两个提供程序中,会创建 SqlClient.SqlConnectionOracleClient.OracleConnection 实例。这将是创建 IDbCommandIDbTransactions 实例的基础。
  • GetParameterCorrector:此方法返回一个 IDbParameterCorrector,它提供从一个数据库引擎规范到另一个数据库引擎规范的查询和参数的转换。通过这种方法,您可以将 SQL Server 查询语法(参数名称前的“@”符号)转换为(例如)Oracle 语法。这是与 Enterprise Library 方法的一个区别。 

数据库提供程序必须在应用程序配置文件中声明。这是使用两个包含的提供程序所需的关联配置部分。

<providers> 
  <add name="sql" 
    type="LWComponents.Datalayer.Providers.SqlProvider, 
          LWComponents.Datalayer"/>
  <add name="oracle"
    type="LWComponents.Datalayer.Providers.OracleProvider, 
          LWComponents.Datalayer"/>
</providers> 

IDbParameterCorrector

IDbProvider 可以定义一个 IDbParameterCorrector。其功能是将应用程序查询从已知的基本语法转换为特定数据库引擎所需的语法。为两个包含的提供程序选择的源基本语法是 SQL Server 语法(这就是为什么 SQL Server 提供程序不需要更正器的原因),但您可以在您的提供程序中使用任何您想要的语法。 

此图说明了这种“更正”如何工作。

IDbParameterCorrector diagram

然后,使用提供的 OracleParameterCorrector 代码

Public Function CorrectParameters(ByVal command As DbCommand) _ 
         As ParametersCorrection _ 
         Implements IDbParameterCorrector.CorrectParameters 
  Dim sql As String = command.CommandText 
  Dim res As New ParametersCorrection(command) 
  For Each par As IDbDataParameter In command.Parameters 
    Dim parName As String = par.ParameterName  
    'This will replace @ParamName by :ParamName in the query 
    sql = sql.Replace(parName, GetQueryParameterName(parName)) 
    'And the IDbDataParameter name is changed form @ParamName to ParamName
    res.AddParameterCorrection(par, GetParameterName(parName)) 
  Next 
  res.NewSql = sql 
  Return res 
End Function

我们的应用程序中可以有这样的代码

Dim cmd As DBCommand = DataBaseFacade.CreateQueryCommand("oracleTest", _ 
             "SELECT Count(*) FROM Customers WHERE Country = @Country") 
cmd.AddParameter("@Country", "Spain"Dim res As String = cmd.ExecuteScalar().ToString

运行时它将被转换为以下代码(当然,仅在运行 Oracle 数据库时)

query: "SELECT Count(*) FROM Customers WHERE Country = :Country" 
cmd.AddParameter("Country", "Spain") 

因此,您只需要为首选的数据库引擎创建提供程序,即可获得真正独立的应用程序。

注意:当然,专有 SQL 函数不会被翻译。这是使用存储过程而不是内联查询的另一个原因。

连接自动关闭

在任何应用程序中,一个常见的问题源于一个在设计时非常难以检测的问题:未关闭的连接。 

有了这个库,这个问题将结束……嗯,“几乎结束”会更准确。

您无需关心 DbConnection 的打开和关闭过程。您只需要获取一个 DBCommand 并执行它。DBCommand 类负责在需要时打开连接,并在完成后关闭它。此规则只有两个例外:

  1. ExecuteReader:当生成 DataReader 时,它始终使用 CommandBehavior.CloseConnection 创建。因此,当 IDataReader 关闭时,连接也会关闭。
  2. Transaction 中运行时:当在 BeginTransaction-Commit/Rollback 块内创建 DBCommand 时,连接将保持打开状态,直到执行 Commit/Rollback 方法。 

所以,这是执行数据库查询的常见模式。

Private Sub NoCloseNeededPatterns() 
    Dim cmd As DbCommand = DataBaseFacade.CreateCommand("somequery...", _
                                                        CommandType.Text) 
    cmd.ExecuteNonQuery() 
    ' or ... 
    cmd.ExecuteScalar() 
    ' or ... 
    cmd.ExecuteDataSet()
End Sub 

Private Sub DataReaderPattern() 
    Dim cmd As DbCommand = DataBaseFacade.CreateCommand( _
                                              "SELECT * FROM Customers", _
                                              CommandType.Text) 
    Using rs As IDataReader = cmd.ExecuteReader() 
      Do While rs.Read 
        'Here we'll do something whit the records 
      Loop 
    End Using 
End Sub 

Private Sub TransactionPattern() 
     Using t As IDbTransaction = DataBaseFacade.BeginTransaction() 
       Dim cmd As DbCommand = DataBaseFacade.CreateCommand("somequery...", _
                                                           CommandType.Text) 
       '... 
       t.Commit() 
     End Using
End Sub  
注意:在 TransactionPattern 示例中,您会注意到 Rollback 没有被直接调用。实际上,如果未调用 Commit 方法(例如,在发生异常时),Rollback 将自动执行。

上下文内和嵌套事务

标准设计规则规定,一个方法或类不应了解其调用者在做什么。相反,它应该专注于自己的任务。 

当谈论数据库事务时,这个常识性的规则就不适用了。如果我们的数据层中有一个方法打开一个数据库命令并执行一个简单的查询,我们必须知道我们的调用者是否已打开事务,因为数据库命令必须使用该事务才能工作,或者我们可以打开一个新的数据库连接来执行此查询,但那样我们可能会读取未更新的数据,甚至更糟的是,创建死锁错误。

注意:在 ADO.NET 2.0 中,Microsoft 引入了 TransactionScope 概念来解决这些问题(System.Transactions.TransactionScope)。实际上,这个新命名空间实现了分布式事务,并且在 Windows Communication Foundation(.NET 3.0)上下文中完美工作。即便如此,我仍然保留此功能以实现向后兼容。此外,它提供了一个更简单(更轻量级)的实现,足以满足大多数应用程序的需求。无论如何,TransactionScope 与本库的其余功能完全兼容,因此您可以自行决定使用哪种方法。用于解释上下文内事务的概念和图表也适用于 TransactionScope

这就是 LWDbComponents 提供的上下文内事务所解决的问题。

有了这个 LWDbComponents,您只需要关注创建事务的方法。在事务使用 Commit/Rollback 关闭之前,后续对 DataBaseFacade.CreateCommand 的调用将知道存在一个活动的事务,并相应地创建 DBCommand

此外,事务还有另一个问题,那就是嵌套事务。在这种情况下,您有一个方法需要事务,并且该方法调用了其他方法,其中一个方法又需要另一个事务。某些数据库引擎不支持这一点(例如 SqlClient)。

LWDbComponents 支持在已有一个 RealTransaction 处于活动状态时创建 NestedTransaction。使用此样本图可以轻松理解这一点。

In-Context transactions diagrams

这可以转换为代码,类似于这样(为了缩短代码,不创建方法)。

  'There is no active transaction, so a RealTransaction is created 
  Using t1 As IDbTransaction = DataBaseFacade.BeginTransaction() 
    'This DBCommand is created using the active transaction T1 
    Dim cmd As DbCommand = DataBaseFacade.CreateCommand("somequery...", _
                                                        CommandType.Text) 
    ' Do something.... 
    'Now, there is an active transaction, 
    'so a NestedTransaction is created 
    Using t2 As IDbTransaction = DataBaseFacade.BeginTransaction() 
      'This DBCommand is created using the active transaction T2, that relys on T1. 
      Dim cmd2 As DbCommand = DataBaseFacade.CreateCommand("somequery...", _
                                                           CommandType.Text) 
      ' Do something.... 
      t2.Commit() 
    End Using 
    'This DBCommand is created using the active 
    'transaction T1, because T2 has already been closed
    Dim cmd3 As DbCommand = DataBaseFacade.CreateCommand("somequery...", _
                                                         CommandType.Text) 
    ' Do something.... 
    t1.Commit() 
  End Using

活动事务计数器

在一个非常复杂的应用程序中,某个开发人员忘记提交/回滚事务很容易导致注册锁定和糟糕的数据库性能。问题是这些问题很可能只会在生产环境中出现。

为了在尽快发现此类问题并在此开发阶段提供帮助,DataBaseFacade 类中包含一些属性(ActiveTransactionsTotalActiveTransactions),它们将返回活动事务的数量。

  Using t1 As IDbTransaction = DataBaseFacade.BeginTransaction()
    Using t2 As IDbTransaction = DataBaseFacade.BeginTransaction()
      Using t3 As IDbTransaction = _
               DataBaseFacade.BeginTransaction("oracleTest")
        'This should return 2: 1 Real SQL Server + 1 Real Oracle 
        Debug.WriteLine("Total transactions:" & _
                        DataBaseFacade.TotalActiveTransactions())
        'This one would return 3, because of the nested t2 transaction
        Debug.WriteLine("Total transactions + nested:" & _
                        DataBaseFacade.TotalActiveTransactions(True))
        'This should return 1
        Debug.WriteLine("Oracle transactions:" & _
                        DataBaseFacade.ActiveTransactions("oracleTest"))
        'This should return 1
        Debug.WriteLine("SQLServer transactions:" & _
              DataBaseFacade.ActiveTransactions())
        'This should return 2, because of the nested t2 transaction 
        Debug.WriteLine("SQLServer transactions + Nested:" & _
                        DataBaseFacade.ActiveTransactions(True))
      End Using
    End Using
  End Using
注意:在 ASP.NET 应用程序中,在 HttpApplication.EndRequest 事件处理程序中使用这些计数器是一个好主意,以确保每个请求后没有未关闭的事务。

DBCommandAdapter

当您在 DataBaseFacade 类中执行 CreateCommand 时,您将收到一个 LWComponents.Datalayer.DbCommand 类的实例。此类实现了 IDbCommand 接口,并将提供标准的 Execute (NonQuery/Scalar/Reader) 方法来运行您的查询。尽管此类拥有标准应用程序所需的一切,但您可能需要做其他事情。例如,监视或记录您运行的每个数据库查询。

对于这些情况,您可以通过定义 DBCommandAdapter 来扩展 DBCommand 的功能。此类必须继承自 DBCommand 并实现其默认构造函数。这可能是实现此日志记录要求的 DBCommandAdapter 的源代码。

Public Class DBCommandAdapterExample 
      Inherits LWComponents.Datalayer.DbCommand 
  Public Sub New(ByVal con As IDbConnection, _ 
                 ByVal paramCorrector As _
                         LWComponents.Datalayer.IDbParameterCorrector) 
    MyBase.New(con, paramCorrector) 
  End Sub 
  Public Overrides Sub OnBeforeExecute(ByVal ev As _
                       LWComponents.Datalayer.DBCommandEventArgs) 
    MyBase.OnBeforeExecute(ev) 
    'TODO: Log queries 
  End Sub 
End Class

以及定义它的所需配置部分。

<commandAdapters> 
  <add name="standard" 
       type="LWComponents.Datalayer.DbCommand, LWComponents.Datalayer"/>
  <add name="MyDBAdapter" 
       type="DBComponentsTest.DBCommandAdapterExample, DBComponentsTest"/>
</commandAdapters> 

配置

所有这些行为都由应用程序配置文件控制。在这里,我们可以看到包含的示例配置文件。

  <lwDB> 
    <providers> 
      <add name="sql" 
          type="LWComponents.Datalayer.Providers.SqlProvider, 
                LWComponents.Datalayer"/> 
      <add name="oracle" 
          type="LWComponents.Datalayer.Providers.OracleProvider, 
                LWComponents.Datalayer"/> 
    </providers> 
    <commandAdapters> 
      <add name="standard" 
          type="LWComponents.Datalayer.DbCommand, 
                LWComponents.Datalayer"/> 
      <add name="MyDBAdapter" 
          type="DBComponentsTest.DBCommandAdapterExample, 
                DBComponentsTest"/> 
    </commandAdapters> 
    <connections default="sqlTest"> 
      <add name="sqlTest" provider = "sql" 
           shareTransactionByConnectionString="true" 
           commandAdapter = "standard" 
           connectionString = "conStringSqlServer"/> 
      <add name="oracleTest" provider = "oracle" 
           shareTransactionByConnectionString="false" 
           commandAdapter = "MyDBAdapter" 
           connectionString = "conStringOracle"/> 
    </connections> 
  </lwDB> 

  <connectionStrings> 
    <add name="conStringSqlServer" 
         connectionString="... a SQL Server connection string..."/> 
    <add name="conStringOracle" 
         connectionString="...an Oracle connection string..."/> 
  </connectionStrings>
  • lwDB/providers:定义的数据库提供程序。每个提供程序都必须实现 IDbProvider
  • lwDB/commandAdapters:要使用的 DBCommand 适配器。此部分是可选的。每个适配器都必须继承自 DBCommand 并实现一个具有签名 (ByVal con As IDbConnection, ByVal paramCorrector As IDbParameterCorrector) 的构造函数。
  • lwDB/connections:数据库连接定义。我们在调用 DatabaseFacade 提供程序时使用其名称。如果未指定名称,则使用 "default" 属性中设置的名称。 
  • connectionStrings:这是标准的 .NET 连接字符串部分。
注意:包含的配置文件中解释了每个部分和属性。

结论

我在我所有的项目中都发现这个库非常有用,但它远非完美。所以,如果您喜欢它,并发现有什么可以做得更好,一些可能很有用的新功能,或者仅仅是您觉得它有用并且正在您的项目中使用它……我很想听听。

更新

  • 2006年11月12日:一些代码更新和调整。
    • 更新了事务类代码(RealTransactionNestedTransaction)以支持“using”模式。
    • 更新了代码示例以使用“Using”语句。
    • 添加了一些函数来检查活动事务的数量。非常有助于检测未关闭的事务。
    • 添加了一些图表以更好地解释某些模式。
    • 其他一些小的代码修复和调整。
© . All rights reserved.