数据库连接器






4.78/5 (9投票s)
一个提供非常简单的数据库访问的插件系统。
引言
这不是第一篇关于数据库访问的文章。甚至不是我写的第一篇。但是,我确实希望它能填补其他文章中没有涉及的空白。
背景
我正在开发的一个小型实用程序(稍后发布)需要访问数据库。它只需要连接、执行一个查询然后断开连接。这当然是一个简单的任务,有很多解决方案。但是,我不想硬编码或以其他方式限制该实用程序可使用的数据库选择。我也不想要求用户重新编译该实用程序以添加对新数据库的支持。显然,插件是解决此类问题的答案。
我的其他数据库访问库都不能作为插件工作,所以我不得不创建一套新的类。本文将介绍这些类以及它们如何交互。
IExecuteSql.cs
插件通常使用接口。每个插件类都实现该接口,而消费者类无需了解插件的其他信息。
嗯,这几乎是真的;在 .NET 中实现时,接口不能指定构造函数,所以仅仅知道接口无助于实例化插件。这是一个相当大的限制,我不知道有什么合适的变通方法。
稍后我将更详细地讨论实例化。一旦插件被实例化,我的插件唯一需要的方法是 ExecuteSql
,但为了方便,我还添加了 CommandTimeout
和 IsolationLevel
。IExecuteSql
接口的定义是
public interface IExecuteSql : System.IDisposable
{
System.Collections.Generic.List<System.Data.DataTable>
ExecuteSql
(
string Statements
,
params object[] Parameters
) ;
int CommandTimeout { get ; set ; }
System.Data.IsolationLevel IsolationLevel { get ; set ; }
}
我选择要求 IDisposable
,因为数据库连接实现了它。
请注意,该方法返回一个 DataTable 列表,这是因为 Statements
实际上可能是一个由分号分隔的 SQL 语句列表。
GenericDatabaseConnector.cs
为了追求一个非常简单的类系统,我最终选择使用一个泛型类来提供基本功能
public class GenericDatabaseConnector
<
ConnectionType
> : PIEBALD.Data.IExecuteSql
where ConnectionType : System.Data.IDbConnection , new()
{
private readonly ConnectionType connection ;
private string parameterprefix = "" ;
private int timeout = -1 ;
private System.Data.IsolationLevel isolation =
System.Data.IsolationLevel.Unspecified ;
<<Discussed below>>
}
构造函数
该类提供一个构造函数,它只是实例化连接并设置连接字符串
public GenericDatabaseConnector
(
string ConnectionString
)
{
this.connection = new ConnectionType() ;
this.connection.ConnectionString = ConnectionString ;
return ;
}
ParameterPrefix
ParameterPrefix
的值用于创建参数名称。开发人员有责任设置正确的值。稍后我将讨论参数名称的创建。
public virtual string
ParameterPrefix
{
get
{
lock ( this.connection )
{
return ( this.parameterprefix ) ;
}
}
set
{
lock ( this.connection )
{
if ( value == null )
{
this.parameterprefix = "" ;
}
else
{
this.parameterprefix = value.Trim() ;
}
}
return ;
}
}
CommandTimeout
允许用户为执行 SQL 语句指定时间限制。并非所有 IDbCommand
的实现都支持此功能;支持的实现会提供一个默认值。如果您使用的 IDbCommand
支持更改 CommandTimeout
,您可以为其指定一个值。默认值 -1
(或任何负值)将阻止更改 IDbCommand
的值。
public virtual int
CommandTimeout
{
get
{
lock ( this.connection )
{
return ( this.timeout ) ;
}
}
set
{
lock ( this.connection )
{
this.timeout = value ;
}
return ;
}
}
IsolationLevel
允许用户指定在对 ExecuteSql
的单次调用中执行的 SQL 语句应在具有指定 IsolationLevel
的事务中执行。默认值 Unspecified 会导致非事务性执行。
public virtual System.Data.IsolationLevel
IsolationLevel
{
get
{
lock ( this.connection )
{
return ( this.isolation ) ;
}
}
set
{
lock ( this.connection )
{
this.isolation = value ;
}
return ;
}
}
处置
Dispose
只是调用连接的 Dispose
public virtual void
Dispose
(
)
{
lock ( this.connection )
{
this.connection.Dispose() ;
}
return ;
}
ExecuteSql
ExecuteSql
是该类的主要方法。它有些冗长,所以我将分段介绍它。
最外层部分锁定并打开连接,创建命令,并在完成后关闭命令。当然,它还处理返回 DataTable 列表。
public virtual System.Collections.Generic.List
ExecuteSql
(
string Statements
,
params object[] Parameters
)
{
System.Collections.Generic.List result =
new System.Collections.Generic.List() ;
lock ( this.connection )
{
this.connection.Open() ;
try
{
using
(
System.Data.IDbCommand cmd
=
this.connection.CreateCommand()
)
{
if ( this.timeout >= 0 )
{
cmd.CommandTimeout = this.timeout ;
}
if ( this.isolation != System.Data.IsolationLevel.Unspecified )
{
cmd.Transaction =
this.connection.BeginTransaction ( this.isolation ) ;
}
<<Discussed below>>
if ( cmd.Transaction != null )
{
cmd.Transaction.Commit() ;
}
}
}
finally
{
this.connection.Close() ;
}
}
return ( result ) ;
}
下一部分实例化为提供的任何参数值创建的参数,声明下一节中使用的局部变量,拆分 SQL 语句(参见下文),然后枚举这些 SQL 语句
if ( Parameters != null )
{
for ( int par = 0 ; par < Parameters.Length ; par++ )
{
System.Data.IDbDataParameter param = new ParameterType() ;
param.ParameterName = string.Format
(
"{0}Param{1}"
,
this.ParameterPrefix
,
par.ToString()
) ;
param.Value =
Parameters [ par ] == null ?
System.DBNull.Value :
Parameters [ par ] ;
cmd.Parameters.Add ( param ) ;
}
}
int table = -1 ;
System.Data.DataRow row ;
foreach
(
string sql
in
PIEBALD.Lib.LibSql.SplitSqlStatements ( Statements )
)
{
<<Discussed below>>
}
请注意,在命名参数时如何使用 ParameterPrefix
属性的值。默认情况下,参数名称是 Param0 到 Paramn,但专门的实现可以通过指定前缀(例如“@”)来更改此设置。
下一部分创建一个 DataTable
来保存下一个 SQL 语句的结果,将 commandtext 设置为当前 SQL 语句,并调用 ExecuteReader
。
如果 ExecuteReader
抛出异常,它将被捕获,并且 DataTable
将包含有关异常的信息。如果 ExecuteReader
成功并且下一节抛出异常,那么在将异常信息放入 DataTable
之前,任何数据都将被清除。catch
还有中断以终止语句的处理。
result.Add ( new System.Data.DataTable ( string.Format
(
"Result {0}"
,
++table
) ) ) ;
cmd.CommandText = sql ;
try
{
using
(
System.Data.IDataReader rdr
=
cmd.ExecuteReader()
)
{
<<Discussed below>>
}
}
catch ( System.Exception err )
{
if ( cmd.Transaction != null )
{
cmd.Transaction.Rollback() ;
}
result [ table ].Rows.Clear() ;
result [ table ].Columns.Clear() ;
result [ table ].Columns.Add
(
"Message"
,
typeof(string)
) ;
row = result [ table ].NewRow() ;
row [ 0 ] = sql ;
result [ table ].Rows.Add ( row ) ;
while ( err != null )
{
row = result [ table ].NewRow() ;
row [ 0 ] = err.Message ;
result [ table ].Rows.Add ( row ) ;
err = err.InnerException ;
}
break ;
}
最内部的部分是用成功执行的结果填充 DataTable
。如果 DataReader
有字段(列),那么我在 DataTable
中创建列并为数据添加行。如果 DataReader
没有字段(如 DML 和 DDL 语句),那么我只报告 RecordsAffected
的值。
if ( rdr.FieldCount > 0 )
{
for ( int col = 0 ; col < rdr.FieldCount ; col++ )
{
result [ table ].Columns.Add
(
rdr.GetName ( col )
,
rdr.GetFieldType ( col )
) ;
}
while ( rdr.Read() )
{
row = result [ table ].NewRow() ;
for ( int col = 0 ; col < rdr.FieldCount ; col++ )
{
row [ col ] = rdr [ col ] ;
}
result [ table ].Rows.Add ( row ) ;
}
rdr.Close() ;
}
else
{
rdr.Close() ;
result [ table ].Columns.Add
(
"RecordsAffected"
,
typeof(int)
) ;
row = result [ table ].NewRow() ;
row [ 0 ] = rdr.RecordsAffected ;
result [ table ].Rows.Add ( row ) ;
}
专用实现
使用 GenericDatabaseConnector
作为基础,创建专用连接器非常容易。您只需从 GenericDatabaseConnector
派生,提供要使用的 IDbConnection
类并添加一个构造函数。
因为 GenericDatabaseConnector
唯一的构造函数需要一个 string
参数,所以任何派生类都必须有一个调用该基构造函数的构造函数。当然,最简单的方法是为派生类提供一个接受 string
参数的构造函数。这是我最接近在接口中拥有构造函数的方式。
以下文件包含用于三个主要 ADO.NET 提供程序的连接器。应用程序可以直接使用这些连接器,它们不必须用作插件。
PIEBALD.Data.OdbcDatabaseConnector.cs
public class OdbcDatabaseConnector : PIEBALD.Data.GenericDatabaseConnector
<
System.Data.Odbc.OdbcConnection
>
{
public OdbcDatabaseConnector
(
string ConnectionString
)
: base
(
ConnectionString
)
{
this.ParameterPrefix = "@" ;
return ;
}
}
PIEBALD.Data.OleDbDatabaseConnector.cs
public class OleDbDatabaseConnector : PIEBALD.Data.GenericDatabaseConnector
<
System.Data.OleDb.OleDbConnection
>
{
public OleDbDatabaseConnector
(
string ConnectionString
)
: base
(
ConnectionString
)
{
this.ParameterPrefix = "@" ;
return ;
}
}
PIEBALD.Data.SqlServerDatabaseConnector.cs
public class SqlServerDatabaseConnector : PIEBALD.Data.GenericDatabaseConnector
<
System.Data.SqlClient.SqlConnection
>
{
public SqlServerDatabaseConnector
(
string ConnectionString
)
: base
(
ConnectionString
)
{
this.ParameterPrefix = "@" ;
return ;
}
}
LibSql.SplitSqlStatements.cs
奖励!!在这里,免费为您提供一个可以拆分由分号分隔的 SQL 语句列表的方法!
使用了正则表达式;我将不胜感激您对改进提出的任何意见。string
字面量中的分号不会导致拆分。没有特殊处理注释。子字符串将被修剪。空子字符串将不被返回。
private static readonly System.Text.RegularExpressions.Regex splitter =
new System.Text.RegularExpressions.Regex
(
"('[^']*'|\"[^\"]*\"|[^;])*"
,
System.Text.RegularExpressions.RegexOptions.Compiled
) ;
public static System.Collections.Generic.List<string>
SplitSqlStatements
(
string Statements
)
{
if ( Statements == null )
{
throw ( new System.ArgumentNullException
(
"Statements"
,
"Statements must not be null"
) ) ;
}
System.Collections.Generic.List<string> result =
new System.Collections.Generic.List<string>() ;
foreach
(
System.Text.RegularExpressions.Match mat
in
splitter.Matches ( Statements )
)
{
string temp = mat.Value.Trim() ;
if ( temp.Length > 0 )
{
result.Add ( temp ) ;
}
}
return ( result ) ;
}
DatabaseConnector.cs
这是一个 static
类,它只包含以下方法。
Connect
负责加载插件程序集,获取插件类型,并返回该类型的一个实例。在此过程中会执行一些验证。正是这个方法要求插件有一个接受连接字符串作为参数的构造函数。
由于篇幅原因,我将分节介绍 Connect
方法。
Connect
需要要加载的文件名和要使用的连接字符串。文件名必须与插件类同名,这可能会导致文件名有些笨重(如上述实现所示),但否则用户将需要同时提供两个名称;我更喜欢这个解决方案,至少它强制执行我的每个文件一个插件的规则。
public static IExecuteSql
Connect
(
string Filename
,
string ConnectionString
)
{
IExecuteSql result = null ;
string name ;
System.Reflection.Assembly assm ;
try
{
name = System.IO.Path.GetFileNameWithoutExtension ( Filename ) ;
/*\
|*| This is the common way to load an assembly:
|*| assm = System.Reflection.Assembly.LoadFrom ( Filename ) ;
|*|
|*| The following is my take on a technique suggested by Sacha Barber:
\*/
assm = System.AppDomain.CreateDomain ( name ).
Load ( System.IO.File.ReadAllBytes ( Filename ) ) ;
}
catch ( System.Exception err )
{
throw ( new System.InvalidOperationException
(
string.Format
(
"Could not load an assembly from file {0}"
,
Filename
)
,
err
) ) ;
}
<<Discussed below>>
return ( result ) ;
}
一旦程序集加载,下一节将尝试获取并验证类型
System.Type type = assm.GetType ( name ) ;
if ( type == null )
{
throw ( new System.InvalidOperationException
(
string.Format
(
"The assembly in file {0} does not contain a public class named {1}"
,
Filename
,
name
)
) ) ;
}
if ( !typeof(PIEBALD.Data.IExecuteSql).IsAssignableFrom ( type ) )
{
throw ( new System.InvalidOperationException
(
string.Format
(
"Type {0} in file {1} does not implement PIEBALD.Data.IExecuteSql"
,
type.Name
,
Filename
)
) ) ;
}
最后,尝试获取并调用所需的构造函数
System.Reflection.ConstructorInfo cons = type.GetConstructor
(
new System.Type[] { typeof(string) }
) ;
if ( cons == null )
{
string.Format
(
"Type {0} in file {1} does not have a constructor that takes a string"
,
type.Name
,
Filename
)
}
try
{
result = (IExecuteSql) cons.Invoke
(
new string[] { ConnectionString }
) ;
}
catch ( System.Exception err )
{
throw ( new System.InvalidOperationException
(
string.Format
(
"Unable to instantiate a {0} with connection string {1}"
,
type.Name
,
ConnectionString
)
,
err
) ) ;
}
DatabaseConnectorTest.cs
zip 文件还包含一个相当简单的测试/演示控制台应用程序。
我主要想指出的是,连接到所选数据库并执行一些 SQL 是多么简单。演示会打印出结果,但不会进行任何花哨的格式化。
using
(
PIEBALD.Data.IExecuteSql con
=
PIEBALD.Data.DatabaseConnector.Connect
(
args [ 0 ]
,
args [ 1 ]
)
)
{
con.IsolationLevel = System.Data.IsolationLevel.ReadCommitted ;
for ( int i = 2 ; i < args.Length ; i++ )
{
foreach
(
System.Data.DataTable dt
in
con.ExecuteSql ( args [ i ] )
)
{
System.Console.WriteLine() ;
System.Console.WriteLine ( dt.TableName ) ;
System.Console.WriteLine() ;
foreach ( System.Data.DataColumn col in dt.Columns )
{
System.Console.Write ( " {0}" , col.ColumnName ) ;
}
System.Console.WriteLine() ;
foreach ( System.Data.DataRow row in dt.Rows )
{
for ( int col = 0 ; col < dt.Columns.Count ; col++ )
{
System.Console.Write ( " {0}" , row [ col ].ToString() ) ;
}
System.Console.WriteLine() ;
}
System.Console.WriteLine() ;
}
}
}
如果您的应用程序不需要多次使用连接,您可以省略 using
语句
foreach
(
System.Data.DataTable dt
in
PIEBALD.Data.DatabaseConnector.Connect
(
args [ 0 ]
,
args [ 1 ]
).ExecuteSql
(
args [ 2 ]
)
)
{
...
}
Using the Code
zip 文件包含上面描述的八个 C# 文件,以及一个 bat 文件和一个 Access (MDB) 文件。
build.bat
我使用 build.bat 来测试类。您如何为自己的项目构建它们取决于您;请记住,如果您使用 DatabaseConnector.Connect
方法,每个连接器都需要位于其自己的程序集中。
@rem Compile these four files to form DatabaseConnector.dll
csc /t:library DatabaseConnector.cs GenericDatabaseConnector.cs
IExecuteSql.cs LibSql.SplitSqlStatements.cs
@rem Compile the test app with a reference to DatabaseConnector.dll
@rem Note that it does not need references to the following dlls
csc DatabaseConnectorTest.cs /r:DatabaseConnector.dll
@rem Compile each of these into its own dll with a reference to DatabaseConnector.dll
csc /t:library PIEBALD.Data.OdbcDatabaseConnector.cs /r:DatabaseConnector.dll
csc /t:library PIEBALD.Data.OleDbDatabaseConnector.cs /r:DatabaseConnector.dll
csc /t:library PIEBALD.Data.SqlServerDatabaseConnector.cs /r:DatabaseConnector.dll
build.bat 还会针对提供的 MDB 文件(您有 Jet 引擎,对吗?)测试类,但行太长,我不会在这里描述它们。一个常规应用程序可能无论如何都不会将所需值作为命令行参数。
结论
如果有一种更简单的方法可以在编译时不知道的数据库引擎上连接并执行一些 SQL,我还没有找到。在写这篇文章时,我意识到我目前依赖于我的重量级数据库访问库的两个现有应用程序可以很容易地转换为使用这个库。
历史
- 2009-01-26 首次提交