您的位置:首页 > 数据库

如何简化数据库的访问与操作,兼谈泛型的应用

2009-07-14 16:47 417 查看

一、一些说明及申明

不积跬步无以至千里,不纳滴水无以成江海。本文谈到的仅仅是实际开发过程的一个简单应用,虽然显得很不深奥,但并不影响一些编程思想在其中的具体体现。授之与鱼,不如授之与渔。我期望的是,通过某个简单应用的编码过程,让大家理解某种(个)技术的发明初衷,以及它的适用场景。

可惜的是,这里大量的内容基于个人的经验总结和理解,毕竟书读得不够、不透,如果某些措辞不当或者指代混乱希望您见谅,好在不是正式出版物,草根说错话肯定比“专家”要少挨很多砖:)

不过两年多的工作经历,自我感觉技术上尚是浅薄,师弟、师妹们可多了解些我的工作心得,或许能少走弯路;至于大侠、小虾之辈,哂笑之后若肯指教一二最好,或者请以宽容之心释己之怀啊!

二、面向对象的抽象过程

在进行后台开发时,我们时常要进行数据库的访问与操作,.net提供了IDbConnection,IDbCommand以及IDbDataParameter等接口可以满足我们的日常需要。通常,我们这样获取数据或进行操作:

//ExecuteReader
using(SqlConnectioncon=newSqlConnection("myconnectionstring")){
using(SqlCommandcmd=newSqlCommand("mycommandstring",con)){
con.Open();
using(SqlDataReaderreader=cmd.ExecuteReader()){
while(reader.Read()){
//Todo
}
}
}
}
//ExecuteScalar
using(SqlConnectioncon=newSqlConnection("myconnectionstring")){
using(SqlCommandcmd=newSqlCommand("mycommandstring",con)){
con.Open();
objectresult=cmd.ExecuteScalar();
}
}
//ExecuteNonQuery
using(SqlConnectioncon=newSqlConnection("myconnectionstring")){
using(SqlCommandcmd=newSqlCommand("mycommandstring",con)){
con.Open();
intresult=cmd.ExecuteNonQuery();
}
}

我在做了相当长一段时间如上编码之后,发觉可以进行如下简化,创建SqlHelper进行辅助。相信当前很多开发人员也是这么做的(指思路相似,细节处肯定不同):

classSqlHelper{
publicstaticSqlDataReaderExecuteReader(stringconStr,stringcmdStr){
//ExecuteReader
SqlConnectioncon=newSqlConnection(conStr);
SqlCommandcmd=newSqlCommand(cmdStr,con);
con.Open();
returncmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);//这表示操作完成后关闭该Reader
}
publicstaticobjectExecuteScalar(stringconStr,stringcmdStr){
//ExecuteScalar
using(SqlConnectioncon=newSqlConnection(conStr)){
using(SqlCommandcmd=newSqlCommand(cmdStr,con)){
con.Open();
returncmd.ExecuteScalar();
}
}
}
publicstaticintExecuteNonQuery(stringconStr,stringcmdStr){
//ExecuteNonQuery
using(SqlConnectioncon=newSqlConnection(conStr)){
using(SqlCommandcmd=newSqlCommand(cmdStr,con)){
con.Open();
returncmd.ExecuteNonQuery();
}
}
}
}

当需要传参时,要么通过string.format()等方法拼接cmdStr(很不安全),要么通过原始方法,或者通过如下变通方案:

structParameter{
publicstringName{get;set;}
publicobjectValue{get;set;}
}
publicstaticintExecuteNonQuery(stringconStr,stringcmdStr,paramsParameter[]pars){
//ExecuteNonQuery
using(SqlConnectioncon=newSqlConnection(conStr)){
using(SqlCommandcmd=newSqlCommand(cmdStr,con)){
foreach(Parameterpinpars){
cmd.Parameters.AddWithValue(p.Name,p.Value);
}
con.Open();
returncmd.ExecuteNonQuery();
}
}
}

到这一步,看上去数据库访问已经简化得差不多:

intresult=SqlHelper.ExecuteNonQuery("myconnectionstring","mycommandstring",
newParameter(){Name="Number",Value=1},newParameter(){Name="Text",Value="afaefawf"});

或者只是一些细节上的优化:

classSqlHelperII{
publicstaticSqlHelperIIInstanceForStudentsSystem{get;privateset;}
staticSqlHelperII(){
InstanceForStudentsSystem=newSqlHelperII(System.Configuration.ConfigurationSettings
.AppSettings["StudentsSystemConnectionString"]);
}
publicSqlHelperII(stringconnectionString){
this.ConnectionString=connectionString;
}
publicstringConnectionString{get;privateset;}
publicintExecuteNonQuery(stringcmdStr,paramsstring[]pars){
//ExecuteNonQuery
using(SqlConnectioncon=newSqlConnection(this.ConnectionString)){
using(SqlCommandcmd=newSqlCommand(cmdStr,con)){
for(inti=0;i<pars.Length;i+=2){
cmd.Parameters.AddWithValue(pars[i],pars[i+1]);
}
con.Open();
returncmd.ExecuteNonQuery();
}
}
}
}

以往的操作可以这样进行:

intresult=SqlHelperII.InstanceForStudentsSystem.ExecuteNonQuery("mycommandstring",
"Number",1.ToString(),"Text","afaefawf");

事实上,一般情况下我个人认为这样确实足够了,但上面的代码全假设在了SqlServer数据库之上,一旦遇到MySql之类,就需要编写某个MySqlHelper等。另外,我在年初某个项目与MID层进行交互时突然悟到,其实任何数据类操作都可以抽象成类数据库的访问方式;当时我为此做了个MIDServicesHelper,其实现了ExecuteReader,ExecuteScalar,ExecuteNonQuery三个方法(我的实践证明,这三个方法大概是足够用了)。

接下来的时间里,我创建了这样的一个接口IDbExecutor:

namespaceQueen.Data{
///<summary>
///数据库服务接口,负责进行数据的存储、读取等基本操作.
///它可以是一个传统的Sql数据库,也可以是其他支持数据操作的系统.
///</summary>
publicinterfaceIDbExecutor{
///<summary>
///链接串
///</summary>
stringConnectionString{
get;
set;
}
///<summary>
///超时设置
///</summary>
intCommandTimeout{
get;
set;
}
///<summary>
///获取执行影响行数.
///实现该接口时注意释放所有相关资源!
///</summary>
///<paramname="cmdText"></param>
///<paramname="keyValuePairs"></param>
///<returns></returns>
intExecuteNonQuery(stringcmdText,paramsstring[]keyValuePairs);
///<summary>
///获取执行值.
///实现该接口时注意释放所有相关资源!
///</summary>
///<paramname="cmdText"></param>
///<paramname="keyValuePairs"></param>
///<returns></returns>
objectExecuteScalar(stringcmdText,paramsstring[]keyValuePairs);
///<summary>
///获取数据读取流.
///调用该接口时务必释放读取流,using(IDataReaderreader=XX.ExecuteReader()){...}
///实现该接口时请勿释放任何资源,返回调用System.Data.SqlCommand.ExecuteReader(
System.Data.CommandBehavior.CloseConnection)
///</summary>
///<paramname="cmdText">数据库执行文本.</param>
///<paramname="keyValuePairs">参数组,比如"@ParaA","valueA","@ParaB","valueB"</param>
///<returns>数据读出流</returns>
System.Data.IDataReaderExecuteReader(stringcmdText,paramsstring[]keyValuePairs);
}
}

凡后来我主导的项目里总会多出这样一个文件及类型DbExecutor,其内部可定义支持SqlServer、MySql甚至其它数据库的内部类,但数据库类型对外界“基本”透明(组织SQL语句时必须知道数据库类型,SqlServer、MySql有各自的SQL语言特性!):

staticclassDbExecutor{
publicstaticQueen.Data.IDbExecutorStudents{get;privateset;}
publicstaticQueen.Data.IDbExecutorFamilies{get;privateset;}
staticDbExecutor(){
//Initstudents,families...
}
privateclassSqlServerExecutor:Queen.Data.IDbExecutor{
#regionIDbExecutor成员
publicstringConnectionString{
get{
thrownewNotImplementedException();
}
set{
thrownewNotImplementedException();
}
}
publicintCommandTimeout{
get{
thrownewNotImplementedException();
}
set{
thrownewNotImplementedException();
}
}
publicintExecuteNonQuery(stringcmdText,paramsstring[]keyValuePairs){
thrownewNotImplementedException();
}
publicobjectExecuteScalar(stringcmdText,paramsstring[]keyValuePairs){
thrownewNotImplementedException();
}
publicSystem.Data.IDataReaderExecuteReader(stringcmdText,paramsstring[]keyValuePairs){
thrownewNotImplementedException();
}
#endregion
}
privateclassMySqlExecutor:Queen.Data.IDbExecutor{
#regionIDbExecutor成员
publicstringConnectionString{
get{
thrownewNotImplementedException();
}
set{
thrownewNotImplementedException();
}
}
publicintCommandTimeout{
get{
thrownewNotImplementedException();
}
set{
thrownewNotImplementedException();
}
}
publicintExecuteNonQuery(stringcmdText,paramsstring[]keyValuePairs){
thrownewNotImplementedException();
}
publicobjectExecuteScalar(stringcmdText,paramsstring[]keyValuePairs){
thrownewNotImplementedException();
}
publicSystem.Data.IDataReaderExecuteReader(stringcmdText,paramsstring[]keyValuePairs){
thrownewNotImplementedException();
}
#endregion
}
}

这样做的好处在于,不但在代码量上减少了开发人员的维护成本,也在系统的迁移上极大减少了代码替换工作(虽然迁移的可能性比较小),同时,也给系统的数据库操作监控带来了便利(比如增加日志功能等,后文将举例实现)。

三、泛型编程

但针对数据库操作这块,我还是严重感觉不够优雅,我觉得SqlServerExecutor、MyServerExecutor等虽然引用了不同的驱动程序,但这些驱动程序均实现了同一类接口(IDbConnection等),通过继承实现多个版本的XxxExecutor简直太丑陋了!而后,通过学习、领会泛型编程思想,我当前使用的版本产生了:

usingSystem.Data;
namespaceQueen.Data{
///<summary>
///提供Sql语言支持的数据库服务,必须声明为IDbExecutor才能使用相关服务.
///</summary>
///<typeparamname="TConnection">链接类型</typeparam>
///<typeparamname="TCommand">命令类型</typeparam>
///<typeparamname="TDataParameter">数据参数类型</typeparam>
publicclassSqlExecutor<TConnection,TCommand,TDataParameter>:IDbExecutor
whereTConnection:IDbConnection,new()
whereTCommand:IDbCommand,new()
whereTDataParameter:IDbDataParameter,new(){
///<summary>
///Sql命令类型.
///</summary>
publicSystem.Data.CommandTypeCommandType{
get;
set;
}
#regionConstruct
publicSqlExecutor(){
CommandType=System.Data.CommandType.Text;
((IDbExecutor)this).CommandTimeout=30;
}
publicSqlExecutor(System.Data.CommandTypecommandType){
CommandType=commandType;
((IDbExecutor)this).CommandTimeout=30;
}
publicSqlExecutor(System.Data.CommandTypecommandType,intcommandTimeout){
CommandType=commandType;
((IDbExecutor)this).CommandTimeout=commandTimeout;
}
#endregion
#regionISqlExecutor成员
stringIDbExecutor.ConnectionString{
get;
set;
}
///<summary>
///链接串,默认30(秒)
///</summary>
intIDbExecutor.CommandTimeout{
get;
set;
}
intIDbExecutor.ExecuteNonQuery(stringcmdText,paramsstring[]keyValuePairs){
using(TConnectioncon=newTConnection()){
con.ConnectionString=((IDbExecutor)this).ConnectionString;
using(TCommandcmd=newTCommand()){
cmd.CommandText=cmdText;
cmd.Connection=con;
if(keyValuePairs!=null){
for(inti=0;i<keyValuePairs.Length-1;i+=2){
TDataParameterp=newTDataParameter();
p.ParameterName=keyValuePairs[i];
p.Value=keyValuePairs[i+1];
cmd.Parameters.Add(p);
}
}
cmd.CommandType=CommandType;
cmd.CommandTimeout=((IDbExecutor)this).CommandTimeout;
con.Open();
returncmd.ExecuteNonQuery();
}
}
}
objectIDbExecutor.ExecuteScalar(stringcmdText,paramsstring[]keyValuePairs){
using(TConnectioncon=newTConnection()){
con.ConnectionString=((IDbExecutor)this).ConnectionString;
using(TCommandcmd=newTCommand()){
cmd.CommandText=cmdText;
cmd.Connection=con;
if(keyValuePairs!=null){
for(inti=0;i<keyValuePairs.Length-1;i+=2){
TDataParameterp=newTDataParameter();
p.ParameterName=keyValuePairs[i];
p.Value=keyValuePairs[i+1];
cmd.Parameters.Add(p);
}
}
cmd.CommandType=CommandType;
cmd.CommandTimeout=((IDbExecutor)this).CommandTimeout;
con.Open();
returncmd.ExecuteScalar();
}
}
}
///<summary>
///获取数据读取流
///调用该接口时务必释放读取流,using(IDataReaderreader=XX.ExecuteReader()){...}.
///</summary>
///<paramname="cmdText"></param>
///<paramname="keyValuePairs"></param>
///<returns></returns>
System.Data.IDataReaderIDbExecutor.ExecuteReader(stringcmdText,paramsstring[]keyValuePairs){
TConnectioncon=newTConnection();
con.ConnectionString=((IDbExecutor)this).ConnectionString;
TCommandcmd=newTCommand();
cmd.CommandText=cmdText;
cmd.Connection=con;
if(keyValuePairs!=null){
for(inti=0;i<keyValuePairs.Length-1;i+=2){
TDataParameterp=newTDataParameter();
p.ParameterName=keyValuePairs[i];
p.Value=keyValuePairs[i+1];
cmd.Parameters.Add(p);
}
}
cmd.CommandType=CommandType;
cmd.CommandTimeout=((IDbExecutor)this).CommandTimeout;
con.Open();
returncmd.ExecuteReader(CommandBehavior.CloseConnection);
}
#endregion
}
}

只需指定TConnection,TCommand,TDataParameter的具体类型,该辅助类并能支持对应工作了;也就是把SqlServer、MySql等的具体辅助类合并到了一起并做了实现,真的是一劳永逸啊!相应的,DbExecutor简化成了:

///<summary>
///数据访问代理
///</summary>
publicstaticclassDbExecutor{
privatestaticIDbExecutor_files;
publicstaticIDbExecutorFiles{get{return_files;}privateset{_files=value;}}
privatestaticIDbExecutor_BK;
publicstaticIDbExecutorBK{get{return_BK;}privateset{_BK=value;}}
privatestaticIDbExecutor_ResourcesProcedure;
publicstaticIDbExecutorResourcesProcedure{get{return_ResourcesProcedure;}privateset{
_ResourcesProcedure=value;}}
privatestaticIDbExecutor_Resources;
publicstaticIDbExecutorResources{get{return_Resources;}privateset{_Resources=value;}}
staticDbExecutor(){
Files=newSqlExecutor<MySqlConnection,MySqlCommand,MySqlParameter>();
Files.ConnectionString=System.Configuration.ConfigurationManager.ConnectionStrings["Files"]
.ConnectionString;
BK=newSqlExecutor<MySqlConnection,MySqlCommand,MySqlParameter>();
BK.ConnectionString=System.Configuration.ConfigurationManager.ConnectionStrings["bk"]
.ConnectionString;
Resources=newSqlExecutor<MySqlConnection,MySqlCommand,MySqlParameter>();
Resources.ConnectionString=System.Configuration.ConfigurationManager.ConnectionStrings[
"Resources"].ConnectionString;
ResourcesProcedure=newSqlExecutor<MySqlConnection,MySqlCommand,MySqlParameter>(
System.Data.CommandType.StoredProcedure);
ResourcesProcedure.ConnectionString=System.Configuration.ConfigurationManager
.ConnectionStrings["Resources"].ConnectionString;
}
}


四、装饰者设计模式的应用

最近,组内希望添加数据库操作人员记录,我只是做了如下的改动便完成了简易日志功能(请参见“装饰者设计模式”):

usingSystem;
usingSystem.Collections.Generic;
usingSystem.Text;
usingQueen.Data;
usingMySql.Data.MySqlClient;
usingSystem.Web;
///<summary>
///数据访问代理
///</summary>
publicstaticclassDbExecutor{
privatestaticIDbExecutor_files;
publicstaticIDbExecutorFiles{get{return_files;}privateset{_files=value;}}
privatestaticIDbExecutor_BK;
publicstaticIDbExecutorBK{get{return_BK;}privateset{_BK=value;}}
privatestaticIDbExecutor_ResourcesProcedure;
publicstaticIDbExecutorResourcesProcedure{get{return_ResourcesProcedure;}
privateset{_ResourcesProcedure=value;}}
privatestaticIDbExecutor_Resources;
publicstaticIDbExecutorResources{get{return_Resources;}privateset{_Resources=value;}}
staticDbExecutor(){
Files=newSqlExecutor<MySqlConnection,MySqlCommand,MySqlParameter>();
Files.ConnectionString=System.Configuration.ConfigurationManager.ConnectionStrings["Files"]
.ConnectionString;
Files=newDecorateExecutor(Files);
BK=newSqlExecutor<MySqlConnection,MySqlCommand,MySqlParameter>();
BK.ConnectionString=System.Configuration.ConfigurationManager.ConnectionStrings["bk"]
.ConnectionString;
BK=newDecorateExecutor(BK);
Resources=newSqlExecutor<MySqlConnection,MySqlCommand,MySqlParameter>();
Resources.ConnectionString=System.Configuration.ConfigurationManager
.ConnectionStrings["Resources"].ConnectionString;
Resources=newDecorateExecutor(Resources);
ResourcesProcedure=newSqlExecutor<MySqlConnection,MySqlCommand,MySqlParameter>(
System.Data.CommandType.StoredProcedure);
ResourcesProcedure.ConnectionString=System.Configuration.ConfigurationManager
.ConnectionStrings["Resources"].ConnectionString;
ResourcesProcedure=newDecorateExecutor(ResourcesProcedure);
}
#regionDecorateExecutor,logable
classDecorateExecutor:IDbExecutor{
privateIDbExecutorvalue;
publicDecorateExecutor(IDbExecutorvalue){
this.value=value;
}
#regionIDbExecutor成员
publicintCommandTimeout{
get{
returnthis.value.CommandTimeout;
}
set{
this.value.CommandTimeout=value;
}
}
publicstringConnectionString{
get{
returnthis.value.ConnectionString;
}
set{
this.value.ConnectionString=value;
}
}
publicintExecuteNonQuery(stringcmdText,paramsstring[]keyValuePairs){
if(null!=HttpContext.Current&&null!=HttpContext.Current.User&&null
!=HttpContext.Current.User.Identity&&!string.IsNullOrEmpty(HttpContext.Current.User.Identity.Name)){
System.Diagnostics.Trace.WriteLine(string.Format("{0}\t{1}进行了操作{2}...",
DateTime.Now.ToShortTimeString(),HttpContext.Current.User.Identity.Name,
cmdText.Length>20?cmdText.Substring(0,20):cmdText));
}
returnthis.value.ExecuteNonQuery(cmdText,keyValuePairs);
}
publicSystem.Data.IDataReaderExecuteReader(stringcmdText,paramsstring[]keyValuePairs){
if(null!=HttpContext.Current&&null!=HttpContext.Current.User&&null
!=HttpContext.Current.User.Identity&&!string.IsNullOrEmpty(HttpContext.Current.User.Identity.Name)){
System.Diagnostics.Trace.WriteLine(string.Format("{0}\t{1}进行了操作{2}...",
DateTime.Now.ToShortTimeString(),HttpContext.Current.User.Identity.Name,
cmdText.Length>20?cmdText.Substring(0,20):cmdText));
}
returnthis.value.ExecuteReader(cmdText,keyValuePairs);
}
publicobjectExecuteScalar(stringcmdText,paramsstring[]keyValuePairs){
if(null!=HttpContext.Current&&null!=HttpContext.Current.User&&null
!=HttpContext.Current.User.Identity&&!string.IsNullOrEmpty(HttpContext.Current.User.Identity.Name)){
System.Diagnostics.Trace.WriteLine(string.Format("{0}\t{1}进行了操作{2}...",
DateTime.Now.ToShortTimeString(),HttpContext.Current.User.Identity.Name,
cmdText.Length>20?cmdText.Substring(0,20):cmdText));
}
returnthis.value.ExecuteScalar(cmdText,keyValuePairs);
}
#endregion
}
#endregion
}


五、小结:

优雅的代码让人看着赏心悦目,虽然每个人的审美会有不同,但至少要让自己觉得舒服。我尝试着在编码的过程中积累软件设计的经验,充分发挥面向对象的长处,使得代码的可复用性、可维护性(可调试的能力?)、可扩展性达到极致。在本文前半段,我与大家分享了我在抽象数据库访问行为时的心得(不过是IDbExecutor接口),而在接下来的文字里,我使用了泛型编程技巧,使得传统的面向对象(通过继承等手段扩展、复用代码?)得到了看似质的突破,极大缓解了“继承泛滥”的苦恼。

面向过程编程,大量的操作是参数的传递,由于参数之间有逻辑上的关联,且“过程”本身又针对某类关联参数,于是有了“面向对象”。这样一来,人们掌握系统全局的能力得到了加强,从.net框架提供的各种服务来看,面向对象实质上降低了编程的门槛,并且提高了人们的生产效率。

通常,除非使用反射,我们很难使得代码不经过扩展而达到“向后兼容”。但反射本身并不优雅,而且执行效率会降低,泛型的推出解决了这样一个矛盾。我个人的理解是,可以将所谓“泛型”看成“类型参数”,传统上,参数基本是基于数据的,而引入了类型参数后,不但不会失去强类型的类型安全特点,还能使得代码被编译器优化,提高执行效率(是指“值类型的装箱、拆箱”操作等)。

但从来没有完美的事,在当真要进行数据库转换、系统迁移的时候,比如从SqlServer变为MySql时,当前的IDbExecutor是不可能让数据库类型透明的:因为开发人员在组织SQL语句时必须知道数据库类型,SqlServer、MySql有各自的SQL语言特性,相同的SQL语句并不能保证在两个数据库平台上成功运行!解决这个缺陷的办法,除了阉割当前数据库的SQL语言特性外(指禁止使用),我看是无解的;但分页操作(关键字TOP/LIMIT)是无法回避的,而且,在SqlServer中参数使用“@p”,而MySql中使用“?p”,诸如此类,因此,这个缺陷就是无解的!数据库类型变化时,SQL语句的改造再所难免,只不过从代码变化的量上,当前的数据访问方式要小得多!

(完)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: