设计一套适合自己的数据库访问层。
net领域提到数据访问层,莫过于微软的企业库,我更喜欢微软早期的sqlhelper轻量级的版本,企业库是一个比较庞大的数据底层,但是并不一定实用,微软的东西有个特点版本更新比较快,尽量不要跟的太紧,企业库里面很多思想是可以借鉴的,模仿企业库编写一个数据访问层,取其精华弃其糟粕,在项目中不断完善,编写适合自己访问层一直是我的梦想,经过多年的积累终于完成了,并在几个大型项目中实验通过,效果还不错。在眼花缭乱的众多框架和平台中寻求一种适合自己的,无论是改造还是原创,让它变成自己的核心技术,才具有战斗力,切记这一点!
下面介绍一下我是如何设计的,数据库访问层需要用到的知识点是数据库的基本访问方法:连接、增删改查等。学会这个是数据库开发的第一步,作为程序员必须掌握的技术。我们要设计的数据访问层肯定要与众不同,要有他的特色!下面先介绍需求和实现原理,重点部分贴出代码,最后有一个完整的demo源码。
先看一下要实现的功能:
1、支持多数据库,最起码支持常见的Sql Server,Oracle,My Sql ,Access 等。实现支持多数据库,这个不是很容易,尽管很多框架都支持多数据库,但是仔细分析并不一定适合自己,一些数据库的sql语法不一样,很难达到统一。我们这里支持多数据库 也是分别使用数据库的驱动进行连接,每个数据库都要建立自己的视图和存储过程。有很多软件通过控制程序编写来支持多数据库,不使用任何视图和存储过程,这个方式在简单的业务中使用还可以,要是复杂的业务就很难做到,最起码性能会大打折扣。根据数据库类型选择使用数据库驱动,这是最好的方案,例如System.Data.SqlClient,System.Data.OracleClient,这是真正的支持多数据库,每种数据库都有自己的存储过程和视图,分别去实现它,需要说明的是Access数据库不支持视图和存储过程,暂时不考虑这种数据库了。
2、支持远程访问,通常我们常见的数据访问层都是访问本地数据库的(虽然可以直接连接远程数据库,但这不是一个好的方式),我们编写一个即支持本地访问又支持远程访问的访问层,这就是我们设计的特色。要做到这一点,需要在中间加一个代理层,这样做的好处可以随时更换代理层,其他层不受影响,这样就可以实现网络版和单机版的切换了。
我们先来看看实现原理:
数据库访问层设计如下图,这个是一个综合对比图,我们的数据库访问层增加了代理层ClientDBAgent(右图粉色部分),如果这个图看不懂先看后面的文章,看完后面的介绍这个图就自然而然的懂了:
数据库访问的核心是提交Sql语句,sql 语句包括insert update select 和存储过程,业务层发送sql语句给数据库访问层,数据访问层根据sql语句判断调用什么类型的数据库驱动,完成一次数据访问,不同的数据库类型创建不同的数据库对象,例如创建sql server的数据库对象,_dataBase = new SqlDatabase();创建Oracle的数据库对象 _dataBase = new OracleDatabase(),在net类库中分别对应System.Data.SqlClient和System.Data.OracleClient,net为每种数据库访问提供了驱动类库,要正确使用对应的类库。
来看一个数据访问的简单的图列,从业务层到数据访问层:
业务层如何告知访问层访问那个数据类型?
需要在sql语句传递一个参数-数据库连接参数,这个参数包含的信息不能太复杂,否则我们调用起来很麻烦,这个参数跟sql语句一起传给数据访问层,为此我们定义一个自定义类型:SqlDataItem,包含的属性有{CommandText,DatabaseName,Parameters...}这三个参数分别对应sql语句,数据库连接字符串名(对应配置文件),sql语句里面需要的参数和值。这样我们在业务层调用数据库的时候就可以这样写:
ClientDBAgent agent = new ClientDBAgent(); SqlDataItem sqlItem = new SqlDataItem(); sqlItem.DatabaseName = "HFbpmDatabase";//数据库连接字符串名,跟配置文件对应 sqlItem.CommandText = "select * from VipCards";//sql 语句 sqlItem.CommandType = CommandType.Text.ToString(); DataSet ds = agent.ExecuteQuery(sqlItem); dataGridView1.DataSource = ds.Tables[0];
这样就可以把sql语句提交给数据访问层了。
数据访问层如何处理这个Sql语句呢?
看上面的代码和图例,sqlitem是用ClientDBAgent来处理的,agent是代理的意思,这种设计是可以随时换掉这个中间层的agent类。我们可以设计2种代理类,一种是直接访问数据库的代理类,一种是可以访问远程服务的代理类。理论上可以任意编写这个代理类,只要提供同名的方法即可,这就是代理的作用。用这种方式我们编写了一个可以访问远程数据库的代理类,这个在后面介绍。我们可以看出要想随意切换代理类,代理类必须具备相同的名字和方法,否则没法替换掉。我们先来看一下直接访问数据库的代理类。
前面一再强调代理类需要使用统一的命名空间、类名以及方法名,为了准确无误的做到这一点,我们为代理类定义了一个统一的接口类IDBAgent,代理类都要实现该接口。在IDBAgent接口中我们定义了所有数据库访问的方法。如下图:
下面我们来剖析ClientDBAgent两种代理类的实现,一种是直接访问数据库的,一种是访问远程数据库服务的。
1、直接访问数据库的代理
这种方式的数据库一般是在本机或者局域网内,直接连接数据库,首先确定访问的数据库类型,然后创建数据库对象,举一个数据库查询的方法。代码如下:
CreateProvider方法根据数据库类型创建不同的数据库对象。看一下这个方法的核心:
根据Sql语句的数据库连接的参数判断创建那种数据库驱动。因为每种数据库的访问存在很多差异,所以每种数据库类型必须单独定义他的方法,如下图:
如上图我们定义了四种数据库。如果需要支持其他数据库需要扩展这个类。HF.DataAccess.DataClient就是数据库访问层,所有代理类的最终实现都在这里。本地访问的类库之间的关系如下图:
2、远程访问数据库的代理
在实际应用中,数据库可能在远程的某台服务器上,这种情况如果使用直接连接的方式并不是最佳选择,从分布式架构上来说,最理想的方法是做服务,每个功能提供一个服务,客户端调用这些服务。这样固然很好,但是比较繁琐,一个功能提供一个方法一个系统下来会有很多这样的方法,虽然理论上都这样推进,但是并不适用。在跨系统数据整合的时候可以使用这样的方式,毕竟跨系统整合针对性比较强,方法也相对固定。对应一个完整的系统,要提供远程的访问,我的建议是只把数据库访问层做成服务。数据访问逻辑如下图:
这个访问逻辑与本地访问逻辑的不同是代理层不是直接访问的数据库DataClient而是访问的数据库服务。代理层的作用就发挥出来了,有兴趣的可以编写轻量级的socket的代理层。
远程服务的提供方式包括WCF、WebService、Remoting、Socket等,Wcf和webservice是最常用的,也是微软大力推荐的,wcf是微软新一代的数据通讯框架,无论从安全性还是执行效率上都是一流的。它既可以宿主与系统服务又可以宿主与iis还可以宿主winform,部署起来也很方便,建议使用系统服务的方式做宿主,稳定性好。我们就用这种方式来实现数据库服务。
开发WCF的数据库服务需要用到两方面的知识,一个是WCF,一个是系统服务WinServer,这两方面的知识讲起来都可以写一本书,可见一个小东西都用到方方面面的知识,这里这说重点,详细的可以参见源码。业务层提交的sql语句都属于文字信息一般不会很大,wcf默认的数据包是65536,如果数据超过这个值,那么提交的时候就会报错,为了避免这种情况,我们把它设成最大值maxReceivedMessageSize="2147483647",客户端也要做相应的设置。一个完整的wcf服务端的配置:
<system.serviceModel> <services> <service behaviorConfiguration="HF.HFService.DatabaseServiceBehavior" name="HF.WCF.DBService.DatabaseService"> <endpoint address="" binding="basicHttpBinding" contract="HF.WCF.DBService.IDataBaseService" bindingConfiguration="basicHttp"> <identity> <dns value="localhost"/> </identity> </endpoint> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> <host> <baseAddresses> <add baseAddress="http://localhost:8801/HFService/DatabaseService/"/> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior name="HF.HFService.DatabaseServiceBehavior"> <serviceMetadata httpGetEnabled="true"/> <serviceDebug includeExceptionDetailInFaults="false"/> <serviceThrottling maxConcurrentCalls="150"/> </behavior> </serviceBehaviors> </behaviors> <bindings> <basicHttpBinding> <binding name="basicHttp" closeTimeout="00:00:10" openTimeout="00:00:10" receiveTimeout="00:00:10" sendTimeout="00:00:10" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize="2147483647" maxBufferPoolSize="524288" maxReceivedMessageSize="2147483647" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true"> <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384"/> <security mode="None"> <transport clientCredentialType="None" proxyCredentialType="None" realm=""/> <message clientCredentialType="UserName" algorithmSuite="Default"/> </security> </binding> </basicHttpBinding> </bindings> </system.serviceModel>
系统服务的开发,VS也提供了模版,我们只要创建系统服务的项目即可这里不做讲解,注册系统服务的时候需要注意一下,如果程序有错误服务是无法启动的,可以查看系统日志跟踪错误;重新发布服务需要先把服务停止,再发布否则会冲突,无法替换。为了注册方便提供注册和注销服务的代码,注意服务名和路径:
sc stop HFDBWinServer sc delete HFDBWinServer sc create HFDBWinServer binpath= D:\work\Net\HF3.0\Release\DBWinServer发布版\HFDbWinService.exe start= auto sc description HFDBWinServer "禾丰软件数据库服务" sc start HFDBWinServer pause
注销服务
sc stop HFDBWinServer
sc delete HFDBWinServer
pause
数据库服务做好了,下面开始设计代理层,这里的代理层要访问远程的数据库服务,我们通过vs添加web引用找到服务,关于如何添加web引用,这是wcf的知识不在本文范畴。添加好web引用后,开始实现IDBAgent接口。HF.DataAccess.DBAgent如下图,在做web服务的时候要注意,服务端的类不支持同名方法,构造函数不支持参数。
客户端的wcf访问配置代码如下:
<system.serviceModel> <bindings> <basicHttpBinding> <binding name="basicHttp" closeTimeout="00:00:10" openTimeout="00:00:10" receiveTimeout="00:00:10" sendTimeout="00:00:10" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize="2147483647" maxBufferPoolSize="524288" maxReceivedMessageSize="2147483647" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true"/> </basicHttpBinding> </bindings> <client> <endpoint address="http://localhost:8801/HFService/DatabaseService/" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IDataBaseService" contract="WCFDatabaseService.IDataBaseService" name="basicHttp" /> </client> </system.serviceModel>
这里需要说明一个binging参数useDefaultWebProxy="true",默认是true,是需要web代理,一般我们是不需要web代理的,所以设置成false会提高wcf的访问速度。
远程访问的代理层设计好以后,业务层是不需要做任何改变的。下面看看业务层是如何调用代理的。
业务层
业务层直接调用代理类ClientDBAgent既可以。一个查询数据库表的操作,如下:
public DataTable GetLogTable(DateTime startTime, DateTime endTime)
{
string tmpSql = "select * from HF_Logs where LogDatetime between @startTime and @endTime order by LogDatetime desc";
try
{
SqlDataItem sqlItem = new SqlDataItem();
sqlItem.CommandText = tmpSql;
sqlItem.AppendParameter("@startTime", startTime,typeof(DateTime));
sqlItem.AppendParameter("@endTime", endTime, typeof(DateTime));
ClientDBAgent agent = new ClientDBAgent();
return agent.ExecuteDataTable(sqlItem);
}
catch (Exception ex)
{
throw ex;
}
一个更新数据表的操作,如下:
public bool SetPassword(string userId, string password)
{
try
{
string tmpMd5 = SysBlack.MD5Encrypt(password);
string tmpSql = "update HF_User set UserPassword=@UserPassword where userid=@userid";
SqlDataItem sqlItem = new SqlDataItem();
sqlItem.CommandText = tmpSql;
sqlItem.AppendParameter("@userId", userId);
sqlItem.AppendParameter("@UserPassword", tmpMd5);
ClientDBAgent agent = new ClientDBAgent();
int i= agent.ExecuteNonQuery(sqlItem);
return i == 1;
}
catch (Exception ex)
{
throw ex;
}
}
源码可以到http://www.51aspx.com/Code/HFWorkFlow下载,这里一个比较完整BPM项目源码,今天提前公布改源码,希望结合本系列教程一步一步学习。