後一頁
前一頁
回目錄
回首頁
第十八章 Delphi用戶伺服器應用開發(四)

18.3.4.1 適化概述 

  所謂適化就是將桌面應用轉化為Client/Server應用。

  適化是一個很複雜的主題,這�堣ㄧ埴蚆翮z。本節將介紹適化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 雇員薪水調整的歷史資訊

   ───────────────────────────

 

每個資料庫表中都定義了關鍵欄位。關於資料庫表中的欄位名、型式、大小,這�堣ㄕA贅述。

 

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;


後一頁
前一頁
回目錄
回首頁