LWDbComponents - 另一个数据库组件






4.13/5 (4投票s)
2006年8月6日
8分钟阅读

44929

130
一个功能齐全且易于使用的数据库组件。
引言
在本文中,我将尝试解释如何使用我多年来一直在创建和扩展的数据库组件。我将尝试解释,当然,为什么我认为它们有用且更好,至少对我的需求而言,比其他现有的数据库组件更好……甚至比 MS Enterprise Library 2.0 更好。当然,只谈论数据库访问。Enterprise Library 将许多良好的功能和实践集中在一个地方,而本库未涵盖。
实际上,您会注意到,数据库提供程序概念在某种程度上与您在 Enterprise Library 中找到的概念相同。
这里是本库涵盖的功能的简要摘要。在接下来的章节中,我将详细解释每一个功能。
- 数据库引擎独立性:您可以使用相同的源代码,无需任何更改,即可针对任何数据库引擎运行查询。
- 连接自动关闭:您不再需要担心未关闭的数据库连接。
- 上下文内和嵌套事务:您可以在不指定 DB 命令执行之前的情况下打开嵌套事务或在已打开的事务中执行查询。此功能与 Enterprise Services 中实现的功能相同。
- DBCommandAdapters:您可以定义自己的
DBCommand
类,以控制查询执行过程。 - 运行时可配置:所有这些都在配置文件中定义,因此您无需重新编译应用程序即可更改所有内容。
数据库引擎独立性
这些组件要强调的第一个功能是它们独立于您需要使用的数据库引擎。
当然,这对于任何好的数据库组件都是必不可少的。并且您也可以在 Enterprise Library 中找到这一点。唯一的区别在于解决此问题的方法。本库提供两个提供程序,一个用于 SQL Server,另一个用于 Oracle,但您可以定义任何其他所需的提供程序,而无需更改您的应用程序代码。
为此,您必须定义一个实现 IDbProvider
的类,该类由以下两个方法定义:
GetDBConnection
:此方法返回与数据库引擎关联的IDbConnection
的新实例。在提供的两个提供程序中,会创建SqlClient.SqlConnection
和OracleClient.OracleConnection
实例。这将是创建IDbCommand
和IDbTransactions
实例的基础。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 提供程序不需要更正器的原因),但您可以在您的提供程序中使用任何您想要的语法。
此图说明了这种“更正”如何工作。
然后,使用提供的 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
类负责在需要时打开连接,并在完成后关闭它。此规则只有两个例外:
ExecuteReader
:当生成DataReader
时,它始终使用CommandBehavior.CloseConnection
创建。因此,当IDataReader
关闭时,连接也会关闭。- 在
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
。使用此样本图可以轻松理解这一点。
这可以转换为代码,类似于这样(为了缩短代码,不创建方法)。
'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
类中包含一些属性(ActiveTransactions
和 TotalActiveTransactions
),它们将返回活动事务的数量。
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日:一些代码更新和调整。
- 更新了事务类代码(
RealTransaction
和NestedTransaction
)以支持“using”模式。 - 更新了代码示例以使用“
Using
”语句。 - 添加了一些函数来检查活动事务的数量。非常有助于检测未关闭的事务。
- 添加了一些图表以更好地解释某些模式。
- 其他一些小的代码修复和调整。
- 更新了事务类代码(