简化的 .NET 数据访问 API
一个通过 ADO.NET 简化数据库访问的 API。
引言
早在 .NET 出现之前,当 ADO 还是一种 ActiveX API 时,连接字符串就一直是很多开发者的困惑点。这主要源于连接字符串是一种结构化信息,具有特定的格式,开发者一直以来都被期望能够记住它,或者不得不查找文档。对我而言,添加他们一直需要的结构是有意义的。
同时,对于大多数目的,我发现 ADO.NET API 比必需的要稍微细粒度一些。这促使我研究创建一个能够简化 API 大部分用法的结构。我见过许多人粗心地使用 API,不注意正确释放连接和/或读取器,或者未能将数据相关的功能正确地封装在中心位置,将读取器从数据访问类传递给业务类等等。我发现用一个更简单的结构来简化 ADO.NET 的使用,有助于促进更好的封装和更优雅的数据使用设计。
我为这些问题提供的解决方案是使用连接管理器类和连接字符串结构的数据访问结构。这个 API 只使用 .NET 框架提供的类,使用 OLEDB 访问非微软数据库,如 MySQL 和 Oracle。之所以做出这个决定,是为了避免用户可能拥有或未拥有的不同版本第三方提供程序的查找和支持问题,从而使其能够被任何安装了框架的开发者使用。通过使用这个 API,我开始相信这是微软出于未知原因决定不提供的最终数据抽象。
IConnectionManager
IConnectionManager
是我们对连接管理器的规范。它列出了所有连接管理器应支持的方法。它提供了一个中心位置来管理与数据库连接相关的所有方面和类型。它还通过扩展 IDisposable
接口,提供了一个中心位置进行资源清理。
AbstractConnectionManager : IConnectionManager
AbstractConnectionManager
是此 API 中所有连接管理器实现的基类。它实现了所有具体实现所需的通用功能。
内部
AbstractConnectionManager
封装了一个 IDbString
、IDbConnection
、IDbTransaction
、IDataReader
和一个 IDbCommand
;它将它们公开为受保护的属性,以便此类实现可以访问它们。这些是管理器类通过一个简单接口进行管理的对象的。管理器只需要你提供一个连接字符串;利用它,可以建立连接并从连接中构建事务和命令。
ResetCommand()
当重用管理器实例时,有必要使用 ResetCommand
方法来设置一个新的命令对象,特别是如果之前使用了带有参数的存储过程。在开始新命令之前,始终使用此方法是一个好习惯。
参数
管理器类提供了一些简单的方法来将参数附加到查询中,用于存储过程和参数化查询。为此提供了 AddParameter(string name, object value)
和 AddOutputParameter(string name, object value)
方法。本文底部的示例代码提供了如何使用这些方法的示例。
事务
默认情况下,连接管理器不使用事务。但当需要时,您所要做的就是调用 BeginTransaction()
方法,并根据需要调用 Commit()
或 Rollback()
。
BeginTransaction()
此方法指示管理器从其内部连接创建事务。
Commit() & Rollback()
正如预期的那样,这些方法完成了您的事务。
抽象
AbstractConnectionManager
有四个抽象方法,所有具体继承类都必须实现。其中大多数方法特定于使用的数据提供程序,因此无法由此类实现。
GetParameter(name, value)
此抽象方法也是 protected
和 internal
,因为它在 AbstractConnectionManager.AddParameter()
方法内部使用。它返回一个 IDbDataParameter
实例,该实例基于提供的值,特定于实现的提供程序。
GetAdapter(sql, IDbConneciton | CommandType)
此方法有两个 protected
重载,它们都接受一个 SQL 字符串。一个还需要 IDbConnection
,另一个需要 CommandType
。此方法在内部用于填充 DataSet
。它返回一个特定于所用提供程序的 IDataAdapter
实例。
FillSchema(table)
此方法用于以 DataTable
的形式检索架构信息。
Clone()
此方法基本上会创建一个当前实现的新实例,并复制其内部连接字符串。
其他方法
此抽象类的其余方法通过使用上述抽象方法来通用地执行各种功能。
ExecuteNonQuery(sql, [type])
ExecuteNonQuery()
有两个重载,一个接受 SQL 字符串和 CommandType
,另一个仅需要 SQL 字符串并假定为 CommandType.Text
。它的行为类似于 IDbConnection.ExecuteNonQuery()
,返回一个整数,表示受影响的行数。
ExecuteReader(sql, [type])
ExecuteReader()
有两个重载,一个接受 SQL 字符串和 CommandType
,另一个仅需要 SQL 字符串并假定为 CommandType.Text
。它的行为类似于 IDbConnection.ExecuteReader()
,返回一个 IDataReader
实例。
Fill(sql, [type])
与 ExecuteNonQuery()
和 ExecuteReader()
方法一样,Fill()
有一个 SQL 字符串重载,以及一个 SQL 字符串和 CommandType
重载。并且,正如预期的那样,它返回一个 DataSet
实例。与 IDbConnection
不同,它不接受 DataSet
实例进行填充,而是在调用时创建一个新的实例。
Close() & Dispose()
Close()
方法会关闭所有内部对象。IConnectionManager
接口扩展了 IDisposable
接口,因此需要一个 Dispose()
方法,该方法在功能上与 Close()
方法类似。因此,C# 用户可以在使用连接管理器时使用 using (...) {...}
结构,并确信所有内部资源都会得到清理。
实现
此 API 为 SQL Server、Microsoft Access、OLEDB、ODBC 和 MySQL 提供了实现。SQL Server、OLEDB 和 ODBC 实现利用了它们各自的框架提供程序。Access 和 MySQL 实现扩展了 OLEDB 实现。
AbstractConnectionManager 的直接后代
SqlConnectionManager
、OdbcConnectionManager
和 OleDbConnectionManager
都提供了特定于其提供程序的基本功能。目前,SqlConnectionManager
是唯一实现 FillSchema()
的。我很少使用其他提供程序,并且没有时间或需要找到从这些提供程序中提取架构信息的解决方案。在未来的更新中,我希望解决这个问题。
初始化
所有实现都有一个接受 int
超时参数的重载构造函数,该参数表示连接超时(秒)。我曾遇到过此参数似乎未按预期工作的情况,尽管我从未发现过原因。在大多数情况下,我认为它是有效的。
AccessConnectionManager : OleDbConnectionManager
Access 实现提供了 Access 特定的初始化,并重写了 Clone()
。
MySqlConnectionManager : OleDbConnectionManager
MySQL 实现提供了 MySQL 特定的初始化,并重写了 FillSchema()
和 Clone()
。目前,FillSchema()
实际上并未实现。我一直打算重新审视此方法并利用 MySQL 专有的架构查询来提供此功能。这些专有架构查询一直让我很感兴趣,我一直希望微软能提供类似的东西。尽管如此,他们可能出于安全考虑而未提供。
连接字符串
此 API 提供了 IDbString
接口的几个不同实现,该接口只需要一个 ToString()
方法。这些都实现为结构,并保存了连接字符串所需的字段。它们都可以隐式转换为 string
,从而基于存在的字段生成连接字符串。这些输出连接字符串的方式是基本的;它们可能无法满足所有可能的需求,但可以满足我遇到的大多数需求。然而,我很少使用 MySQL,并且像躲避瘟疫一样躲避 Oracle,因此这些实现的可测试性和可用性验证非常少。它们都是简单的实现,如果需要,应该很容易更改。
ConnectionStringUtility
ConnectionStringUtility
类提供了三个静态方法
public static IConnectionManager GetManager(IDbString dbs)
根据提供的IDbString
类型创建一个IConnectionManager
实例。public static IConnectionManager GetManager(string dbs)
根据解析的IDbString
类型创建一个IConnectionManager
实例。public static IDbString ParseConnectionString(string dbs)
尝试解析连接字符串并返回一个对应的IDbString
。
AccessDbString
AccessDbString
有三个属性:FileName
、User
和 Password
。它的构造函数有三个重载,具体取决于您的需求;大多数情况下,您可能只会使用最简单的,它只需要 MDB 文件的文件名。它代表一个 OLEDB 连接字符串。它将输出一个类似于以下内容的连接字符串
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=\somepath\mydb.mdb;User Id=admin;Password=;
MsSqlDbString
Microsoft SQL Server 连接字符串有六个属性:Host
、Database
、UserId
、Password
、Trusted
和 Timeout
。Trusted
在未提供密码时使用。Timeout
是连接超时(秒)。它将输出一个类似于以下内容的连接字符串
Server=Aron1;Database=pubs;User ID=sa;Password=asdasd;Trusted_Connection=False;
MySqlDbString
MySqlDbString
有四个属性:Host
、Database
、UserId
和 Password
。它将输出一个类似于以下内容的连接字符串
Provider=MySQLProv;Data Source=mydb;User Id=UserName;Password=asdasd;
OdbcDbString
OdbcDbString
有五个属性:Driver
、Host
、Database
、UserId
和 Password
。Driver
是通常会出现在 ODBC 连接字符串中的驱动程序名称。它将输出一个类似于以下内容的连接字符串
Driver={SQL Native Client};Server=Aron1;Database=pubs;UID=sa;PWD=asdasd;
OleDbString
OleDbString
有五个属性:Provider
、Host
、Database
、UserId
和 Password
。Provider
是您通常会提供给 OleDB 连接字符串的提供程序名称。它将输出一个类似于以下内容的连接字符串
Provider=sqloledb;Data Source=Aron1;Initial Catalog=pubs;User Id=sa;Password=asdasd;
OraDbString
Oracle 连接字符串有三个属性:Host
、UserId
和 Password
。它将输出一个类似于以下内容的连接字符串
Provider=OraOLEDB.Oracle;Data Source=MyOracleDB;User Id=Username;Password=asdasd;
配置 XML 序列化
此 API 已发展为包含一个配置文件结构,该结构允许定义多个连接并在不同环境之间轻松交换。所有与配置相关的类都位于 BoneSoft.Data.Config
命名空间中。我用来处理数据连接配置文件的配置主要有两个部分:“Connections
”列表,其中定义了连接详细信息,以及“NamedConnections
”部分,其中连接信息与名称关联,以便通过代码轻松访问。这样,您可以定义任意数量的连接集,并且通过仅更改相应“DbConnection
”节点中的“ConnectionName
”来更改代码中引用的连接。下面讨论的类结构显示了应用程序如何使用这些类来检索连接信息。
首先,让我们看一下用户代码将用于访问配置信息的类...
SqlConnectionList
SqlConnectionList
类负责加载数据配置文件并根据连接名称从配置中检索连接信息。它使用配置文件的路径进行初始化,并有两个用于使用配置信息的方法:IDbString GetConnectionString(string name)
可用于根据 DbConnectionList/NamedConnections/DbConnection/@Name
(XPath 格式)中的值获取连接字符串实例,IConnectionManager GetConnection(string name)
可用于获取基于同一 DbConnection
节点 XML 属性值的连接实例。SqlConnectionList
还通过 DbConnectionList
类型的 Connections
属性提供对根配置对象的访问。
配置类以 XML 结构为模型,该结构在关于序列化的下一节中显示。此类模型使用 Skeleton Crew 2.0 生成,Skeleton Crew 2.0 是一个代码生成工具,部分设计用于替换 XSD.exe,用于从 XML 结构生成 XML 可序列化代码。
DbConnectionList 和 DbConnectionListSerializer
SqlConnectionList
是配置加载类,但配置本身由 DbConnectionList
对象表示。DbConnectionList
是序列化到配置文件根节点的类。从下面的类图可以看出,此类有两个属性,一个类型为 NamedConnections
,一个类型为 Connections
。如前所述,数据配置的类模型反映了 XML 结构。
以下示例 XML 配置定义了两个命名连接:“ErrorLogging
”(引用“ErrorLog
”连接定义)和“StatsLogging
”(引用名为“StatsLog
”的连接信息)。您可以为不同的数据库(开发、暂存和生产)拥有两个或三个日志记录统计信息的定义,然后只需更改 DbConnection
的“ConnectionName
”属性以匹配正确的 ConnectionInfo
名称,即可在请求“StatsLogging
”时更改环境。Connections
节点只是连接信息的存储区域。SqlConnectionList
在按名称请求连接时会查找 NamedConnections
节点。“DbConnection
”上的“ConnectionName
”属性告诉它从何处获取实际的连接详细信息。
<DbConnectionList>
<!--
Code will ask for connections by "Name" from this list,
each entries "ConnectionName" determines which definition
to use from the "Connections" list below.
-->
<NamedConnections>
<DbConnection Name="ErrorLogging" ConnectionName="ErrorLog" />
<DbConnection Name="StatsLogging" ConnectionName="StatsLog" />
</NamedConnections>
<!--
You can specify as many sets of connection information as you want here.
The only ones that will be loaded are those named above.
-->
<Connections>
<ConnectionInfo Type="SQL" Name="StatsLog">
<Host>StatsSQL</Host>
<Database>Logs</Database>
<UserId Trusted="false">uid</UserId>
<Password>pwd</Password>
</ConnectionInfo>
<ConnectionInfo Type="SQL" Name="ErrorLog">
<Host>ErrorSQL</Host>
<Database>Logs</Database>
<UserId Trusted="true">uid</UserId>
</ConnectionInfo>
</Connections>
</DbConnectionList>
使用上面的配置示例,要获取日志记录错误的连接信息,您将执行类似于以下示例的操作
using System;
using System.Data;
using BoneSoft.Data;
using BoneSoft.Data.Config;
namespace Example {
class ConfigExample {
public void Test() {
string configPath = System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly()
.CodeBase.Replace("file:///", "")) +
@"\data.config";
// initialize a SqlConnectionList with a config path..
SqlConnectionList conns = new SqlConnectionList(configPath);
// use the SqlConnectionList to get the connection information
// with the name "ErrorLogging"
using (IConnectionManager icm = new SqlConnectionManager(
(MsSqlDbString)conns.GetConnectionString("ErrorLogging"))) {
icm.ResetCommand();
IDataReader idr = icm.ExecuteReader("select * from Errors");
while (idr.Read()) {
// process record...
}
idr.Close();
}
}
}
}
使用此 API,只需几行代码即可轻松从配置中获取连接信息并查询数据库。
示例应用程序:配置编辑器
本文的示例项目是一个用于创建和编辑数据配置文件的小型应用程序。由于 NamedConnection
s 引用 Connections
,因此您必须先创建一个 NamedConnection
。只需右键单击任一列表视图即可访问其上下文菜单。文件菜单允许您打开或保存配置文件。
示例
好的,再举一个使用该 API 的简单示例。下面的示例使用存储过程来生成一个 IDataReader
进行浏览。它演示了如何初始化连接字符串对象、设置连接管理器、使用它设置一些参数,以及执行和处理数据读取器。
using System;
using System.Data;
using BoneSoft.Data;
namespace Example {
class DataExample {
public int ExecuteProc(string val1, int val2) {
int answer = 0;
// Setup connection string
MsSqlDbString dbs = new MsSqlDbString("host",
"database", "user", "password");
//Console.WriteLine(dbs.ToString());
// Setup a connection manager taking advantage
// of the fact that it is IDisposable
using (IConnectionManager icm = new SqlConnectionManager(dbs)) {
// If it's being reused, it's important to reset the command
icm.ResetCommand();
// Add parameters for our stored procedure
icm.AddParameter("@param1", val1);
icm.AddParameter("@param2", val2);
// With a scalar stored proc, we could do this...
/*
icm.AddOutputParameter("@outparam", answer);
icm.ExecuteNonQuery("StoredProc", CommandType.StoredProcedure);
*/
// or we could use ExecuteReader...
IDataReader idr = icm.ExecuteReader("StoredProc",
CommandType.StoredProcedure);
if (idr.Read()) {
answer = Convert.ToInt32(idr[0]);
}
// Always close your readers explicitly
idr.Close();
}
return answer;
}
}
}
结论
所有这些信息似乎很多,但它实际上是一个相当简单易用的 API。正如我在介绍中所述,我认为这个 API 大大地简化了基本数据访问,并且还促进了更好的编码实践,并有助于分离数据访问逻辑。我希望您发现它很有用,或者至少从中找到一些创建自己的数据访问组件的好主意。