打印本文 打印本文  关闭窗口 关闭窗口  
用vfp建立3层客户服务器程序
作者:佚名  文章来源:不详  点击数  更新时间:2008/10/22 21:40:48  文章录入:杜斌  责任编辑:杜斌

  应用程序开发的一个伟大的事实是:对于每一件事情都可用多种方法来完成.建造应用程序时的最困难的事情是选择一种方法及确定它是否比其它的方法更好. 在客户服务器开发中,这种情况是你是在用两种数据库引擎进行处理, Visual FoxPro 在前端而一个强有力的 SQL 数据库引擎在后端.

  《整合 SQL Server 和 Visual FoxPro》一文探索了用 SQL pass-through. 创建两层客户服务器程序. 在本文中,用Visual FoxPro建立一个 OLE 服务器用于创建三层客户服务器程序. 添加一个中间层,给你以额外的地方来放置代码和执行验证.

  胖客户问题

  客户服务器应用程序开发者如何分离地处理前端和后端的困境是众所周知的. Visual FoxPro 和 SQL Server 两者都具有非常有力的数据库引擎. 两者都有有力的编程语言并有能力验证数据和强制商业规则.

  近来有很多关于工业上的胖客户和瘦客户以及它们对客户服务器应用程序开发的影响. 作为可用的前端开发工具已发展成熟,并有越来越依赖它们的倾向. 后端的负担并不轻,但也会造成问题.

  载入前端的第一个问题是需要一个更为有力的计算机. 当然如果你使用 Visual FoxPro 作为后端你可能需要至少 486 和 12 MB 内存. 这个问题是术语"胖客户"是如何产生的. 如果你正在选择要使用的后端,前端是当然要考虑的. 你会权衡 Visual FoxPro, Visual Basic, Access, Delphi, PowerBuilder, Java 以及其它所有编程语言的优缺点和资源需求. Java 是动人的,因为它提供真正的拥有足够能力的瘦客户前端承诺. Visual FoxPro 5.0 值得注目地比 3.0 减肥.

  前端执行大量处理的第二个问题是假如你的验证和商业规则发生变化你必须修改在前后中的代码. 这可能是或不是一个问题. 假设你正在开发 Visual FoxPro 前端, 你也要发生对 SQL Server 后端的修改. 如果你是验证和商业规则代码的编写者, 你大概会只写一次, 用 Visual FoxPro 或 SQL Server 储存过程或两者的组合. 如果某些方法必须修改,而你是修改者, 很大的问题的代码位于什么地方?

  在另一方面, 你可能编写前端而不写后端. 你可能不被允许编写你自己的 Select, Insert, Update 和Delete. 后端管理人可能限制你只能使用储存过程. 这种方法的好处是保证前端的正确执行. 如果只允许你通过储存过程访问数据, 你将被迫按正确的顺序执行. 例如, 如果你删除一个成员则不能忘记删除该成员的预约. 如果你添加一个新成员则不能忘记放入新的 ID 到 Adult 表中.

  如果你的前端只是一系列访问相同后端的前端的一个又会怎么样呢? 在这种情况下你会想要所有逻辑在后端上发生. 那种方法, 如果要进行任何修改,只需要修改一个地方.

  三层体系

  客户服务器开发最近的一个进步是三层体系方案. 传统的客户服务器是两层的, 由一个客户和一个服务器组成. 正如上面的讨论,如果太多的处理集中在客户或服务器上, 可能会有潜在的问题. 三层体系引入一个中间层, 用于减轻两端的负担.

  在一个典型的三层体系中, 如下表所示, 各层负责提供一个服务. 客户提供用户服务, 它主要由用户界面组成. 服务器提供数据服务, 由数据和维护访问方法组成. 中间层提供商业服务, 由数据验证和强制商业方法组成

  表1: 分层应用程序体系

  要充分认识到该结构的潜力, 你应该使中间层易于访问多个前端. 如果它强制你规则,则所有客户需要与它交谈. 不管客户是用 Visual FoxPro,Visual Basic 还是其它语言写成,这都是事实.

  中间层将会运行于不同的机器上. 这允许它可被多个客户访问,而且它提供最佳性能. 记住, 胖客户问题是客户机器负担过重. 在一个两层结构中你可能必须为每一个客户提供 16 MB (功更多) 内存的 Pentium 机器. 在一个三层结构中你可以将中间层放在一个 32 MB 双处理器 Pentium 机器,并愉快地在 12 MB 内存的486 机器上运行客户程序.

  至少可以用两种方法来设置中间层. 一种方法是用一个 SQL Server 机器作为服务器和中间层. 本文中使用的方法是使用一个在 Visual FoxPro 中创建的 OLE server 作为中间层. OLE server 从 Visual FoxPro 中调用, 或对于任何 OLE 控制器, 使用 CreateObject() 函数, 与你调用 Excel 或 Word 作为 OLE Automation 服务器一样.

  创建Visual FoxPro OLE 服务程序

  如果你还不知道如何在 Visual FoxPro 中创建一个 OLE 服务器请参阅《Visual FoxPro 作为 OLE Automation Servers》 和《自定义 OLE Servers》. 你也可以从 Visual FoxPro 联机帮助文档中 (Visual FoxPro 开发指南第十六章)知道如何做.

  在 Win32 的世界中, OLE 服务器可以作为 InProc 或 OutOfProc 服务器创建(Windows 95 和 Windows NT) ,它是一个运行应用程序的处理并可以寻址到 4 GB 的内存, 一半是用于保存处理代码,另一半是系统使用. InProc 服务器是编译成 DLL 并运行在调用它的过程的相同的地址空间. OutOfProc 服务器是编译成 EXE 文件并作为单独的过程运行. InProc 服务器可提供最佳司长能,因为过程中通信比内部通信慢. 在另一方面, 由于 OutOfProc 服务器运行在一个单独的过程中,你可以利用多处理器的优势.

  OLE 服务器也可在本地或远程运行. 典型地一个远程 OLE 服务器是一个 OutOfProc 服务器并位于超级计算机上. 这允许任意多个客户机器访问它. Remote Automation Connection Manager 可用于配置服务器和客户机器来允许远程自动控制.

  示例数据

  本文使用的示例数据是来自于 SQL Server 6.5 的图书馆应用程序. 图书馆应用程序想要保持对其成员、图书和借阅的跟踪.

  表结构

  图书馆应用程序中的主要表之一是 Member 表, 图书馆中的每一位成员在其中有一条记录. 这里一个有趣的手法是少年必须要有成年人担保才能成为图书馆成员. 大概由于少年会在相同的地方成长为成年人, 因此有两个单独的表 Adult(成年) 和 Juvenile(少年). 这样节约了磁盘空间,因为一但你知道了成人的地址则所有少年的地址信息是多余的. 另外, 少年的期满日期与成人相同. 进一步说, 你不用在意成人的出生日期,你只需注意少年的出生日期及何时满18 岁变为成人(至少在表中要反映出来!).

  以下代码显示了用于创建 Member, Adult 和 Juvenile 表的SQL Server 语句:

  CREATE TABLE member

  ( member_no member_no NOT NULL identity(1,1),

  lastname shortstring NOT NULL ,

  firstname shortstring NOT NULL ,

  middleinitial letter NULL ,

  photograph IMAGE NULL )

  CREATE TABLE adult

  ( member_no member_no NOT NULL ,

  street shortstring NOT NULL ,

  city shortstring NOT NULL ,

  state statecode NOT NULL ,

  zip zipcode NOT NULL ,

  phone_no phonenumber NULL ,

  expr_date DATETIME NOT NULL )

  CREATE TABLE juvenile

  ( member_no member_no NOT NULL ,

  adult_member_no member_no NOT NULL ,

  birth_date DATETIME NOT NULL )

  Member 表中的 member_no 字段在添加新记录时会由 SQL Server 自动生成。该字段是一个 Identity 列。起始值为 1 ,增量值也是 1。这样在表中输入的第一条记录的 member_no 值就是 1。对于后来插入到表中的记录 member_no 的值自动增加 1。当添加一条记录时如果客户没有指定 member_no 的值。SQL Server 自动维护它并询问客户使用什么值。

  在 Adult 和 Juvenile 表中的 member_no 不是 Identity 列。这些记录中的值必须与 Member 表中相应的 member_no 值相匹配。当新记录添加到图书馆库时,一个记录首先会添加到 Member 表中。SQL Server 的全局变量 @@Identity 包含了自动生成的 member_no。然后添加到 Adult 或 Juvenile 表中的记录的 member_no 值将使用 @@Identity 中的值。

  定义参照完整性

  在早期版本的 SQL Server 参照完整性是通过使用触发器强制执行,这与 Visual FoxPro 强制参照完整性相同。SQL Server 6.0 添加了可申明的参照完整性,这允许你定义你自己的作为数据结构一部分的参照完整性规则。第一步是在各表中创建基本关键字约束,如以下代码所示:

  ALTER TABLE member

  ADD constraint member_ident PRIMARY KEY clustered

  (member_no)

  ALTER TABLE adult

  ADD constraint adult_ident PRIMARY KEY clustered

  (member_no)

  ALTER TABLE juvenile

  ADD constraint juvenile_ident PRIMARY KEY clustered

  (member_no)

  基本关键字约束创建一个唯一索引,用于强制 member_no 的唯一性。在示例中创建一组索引用于对数据进行物理排序。

  定义可申明的参照完整性的第二步是在相关表之间创建外部关键字约束,如以下代码所示:

  ALTER TABLE adult

  ADD CONSTRAINT adult_member_link FOREIGN KEY (member_no)

  REFERENCES member (member_no)

  ALTER TABLE juvenile

  ADD CONSTRAINT juvenile_member_link FOREIGN KEY

  (member_no) REFERENCES member (member_no)

  ALTER TABLE juvenile

  ADD CONSTRAINT juvenile_adult_link FOREIGN KEY

  (adult_member_no) REFERENCES adult (member_no)

  第一个 Alter Table 定义了一个 Member 和 Adult 表之间的关系。这是一个一对一关系,虽然这里没有代码指明或强制是这种类型的关系。第二个 Alter Table 在 Member 和 Juvenile 表部定义了一个关系。最后一个 Alter Table 在 Adult 和 Juvenile 表之间定义一个关系。这是一个一对多关系。

  要意识到 SQL Server 当前不支持级联更新或删除。如果你想那样做就应该用触发器代替约束。

  图书馆 OLE 服务程序使用 SQL pass-through 来与 SQL Server 图书馆数据库交谈. 服务器将包含采取行动(如获取一个成员的信息,添加一个新成员修改成员的信息和删除一个成员)的方法. 服务器提供少量的属性, 包括发生了何种错误的属性.

  OLE 服务程序项目是 LIBRARYSERVER.PJX. 该项目包含一个自定义类叫做Members. 该类已被识别为OLE Public (要这样做,在类信息对话框中复选适当的复选项).

  Members 类的成员和属性, 分别在表1 和 表 2中列出.

  方法 可视度 描述

  InitConnection 公共 初始化与SQL Server 的连接

  CloseConnection 公共 关闭与SQL Server 的连接

  GetMember 公共 接收一个成员的信息

  AddMember 公共 添加一个新成员

  UpdateMember 公共 更新一个成员的信息

  RemoveMember 公共 移除一个成员

  SetError 受保护 保存 SQL Server 错误

  Convert 受保护 转换值到串

  表 2: 图书馆 OLE 服务程序属性

  属性 可视度 描述

  NHandle 公共 连接句柄

  NewID 公共 新添加的成员的ID

  LastErrDesc 公共 最后出现的错误的描述

  在图3显示的项目信息对话框中, 项目的名称为 VFPLibraryOLEServer. 结合类名 Members, 就产生了OLE 名称 VFPLibraryOLEServer.Members. 这可以在注册表的 HKEY_CLASSES_ROOT 中找到, 如图4所示.

  OLE 服务程序是在生成时自动注册到机器上. 如果你把它移动到另一台机器上可以用 REGSVR32.EXE 注册它, REGSVR32.EXE 可以在 Windows 95 下的 System 目录和 Windows NT 下的 System32 目录中找到.

  要在客户应用程序上使用 OLE 服务程序,应使用下面这样的代码.

  oLibrary = CreateObject("VFPLibraryOLEServer.Members")

  当 Visual FoxPro 遇到该代码时它首先检查对象名是否是一个 Visual FoxPro 类. 如果不是, 则检查注册表. 当找到对象名和对象的唯一标识符 CLSID, is noted.

  也是在注册表的 HKEY_CLASSES_ROOT 中有一个名为 CLSID 的键值, 它包含一个所有具有 CLSID 的对象的入口. 就象你在图5中看到的一样, 注册表保存着与可执行文件相关的 CLSID. 这就是为什么当 Visual FoxPro 执行它的 CreateObject 命令时 Windows 知道如何启动 OLE 服务程序.

  使用图书馆 OLE 服务程序

  表单 MEMBOLEFP.SCX 使用 Visual FoxPro 图书馆OLE 服务程序作为中间层来与 SQL Server 图书馆数据库图书馆. 在调用服务程序后表单调用方法来获取成员信息, 删除和更新成员等.

  载入表单

  要调用 OLE 服务程序, 表单使用与调用 Excel 或 Word 相同的CreateObject() 函数. 出现在Windows 注册表中的 OLE 程序程序的正式的注册名是 vfpLibraryOLEServer.Members. 以下是表单的 Load 事件中的代码.

  THIS.olibrary = CREATEOBJECT("vfpLibraryOLEServer.Members")

  IF TYPE("This.oLibrary") # "O" OR ISNULL(THIS.olibrary)

  = MESSAGEBOX("Couldn't set up the OLE Server.", mb_iconinformation)

  THIS.lconnected = .F.

  RETURN

  ELSE

  IF THIS.olibrary.initconnection("robertg","vfpguru")

  = MESSAGEBOX("Welcome to the library", mb_iconinformation)

  THISFORM.lconnected = .T.

  ELSE

  = MESSAGEBOX("Access to the library denied", mb_iconinformation)

  THIS.lconnected = .F.

  RETURN

  ENDIF

  ENDIF

  这里的代码试着创建一个 OLE 服务程序的引用. 如果成功, 表单属性 oLibrary 将包含一个 OLE 服务程序的引用. 如果引用不能创建则剩下的事不会继续. 如果对象可以创建, 表单将调用 OLE 服务程序的 InitConnection 方法来试图登录到 SQL Server. 如果失败, 则什么也不做.

  如果连接成功, 表单创建一个游标然后设置开放式行缓存. 游标将用于添加和修改, 并允许你使用你熟悉的数据输入函数如 GetFldState() 和 OldVal(), 你将在稍后看到.

  SET MULTILOCKS ON

  CREATE CURSOR c_member ;

  ( member_no i, firstname c(50), middleinitial c(1), ;

  lastname c(50), street c(50), city c(50), state c(10), ;

  zip c(10), phone_no c(20), expr_date T, ;

  birth_date T, adult_member_no i )

  = CURSORSETPROP("Buffering", db_bufoptrecord, 'c_member')

  连接到 SQL SERVER

  OLE 的 InitConnection 方法用用户 ID 和口令作为参数然后用 SQLConnect() 来连接到 SQL Server. 如果连接成功该方法返回 .T., 否则返回 .F..

  lParameters cUserID, cPassword

  This.nHandle = SQLConnect('Library', cUserID, cPassword)

  Return Iif(This.nHandle < 0, .F., .T.)

  定位成员

  OLE 服务程序的 GetMember 方法用于获取一个成员信息. 在《整合 SQL Server 和 Visual FoxPro》一章中的 SQL pass-through 示例中的示例表单使用 SQLExec() 来运行一条 Select 语句或执行一个储存过程. 两种情况下结果将作为一个游标返回, 该游标将用于表单中的数据操作.

  当使用特别用于客户的 OLE 服务程序时(包括 Access 和 Visual Basic), 生活不是这么简单了. 因此它不会简单地在一个 Visual FoxPro 游标中返回结果.

  OLE 服务程序的 GetMember 方法接受三个参数: 一个需要获取的数据的列表的串, 你要用作为分隔符的字符和你想要获取的成员的 ID . 表单的 Locate 按钮的 Click 事件调用 OLE 服务程序的 GetMember 方法.

  该串是按引用传递, 因为 GetMember 将用成员信息填充它. 分隔符字符将用于分隔信息中的字段. 如果 GetMember 返回 0, 没有所提供的 ID 成员. 如果它返回一个负数, 则发生了错误, 该错误信息保存在 OLE 服务程序的 LastErrDesc 属性中.

  nretval = THISFORM.olibrary.getmember(@lcmemberinfo, "|", ;

  THISFORM.txtmemberid.value)

  IF nretval < 0

  lcmessage = THISFORM.olibrary.lasterrdesc

  = MESSAGEBOX(SUBSTR(lcmessage, RAT(']',lcmessage)+1), ;

  mb_iconinformation)

  <省略的代码>

  IF nretval = 0

  = MESSAGEBOX("There is no member with this ID.", ;

  mb_iconinformation)

  <省略的代码>

  如果找到了成员信息, 就需要分解中来读取信息中的各个字段值. 表单的 Load 方法中创建的游标用这些信息填充且表单显示被刷新.

  FOR i = 1 TO 10

  j = ALLTRIM(STR(i))

  npipe&j = AT("|", lcmemberinfo, i)

  NEXT

  npipe11 = LEN(lcmemberinfo)

  SELECT c_member

  APPEND BLANK

  REPLACE firstname WITH SUBSTR(lcmemberinfo, 1, npipe1 - 1), ;

  middleinitial WITH SUBSTR(lcmemberinfo, npipe1 + 1, ;

  npipe2 - npipe1 - 1), ;

  lastname WITH SUBSTR(lcmemberinfo, npipe2 + 1, ;

  npipe3 - npipe2 - 1), ;

  street WITH SUBSTR(lcmemberinfo, npipe3 + 1, ;

  npipe4 - npipe3 - 1), ;

  city WITH SUBSTR(lcmemberinfo, npipe4 + 1, ;

  npipe5 - npipe4 - 1), ;

  state WITH SUBSTR(lcmemberinfo, npipe5 + 1, ;

  npipe6 - npipe5 - 1), ;

  zip WITH SUBSTR(lcmemberinfo, npipe6 + 1, ;

  npipe7 - npipe6 - 1), ;

  phone_no WITH SUBSTR(lcmemberinfo, npipe7 + 1, ;

  npipe8 - npipe7 - 1), ;

  expr_date WITH CTOT(SUBSTR(lcmemberinfo, npipe8 + 1, ;

  npipe9 - npipe8 - 1)), ;

  birth_date WITH CTOT(SUBSTR(lcmemberinfo, npipe9 + 1, ;

  npipe10 - npipe9 - 1)), ;

  adult_member_no WITH VAL(SUBSTR(lcmemberinfo, npipe10 + 1, ;

  npipe11 - npipe10 - 1))

  <省略的代码>

  THISFORM.REFRESH

  <省略的代码>

  在 For loop 中的代码检查各管道分隔符的位置. 第一个分隔符前的是该成员的 first name. 第一个分隔符和第二个分隔符间的东西是成员姓名的中间大写字母. 第一个分隔符和第二个分隔符间的东西是成员的 last name. 等等.

  OLE 服务程序的GetMember 方法

  正如上面所提及, OLE 服务程序的 GetMember 方法接受三个参数, 一个包含成员信息的分隔符分隔的列表, 分隔符和所需成员的成员 ID. 构造一个 Select 语句并发送到 SQL Server 来获取信息. 如果找到成员, 串被创建. 由于是按引用传递它将被发送回表单. 如果成员没有找到 GetMember 方法自己返回 0 , 否则返回 1.

  LPARAMETERS cmemberinfo, cdelimiter, csearchid

  lcsql = "Select firstname, middleinitial, lastname, street, " + ;

  " city, state, zip, phone_no, expr_date, " + ;

  " birth_date = null, adult_member_no = null " + ;

  "from member, adult " + ;

  "where member.member_no = adult.member_no " + ;

  " and member.member_no = " + ALLTRIM(csearchid) + " " + ;

  "union " + ;

  "select firstname, middleinitial, lastname, street, " + ;

  " city, state, zip, phone_no, expr_date, " + ;

  " birth_date, adult_member_no " + ;

  "from member, adult, juvenile " + ;

  "where member.member_no = juvenile.member_no " + ;

  " and adult.member_no = juvenile.adult_member_no " + ;

  " and member.member_no = " + ALLTRIM(csearchid)

  <省略的代码>

  IF RECCOUNT("c_member") = 0

  RETURN 0

  ELSE

  cmemberinfo = ALLTRIM(THIS.convert(c_member.firstname)) + ; +

  cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.middleinitial)) + ;

  cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.lastname)) + ;

  cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.street)) + ;

  cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.city)) + cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.state)) + ;

  cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.zip)) + cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.phone_no)) + ;

  cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.expr_date)) + ;

  cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.birth_date)) + ;

  cdelimiter + ;

  ALLTRIM(THIS.convert(c_member.adult_member_no)) + ;

  cdelimiter

  RETURN RECCOUNT("c_member")

  ENDIF

  添加一个Adult

  OLE 服务程序的 AddMember 方法用于添加一个成员到库中. 为了易于使用该方法使用一个两维数组作为参数. 第一列包含字段名第二行包含相关信息. 表单的 AddMember 方法在 Save 按钮的 Click 事件中调用并依次调用OLE 服务程序的 GetMember 方法.

  DIMENSION lamember[11,2]

  lamember[1,1] = "firstname"

  lamember[2,1] = "middleinitial"

  lamember[3,1] = "lastname"

  lamember[4,1] = "street"

  lamember[5,1] = "city"

  lamember[6,1] = "state"

  lamember[7,1] = "zip"

  lamember[8,1] = "phone_no"

  lamember[9,1] = "expr_date"

  lamember[10,1] = "birth_date"

  lamember[11,1] = "adult_member_no"

  * 一些数据保留为空或被服务器改写

  lamember[1,2] = ALLTRIM(THISFORM.txtfirstname.value)

  lamember[2,2] = ALLTRIM(THISFORM.txtmiddleinitial.value)

  lamember[3,2] = ALLTRIM(THISFORM.txtlastname.value)

  lamember[4,2] = ALLTRIM(THISFORM.txtstreet.value)

  lamember[5,2] = ALLTRIM(THISFORM.txtcity.value)

  lamember[6,2] = ALLTRIM(THISFORM.txtstate.value)

  lamember[7,2] = ALLTRIM(THISFORM.txtzip.value)

  lamember[8,2] = ALLTRIM(THISFORM.txtphonenumber.value)

  IF THISFORM.olibrary.addmember(@lamember) < 0

  lcmessage = THISFORM.olibrary.lasterrdesc

  = MESSAGEBOX(SUBSTR(lcmessage, RAT(']',lcmessage)+1), ;

  mb_iconinformation)

  ELSE

  = MESSAGEBOX("This member has been added.", mb_iconinformation)

  * 找到新成员的 member_no

  THISFORM.txtmemberid.value = ALLTRIM(STR(THISFORM.olibrary.newid))

  <省略的代码>

  数组的第一列 laMember 包含字段的名字. 第二列包含被表单上的控件使用的实际的数据. 数组按引用传递以确保它到达 AddMember 方法, 如果成员成功地添加了 AddMember 返回 1否则返回 -1. 如果添加不成功, OLE 服务程序的 LastErrDesc 属性包含错误信息.

  OLE 服务程序的 AddMember 方法

  正如上面所提及, OLE 服务程序的 AddMember 方法接受一个参数, 一个字段名和值的数组.

  LParameters aMemberInfo

  添加一个成员到图书馆数据库是一个两步过程. 首先一个新的包括成员名字的记录添加到 Member 表. 该表中的 member_no 字段已经分派了 Identity 属性且 SQL Server 自动决定下一个要使用的值. 其次是添加一个相应的包含成员地址的记录到 Adult 表. 相同的 member_no 必须两个表中以维护一对一的关系.

  两步都需要成为事务处理的一部分, 因此要么两个表的记录都可以正确地加入到表中, 要么都不加入进去.

  = SQLSetProp(This.nHandle, "Transactions", 2)

  接着, 一条针对于 Member 表的Insert 语句被生成并发送到 SQL Server. 如果 Insert 失败则回滚事务处理且 AddMember 方法返回-1, 提示用户插入失败.

  csql = "Insert member (firstname, middleinitial, lastname, " + ;

  "photograph) " + ;

  "values ('" + ALLTRIM(amemberinfo[1,2]) + "', " + ;

  "'" + ALLTRIM(amemberinfo[2,2]) + "', " + ;

  "'" + ALLTRIM(amemberinfo[3,2]) + "', " + ;

  "null)"

  IF sqlexec(THIS.nhandle, csql) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  SQL Server 的全局变量 @@Identity 保存着最近插入到 Identity 列的值. 在此情况下它包含着新的用户的 member_no. 该数值保存在 OLE server 的 NewID 属性中.

  IF sqlexec(THIS.nhandle, "Select @@identity") < 0

  THIS.seterror

  *回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  THIS.newid = sqlresult.EXP

  接着, 添加Adult 表中的相关记录. member_no 的值来自 @@Identity. 正如前面所说, 如果插入失败, 回滚整个事务处理且AddMember 返回-1.

  csql = "Insert adult (member_no, street, city, state, zip, " + ;

  "phone_no, expr_date) " + ;

  "values (" + ALLTRIM(STR(THIS.newid)) + ", " + ;

  "'" + ALLTRIM(amemberinfo[4,2]) + "', " + ;

  "'" + ALLTRIM(amemberinfo[5,2]) + "', " + ;

  "'" + ALLTRIM(amemberinfo[6,2]) + "', " + ;

  "'" + ALLTRIM(amemberinfo[7,2]) + "', " + ;

  "'" + ALLTRIM(amemberinfo[8,2]) + "', " + ;

  "'" + TTOC(DTOT(GOMONTH(DATE(),12))) + "' )"

  IF sqlexec(THIS.nhandle, csql) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  最后一步是试着提交事务处理. 如果失败, 回滚一切东西. 如果提交成功, 新的成员已经添加且 AddMember 返回 1, 指明操作成功.

  IF SQLCOMMIT(THIS.nhandle) < 0

  THIS.seterror

  * Rollback the transaction

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ELSE

  RETURN 1

  ENDIF

  保存修改

  表单的 UpdateMember 方法调用 OLE server 的 UpdateMember 方法. 表单传递到 OLE server 一个二维数组和成员的 ID. 如同上面的 AddMember 方法一样, 数组的第一列包含要更新的字段名, 第二列包含一个特定成员的新信息.

  当一个成员添加后, 数组中包括记录中的各字段. 但在此情况下, 它将只包含字段值被修改了的记录. 没有办法让 SQL Server 更新没有修改的信息.

  表单搜索各控件来看它的值是否被修改. 如果是, 添加一行到数组中. 注意使用了 OldVal() 来查看字段的值是否改变了. 使用缓存了的游标使这样做成为可能.

  i = 0

  IF c_member.firstname <> OLDVAL("c_member.firstname")

  i = i + 1

  DIMENSION lamember[i, 2]

  lamember[i,1] = "firstname"

  lamember[i,2] = ALLTRIM(THISFORM.txtfirstname.value)

  ENDIF

  IF c_member.lastname <> OLDVAL("c_member.lastname")

  i = i + 1

  DIMENSION lamember[i, 2]

  lamember[i,1] = "lastname"

  lamember[i,2] = ALLTRIM(THISFORM.txtlastname.value)

  ENDIF

  <省略的代码>

  IF no FIELDS were changed, there IS nothing TO DO AND therefore no POINT IN bothering SQL SERVER.

  IF i = 0

  = MESSAGEBOX("没有要保存的东西.", mb_iconinformation)

  RETURN

  ENDIF

  如果有被修改了的字段, 表单调用 OLE server 的 UpdateMember 方法. 向它传递数组及成员的 ID. 如果 UpdateMember 返回 -1 则更新失败并显示失败的原因给用户.

  IF THISFORM.olibrary.updatemember(@lamember, ;

  THISFORM.txtmemberid.value)<0

  lcmessage = THISFORM.olibrary.lasterrdesc

  = MESSAGEBOX(SUBSTR(lcmessage, RAT(']',lcmessage)+1), ;

  mb_iconinformation)

  ELSE

  = MESSAGEBOX("该成员信息已保存.", mb_iconinformation)

  <省略的代码>

  OLE 服务程序的UpdateMember 方法

  正如前面所提及, OLE server 的 UpdateMember 方法接受两个参数, 字段名和数据数组及要更新的成员的ID.

  LParameters aMemberInfo, cSearchID

  就象添加一个成员一样, 要求两个插入语句封装到一个事务处理中, 更新一个成员要求两个 Update 封装在一个事务处理中. 要创建各 Update 语句, UpdateMember 搜索数组中的各字段. 例如, 如果在数组中找到"firstname" 且它是在第一列, Member 表中的 firstname 字段将被更新. 构造两个串, cSQL1 和 cSQL2, 各串包含一部分 Update 语句.

  csql1 = ""

  npos = ASCAN(amemberinfo, "firstname")

  IF MOD(npos, 2) = 1

  csql1 = csql1 + "firstname = '" + amemberinfo[nPos + 1] + "', "

  ENDIF

  npos = ASCAN(amemberinfo, "lastname")

  IF MOD(npos, 2) = 1

  csql1 = csql1 + "lastname = '" + amemberinfo[nPos + 1] + "', "

  ENDIF

  <省略的代码>

  csql2 = ""

  npos = ASCAN(amemberinfo, "street")

  IF MOD(npos, 2) = 1

  csql2 = csql2 + "street = '" + amemberinfo[nPos + 1] + "', "

  ENDIF

  npos = ASCAN(amemberinfo, "city")

  IF MOD(npos, 2) = 1

  csql2 = csql2 + "city = '" + amemberinfo[nPos + 1] + "', "

  ENDIF

  然后开始一个事务处理.

  = SQLSetProp(This.nHandle, "Transactions", 2)

  如果所有东西都保存到变量 cSQL1 中, 接着更新成员表的 Update 语句被创建且传递到 SQL Server. 如果失败则回滚事务处理.

  IF LEN(csql1) > 0

  * Add the Update, strip off the last comma, add a Where clause

  csql1 = "Update member Set " + LEFT(csql1, LEN(csql1) - 2) + ;

  "where member_no = " + ALLTRIM(csearchid)

  IF sqlexec(THIS.nhandle, csql1) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  ENDIF

  同样, 如果所有东西已经保存到变量 cSQL2 中, 接着创建更新 Adult 表的 Update 语句并传递到 SQL Server. 如果失败则回滚事务处理.

  IF LEN(csql2) > 0

  * 添加 Update, 去掉最后的逗号, 添加一个Where 子句

  csql2 = "Update adult Set " + LEFT(csql2, LEN(csql2) - 2) + ;

  "where member_no = " + ALLTRIM(csearchid)

  IF sqlexec(THIS.nhandle, csql2) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  ENDIF

  最后一步是提交事务处理. 如果提交失败则回滚事务处理.

  IF SQLCOMMIT(THIS.nhandle) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ELSE

  RETURN 1

  ENDIF

  删除一个成员

  OLE Server 的 RemoveMember 方法用于删除一个成员. 该方法接受一个参数:要删除的成员的 ID. 如果删除成功 RemoveMember 返回 1 否则返回 -1, OLE server 的属性 LastErrDesc 包含着失败的理由. 如果违反了参照完整性删除将会失败, 例如一个未清借出或活动的少年的成员是不能被删除的. 也可能因为SQLExec() 执行失败而造成删除失败. 无论是什么原因造成删除失败, 表单代码从 oLibrary.LastErrDesc 获取失败原因并可以显示给用户.

  IF THISFORM.olibrary.removemember(THISFORM.txtmemberid.value) < 0

  lcmessage = THISFORM.olibrary.lasterrdesc

  = MESSAGEBOX(SUBSTR(lcmessage, RAT(']',lcmessage)+1), ;

  mb_iconinformation)

  ELSE

  = MESSAGEBOX("成员已经删除.", mb_iconinformation)

  <省略的代码>

  OLE 服务程序的 RemoveMember 方法

  OLE server 的 RemoveMember 方法接受一个参数, 想要删除的成员的成员 ID.

  LParameters cSearchID

  有多种理由会使删除失败. 以这进而使用的数据来说最通常的原因是成员有未结清的借书或是一个活动的少年的保证人. 如果其中任一理由为真则删除将失败.

  RemoveMember 方法代码可以写来试着删除并查看是否因为这两个理由失败来看监视删除的失败. 但这样做的效率不高, 特别是在要首先检查比较这两个条件时. 如果成员未结清借书或是一个少年的保证人则没必须尝试删除. 这样做更容易捕捉错误并节约 SQL Server 上的处理.

  * 首先检查该成员是否还有未还清的借书

  csql = "Select member_no From loan Where member_no = " + ;

  ALLTRIM(csearchid)

  IF sqlexec(THIS.nhandle, csql) < 0

  THIS.seterror

  RETURN -1

  ELSE

  IF RECCOUNT("sqlresult") <> 0

  THIS.lasterrdesc = "该成员不能删除. " + ;

  "他/她还有借书未还清."

  RETURN -1

  ENDIF

  ENDIF

  * 现在检查这是不是一个活动的少年的保证人

  csql = "Select member_no From juvenile Where adult_member_no = " + ;

  ALLTRIM(csearchid)

  IF sqlexec(THIS.nhandle, csql) < 0

  THIS.seterror

  RETURN -1

  ELSE

  IF RECCOUNT("sqlresult") <> 0

  THIS.lasterrdesc = "该成员不能删除. " + ;

  "他/她是一个活动的少年的保证人."

  RETURN -1

  ENDIF

  ENDIF

  如果有其它的事要检查, 检查代码可以接着上面的代码后面. 如果没有其它的问题, 开始一个事务处理度处理删除.

  = SQLSetProp(This.nHandle, "Transactions", 2)

  一个成员可能在Loanhist 表中有相关记录,每一次归还了的借书都在其中有一条记录, 或在 Reservation 表中,每一本预约的图书在其中有一条记录. 首先删除这些相关记录. 如果不这样做, 在删除成员时将会违反参照完整性. 如果其中任一删除失败则回滚事务处理.

  * 删除该成员的借书历史记录

  csql = "Delete loanhist Where member_no = " + ALLTRIM(csearchid)

  IF sqlexec(THIS.nhandle, csql) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  * 删除该成员的预约记录

  csql = "Delete reservation Where member_no = " + ALLTRIM(csearchid)

  IF sqlexec(THIS.nhandle, csql) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  所有其它相关记录也将在此时删除. 一但所有相关记录删除了, 就可以删除成员了, 首先从 Adult 表然后从 Member 表中删除一个成员. Delete 必须按该顺序以保护参照完整性. 再说一遍, 如果任一删除失败, 回滚事务处理

  * 删除 Adult 表中成员的记录

  csql = "Delete adult Where member_no = " + ALLTRIM(csearchid)

  IF sqlexec(THIS.nhandle, csql) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  * 删除 Member 表中成员的记录

  csql = "Delete member Where member_no = " + ALLTRIM(csearchid)

  IF sqlexec(THIS.nhandle, csql) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ENDIF

  如果不熟悉下一部分, 你不是没有读前面部份的这种注释就是没有睡醒. 如果成功就提交, 失败就回滚. 如果删除失败 RemoveMember 方法返回-1 否则返回1.

  IF SQLCOMMIT(THIS.nhandle) < 0

  THIS.seterror

  * 回滚事务处理

  = SQLROLLBACK(THIS.nhandle)

  RETURN -1

  ELSE

  RETURN 1

  ENDIF

  保存 SQL SERVER 错误

  OLE server 的 SetError 方法保存 SQL Server 错误到 LastErrDesc 属性中, 这样该属性可以在客户机上获得.

  lnError = AError(laError)

  This.LastErrDesc = Substr(laError[3], RAt(')',laError[3]) + 1)

  OLE 服务程序的编程黑盒

  文章 BKO02 包括了使用视图和 SQL pass-through 来创建两层客户服务器应用程序. 示例之一使用 SQL Server 储存过程来添加, 更新和删除成员. 该技术的一个优势是处理发生在服务器上而不是客户机上. 这不仅避免了胖客户问题也可以避免开发者弄乱数据. 例如, 如果你所做的是调用一个储存过程, 你不会忘记删除成员的相关借书历史记录.

  使用三层体系与使用储存过程用术语来说是, 在编码困难程度(低)和你保留的控制等级(也属于低)上非常相似. 例如, 调用储存过程来删除一个成员的代码是

  If SQLExec(ThisForm.nHandle, "Execute RemoveMember " + ;

  ThisForm.txtMemberID.value) < 0

  调用等价的OLE server 方法代码是

  If ThisForm.oLibrary.RemoveMember(ThisForm.txtMemberID.value) < 0

  在两种情况下所有的开发者都需要知道的是方法或过程的名字, 需要什么参数及返回什么. SQL Server 储存过程和OLE server 方法都成了黑盒.

  某些只是管理前端的人不需要知道或关心相对于后端的中间层中有些什么. 他或她可能只想知道如何调用方法和如何知道调用是否成员. 这使得高速的后端处于高速的开发中.

  胖在何处?

  在来自 BKO02 SQLExec() 中的一个示例中使用了来自客户的表单来处理所有的与 SQL Server 的通信. Selects, Inserts, Updates 和 Deletes 被发送且结果按需要操作.除真正的实际数据管理外的大多数处理是在客户端完成的. 这里的风险是以胖客户终结.

  在另一个来自 BKO02 SQLExec() 中的示例使用了调用位于 SQL Server 上的储存过程. 在客户机上没有做什么事, 除了管理一个每次填入一条记录的本地游标外. 数据操作和数据验证都在服务器上执行. 这里的风险是以胖服务器终结.

  在本文中使用的示例中,前端除了调用 OLE server 的方法外什么也没做. 事实上, 在这个无可否认的简单的示例中, 中间层与第一个SQLExec() 惊人地相似. 实际上, 许多 OLE server 的代码取自表单MEMBEXEC.SCX.

  如果所有数据验证和商业规则检查放到中间层中会不会造成一个胖中间层问题? 是不是胖问题只是上移了一个层次? 固然, OLE server 也可能超载而不能令人满意地执行它的任务. 但是, 由于它是运行在一个机器上, 因此可以比胖客户更容易通过硬件来解决. 对于一个相对内存较少的机器你可以买一个或两个以上的处理器和16 或 32 MB 以上的内存用于中间层机器, 而且这样会令人信服地看到性能问题的消失. 当然, 这仅限于机器的处理能力成为瓶颈的时候.

  重要的一点是这是因为 Visual FoxPro 和 SQL Server, 如果是 Oracle 等具有数据引擎能力和语言能力的其它语言, 你可以很好的调整胖的问题. 你可以决定代码放在何处和在何处放置处理负担而且仔细的调整它们直到性能达到满意程序.

  什么时候需要这样做?

  要创建一个 Visual FoxPro OLE server 并不困难. 要从一个表单中或其它代码中调用一个 OLE server 的方法也不困难. 设置远程自动控制也不困难. 因此用三层体系写一个客户服务器应用程序只比使用两层体系稍稍困难些许.

  因此你为什么要这样做? 什么时候你需要上移到三层体系. 毫无疑问, 性能是一个原因. 在你的客户机器动力不足时这样做, 依靠移动巨大的代码块到一个更有力的中间层盒子中, 你可以引人注目改进性能.

  这有多大的可能性? Visual FoxPro 版本 5.0 的资源要求降低了这是事实, 你任然需要至少 486 加 12 MB 内存. Pentiums 加 32 MB 内存的机器目前的价格是 $2000 美元. 考虑一个公司要从大型机移开一个应用程序并用 Visual FoxPro 和 SQL Server 来重写. 他们买 100 台新机器并有一个相当快的网络. 在此情况下你可能没有认识到一个使用三层体系相对于两层体系的巨大的性能上的获益. 在这里请注意最重要的词.

  可能更多的引人注目的考虑三层的理由是你可以移动你的数据给证和商业规则代码到一个地方. 假设有一个规则: 如果一个客户有 $20,000 以上的货款超过了60天没有支付则不能为该客户下新的订单. 在一个两层体系中你可以简单地在添加按钮的 Click 中编写代码并立即显示一个信息告诉用户该客户不能下订单. 用户喜欢这样且每人都高兴.

  假定该规则修改了并把限制提高到 $30,000. 有多少客户机器上的代码需要修改? 你是不是需要重新生成APP 或 EXE 文件并重新分发它? 这是一个相当大的负担很大的花费. 你可以设计系统为把商业规则保存在某个地方的一个元数据表中. 如果是这样, 对于你来说没有什么问题. 如果不是, 对于一个看来如此简单的修改有着大量的工作要做.

  但是如果商业规则是在一个 OLE server 上, 修改是相当地简单. 你可以修改代码并重新编译一个 EXE. 没有客户机器需要修改. 该规则将在 EXE 运行时生效, 且维护的工作量要少得多了.

  即使一个远程 OLE server 稍稍减少了全部性能你可能任然会使用它以利用它减少维护的负担.

  结论

  OLE server 中间层方法提供了一系列诱人的好处. 它运行在一个单一的机器上的事实允许你运行你的验证和商业规则在一个强有力的机器上而不会打破你的预算. 你可以购买一个超级机器来增强每一个客户机.

  当它来到时, 在需要修改验证或商业规则时你只需要修改中间层. 你可以修改 Visual FoxPro 代码和重新生成 OLE server. 每一个客户都会自动使用新的规则, 因为这些规则是在相同的位置上.分发修改的问题和保证所有客户使用相同代码问题, 以及更新, 规则等问题都变得令人注目的简单了.

  相关链接:

  2008年下半年全国计算机等级考试报名信息汇总

  更多信息请访问:计算机等级考试站 计算机等级考试在线题库 计算机等级考试论坛

打印本文 打印本文  关闭窗口 关闭窗口