三层体系结构与数据库编程接要本文主要介绍了基于三层体系结构的网络数据库设计,并结合面向对象,分布式数据库开发等理论。
全文围绕一个典型而简单的例子,通过VB编程语言,从分析、建模、设计、编码等各个角度对三层体系与数据库进行了全面而详细的阐述,文中提供了全部源代码。
关键词三层体系数据库面向对象分布式开发1. 三层体系结构我们经常会看到许多应聘者在简历上写着“精通数据库编程”的字样,也经常会在招聘网站上看到软件公司的招聘要求中某一项为“精通数据库编程”。
于是这些应聘者去这些软件公司面试,于是我们看到了许多“精通”者落选的现象。
一些程序员在设计数据库应用时,通常会采用数据控件绑定的方法实现。
用鼠标拉几个控件,再用鼠标设置几个属性,连键盘都不用动,就完成了一个数据库应用的开发!当然,这的确是一种快速的数据库应用开发方式,但快速并不意味着精通。
对于大型的数据库应用系统,或是拥有众多客户端的应用系统,我们需要另外一种“精通”,这就是几乎每个程序员都听说过的“三层体系结构”。
1.1. 传统的C/S模式在传统的数据库应用体系中,客户端与数据库完全分开,在客户端上运行了大部分服务,如数据访问规则、业务规则、合法性校验等等。
每一个客户端都存在数据引擎,并且每个客户端与数据库服务器建立独立的数据库连接(DB Connection)。
基于该种体系的数据库应用系统的优势:开发周期较短,能够适应大部分中小型数据库应用系统的要求(当客户端数量少于50时)。
但是,随着数据库应用的日渐发展、数据容量的不断增加、客户端数量的不断增加,该种体系结构显示出了诸多缺陷,主要体现在以下几个方面:1、可扩充性:对于数据库服务器端,每当建立一个数据连接,就会占用大量的系统资源,当数据连接达到一定数量(如20个)时,数据库服务器的响应速度与处理速度将大打折扣。
2、可维护性:基于传统C/S的数据库应用系统,业务规则通常置于客户端应用程序中。
如果业务规则一旦发生变化(随便举个例子,如身份证号码有可能升为19位)时,我们就必须修改客户端应用程序,并且将每个客户端进行相应的升级工作。
3、可重用性:采用传统C/S的设计模式时,数据库访问、业务规则等都固化在客户端应用程序中。
如果客户另外提出了B/S的应用需求,则需要在WEB服务器中重新进行数据库访问、业务规则、合法性校验等编码(例如将数据库访问写入ASP代码),而所做的工作与客户端应用程序中的功能完全重复,从而加大了工作量,又使得程序开发者心里感到极不舒服。
正因为以上的诸多缺陷,使得三层(多层)体系结构成为目前数据库应用开发的首选,甚至客户有时也会提出该种技术需求。
1.2. 三层体系结构所谓三层体系结构,是在客户端与数据库之间加入了一个“中间层”,也叫组件层。
这里所说的三层体系,不是指物理上的三层,不是简单地放置三台机器就是三层体系结构,也不仅仅有B/S应用才是三层体系结构,三层是指逻辑上的三层,即使这三个层放置到一台机器上。
三层体系的应用程序将业务规则、数据访问、合法性校验等工作放到了中间层进行处理。
通常情况下,客户端不直接与数据库进行交互,而是通过COM/DCOM通讯与中间层建立连接,再经由中间层与数据库进行交互。
这样的好处显而易见:1、由于数据访问是通过中间层进行的,因此客户端不再与数据库直接建立数据连接。
也就是说,建立在数据库服务器上的连接数量将大大减少。
例如一个500个客户端的应用系统,500个客户端分别与中间层服务器建立DCOM连接,而DCOM通讯所占用的系统资源极为有限,并且是动态建立与释放连接,因此客户端数量将不再受到限制。
同时,中间层与数据库服务器之间的数据连接通过“连接池”进行连接数量的控制,动态分配与释放数据连接,因此数据连接的数量将远远小于客户端数量。
2、可维护性得以提高。
因为业务规则、合法性校验存在于中间层,因此当业务规则发生改变时,只需更改中间层服务器上的某个组件(如某个DLL文件),而客户端应用程序不需做任何处理,有些时候,甚至不必修改中间层组件,只需要修改数据库中的某个存储过程就可以了。
3、良好的可重用性。
同样,如果需要开发B/S应用,则不必要重新进行数据访问、业务规则等的开发,可以直接在WEB服务器端调用现有的中间层(如可以采用基于IIS的WebClass开发,或直接编写ASP代码)。
4、事务处理更加灵活,可以在数据库端、组件层、MTS(或COM+)管理器中进行事务处理。
如果现在你仍然感到不理解,没关系,请看下面的例子。
2. 简单的人事管理系统下面以一个极为简单的人事管理系统为例详细讲述如何实现三层体系结构。
编程语言为Visual Basic 6.0。
为了全面介绍程序设计方法,VB代码中采用了不同的方法实现相同的功能,如数据库访问中,同时采用了存储过程与ADO连接。
读者可自行选择最适合的方法。
由于在代码中加入了大量注释,因此不再过多地说明函数功能与原理。
在团队开发中,代码中注释部分应占整个代码的1/3左右,而且应在代码编写前就写好注释。
如果另一个程序员认为你的代码中注释全部是废话,那么这些注释肯定是在写完代码之后才加上去的!2.1. 需求简单的部门/人员管理系统,要求:1、部门的属性有部门名称,人员的属性有姓名、年龄、性别;2、 部门存在上下级关系;3、 人员必须属于一个部门;4、 人员、部门需要实现增加、删除、修改功能5、 可以按人员的名称、年龄查询人员6、 如果一个部门存在人员,或存在下级部门,则该部门不可删除 以上即为系统的简单需求。
2.2. 数据库数据库采用SQL Server 7设计,数据库名称为“TEST ”,存在两个数据表(此处假设读者已掌握数据库设计,因为这个数据库实在太简单了)。
表tDept字段名称 类型 nIDIntDeptName Char(50) SuperID Int tEmployee 字段名称 类型 nID Int DeptIDInt EmpName Char(10) EmpAge Smallint EmpGender Bit其中,tDept 中nID 与SuperID 为表内关联。
2.3. 中间层打开VB6,选择“新建ActiveX DLL ”,并引用ADO 2.5。
新添加一个模块,命名为mdlPublic ,新填加5个类,分别命名为cDept 、cEmp 、cDepts 、cEmps 、cPublic 。
其中,cEmps 与cDepts 分别为cEmp 与cDept 的集合类,cPublic 为定义枚举的类,无实际意义。
将工程的启动模块设为“Sub Main ”(重要!)。
在SQL Server 的TEST 库中,添加一个存储过程AddDept 。
全部代码如下:2.3.1. mdlPublic.basOption ExplicitPublic g_Cn As Connection '用于全局的数据连接'ActiveX DLL的启动程序,为DLL初始化时执行Public Sub Main()If ConnectToDatabase = False ThenErr.Raise vbObjectError + 1, , "连接数据库出错!"End IfEnd Sub'连接到数据库Public Function ConnectToDatabase() As BooleanOn Error GoTo ERR_CONNSet g_Cn = New Connection'设置服务器名称,数据库名称,登录名(此时假设密码为空)Dim ServerName As String, DBName As String, UserName As StringServerName = "gxc-notepad"DBName = "TEST"UserName = "sa"'连接到数据库With g_Cn.CursorLocation = adUseClient.CommandTimeout = 10.ConnectionString = "Provider=SQLOLEDB.1;Persist Security Info=True;User ID=" & UserName & ";Initial Catalog=" & DBName & ";Data Source=" & ServerName .OpenEnd WithConnectToDatabase = TrueExit FunctionERR_CONN:ConnectToDatabase = FalseEnd Function'去掉字符串中的单引号Public Function RealString(strOrigional) As StringRealString = Replace(strOrigional, "'", "")End Function'得到某个数据表中主键的下一个值,即当前主键值加1Public Function NextID(ByVal strTable As String, ByVal strID As String) As Long '两个参数分别是表的名称与主键的名称Dim rs As RecordsetSet rs = g_Cn.Execute("SELECT MAX(" & strID & ") FROM " & strTable)If IsNull(rs(0)) Then'如果值为NULL,则说明无任何数据记录,此时ID应为1NextID = 1Else'使新ID为最大ID值+1NextID = rs(0).Value + 1End IfEnd Function'查看某个数据表中,是否存在某个字段等于某个值的记录(整型)Public Function ExistByID(ByVal strTable As String, ByVal strID As String, ByVal lngID As Long) As Boolean'第一个参数为表名,第二个为字段名,第三个为具体的字段值Dim rs As RecordsetSet rs = g_Cn.Execute("Select Count(*) from " & strTable & " where " & strID & "=" & lngID)ExistByID = (rs(0).Value = 1)End Function'查看某个数据表中,是否存在某个字段等于某个值的记录(字符型)Public Function ExistByName(ByVal strTable As String, ByVal strFieldName As String, ByVal strName As String, ByVal ThisID As Long) As Boolean'第一个参数为表名,第二个为字段名,第三个为具体的字段值Dim rs As RecordsetSet rs = g_Cn.Execute("Select Count(*) from " & strTable & " where " & strFieldName & "='" & strName & "' and nID<>" & ThisID)ExistByName = (rs(0).Value = 1)End Function'以上两个函数实际上可以合并,本程序中为了说明问题,故而分开2.3.2. cPublic.clsOption Explicit'该类无实际意义,只为保存一些自定义枚举'自定义枚举,用于表示性别Public Enum gxcGenderMale = 1Female = 0End Enum''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''以下枚举用于“部门”对象的操作'用于表示部门删除结果的枚举Public Enum gxcDeleteDeleteOK = 0DeleteFail = 1 '未知原因导致不能删除DeleteSubExists = 2 '由于存在子部,因此不能删除DeleteEmpExists = 3 '该部门存在人员,不能删除End Enum'用于表示部门更新结果的枚举Public Enum gxcUpdateUpdateOK = 0UpdateFail = 1DuplicateName_Update = 2 '名字不可重复RecordNotExist = 3 '当前更新的记录已被其它客户端删除End Enum'用于表示部门新增结果的枚举Public Enum gxcAddNewAddNewOK = 0AddNewFail = 1DuplicateName_AddNew = 2 '名字不可重复SuperNotExist = 3 '指定的上级部门的ID不存在End Enum''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''2.3.3. cDept.clsOption ExplicitPrivate mvarDeptName As StringPrivate mvarID As LongPrivate mvarSuperID As Long'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''以下为部门的属性'上级部门IDPublic Property Let SuperID(ByVal vData As Long)mvarSuperID = vDataEnd PropertyPublic Property Get SuperID() As LongSuperID = mvarSuperIDEnd Property'本部门的IDPublic Property Let ID(ByVal vData As Long)mvarID = vDataEnd PropertyPublic Property Get ID() As LongID = mvarIDEnd Property'本部门的名称Public Property Let DeptName(ByVal vData As String)vData = Trim(vData) '去除两边的空格'控制名称的长度不可大于50If Len(vData) > 50 Then vData = Left(vData, 50)mvarDeptName = vDataEnd PropertyPublic Property Get DeptName() As StringDeptName = mvarDeptNameEnd Property'属性结束''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''以下为方法'新增一个部门,并返回操作的结果Public Function AddNew(Optional strName As String = "", _Optional lngSuperID As Long = -1) As gxcAddNew '根据传入的参数更新属性值On Error GoTo ERR_ADDNEW'如果参数被传入,则以传入的参数更新属性If strName <> "" Then Me.DeptName = strNameIf lngSuperID <> -1 Then Me.SuperID = lngSuperID '上级部门的ID'通过Command对象调用存储过程,由存储过程'进行添加部门的操作,并由存储过程返回操作结果Dim cmd As mandSet cmd = New mandWith cmdSet .ActiveConnection = g_Cn.CommandType = adCmdStoredProc '设置Command类型为“存储过程”.CommandText = "AddDept" '存储过程的名称'传入两个参数,分别为部门的名称与上级部门的ID.Parameters.Append .CreateParameter("@Name", adChar, adParamInput, 50, Me.DeptName).Parameters.Append .CreateParameter("@SuperID", adInteger, adParamInput, , Me.SuperID)'传入两个返回型的参数,分别返回新记录的ID与操作结果.Parameters.Append .CreateParameter("@ID", adInteger, adParamOutput).Parameters.Append .CreateParameter("@Return", adInteger, adParamOutput).ExecuteEnd WithDim RTN As gxcAddNewRTN = cmd.Parameters("@Return").Value '得到操作结果'如果操作成功,则给对象赋以ID值If RTN = AddNewOK Then Me.ID = cmd.Parameters("@ID").ValueAddNew = RTN '返回操作结果Set cmd = NothingExit FunctionERR_ADDNEW:'来到这里,则说明出错了If Not cmd Is Nothing Then Set cmd = NothingAddNew = AddNewFailEnd Function'修改部门信息,返回操作结果Public Function Update() As gxcUpdate'通过ID判断是否存在该记录,即该记录是否被其它客户端删除'如果不存在该记录,则返回相应的操作结果给调用者If Not ExistByID("tDept", "nID", Me.ID) ThenUpdate = RecordNotExistExit FunctionEnd If'通过名称判断是否存在相同名称的记录,如果存在相同的名称,'则返回调用者“存在相同名称”的信息If ExistByName("tDept", "DeptName", Me.DeptName, Me.ID) ThenUpdate = DuplicateName_UpdateExit FunctionEnd IfOn Error Resume NextDim strSQL As String'构造SQL语句,注意需调用RealString函数去除字符串中的单引号strSQL = "Update tDept Set DeptName='" & RealString(Me.DeptName) & "'," strSQL = strSQL & "SuperID=" & IIf(Me.SuperID = 0, "null", Me.SuperID) strSQL = strSQL & " where nID=" & Me.IDg_Cn.Execute strSQL '执行SQL语句'根据是否出错,返回给调用者相应的信息If Err.Number = 0 ThenUpdate = UpdateOKElseUpdate = UpdateFailEnd IfEnd Function'删除一个部门Public Function Delete(Optional ByVal lngID As Long = 0) As gxcDelete '如果调用该函数时传入了ID,则更新该对象的IDIf lngID <> 0 Then Me.ID = lngID'如果该部门下面有人员,则也不能删除If ExistByID("tEmployee", "DeptID", Me.ID) ThenDelete = DeleteEmpExistsExit FunctionEnd If'如果该部门下有子部门,则不能删除If ExistByID("tDept", "SuperID", Me.ID) ThenDelete = DeleteSubExistsExit FunctionEnd IfOn Error Resume Next'执行删除操作并返回操作结果g_Cn.Execute "Delete from tDept where nID=" & Me.IDDelete = IIf(Err.Number = 0, DeleteOK, DeleteFail)End Function'得到本部门的所有员工Public Function Employees() As cEmpsDim objEmps As New cEmps'调用cEmps类的Find方法,只传第三个参数,即“部门ID”Set Employees = objEmps.Find(, , Me.ID)End Function'得到本部门的所有子部门Public Function SubDepartments() As cDeptsDim objDepts As New cDepts'调用cDepts的Find方法,通过上级部门的ID查找Set SubDepartments = objDepts.Find(, Me.ID)End Function'得到本部门的上级部门,以对象返回Public Function SuperDepartment() As cDeptDim objDepts As New cDepts'调用cDepts的Find方法,将该类的“SuperID”作为查找条件'从而查找出其上级部门objDepts.Find Me.SuperIDIf objDepts.Count > 0 Then Set SuperDepartment = objDepts.Item(1) End Function'方法结束'''''''''''''''''''''''''''''''''''''''''''''''''''''''2.3.4. cDepts.clsOption ExplicitPrivate mCol As Collection'往集合中加入一个“部门”对象Public Sub Add(objDept As cDept)mCol.Add objDept, "A" & objDept.ID'在加入对象是,最好同时加入其“KEY”属性'“KEY”属性不可以是数字型,因此在前面随便加'一个字母,此处加了一个“A”End SubPublic Property Get Item(vntIndexKey As Variant) As cDeptSet Item = mCol(vntIndexKey)End PropertyPublic Property Get Count() As LongCount = mCol.CountEnd PropertyPublic Sub Remove(vntIndexKey As Variant)mCol.Remove vntIndexKeyEnd SubPublic Property Get NewEnum() As IUnknown'本属性允许用 For...Each 语法枚举该集合。