DELPHI基础教程
第十八章 Delphi客户服务器应用开发(四)
18.3.4.1 适化概述 所谓适化就是将桌面应用转化为 Client/Server 应用。 适化是一个很复杂的主题,这里不详细讲述。本节将介绍适化 Delphi 应用程序中最重要的方面。 适化的主要方面有: ● 将数据库从桌面平台到服务器的适化 ● 将应用程序转化为 Client/Server 的适化
适化还需要实现从桌面环境到 Client/Server 环境的转化。 桌面数据库和 SQL 服务器数据库在许多方面有不同之处。例如: ● 桌面数据库用于同一时刻单用户的访问,而服务器用于多用户访问 ● 桌面数据库是面向记录的,而服务器是面向集合的 ● 桌面数据库将每个表存储在独立的文件中, 而服务器将所有的表存储在数据库中 Client/Server 应用必须解决更新的问题,最复杂的是联接、网络和事务控制
18.3.4.2 适化数据库
适化数据库包含下列步骤: ● 在桌面数据库结构的基础上,定义服务器上的元数据 ● 将数据从桌面转化到服务器中 ● 解决下列问题: ● 数据类型差异 ● 数据安全性和完整性 ● 事务控制 ● 数据访问权 ● 数据合法性 ● 锁定
Delphi 提供了两种方法适化一个数据库。 ● 使用 Database Desktop 工具,选择菜单 Tools/Utilities/Copy to 命令将数据库表从桌面方式拷贝到 SQL 格式 ● 建立应用 TBatchMove 部件的应用程序
这两种方法都可以将表结构和数据从桌面数据源转化到服务器上。依靠这些数据库,可能需要改变结果表。例如,可能想进行不同数据类型的映射。 也可以将下列特征加入数据库: ● 完整性约束 ● 索引 ● 检测约束 ● 存储过程和触发器 ● 其它服务器特征
如果用 SQL 脚本和服务器数据定义工具定义元数据会更有效。然后用前面介绍的两种方法转移数据。因为如果是手工定义数据库表, Database Desktop 和 TBatchMove 部件将只拷贝数据。
18.3.4.3 适化应用程序
在理论上,设计用来访问局部数据的 Delphi 应用程序做很少的修改就可以访问远程服务器上的数据。如果在服务器上定义适合的数据源,你就能将应用程序指向访问它,这只需简单地改变应用程序中 TTable 或 TQuery 部件的 DatabaseName 属性。 实际上,在访问局部和过程数据源之间有许多重要的不同之处。 Client/Server 应用程序必须解决大量的在桌面应用中所没有的问题。 任何 Delphi 应用程序都能用 TTable 或 TQuery 部件访问数据。桌面应用程序通常都是使用 TTable 部件。当适化到 SQL 服务器上时,用 TQuery 会更有效,如果应用程序要检索大量记录,则 TQuery 部件要略胜一筹。 如果应用程序使用统计或数学函数,那么在服务器上通过存储过程执行这些函数会更有效。因为存储过程执行更快,使用存储过程还可以减少网络负载,特别是大量行数据的函数。 例如,计算大量记录的标准差: ● 如果该函数在客户端执行,所有的值从服务器上检索出来并送到客户端,导致网络拥塞 ● 如果该函数在服务器端执行,则应用程序只需要服务器上的答案
18.4 Delphi 客户 服务器应用实例分析
本节中采用的实例是 Delphi2.0 数据库的例子 CSDEMO 。 CSDEMO 是 Delphi 客户 / 服务器编程的示例程序,它采用的数据库服务器是 Local InterBase Server 。 CSDEMO 较好地示范了 BDE 环境的配置, InterBASE Server 高级功能应用, SQL 服务器联接,触发器应用、存储过程编程和事务控制技术等,具有较高的参考价值。本节讲述下列内容: ● 数据库环境介绍 ● TDatabase 的应用 ● 不同数据库表的切换 ● 触发器编程 ● 存储过程编程 ● 事务控制应用
18.4.1 数据库环境介绍
本例中采用的数据库服务器是 Local InterBase Server 。 Local InterBase 是 InterBase Server 的单用户版 32 位、兼容 ANSI SQL 。 Local InterBase 支持客户 服务器应用在单机上的开发和测试,并且可以很容易地适化到 InterBase Server 上。因此,开发客户 / 服务器应用采用 Local InterBase 作为原型开发环境是很方便的。
18.4.1.1 IBLOCAL 的 BDE 参数
本例中的 SQL 数据库是 IBLOCAL 。它是由 BDE 配置工具 (BDECFG32.EXE) 设置参数值。它的各项参数值列于下表:
表 18.13 IBLOCAL 的各项参数值 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 参 数 名 参 数 值 ──────────────────────────────────── TYPE INTRBASE PATH SERVER NAME C:\INTRBASE\EXAMPLES\EMPLOYEE.GDB USER NAME SYSDBA OPEN MODE READ/WRITE SCHEMA CACHE SIZE 8 LANGDRIVER SQLQRYMODE SQLPASSTHRU MODE SHARED AUTOCOMMIT SCHEMA CHCHE TIME -1 MAX ROWS -1 BATCH COUNT 200 ENABLE SCHEMA CACHE FALSE SCHEMA CACHE DIR ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18.4.1.2 数据库结构介绍
IBLOCAL 数据库的结构都是由 InterBase 服务器工具交互式 SQL 工具 (ISQL) 定义的。 用 ISQL 定义数据库,首先要用 Create Database 命令建立数据库,建立的新数据库一般是以 GDB 为扩展名。建立好后,就可以用 SQL 语言定义数据库表,例如建立 EMPLOYEE 表的 SQL 语句如下:
定义域名数据类型:
CREATE DOMAIN FIRSTNAME AS VARCHAR(15); CREATE DOMAIN LASTNAME AS VARCHAR(20); CREATE DOMAIN COUNTRYNAME AS VARCHAR(15); CREATE DOMAIN EMPNO AS SMALLINT; CREATE DOMAIN DEPTNO AS CHAR(3) CHECK (VALUE = '000' OR (VALUE > '0' AND VALUE <= '999') OR VALUE IS NULL); CREATE DOMAIN JOBCODE AS VARCHAR(5) CHECK (VALUE > '99999'); CREATE DOMAIN JOBGRADE AS SMALLINT CHECK (VALUE BETWEEN 0 AND 6); CREATE DOMAIN SALARY AS NUMERIC(15, 2) DEFAULT 0 CHECK (VALUE > 0);
建立 EMPLOYEE 表:
CREATE TABLE EMPLOYEE (EMP_NO EMPNO NOT NULL, FIRST_NAME FIRSTNAME NOT NULL, LAST_NAME LASTNAME NOT NULL, PHONE_EXT VARCHAR(4), HIRE_DATE DATE DEFAULT 'NOW' NOT NULL, DEPT_NO DEPTNO NOT NULL, JOB_CODE JOBCODE NOT NULL, JOB_GRADE JOBGRADE NOT NULL, JOB_COUNTRY COUNTRYNAME NOT NULL, SALARY SALARY NOT NULL, FULL_NAME COMPUTED BY (last_name || ', ' || first_name), PRIMARY KEY (EMP_NO));
CHECK 语句是给数据库字段取值范围加约束条件。 PRIMARY_KEY 语句是给表建立关键字索引。 如法炮制,就可以定义 IBLOCAL 中的所有表。 IBLOCAL 中的表包括:
EMPLOYEE CUSTOMER DEPARTMENT EMPLOYEE_PROJECT PROJECT SALES SALARY_HISCORY
各数据库表中的内容如下:
表 18.14 EmployeeDemoDB 中各数据库表的内容 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 数据库表名 表中内容 ─────────────────────────── EMPLOYEE 雇员信息 CUSTOMER 客户信息 DEPARTMENT 部门信息 EMPLOYEE_PROJECT 雇员负责的工程 PROJECT 工程信息 SALES 销售信息 SALARY_HISTORY 雇员薪水调整的历史信息 ━━━━━━━━━━━━━━━━━━━━━━━━━━━
每个数据库表中都定义了关键字段。关于数据库表中的字段名、类型、大小,这里不再赘述。
18.4.2 应用程序分析
18.4.2.1 TDatabase 部件的使用
CSDEMO 程序中定义了一个数据库模块部件—— TDmEmployee ,它是继承于 TDataModule 。 TDataModule 是在 Delphi2.0 中才出现的专门放置数据访问部件 ( 如 TDatabase 、 TTable 和 TQuery 等 ) 的框架。其它涉及数据库访问的窗体,只要在 uses 语句中插入数据库模块所在的库单元,该窗体上的数据库部件就可引用相应的数据库访问部件。 在 TDmEmployee 中定义了一个 TDatabase 类型的部件── EmployeeDatabase 。 EmployeeDatagase 的主要属性及属性值如下:
表 18.15 EmployeeDatabase 部件主要属性的取值 ━━━━━━━━━━━━━━━━━━━━━━━ 属性 属性值 ─────────────────────── AliasName IBLOCAL DatabaseName EmployeeDemoDB KeepConnection True LoginPrompt False TransIsolation tiReadCommitted Params USERNAME = SYSDBA PASSWORD = masterkey Connected True ━━━━━━━━━━━━━━━━━━━━━━━
AliasName 属性所指定的 IBLOCAL ,必须已经在 BDE 中配置好, DatabaseName 属性指定要使用的数据库名,该数据库名是由应用程序自己定义的,因此不反应到 BDE 中,该属性值被 TTable 、 TQuery 等 DataSet 部件引用,并且出现在 DataSet 部件的 DatabaseName 下拉式列表框中。本例中的“ EmployeeDemoDB ”,被 EmployeeTable , SalesTable 等所有 DataSet 部件引用。 Connected 为 True 表明,应用程序与数据库将保持联接。 KeepConnection 属性为 True ,表明多次打开和关闭 EmployeeDemoDB 数据库中的任意表,应用程序将始终与数据库保持联接,这省却了重复注册的开销。 LoginPrompt 属性为 False ,表明应用程序自动处理与数据库的联接注册,因此, Params 属性中定义了注册的用户名和口令:
USERNAME = SYSDBA PASSWORD = masterkey
TransIsolation 属性为 tiReadCommitted 表明,如果存在多个同时事务,则某一事务只允许读由其它事务提交了的数据。 程序中 EmployeeDatabase 的应用还与事务控制等有关。下文中会介绍这方面的内容。
18.4.2.2 不同数据库表的切换
在许多数据库应用中都要在不同数据库表之间相互切换,以响应用户输入条件或系统状态的变化。这时,往往需要特别的处理,例如改变光标形状或隐藏数据改变等,尤其是在客户 / 服务器应用程序中。因为是用 SQL 语句访问远程数据库,有时还要在服务器端执行计算任务,所以客户端的数据变化会有一定的间隔,因此应该让用户明白发生了什么。下面是 CSDEMO 在数据库表切换时的处理办法:
procedure TFrmViewDemo.ShowTable( ATable: string ); begin Screen.Cursor := crHourglass; { 向用户提示当前操作状态 } VaryingTable.DisableControls; { 隐藏数据变化 } VaryingTable.Active := FALSE; { 关闭原来的数据库表 } VaryingTable.TableName := ATable; { 更新数据库表名 } VaryingTable.Open; { 打开数据库表 } VaryingTable.EnableControls; { 显示所作的修改 } Screen.Cursor := crDefault; { 重新设置光标形状 } end;
crHourglass 型光标表明正在执行 SQL 查询。 DisableControls 和 EnableControls 的作用是隐藏和显示数据变化。
18.4.2.3 InterBase 触发器 (Trigger) 的应用
在 CSDEMO 应用程序中,演示触发器应用的窗体是 TFromTriggerDemo ;
在该窗体中包含两个 TDBGrid 对象。 DBGrid1 显示 EmployeeTable 中的数据, DBGrid2 显示 SalaryHistoryTable 中的数据。它们的主要属性及属性值如下:
表 18.16 EmlpoyeeTable 部件主要属性的取值 ━━━━━━━━━━━━━━━━━━━━━ 属 性 属 性 值 ───────────────────── DatabaseName EmployeeDemoDB IndexFieldName Emp_No TableName EMPLOYEE ━━━━━━━━━━━━━━━━━━━━━
表 18.17 SalaryHistoryTable 部件主要属性的取值 ━━━━━━━━━━━━━━━━━━━━━ 属 性 属 性 表 ───────────────────── DatabaseName EmployeeDemoDB IndexFieldName Emp_No MasterFields Emp_No MasterSource EmployeeSource TableName SALARY_HISTORY ━━━━━━━━━━━━━━━━━━━━━
这两个表之间存在两种关系: ● 连接关系 EmployeeTable 的记录变化时, SalaryHistoryTable 的数据要作相应的变化。这种连接关系是通过索引来实现的。 ● 数据一致性 对 EmployeeTable 中的 Salary 字段的值作修改必须反映到 SalaryHistoryTable 中, SalaryHistoryTable 维护的是 Salary 变化的历史信息。这种数据一致性要求在本程序中是通过触发器实现的。 触发器是在 SQL 服务器端执行的一段程序,它在服务器端被触发执行完成一定的数据计算任务。 下面是 InterBase 服务器上与 Employee 表相关的触发器程序:
Triggers on Table EMPLOYEE: SAVE_SALARY_CHANGE, Sequence: 0, Type: AFTER UPDATE, Active AS BEGIN IF (old.salary <> new.salary) THEN INSERT INTO salary_history (emp_no, change_date, updater_id, old_salary, percent_change) VALUES ( old.emp_no, 'now', user, old.salary, (new.salary - old.salary) * 100 / old.salary); END
因为触发器是相应于 EMPLOYEE 表上的数据修改由服务器自动触发执行的,所以在客户应用程序上没有显式的调用。在客户端有打开并显示数据库表内容的程序和当 SALARY_HISTORY 表中数据变化时的更新显示的操作。
procedure TFrmTriggerDemo.FormShow(Sender: TObject); begin DmEmployee.EmployeeTable.Open; DmEmployee.SalaryHistoryTable.Open; end;
procedure TDmEmployee.EmployeeTableAfterPost(DataSet: TDataSet); begin { 一个雇员的薪水变化将触发薪水调整历史记录的变化 , 因此,如果 SalaryHistory 打开的话,就需要更新显示 } with SalaryHistoryTable do if Active then Refresh; end;
18.4.2.4 存储过程编程
存储过程也是 SQL 服务器上的一段程序,它接收输入参数,在服务器端执行,并将结果返回客户端,存储过程是必须在客户应用程序中显式调用的。 对于数据库表中大量记录的统计和函数计算,存储过程是很有用,这样可以将重复性计算任务转换到服务器,提高数据库应用的性能。 Delphi 中有两个部件能操作远程数据库服务器上的存储过程: TQuery 和 TStoredProc 。 1. TQuery 的存储过程编程 CSDEMO 中演示用 TQuery 调用存储过程的窗体是 TFrmQueryProc 。 TFrmQueryProc 中有两个 TDBGrid 部件。 DBGrid1 显示 EmployeeTable 中的数据。 DBGrid2 显示 Project 表中的数据。使用存储过程的 TQuery 部件名为 EmployeeProjectsQuery ,它的作用是建立 Employee 表和 Project 表的连接,以实现当 DBGrid1 中记录改变时, DBGrid2 中的数据作相应的改变。具体的连接任务是由服务器上的存储过程 Get_Emp_Proj 完成。下面是 Get_Emp_Proj 的程序:
PROCEDURE Get_Emp_Proj BEGIN FOR SELECT proj_id FROM employee_project WHERE emp_no = :emp_no INTO :proj_id DO SUSPEND; END
EMP_NO INPUT SMALLINT PROJ_ID OUTPUT CHAR(5)
该过程带两个参数: EMP_NO 是输入参数,类型是 SMALLINT. PROJ_ID 是输出参数,类型是 CHAR(5)
相应地, EmployeeProjectsQuery 的主要属性如下:
表 18. 18 EmployeeProjectsQuery 部件主要属性的取值 ━━━━━━━━━━━━━━━━━━━━━━━━━━ 属 性 属 性 值 ────────────────────────── DatabaseName EmployeeDemoDB Params EMP_No( 输入参数, Smallint 类型 ) SQL Select * from Get_Emp_Proj(:EMP_NO) ━━━━━━━━━━━━━━━━━━━━━━━━━━
TQuery 部件是在 SQL 语句中直接调用存储过程。 下面是客户端的程序:
procedure TFrmQueryProc.FormShow(Sender: TObject); begin DmEmployee.EmployeeTable.Open; EmployeeSource.Enabled := True; with EmployeeProjectsQuery do if not Active then Prepare; end;
用 Prepare 显式地准备 SQL 语句,虽非必须,但可以优化 SQL 的执行。
procedure TFrmQueryProc.EmployeeDataChange(Sender: TObject; Field: TField); begin EmployeeProjectsQuery.Close; EmployeeProjectsQuery.Params[0].AsInteger := DmEmployee.EmployeeTableEmp_No.Value; EmployeeProjectsQuery.Open;
WriteMsg('Employee ' + DmEmployee.EmployeeTableEmp_No.AsString + ' is assigned to ' + IntToStr(EmployeeProjectsQuery.RecordCount) + ' project(s).'); end;
该事件处理过程与 EmployeeSource 的 OnDataChange 属性相联。用于当 EmployeeTable 数据记录变化时,修正存储过程的输入参数,并执行 SQL 语句。 2. TStoredProc 部件的存储过程编程 TStoredProc Delphi 专门用来使用服务器存储过程的部件。 CSDEMO 中演示用 TStoredProc 调用存储过程的窗体是 TFrmExecPr 在程序运行中,当按下 ShipOrder 按钮,要求对 ORED_STA_TUS 等字段的内容作修改以维护数据库的一致性。字段内容的修改任务由服务器上的存储过程 SHIP_ORDER 完成。 SHIP_ORDE 的程序如下:
PROCEDURE SHIP_ORDER DECLARE VARIABLE ord_stat CHAR(7); DECLARE VARIABLE hold_stat CHAR(1); DECLARE VARIABLE cust_no INTEGER; DECLARE VARIABLE any_po CHAR(8); BEGIN SELECT s.order_status, c.on_hold, c.cust_no FROM sales s, customer c WHERE po_number = :po_num AND s.cust_no = c.cust_no INTO :ord_stat, :hold_stat, :cust_no; IF (ord_stat = "shipped") THEN BEGIN EXCEPTION order_already_shipped; SUSPEND; END ELSE IF (hold_stat = "*") THEN BEGIN EXCEPTION customer_on_hold; SUSPEND; END
FOR SELECT po_number FROM sales WHERE cust_no = :cust_no AND order_status = "shipped" AND paid = "n" AND ship_date < 'NOW' - 60 INTO :any_po DO BEGIN EXCEPTION customer_check;
UPDATE customer SET on_hold = "*" WHERE cust_no = :cust_no;
SUSPEND; END
UPDATE sales SET order_status = "shipped", ship_date = 'NOW' WHERE po_number = :po_num; SUSPEND; END
Parameters: PO_NUM INPUT CHAR(8)
该过程只带有一个输入参数: PO_NUM ,类型是 CHAR(8) 。 在客户端使用该过程的 TStoreProc 部件是 ShipOrderProc ,其主要属性如下表:
表 18.19 ShipOrderProc 部件主要属性的取值 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 属性名 属 性 值 ──────────────────────────── DatabaseName EmployeeDemoDB ParamBindMode pbByName Params PO_NUM( 输入参数, String 类型 ) StoredProcName SHIP_ORDER ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端执行 SHIP_ORDER 的程序如下:
procedure TFrmExecProc.BtnShipOrderClick(Sender: TObject); begin with DmEmployee do begin ShipOrderProc.Params[0].AsString := SalesTable['PO_NUMBER']; ShipOrderProc.ExecProc; SalesTable.Refresh; end; end;
当用户按 ShipOrder 按钮时,执行这段程序。程序中先准备输入参数,用 ExecProc 方 法执行存储过程。调用 SalesTable.Refresh 方法刷新数据显示。 在 CSDEMO 应用程序中另一个使用存储过程的 TStoredProc 部件是 DeleteEmployeeProc 。它完成的任务是删除 Employee 表中的记录,并修改所有相关的表, 以维护数据的一致性。其属性如下:
表 18.20 DeleteEmployeeProc 部件主要属性的取值 ━━━━━━━━━━━━━━━━━━━━━━━━━━ 属性名 属 性 值 ────────────────────────── DataBaseName EmployeeDemoDB ParamBindMode PbByName Params EMP_NUM( 输入参数,整型 ) StoredProcName DELETE_EMPLOYEE ━━━━━━━━━━━━━━━━━━━━━━━━━━
存储过程 DELETE_EMPLOYEE 的程序如下:
PROCEDURE DELETE_EMPLOYEE DECLARE VARIABLE any_sales INTEGER; BEGIN any_sales = 0; SELECT count(po_number) FROM sales WHERE sales_rep = :emp_num INTO :any_sales; IF (any_sales > 0) THEN BEGIN EXCEPTION reassign_sales; SUSPEND; END UPDATE department SET mngr_no = NULL WHERE mngr_no = :emp_num; UPDATE project SET team_leader = NULL WHERE team_leader = :emp_num; DELETE FROM employee_project WHERE emp_no = :emp_num; DELETE FROM salary_history WHERE emp_no = :emp_num; DELETE FROM employee WHERE emp_no = :emp_num; SUSPEND; END
Parameters: EMP_NUM INPUT INTEGER
从上述存储过程的例子中,我们看到存储过程在维护服务器上的数据一致性方面有很强的能力,它节省了系统开销,提高了客户端的性能。
18.4.2.5 事务控制编程
在客户 / 服务器应用程序中,事务控制是一项很重要的技术。它对于提高系统的可靠性,维护数据一致性有着重要的意义。 Delphi 中提供了事务的隐式和显式两种控制方法。其中显式控制的性能较高,下面介绍 Delphi 事务显式控制的编程方法。 Delphi 担当事务控制任务的部件是 TDatabase 。 TDatabase 用于事务控制的属性是 TransIsolation ,方法有 StartTranstion 、 Commit 和 Rollback 。关于这些属性和方法作用和使用方法请参阅客户 / 服务器事务管理。 在 CSDEMO 中 TDatabase 部件为 EMployeeDatabase ,其 TransIsolation 属性值为 tiReadCommitted ,意为如果存在多个同时事务访问数据库,则其中任一事务只能读其它事务提交的了数据。 CSDEMO 中演示事务控制的窗体是 TFrmTransDemo 。 DBGrid1 中显示 EmployeeTable 中的内容。当窗口显示时, EmployeeDatabase 开始一次事务控制并激活 EmployeeTable :
procedure TFrmTransDemo.FormShow(Sender: TObject); begin DmEmployee.EmployeeDatabase.StartTransaction; DmEmployee.EmployeeTable.Open; end;
当窗口被关闭或隐藏时, EmployeeDatabase 提交事务:
procedure TFrmTransDemo.FormHide(Sender: TObject); begin DmEmployee.EmployeeDatabase.Commit; end;
窗口中有两个按钮 BtnCommitEdits 和 BtnUndoEdits 。按下 BtnCommitEdits 按钮将提交当前事务,并开始新的事务控制并刷新数据。
procedure TFrmTransDemo.BtnCommitEditsClick(Sender: TObject); begin if DmEmployee.EmployeeDatabase.InTransaction and (MessageDlg('Are you sure you want to commit your changes?', mtConfirmation, [mbYes, mbNo], 0) = mrYes) then begin DmEmployee.EmployeeDatabase.Commit; DmEmployee.EmployeeDatabase.StartTransaction; DmEmployee.EmployeeTable.Refresh; end else MessageDlg('Can''t Commit Changes: No Transaction Active', mtError, [mbOk], 0); end;
按下 BtnUndoEdits 按钮将返转当前事物,恢复原来的数据,开始新的事务控制,并刷新数据的显示。
procedure TFrmTransDemo.BtnUndoEditsClick(Sender: TObject); begin if DmEmployee.EmployeeDatabase.InTransaction and (MessageDlg('Are you sure you want to undo all changes made during the ' + 'current transaction?', mtConfirmation, [mbYes, mbNo], 0) = mrYes) then begin DmEmployee.EmployeeDatabase.Rollback; DmEmployee.EmployeeDatabase.StartTransaction; DmEmployee.EmployeeTable.Refresh; end else MessageDlg('Can''t Undo Edits: No Transaction Active', mtError, [mbOk], 0); end; |