天天看點

CursorAdapter 起步 之 三:可重用資料類

CursorAdapter 起步 之 三:可重用資料類

作者:Doug Hennig

譯者:fbilo

VFP 的程式員們想要一個可重用的資料類已經很久了。盡管在過去的版本中也有許多解決這個問題的辦法,不過總是有點美中不足。現在在 VFP 8裡,我們有了真正的可重用資料類。這個月,Doug 為我們示範了怎樣通過建立 CursorAdapter 和 DataEnvironment 的子類來建立可重用的資料類、以及怎樣在表單和報表中使用它們。

正文

××

在過去的兩期雜志中,我們讨論了在 VFP 8 中新增的 CursorAdapter 基礎類。我個人的觀點是,這是 VFP 8 中最重要的改動之一,因為它向我們提供了一個對象SQL Server這樣的非VFP資料源的簡單易用、統一的接口。此外,如你本月所能見到的那樣,它們還形成了可重用資料類的基礎。

在講述可重用資料類之前,讓我們先來看一下我建立的一些 CursorAdapter 和 DataEnvironment 的子類,我給它們增加了一些額外的功能,它們将成為我們的可重用資料類的起點。

SFCursorAdapter

***************

SFCursorAdapter (在附件 SFDataClasses.vcx 中) 是 CursorAdapter 的一個子類,它擁有一些額外增加的功能,如下:

※ 它可以自動處理參數化查詢:你可以靜态(一個常量)也可以動态(一個表達式,例如“=Thisform.txtName.value”,當 Cursor 被打開或者重新整理的時候,這個表達式會被運算)的定義一個參數值。

※ 它可以在 Cursor 被打開以後自動在該 Cursor 上建立索引。

※ 對于 ADO,它還會執行一些特殊的工作,例如把 DataSource 屬性設定為一個 ADO RecordSet,把這個 RecordSet 的 ActiveConnection 屬性設定為一個 ADO Connection 對象,當用到一個參數化查詢的時候,它還會建立一個 ADO Command 對象并把這個對象傳遞給 CursorFill 方法。

※ 它提供了簡單的錯誤處理(cErrorMessage 屬性裡會有錯誤的資訊)。

※ 它還有 CursorAdapter 中缺少的 Update 和 Release 方法。

這個類的 INIT 方法建立兩個集合(使用新的 Collection 基礎類,它是維護某些東西的集合用的),一個是為 SelectCmd 屬性可能會用到的參數而準備的,另一個是用于在 Cursor 被打開以後應該自動建立的标記。它還會 SET MULTILOCK ON,因為這是 CursorAdapter Cursor 的需求。

This.oParameters = CreateObject('Collection')

This.oTags = CreateObject('Collection')

Set multilocks on

AddParameter 方法象 parameters 集合添加一個參數。給這個方法傳遞參數的名稱(這個名稱應該與該參數出現在 SelectCmd 屬性中的那個名稱相一緻),根據需要也可以付上參數的值(如果你現在不給它傳遞參數的值,也可以以後再調用 Getparameter 方法來傳遞)。這段代碼示範了一對 VFP 8 中的新功能:新的 empty 基礎類,它沒有任何屬性、事件或者方法,是以是建立一個輕量級的對象的理想選擇;還有 AddProperty() 函數,它的作用跟 AddProperty 方法類似,差別是它用于那些沒有這個方法的對象。

lparameters tcName, tuvalue

local loParameter

loParameter = createobject('Empty')

addproperty(loParmeter, 'Name', tcName)

addproperty(loParmeter, 'value', tuvalue)

This.oParameters.Add(loParameter, tcName)

使用 GetParmeter 方法來傳回一個特殊的 parameter 對象——通常是用在需要設定用于參數的值的時候。

lparameters tcName

local loParameter

loParameter = This.oParameters.Item(tcName)

return loParameter

SetConnection 方法用于将 DataSource 屬性設定為希望的連接配接。如果 DataSourceType 是 “ODBC”,就給這個方法傳遞一個連接配接句柄;如果是“ADO”,DataSource 必須是一個ADO Recordset 對象,而且該對象的 ActiveConnection 屬性必須要設定為一個活動 ADO Connection 對象,是以,我們需要向 SetConnection 方法傳遞這個 ADO Connection 對象, SetConnection 會建立一個 RecordSet,并且把這個 RecordSet 的 ActiveConnection 設定為被傳遞的 ADO Connection 對象。

lparameters tuConnection

with this

do case

  case .DataSourceType = 'ODBC'

   .DataSource = tuConnection

  case .DataSourceType = 'ADO'

   .DataSource = Createobject('ADODB.RecordSet')

   .DataSource.ActiveConnection = tuConnection

endcase

endwith

為了建立 Cursor,我們調用 GetData 方法而不是 CursorFill 方法,因為 GetData 能夠自動處理參數和錯誤。如果你想要建立一個不帶資料的 Cursor,那麼就給 GetData 方法傳遞一個 .T.。這個方法建立的第一樣東西,是與定義在 parameters 集合中的參數們同名的私有變量(在這裡調用了 GetParametervalue 方法,該方法會傳回參數對象的值,如果該對象的值是一個以“=”開頭的表達式,那麼傳回的将是運算該表達式之後所獲得的值。)下一步,如果我們是在使用 ADO 并且已經有了一些參數,這段代碼會建立一個 ADO Command 對象,并把該對象的 ActiveConnection 屬性設定為 Connection 對象,然後把這個 Connection 對象傳遞給 CursorFill 方法——這是 CursorAdapter 處理 ADO 參數化查詢的需要。如果我們不是在用 ADO 、或者沒有任何參數,那麼代碼會簡單的調用 CursorFill 來填充 Cursor。注意,如果給 GetData 方法傳遞了 .T.,并且 CursorSchema 已經被填寫好了,那麼就是告訴 GetData 方法去使用 CursorSchema(這也是我想讓 CursorAdapter 基類擁有的功能)。現在如果 Cursor 被建立起來了,代碼會調用 GreateTags 方法來為該 Cursor 建立想要的索引;如果 Cursor 沒有被建好,那麼它會調用 HandleError 方法來處理任何發生了的錯誤。

lparameters tlNoData

local loParameter, ;

lcName, ;

luvalue, ;

llUseSchema, ;

loCommand, ;

llReturn

with This

* tlNoData參數指定是否要向 Cursor 中填充資料,如果要填充的話,

* 我們就要把建立存儲所有參數的變量的活在這裡就做掉而不是放到一個其它的方法中去。

* 因為我們希望這些變量的有效範圍是私有的。

if not tlNoData

  for each loParameter in .oParameters

   lcName  = loParameter.Name

   luvalue = .GetParametervalue(loParameter)

   store luvalue to (lcName)

  next loParameter

endif not tlNoData

* 如果我們正在使用 ADO,并且有了一些參數,那麼就需要有一個處理這些參數的 Command對象

llUseSchema = not empty(.CursorSchema)

if '?' $ .SelectCmd and (.DataSourceType = 'ADO' or (.UseDEDataSource and ;

  .Parent.DataSourceType = 'ADO'))

  loCommand = createobject('ADODB.Command')

  loCommand.ActiveConnection = iif(.UseDEDataSource, ;

   .Parent.DataSource.ActiveConnection, .DataSource.ActiveConnection)

  llReturn = .CursorFill(llUseSchema, tlNoData, .nOptions, loCommand)

else

* 填充這個 cursor.

  llReturn = .CursorFill(llUseSchema, tlNoData, .nOptions)

endif '?' $ .SelectCmd ...

* 如果 Cursor 建立成功,則為之定義所有的 Tag,否則則處理發生的錯誤。

if llReturn

  .CreateTags()

else

  .HandleError()

endif llReturn

endwith

return llReturn

還有一些方法這裡我們就不說了,你可以自己去研究它們。HandleError 方法使用 Aerror() 來判斷發生了什麼錯誤,并把錯誤數組的第二個元素放到 cErrorMessage 屬性中去。Requery 方法與 GetData 類似,不過它是用來重新整理 Cursor 中的資料。調用這個方法而不是 CursorRefresh 方法的原因就象 GetData 一樣:它能夠處理參數和錯誤。Update 方法很簡單:它就是調用 TableUpdate() 來送出目前資料源的更新,如果送出更新失敗,則調用 HandleError 方法來處理錯誤。AddTag 用于在 Cursor 被建立後将你想要建立的索引的資訊添加到 Tags 集合中,而 GetData 方法會調用的 CreateTags 方法則會在自己的 Index ON 語句中用到這個集合中的資訊。

這裡是使用這個類的一個例子,是從附件中的 TestCursorAdapter.prg 中拿來的。它從 SQL Server 自帶的 Northwind 資料庫的 Customers 表中取得資料。它的 SelectCmd 屬性裡是一個參數化查詢的 Select 語句,向你示範了怎樣用 AddParameter 方法來處理參數,以及怎樣用 AddTag 方法來自動地為 Cursor 建立索引辨別。

local loCursor as SFCursorAdapter of SFCursorAdapter, ;

loConnMgr as SFConnectionMgrODBC of SFRemote, ;

loParameter as Empty

lnHandle = sqlstringconnect('driver=SQL Server;server=(local);' + ;

'database=Northwind;uid=sa;pwd=;trusted_connection=no')

&& change password to appropriate value for your database

loCursor = newobject('SFCursorAdapter', 'SFDataClasses')

with loCursor

.DataSourceType = 'ODBC'

.Alias          = 'Customers'

.SelectCmd      = 'select * from customers where country = ?pcountry'

.SetConnection(lnHandle)

.AddParameter('pcountry', 'Brazil')

.AddTag('CustomerID', 'CustomerID')

.AddTag('Company',    'upper(CompanyName)')

.AddTag('Contact',    'upper(ContactName)')

if .GetData()

  messagebox('Brazilian customers in CustomerID order')

  set order to CustomerID

  go top

  browse

  messagebox('Brazilian customers in Contact order')

  set order to Contact

  go top

  browse

  messagebox('Canadian customers')

  loParameter = .GetParameter('pcountry')

  loParameter.value = 'Canada'

  .Requery()

  browse

else

  messagebox('Could not get the data. The error message was:' + ;

   chr(13) + chr(13) + .cErrorMessage)

endif .GetData()

* Now try to do an invalid SELECT statement.

.SelectCmd = 'select * from customersx'

if .GetData()

  browse

else

  messagebox('Could not get the data. The error message was:' + ;

   chr(13) + chr(13) + .cErrorMessage)

endif .GetData()

endwith

sqldisconnect(lnHandle)

SFDataEnvironment

*****************

在附件 SFDataClasses.vcx 中的這個資料和環境類要比 SFCursorAdapter 簡單的多。但它增加了一些非常有用的功能:

×× GetData 方法會調用所有在這個資料環境類裡面的 SFCursorAdapter 成員類的 GetData 方法,這樣你就不需要自己去一個個的調用它們。與此類似的是,Requery 方法和 Update 方法也會調用每個 SFCursorAdapter 成員類的 Requery 和 Update 方法。

×× 象 SFCursorAdapter 一樣,SetConnection 方法會把 DataSource 設定為一個 ADO Recordset,并把這個 Recordset 的 ActiveConnection 屬性設定為一個 ADO Connection 對象。不過,它還會調用所有 UseDEDataSource 屬性被設定為 .F. 的 SFCursorAdapter 成員類的 SetConnection 方法。

×× 它提供了一些簡單的錯誤處理(cErrorMessage 屬性會被填入錯誤資訊)

×× 它有一個 Release 方法。

現在我們看看這個類的一對方法。GetData 非常簡單:如果這個資料環境類的任何成員對象擁有 GetData 方法,則調用該方法:

lparameters tlNoData

local loCursor, ;

llReturn

for each loCursor in This.Objects

if pemstatus(loCursor, 'GetData', 5)

  llReturn = loCursor.GetData(tlNoData)

  if not llReturn

   This.cErrorMessage = loCursor.cErrorMessage

   exit

  endif not llReturn

endif pemstatus(loCursor, 'GetData', 5)

next loCursor

return llReturn

SetConnection 方法稍微複雜一點:如果它的任何成員對象有 SetConnection 方法、并且該成員對象的 UseDEDataSource 屬性被設定為 .F.,則調用該成員對象的 SetConnection 方法;然後,如果有任何一個 CursorAdapter 對象的 UseDEDataSource 屬性被設定為了 .T.,則使用類似于 SFCusrorAdapter 中的那樣的代碼來設定自己的 DataSource:

lparameters tuConnection

local llSetOurs, ;

loCursor, ;

llReturn

with This

* Call the SetConnection method of any CursorAdapter that isn't using our

* DataSource.

llSetOurs = .F.

for each loCursor in .Objects

  do case

   case upper(loCursor.BaseClass) <> 'CURSORADAPTER'

   case loCursor.UseDEDataSource

    llSetOurs = .T.

   case pemstatus(loCursor, 'SetConnection', 5)

    loCursor.SetConnection(tuConnection)

  endcase

next loCursor

* If we found any CursorAdapters that are using our DataSource, we'll need to

* set our own DataSource.

if llSetOurs

  do case

   case .DataSourceType = 'ODBC'

    .DataSource = tuConnection

   case .DataSourceType = 'ADO'

    .DataSource = createobject('ADODB.RecordSet')

    .DataSource.ActiveConnection = tuConnection

  endcase

endif llSetOurs

endwith

TestDE.prg 做了一個示範,它使用 SFDataEnvironment 作為容器,該容器中有一對 SFCursorAdapter 類。因為這個例子用的是 ADO,是以每個 SFCursorAdapter 都需要它自己的 DataSource,是以它們的 UseDEDataSource 屬性都被設定成了 .F.(預設設定)。注意:隻要簡單的調用一下 SFDataEnvironment 的 SetConnection 方法就能搞定為每個 CursorAdapter 設定好 DataSource 屬性的事情。

local loConn as ADODB.Connection

loConn = createobject('ADODB.Connection')

loConn.ConnectionString = 'provider=SQLOLEDB.1;data source=(local);' + ;

'database=Northwind;uid=sa;pwd='

&& change password to appropriate value for your database

loConn.Open()

set classlib to SFDataClasses

loDE = createobject('SFDataEnvironment')

with loDE

.AddObject('CustomersCursor', 'SFCursorAdapter')

with .CustomersCursor

  .Alias          = 'Customers'

  .SelectCmd      = 'select * from customers'

  .DataSourceType = 'ADO'

endwith

.AddObject('OrdersCursor', 'SFCursorAdapter')

with .OrdersCursor

  .Alias          = 'Orders'

  .SelectCmd      = 'select * from orders'

  .DataSourceType = 'ADO'

endwith

.SetConnection(loConn)

if .GetData()

  select Customers

  browse nowait

  select Orders

  browse

else

  messagebox('Could not get the data. The error message was:' + ;

   chr(13) + chr(13) + .cErrorMessage)

endif .GetData()

endwith

loConn.Close()

可重用資料類

************

現在我們已經有了可用的 CursorAdapter 和 DataEnvironment 的子類,讓我們來談談可重用資料類的事情。

一件 VFP 程式員們已經向 Microsoft 要求了很久的事情是可重用資料類。例如,你可能有一個表單和一個報表,它們使用的是完全相同的一套資料,然而你卻不得不重複的去手動向資料環境中添加一個個表或者視圖——因為資料環境是不可重用的。某些程式員(以及幾乎所有的應用程式架構提供商)采用了通過代碼來建立可重用資料環境(那時候資料環境是不能可視化的建立子類的)的方法,并且在表單上使用一個 “loader”對象來建立 DataEnvironment 子類的執行個體。不管怎麼說,這種方法總不是那麼正規,并且無法用于報表。

現在,在 VFP8 裡,我們不僅擁有了建立“能夠向任何需要資料的對象提供來自任何資料源的資料”的可重用資料類的能力,還有了建立“能夠寄宿資料類的”資料環境類的能力。在我寫這篇文章的時候,你還不能在報表中使用 CursorAdapter 或者 DataEnvironment 的子類,不過你可以程式設計的添加 CursorAdapter 子類(例如把這些代碼寫在 DataEnvironment 的 INIT 方法中)來利用可重用類的優點。

讓我們為 Northwind 資料庫的 Customers 和 Orders 表建立一些資料類。CustomersCursor (在 NorthwindDataClasses.vcx 中)是 SFCursorAdapter 的一個子類,其屬性如表1:

表 1. CustomersCursor 的屬性

屬性   值

Alias   Customers

CursorSchema CUSTOMERID C(5), COMPANYNAME C(40), CONTACTNAME C(30), CONTACTTITLE C(30), ADDRESS C(60),

    CITY C(15), REGION C(15), POSTALCODE C(10), COUNTRY C(15), PHONE C(24), FAX C(24)

KeyFieldList CUSTOMERID

SelectCmd  select * from customers

Tables   CUSTOMERS

UpdatableFieldList CUSTOMERID, COMPANYNAME, CONTACTNAME, CONTACTTITLE, ADDRESS, CITY, REGION, POSTALCODE, COUNTRY, PHONE, FAX

UpdateNameList CUSTOMERID CUSTOMERS.CUSTOMERID, COMPANYNAME CUSTOMERS.COMPANYNAME, CONTACTNAME CUSTOMERS.CONTACTNAME, CONTACTTITLE CUSTOMERS.CONTACTTITLE, ADDRESS CUSTOMERS.ADDRESS, CITY CUSTOMERS.CITY, REGION CUSTOMERS.REGION, POSTALCODE CUSTOMERS.POSTALCODE, COUNTRY CUSTOMERS.COUNTRY, PHONE CUSTOMERS.PHONE, FAX, CUSTOMERS.FAX

你不會以為我會是手動在屬性視窗中輸入所有這些屬性的值吧?當然不是!我是用 CursorAdapter 的生成器來幹的。這裡的技巧是打開“Use connection settings in builder only(隻使用在生成器中的連接配接設定)”,填入連接配接資訊以獲得一個活動連接配接,再填好 SelectCMD 以後,最後再用生成器來生成其它的屬性設定。

現在,任何時候你需要 Northwind 的 Customers 表中的資料,隻要簡單的放一個 CustomersCursor 類就夠了。當然,我們還沒有定義任何連接配接資訊,不過做到這樣就已經很不錯了,有了這個類就不需要擔心怎麼獲得資料(ODBC、XML、ADO)、使用哪種資料引擎(比如 SQL Server、Access 以及 VFP 8中都有 Northwind 資料庫)之類的事情了。

不過,要注意的是這個 Cursor 對付的是 Customers 表中所有的記錄。可有時候,你又隻想要一個客戶的資料。那麼,CustomerByIDCursor 是 CustomersCursor 的一個子類,它的 SelectCmd 已經被改成 “Select * from customers where customerid = ?pcustomerid”,還有下面增加的 INIT 方法的代碼:

lparameters tcCustomerID

dodefault()

This.AddParmeter('pCustomerID', tcCustomerID)

這段代碼會建立一個叫做 pCustomerID 的參數(它跟在 SelectCmd 中指定的是同一個),并且被設定成傳遞進來的任何值。如果沒有值被傳遞進來的話,那麼使用 GetParameter 方法來為這個參數傳回一個對象,并在調用 GetData 之前設定它的 value 屬性。

OrdersCursor 類類似于 CustomersCursor,隻是它傳回的是 Orders 表中的所有資料,而 OrdersForCustomerCursor 是它的一個子類,用于傳回一個指定客戶的所有訂單。

要測試一下的話,請運作 TestCustomersCursor.prg,它會從 SQL Server 版本的 Northwind 資料庫中 Customers 表的一個客戶,然後做到 Access 版本的 Northwind 資料庫所做的同樣的事情。這個示例示範了怎樣不為類指定連接配接資訊,這個類自己就能靈活的完成任務,是以,它的可重用性是很強的。

示例:表單

**********

現在我們已經有了一些可重用類,讓我們來用用它們。首先,我們來建立 SFDataEnvironment 的一個子類 CustomersAndOrdersDataEnvironment (哈哈,名字可有夠長的,D.H牌冰糖葫蘆!),它包含着一個 CustomerByIDCursor 類和一個 OrdersForCustomerCursor 類。由于我們希望在打開表之前設定連接配接資訊,是以把它的 AutoOpenTables 屬性設定為了 .F.,而且把前面兩個 CursorAdapter 的 UseDEDataSource 屬性都設定為了 .T.。現在,這個資料環境類已經可以被用來在某個表單中顯示關于一個指定客戶的資訊以及他的訂單了。

讓我們來建立這麼一個表單。附件中的 CustomerOrders.scx 表單的 DEClass 和 DEClassLibrary 屬性已經被設定為了CustomersAndOrdersDataEnvironment 和 NorthwindDataClasses.vcx,這樣就用上了我們的可重用資料環境類。這個表單的 Load 方法裡面的代碼有點多,不過這是因為它要支援 ADO、ODBC、以及 XML 資料源,并且它還要建立自己的連接配接,是以大多數代碼都是處理這些問題的。如果它隻支援一種資料源的話,比如隻用 ODBC,再比如由另一個對象來管理連接配接(實際的應用程式開發中常常就是這樣的),代碼就會簡單多了:

with This.CustomersAndOrdersDataEnvironment

  * 獲得連接配接

    lnHandle = oApp.oConnectionMgr.GetConnectionHandle()

   .SetConnection(lnHandle)

  * 指定從 CustomerID 文本框中取得 cursor 參數的值

    loParameter       = ;

     .CustomerByIDCursor.GetParameter('pCustomerID')

   loParameter.value = '=Thisform.txtCustomerID.value'

   loParameter       = ;

     .OrdersForCustomerCursor.GetParameter('pCustomerID')

   loParameter.value = '=Thisform.txtCustomerID.value'

  * 建立一個空的 cursor,如果失敗的話則顯示錯誤資訊

    if not .GetData(.T.)

    messagebox(.cErrorMessage)

     return .F.

   endif not .GetData(.T.)

endwith

這段代碼用上了那兩個 CursorAdapter 對象的 GetParameter 方法來把 pCustomerID 參數設定為表單上一個文本框中的值。注意在值裡面用到的'=',它表示在你需要 value 屬性的時候再去運算它的值,是以我們實際上有了一個動态的參數(這樣就順應了當使用者在文本框中輸入了新的值以後要将改動反應到參數中去的需要)。調用 GetData 方法是為了建立一個空的 Cursor,這樣才能安頓那些資料綁定的控件。

txtCustomerID 文本框沒有綁定什麼資料。它的 Valid 方法先調用資料環境的 Requery 方法,然後再調用表單的 Refresh 方法。這樣,當使用者輸入一個新的客戶ID的時候,就能夠 Requery 那個 Cursor,接着表單上其它控件也會被重新整理。表單上的其它文本框被綁定到由 CustomersByIDCursor 對象建立的 Customers cursor 的字段中。那個 Grid 被綁定到由 OrdersForCustomerCursor 對象建立的 Orders Cursor。

運作這個表單,并輸入一個 Customer ID 為“ALFKI”(見圖1)。當你按下 Tab 鍵跳出這個文本框的時候,你會看到該客戶的位址資訊以及他的訂單就出現了。試着改動一些這個客戶的資料或者訂單資料,然後關閉表單再打開,再輸入一次“ALFKI”,你會看到你沒做什麼工作這些改動就都已經被寫到背景資料庫中了。

此主題相關圖檔如下:

圖1、

酷吧,嗯?跟建立一個基于本地表或者視圖的表單相比,并沒多多少工作。更棒的是:試試把定義在 Load 方法中的 ccDATASOURCETYPE 常量改變為 “ADO”或者“XML”,然後這個表單無論是看起來還是實際工作起來都跟沒改過之前一摸一樣(如果你想用 XML,你需要象上個月的文章中所說的那樣為 Northwind 資料庫設定一個 SQLXML 虛拟目錄,并把本月附件中的 XML 模闆檔案拷貝到那個目錄裡)。

示例:報表

**********

我們來試試報表。這裡最大的問題是:與表單不同,我們既不能告訴報表去使用一個資料環境子類,也不能拖放一個 CursorAdapter 子類到資料環境中去。是以我們不得不向報表放入一些代碼以将 CursorAdapter 添加到資料環境。盡管從邏輯上來看應該把這些代碼放到報表資料環境的 BeforeOpernTables 事件中去,不過事實上這樣做卻是行不通的,因為——由于某些我不能了解的原因—— BeforeOpenTables 事件隻會在你預覽報表的每一頁的時候才會觸發。是以,我們隻好把代碼放在 Init 方法裡。因為示範的需要,報表 CustomerOrders.frx 跟表單 CustomerOrders.scx 一樣,要比實際開發的情況下會用到的代碼更複雜一些。如果沒有這些示範的需求的話,實際上可以簡化到下面這樣:

with This

   set safety off

  * 獲得連接配接

    .DataSource = oApp.oConnectionMgr.GetConnectionHandle()

  * 建立客戶和訂單的 CursorAdapter 對象

    .NewObject('CustomersCursor', 'CustomersCursor', ;

     'NorthwindDataClasses')

   .CustomersCursor.AddTag('CustomerID', 'CustomerID')

   .CustomersCursor.UseDEDataSource = .T.

   .NewObject('OrdersCursor', 'OrdersCursor', ;

     'NorthwindDataClasses')

   .OrdersCursor.AddTag('CustomerID', 'CustomerID')

   .OrdersCursor.UseDEDataSource = .T.

  * 取得資料,如果失敗,則顯示錯誤資訊

    if not .CustomersCursor.GetData()

     messagebox(.CustomersCursor.cErrorMessage)

     return .F.

   endif not .CustomersCursor.GetData()

   if not .OrdersCursor.GetData()

     messagebox(.OrdersCursor.cErrorMessage)

     return .F.

   endif not .OrdersCursor.GetData()

  * 從 Customers 設定一個與 Orders 的關系

    set relation to CustomerID into Customers

endwith

這裡的代碼比表單示例的要多一些,這是因為我們不能使用自定義的資料環境類導緻不得不自己手動編碼來代替。

現在,我們怎樣才能盡可能簡單的就把那些字段放到報表上去呢?由于 CursorAdapter 對象是我們用代碼在運作時才添加到資料環境中去的,在設計時就沒辦法享受到拖放字段到報表上的友善了。這裡有個小技巧:建立一個會建立這些 Cursor 的 PRG檔案,并讓這些 Cursor 處于有效範圍内(可以采用挂起 PRG 的運作或者把 CursorAdapter 對象聲明成 Public 的辦法),然後使用快速報表功能來把那些字段放到報表上,這樣報表控件的大小也設定好了。

圖2展示了當你預覽報表的時候該報表的情況。如果結合表單一起使用的話,你可以試試改動表單資料環境中的 #DEFINE 語句來換用其它類型的資料源。

總結

*****

    我們對新的 CursorAdapter 基礎類的研究就到這裡了。我個人對 CursorAdapter 的出現感到非常的興奮,并計劃給我的應用程式架構中的資料處理部件升更新以更充分的利用它的優點。

作者:huangdehua

專家分:5680

 會員資訊  發短消息    所屬BLOG

發表時間:2004-5-21 17:46:00    [回複]   [引用] 3 樓   一個使用說明,關于cursoradapter類的

作者:四維

vfp8最激動人心的變化是  CursorAdapter 類,它為不同的資料源提供通用的資料接口。

下面介紹怎樣用 CursorAdapter 改變在 VFP 8 中連接配接資料的方式,包括,native tables, ODBC, OLE DB, XML.  。   

CursorAdapter 類是 VFP 8 開發組的最給人印象深刻的成就之一。 它将會改變許多開發者連接配接各種不同的資料來源的方式。 開發小組在 VFP 存取資料的方式方面作了重要改變,将本地和遠端資料連接配接方式進行了統一。另外,建立 CursorAdapter 類對那些已經熟練使用視圖和SPT人來說并不費力。對使用ADO RecordSets 或XML可擴充标示語言檔案的人也不費力。

CursorAdapter 類的獨特之處在于,它是第一個提供本地遊标、ODBC、ADO、XML資料源連接配接的基類,在一個類裡面實作了所有連接配接功能。換句話說,要将ODBC資料源,ADO RecordSet 或XML可擴充标示語言檔案翻譯成一個 VFP 遊标,CursorAdapter 類完全可以勝任。

你或許會說 CursorAdapter 是較早版本中本地視圖和遠端視圖技術的替代 (注意: VFP 8中仍然保留這些功能). 但是在一些情形中,它也代替了SPT, 以及減少了使用ADO和XML時的代碼量,可以直接使用ADO和XML。

CursorAdapter 的最大好處是,需要在同一個程式内連接配接到多個資料源的時候,它為你提供友善。舉例說,如果你的程式大部分資料來自 SQL server,但是同時需要與XML可擴充标示語言連接配接,CursorAdapter 可以整合這兩種情況,使程式取回的資料作為VFP的遊标。

另外的一個例子是資料現在被儲存在 VFP 表中 , 但是計劃要移到一個資料庫伺服器 , 比如 SQL server或Oracle。 你需要先建立一組 VFP CursorAdapter 類,必要的時候以 SQL server以外的資料庫代替這些類。

但是,就象我們能跑之前必須學會走一樣,先概覽一下 CursorAdapter 類和它的特性。 然後,使用 CursorAdapter 類來設計資料類是比較容易的。

建立第一個 CursorAdapter 類,象其他類一樣,學習怎樣使用它的最好辦法是了解建立過程。第一次建立這些類的時候複雜程度低一些,我們開始用 CursorAdapter 類存取 VFP 本地的資料。 這很象使用本地視圖取回 VFP 本地表的資料。 稍後在這一篇文章中,我們将會使用另一個 CursorAdapter 類連接配接到 SQL server資料庫,ODBC和XML。

首先,你有二個方法建立 CursorAdapter 。 你能使用資料環境建立,也可以手動通過程式或類設計器來建立一個經過一個CursorAdapter類 。 這一個例子将會使用資料環境建立;較遲的例子将會手動建立.

如果你不熟悉 VFP 8 變化後的資料環境, 你可能認為在設計環境下建立的 CursorAdapter 隻能在表單中使用,不能用于類。 然而,  VFP 8 中已經改善了設計環境,是以不需要在表單中就可以建立。

用creat class指令建立一個新的資料環境類。

此主題相關圖檔如下:

确定從下拉清單中選擇based on資料環境類(dataenvironment) 。類名為Tests,所屬類庫名為tests.vcx.

此主題相關圖檔如下:

建立的類在類設計器中出現後,右鍵單擊Data Environment ,選擇builder,起動資料環境建立向導。

此主題相關圖檔如下:

在資料源類型項下,注意可選的選項。 因為第一個例子将會連接配接到本地的 VFP 表,選擇native。 選擇完以後, 使用‘省略号’按鈕選擇 Northwind 資料庫(我這裡是gzdata資料庫)。 (預設位置是 c:/ program files/microsoft visual foxpro 8/ sample/northwind/ northwind.dbc)

此主題相關圖檔如下:

下一步,點cursors 頁, 它初始值是空的。 在清單框中,選擇new按鈕用 CursorAdapter 設計器建立一個新的 CursorAdapter 類。

此主題相關圖檔如下:

首先,你應該看看Properties頁,這裡提供選項來選擇類的名字和由類産生的遊标的别名。

确定提供一個不同于表名字的别名,避免産生混亂。 在這裡,使用 caCustomer 做類名, cCustomer 作為别名。 如果想讓這個類用和資料環境一樣的資料源,應該選擇 "Use DataEnvironment data source" 。 注意你可以為 CursorAdapter 設定不同的資料源, 允許你在不同的類之間整合資料源.( 例如一個類使用ODBC資料源,另一個類使用XML資料源)

此主題相關圖檔如下:

要定義CursorAdapter 如何傳回來自資料源的資料,使用設計器中的Data Access 頁。 按build按鈕激活一個對話框,可以選擇遊标包含的字段。

此主題相關圖檔如下:

在這個例子中,選擇Customers表, 然後選擇Customers.*。 點選向右的箭頭移動選擇項, 然後點ok。

此主題相關圖檔如下:

這為你建立下列SQL語句:

   select CUSTOMERS.* from CUSTOMERS

如果你想添加過濾器,連接配接, 或其他條件到查詢,你可以在清單框中直接鍵入。 如果你想建立帶參數的查詢,有一些選項,在本文後面介紹。 現在, 讓我們添加WHERE子句:

   select CUSTOMERS.* from CUSTOMERS where

   companyname like 'C%'

此主題相關圖檔如下:

這裡可以看出源表和遊标的不同,因為隻有少數記錄符合WHERE子句。

在第二個編輯框(schema)中已經為為你建立了字段清單。通常在繼續以前花幾分鐘看看字段順序是否符合你的習慣是有好處的

在這一頁的下半部分有資料包設定對話框(data fetching),用來設定怎樣處理遠端資料包,當用vfp作為資料源的時候,這裡的設定不發揮作用。(如上圖)這裡我們保留預設設定,稍後再講述具體細節。在本頁的底部附近是緩沖模式設定, 允許你設定任何被關聯的表單的緩沖模式。有兩個選項:開放式行緩沖和開放式表緩沖。

此主題相關圖檔如下:

通常,你使用開放的表緩沖模式,除非你有特殊要求使用行緩沖模式。在這個例子中設定為開放式的表緩沖。最後,“break on error”控制CursorAdapter類怎樣來處理錯誤。預設設定是類(class)自行捕獲錯誤,并且允許你用aerror()函數捕獲這些錯誤。標明這個設定,CursorAdapter類内部不管發生什麼錯誤,vfp都會出現錯誤資訊。也就是說,你需要使用ON ERROR指令或者‘類’的ERROR事件來排除不需要報錯的情況。通常情況下不選這項設定,以便程式能處理任何發生的例外情況。

最後一頁 (auto update) 配置如何更新源表。在通常情況下,選擇“自動更新(auto-update)”和“更新所有字段(update all fields)”。

此主題相關圖檔如下:

這将使cursoradaper類對遊标(cursor)中資料的任何改變自動建立适當的更新、插入、删除機制。然而,你必須選擇遊标中的主關鍵字段,以便這些機制(更新、删除、插入)唯一的識别源表中的記錄。在本例中,CustomerID是關鍵字段。是以,需要在其前面大上‘對号’。其他的設定暫時保留預設值。具體設定辦法在本文後面講述。設定完cursoradaper後,點選“ok”按鈕,回到資料環境設定。此時,你應該在左邊清單框中能看到caCustomer類, 在右邊看到細節。如果你想更改這個類,你可以随時用資料環境‘builder’更改,選擇需要更改的CursorAdapter 類,然後點選builder按鈕。

存取 VFP 資料

此時,你可以測試資料環境,看看是否能取回在 CursorAdapter 的指令中篩選的資料。 使用指令窗戶,例示 DE 類而且喚起 OpenTables 方法:

lo = NewObject("deTest","Tests.vcx")

   ? lo.OpenTables()

   BROWSE  

一個特殊情況是,CursorAdapter的遊标連接配接到其它對象,如果你毀壞了指向CursorAdapter類的對象,會丢失遊标和其中的記錄。這就是說,你必須確定CursorAdapter對象參數在你打算存取的關聯遊标的範圍内

編輯 VFP 資料

現在, 讓我們看看是否能夠更新遊标,并且把更新準确地發送到源表。在指令視窗測試以下指令:

REPLACE contactname WITH 'My Name Here'

   ?TABLEUPDATE()

   SELECT customers

   BROWSE

浏覽customers别名,你會發現修改過的記錄已經更新到源表中。 如果你在發送replace指令之前沒有移動記錄指針,客戶ID中 'CACTU' 所在的記錄被修改。 不管你修改哪條記錄, 這證明 CursorAdapter 是能夠被更新,而且更新能夠準确地被發送到源表。

讓我們打開你剛剛測試的資料環境類,這不隻是一個練習—它是一個很棒的方法學習當你決定在資料環境以外建立自己的類的時候,該如何正确地配置一個 CursorAdapter 類。

雖然資料環境有一些屬性改變和一個方法, 我們實際上對那些改變不感興趣。看一下下拉清單框中的屬性清單,選擇 caCustomer 類,看看建立 CursorAdapter 類的時候需要進行的設定。 表 1 概述被builder和每個 PEM 所做改變。

所有的屬性包含在 "see Init" ,通過INIT方法中的代碼來設定這些屬性。 那一段代碼顯示在list 1中。

在builder設定完以後,看看一些屬性是怎樣設定的,這是最好的學習方法。你可以在這裡或者通過builder改變設定值。然而,如果在這裡改變設定值,你需要冒破壞一些功能的風險,因為你在這裡改變的屬性有可能在builder裡不會發生改變。

不管怎樣,你能在 Init() 代碼中看到 SelectCmd 屬性是如何叙述的, 同樣在 KeyFieldList , UpdatableFieldList 和 UpdateNameList中也可以看到。 特别注意 UpdateNameList 屬性—這個屬性列出遊标的每一個字段以及源表中對應的字段(帶表名稱)。

當從頭建立你自己的 CursorAdapter 類的時候,你可能想在這個清單中省略表名字。 然而,如果不使用精确的格式,你的更新将會失敗, 但是沒有錯誤提示。 在後面講不通過builder建立一個類的時候我将再說這一點。

前面我說過 CursorAdapter使用本地的資料源的時候 , 本質上是一個替代了本地視圖。如果你曾經用過本地視圖,你可以發現類似的地方: 生成一個SQL select語句,定義哪些字段将被更新,設定關鍵字段,剩下的事情讓 VFP 來做。 一旦遊标中取回資料,可以使用 TableUpdate() 發送更新到源表,而且 VFP 自動地建立必要的update,insert,delete語句。

還是以上面的例子, 取消cCustomer 别名中contact字段被替換的資料。通過TableUpdate , VFP 自動地産生 (并送出)下列更新指令嘗試更新:

UPDATE customers ;

     SET CONTACTNAME=ccustomer.contactname ;

     WHERE ;

      CUSTOMERID=OLDVAL('customerid','ccustomer');

     AND ;

      CONTACTNAME=OLDVAL('contactname','ccustomer')

VFP 能夠根據CursorAdapter的KeyFieldList屬性和部分UpdateNameList屬性中的值産生where子句。它通過子句記錄那些已經被改變或添加的記錄,保證你不會更新那些别人已經更新過的記錄。注意,這是因為我們在它的WhereType 屬性中保留預設值 "key fields and any modified fields."

在本文中出現table1,list1等,可惜我當時沒下載下傳,現在已經找不到了,不過不會對了解本文有什麼影響,下次發“cursoradapter類的錯誤處理"

錯誤處理

明顯的,當用 CursorAdapter 更新資料時候 ,并不能完全如你所願。你知道,多種原因可以導緻TableUpdate 更新失敗, 例如更新沖突或記錄加鎖失敗。你必須對 CursorAdapter 類做特别處理來發現這些問題嗎? 答案是,"視情況而定 ."

我們來建立一個簡單的更新問題:給CursorAdapter嘗試更新的一條記錄加鎖。如果類設計器仍然是開着的,關掉它。然後, 像前面做過的一樣,用 NewObject 函數啟動detest類,而且啟動 OpenTables 方法。浏覽遊标以便你能看到資料,但是先不要改變任何東西。

現在打開 VFP 8 的第二個執行個體,這樣就能夠加鎖記錄。 在指令視窗中運作下列語句加鎖你将嘗試更新的記錄:

OPEN DATABASE (HOME(2)+"Northwind/northwind.dbc")

   USE customers

   LOCATE FOR customerid = 'CACTU'

   ?RLOCK()

你應該能傳回.T.,表明記錄确實被 VFP 的這一個執行個體加鎖。

回到 VFP 的第一個執行個體,在指令視窗中運作下面的代碼:

REPLACE contactname WITH 'updated'

   SET REPROCESS TO 2 SECONDS

   ?TABLEUPDATE()

在這種情況, TableUpdate 傳回.F.,表現記錄鎖阻止更新成功。 如果你用 AERROR() 捕獲錯誤而且顯示錯誤結果, 你會看到錯誤資訊 "記錄沒加鎖"。 這意味着如果你對設定緩沖的表直接操作而不是一個遊标,你能用同樣的辦法處理錯誤。

不幸地,不是所有的預期錯誤都會這樣表現出來。需要特别注意的是更新沖突,比如一個使用者企圖覆寫其他人所作的修改。想看看這種行為的結果,在目前的 VFP 執行個體中(CursorAdapter 正在使用)運作下面的代碼:

?TABLEREVERT(.T.)

   REPLACE contactname WITH 'client 1'

現在轉變在到第二個例證并且發行下列指令:

CLOSE DATABASES all

   OPEN DATABASE (HOME(2) + "Northwind/northwind.dbc")

   USE customers

   LOCATE FOR customerid = 'CACTU'

   REPLACE contactname WITH 'client 2'

   BROWSE

傳回第一個執行個體, 嘗試用 TableUpdate 發送更新:

?TABLEUPDATE()

在這種情況, TableUpdate 錯誤地傳回.T.,使你相信更新成功了! 然而, 事實上并沒有成功,而且這可以被CursorAdapter的CursorRefresh() 方法證明, 用下列代碼:

?TABLEREVERT(.T.)

   ?lo.caCustomer.CursorRefresh()

CursorRefresh 方法告訴 CursorAdapter 再運作 SelectCmd 裡的語句,并且取回來自源表的最新資料。 對ContactName 字段的測試說明 CursorAdapter 根本沒更新字段值!

解決這一個問題的最簡單的方法要利用 CursorAdapter 的 AfterUpdate 方法。這一個方法在 TableUpdate 發送儲存每條記錄的請求後被執行。注意這個‘方法’隻對目前記錄有效。 如果記錄是新的,或者記錄已經被删除,會激活AfterInsert 或 AfterDelete 方法。

AfterUpdate 方法捕獲一些參數, 包括最初的字段狀态,是否被強制更改, 和作為更新的指令。 最後一個參數 , lResult,是我們這部分主題最重要的,因為它告訴我們更新過程是否成功。

使用的另一個重要角色解決更新沖突問題的是系統變量 _tally, 它告訴我們最後一次操作影響了多少記錄。 是以,如果 lResult 放回成功的, 但是 _tally是零,那麼說明沒有記錄被更新,那麼你可以判定這種情況是一個更新沖突。

簡單說,解決這一個問題的簡單方法是把下列代碼加入 CursorAdapter 類的 AfterUpdate 方法:

LPARAMETERS cFldState, lForce, nUpdateType, cUpdateInsertCmd, cDeleteCmd, lResult

   IF lResult AND _TALLY = 0 THEN

     ERROR 1585 && update conflict

   ENDIF

有趣的是你在螢幕上看不到錯誤資訊;錯誤資訊被 TableUpdate “限制住了”,這就迫使你使用 AError 函數分析更新失敗的原因。出現這樣的現象是因為 BreakonError 屬性保留了預設值 ,也就是錯誤不引起程式中斷。如果你把這個屬性設為“ture",那麼"更新沖突" 錯誤資訊就會出現,如果作了定義,你的ON ERROR句柄會被運作。

這一個更新沖突問題是為 CursorAdapter設計的,因為 VFP 8 沒有自動發現這個問題方法。是以,當不用本地資料源的時候,這個代碼( 或相似的代碼)會用在你的CursorAdapter類設計中。

CursorAdapter with ODBC

現在你已經有了一定的基礎, 讓我們繼續看看當背景資料庫用 SQL server的時候怎樣進行設定。我們将從使用 VFP 通過ODBC連接配接 SQL server上的 Northwind 資料庫開始。同樣, 讓我們從頭建立 CursorAdapter 以便可以看見類需要設定的各個方面。

首先,用下列指令在類設計器中建立一個新類:

CREATE CLASS caODBC OF tests as CursorAdapter

這裡需要設定的最重要的屬性是 DataSourceType 屬性。 因為我們想通過ODBC連接配接 SQL server,将這一屬性設定為ODBC。當使用這種連接配接方式的時候 , DataSource 屬性需要有一個确切的連接配接句柄,可以用 SQLConnect 或 SQLConnectString 函數建立。 在任一情況,這些功能應該在 CursorAdapter 類的 Init 方法中使用下列代碼:

LOCAL lcConnStr, lnConn

   ** string assumes trusted connection (integrated security)

   lcConnStr = "Driver=SQL Server;Server=(local);DATABASE=Northwind"

   lnConn = SQLSTRINGCONNECT(lcConnStr)

   IF lnConn > 0 THEN

     THIS.DATASOURCE = lnConn

   ELSE

     ** unable to connect

   ENDIF

連接配接字元串假定你使用可信賴的關聯到 SQL server;如果你使用安全的連接配接到 SQL server,把 "uid"= 和 "pwd"= 串加入連接配接句柄指定使用者名稱和密碼。 任何一種情況下,傳回的是連接配接柄 , 或錯誤發生時的否定值。 這一個連接配接柄被指定為 DataSource 屬性以便 CursorAdapter 知道該到哪裡聲明。

下一個步驟是建立 SelectCmd 以便 CursorAdapter 知道需要從資料源取回什麼資料。還有一個最好的放置地方是 Init 方法, 因為屬性表限制字元串的長度。 把下列代碼加入 Init 方法中,設定 DataSource 屬性代碼之後, 取回公司名字中以 "C" 開頭的客戶清單:

This.SelectCmd = "SELECT " + ;

     "CustomerID, CompanyName, ContactName, " + ;

     "Address, City, Region, Country " + ;

     "FROM Customers WHERE CompanyName LIKE 'C%'"

然後,你應該告訴 CursorAdapter 通過調用CursorFill 方法填充被關聯的遊标。 你可以省略這一個調用,并且手動從類之外調用, 或放在 Init 方法中,以便它能夠自動填充遊标。在Init 方法中設定 SelectCmd 之後,可以用 This.CursorFill()調用。

最後,你應該在類的Destroy方法中加上一些代碼,以便在對象從記憶體中釋放以後減少到伺服器的連接配接。 沒有這些代碼的話,每一個新的執行個體将會産生一個到伺服器的新連接配接, 并且不會釋放它:

IF this.DataSource > 0 THEN

     SQLDISCONNECT(this.DataSource)

ENDIF

藉由這些變化,你擁有一個實用的 CursorAdapter 類,它能夠生成一個隻讀的遊标。在允許更新之前,測試一下類,確定可用,并能準确取回資料。用下列代碼測試:

lo = NEWOBJECT("caODBC","tests")

   BROWSE

注意,你不需要像資料環境那樣調用 OpenTables 方法。這是因為你把 CursorFill 方法直接加入了 Init 方法,使類在執行個體化之後自動地填充遊标。

Updating ODBC Data(更新ODBC資料)

為了使這個類可更新,你必須正确地給Tables,  KeyFieldList , UpdatableFieldList 和 UpdateNameList 屬性指派。 并且設定 AllowInsert , AllowUpdate 和 AllowDelete 屬性為true, 确定自動更新特征被激活。 再一次強調,最好的辦法是在Init 方法中用代碼設定這些屬性。 Init 方法一些代碼在list2 中。

在關閉類設計器之前,你可能想将 BufferModeOverride 屬性換成 "5",“開放的表緩沖”以便移動記錄指針的時候 , 自動更新不發生。

測試 CursorAdapter 的更新能力,把它作為一個執行個體,浏覽遊标,作一個更改, 然後送出 TableUpdate。 确定所作的更改被響應,調用 CursorAdapter 的 CursorRefresh 方法,再浏覽一次。

處理ODBC錯誤(Handling ODBC Errors)象使用本地 CursorAdapter一樣 ,大多數的錯誤處理是按照 "傳統的"方法—測試 TableUpdate 傳回的值,如果失敗,使用 AError 來判斷原因。 不幸的是,更新沖突的檢測也是ODBC連接配接類型 CursorAdapter 的一個問題。

本地 CursorAdapter 錯誤的解決辦法是在 AfterUpdate 方法中設定錯誤處理代碼, 但這種方法對ODBC型 CursorAdapter 是無效的,因為更新失敗的時候,我們不是要處理VFP錯誤,而是ODBC錯誤。 是以,最好答案是使用一個存儲過程,或在更新句柄中增加一些代碼發送到伺服器。

回想一下,為本地的 CursorAdapter 更新錯誤的解決辦法是檢查 _TALLY,看看是否有記錄被更新。為ODBC連接配接的解決辦法與本地CursorAdapter是相似的,但是我們不能使用 _TALLY,因為在遠端資料檢測上它是不可靠的。 我們可以使用SQL server的@@Rowcount 系統函數來判定記錄是否被更新。

如果你要寫一個 T- SQL 批量更新語句來更新一筆記錄,你應該象下面這樣寫:

[email protected] and @oldContact set by earlier code or parameters

   UPDATE customers

     SET ContactName = @newContact

    WHERE CustomerID = @custID

       AND ContactName = @oldContact

   IF @@ROWCOUNT = 0

     RAISERROR('Update failed.',16,1)

RaisError T- SQL 函數使 VFP 接收一個ODBC錯誤 (1526 号),  在第一個參數裡寫錯誤資訊.(另外二個參數指出嚴重和錯誤的狀态) 在這種情況下, 當@@Rowcount=0的時候RaisError 被調用, 表明早先的 T- SQL 語句沒影響任何的記錄。

所有讨論的這一些表明你可以使用 CursorAdapter 的 BeforeUpdate 方法來描述發送到伺服器用來更新的語句。 BeforeUpdate 方法接受五個參數, 有趣的是最後的二個 (cUpdateInsertCmd 和 cDeleteCmd) 建立了關聯(注:原文the last two (cUpdateInsertCmd and cDeleteCmd) are interesting in that they are passed by reference. )。 在它們被送去資料源之前 , 這允許你修改指令。

在我們的例子中,我們使用這一個方法增加一個對@@Rowcount的測試然後調用 RaisError 。把下列代碼寫入 BeforeUpdate :

LPARAMETERS cFldState, lForce, nUpdateType, ;   

     cUpdateInsertCmd, cDeleteCmd

   IF nUpdateType = 1 THEN

     cUpdateInsertCmd = cUpdateInsertCmd + ;

       " if @@ROWCOUNT = 0 "+ ;

       "RAISERROR('Update Failed due to update " + ;    

         "conflict.',16,1)"

   ENDIF

現在,為發送到背景資料庫的每一行記錄,用這段代碼測試行是否被更新。 如果沒更新, VFP 将會接收到錯誤,TableUpdate 将會失敗,而且 AError 将會顯示1526号錯誤,并顯示預設的錯誤資訊。

存在二個的問題。 首先,這是特别為 SQL server設定的;對其他的ODBC資料源 ( 比如ORACLE) ,這一段代碼無效。 其次,這個錯誤資訊是指一類錯誤,總是産生相同的 VFP 錯誤号, 而且在 VFP 中建立正确的錯誤處理句柄有一點困難。這一個問題可以通過在SQL SERVER伺服器上建立通用的錯誤資訊,每條資訊對應着自己的唯一錯誤号碼。

另外一個更好的解決方法是使用存儲過程來執行更新,代替用VFP建立一個查詢。 當然,采用存儲過程的不利因素是不能使用VFP 的自動更新句柄來更新資料。

參數化設定   Parameterization

通過學習,你已經知道如何為 CursorAdapter 類的方法、屬性添加指令。 基本上,在類中使用的每個事件(event)都有一組before和after方法, 比如 BeforeUpdate 和 AfterUpdate 。然而,沒有 BeforeSelect 或 AfterSelect-替代它們叫做 BeforeCursorFill 和 AfterCursorFill,因為遊标是用 SelectCmd 的結果來填充的。

BeforeCursorFill 方法接受三個參數, 而且傳回 Boolean 值。 第一個參數 , lUseCursorSchema, 叙述 CursorSchema 屬性是否控制遊标的結構。 第二個參數 , lNoDataOnLoad,與視圖的 NODATA 子句類似,隻取回表結構,但是沒有從資料源取回資料。

對于現在的讨論 , 第三參數 , cSelectCmd,是最有意思的。 我們已經提到過它 (象 BeforeUpdate 的 cUpdateInsertCmd 參數) ,先使用 SelectCmd 的目前設定。然而,如果你改變這一個參數的值,它不改變 SelectCmd 屬性的值;取而代之的是,它的值改為被傳給資料源的語句 。

舉例來說, 假如你已經把 CursorAdapter 對象的 SelectCmd 設定為下列代碼:

SELECT CustomerID, CompanyName, ContactName, City,

   Region, Country FROM Customers

在呼叫 CursorAdapter 的 CursorFill 方法之前, BeforeCursorFill 方法的 cSelectCmd 參數會包含這個語句。 假如你在這一個方法中用下列代碼:

cSelectCmd = cSelectCmd + ;

     " WHERE CompanyName LIKE '" + ;

     this.cCompanyName + "%'"

這就導緻實際的select指令總是包含如上面代碼描述的where子句和 this.cCompanyName(自定義屬性) 的目前值.因為它不改變 SelectCmd 的初始值, 你不必用任何特别的代碼來确定在select指令中是否包含兩個where子句。

參數設定第二部分

如果你以前用過視圖,或 SQL pass through,那麼你可能熟悉在參數前使用 "?" 字元。 這一個用法也适用于 CursorAdapter 。 下面的代碼例子告訴你如何在 CursorAdapter 的 SelectCmd 屬性中使用參數:

This.SelectCmd = "SELECT * FROM Customers " + ;

     " WHERE CompanyName like ?lcMyVar "

   lcMyVar = 'C%'

   This.CursorFill()

最要緊的是确定變量 "lcMyVar" 在 CursorFill 方法被請求之前被定義。否則的話,VFP會提示找不到值,使用者會無所适從。

你可以使用 CursorAdapter 的屬性作為參數,用來替代本地的變量。這樣做有這樣的優點:隻要對象存在,屬性也會存在,而且你可以通過使用access/assign方法來确定配置設定的值符合規則。

使用存儲過程

在上面說過, 使用儲存過程是一個突破錯誤處理限制的好辦法。讓我們逐漸探究在ODBC CursorAdapter中使用存儲過程的方法。這樣我們就能感覺出手動處理 CursorAdapter 類的更新錯誤是多麼複雜。

這一段是關于通過調用資料源的存儲過程來代替自動執行的update、insert和delete指令。 這意味着你必須處理 UpdateCmd , InsertCmd 和 DeleteCmd 屬性, 而且假設 SQL server上的 Northwind 資料庫已經建立存儲過程來提供這些功能。  

例子, 讓我們看一看一個簡單存儲過程的完整代碼,你可以用它更新 Northwind 資料庫的customer表的 ContactName 字段:

--T-SQL code, not VFP

   CREATE PROCEDURE UpdateCustomerContact (

      @CustomerID nchar (5),

      @ContactName nvarchar (30),

         @oldContact nvarchar (30)

     )

   AS

     IF @CustomeriD IS NULL

       RAISERROR('CustomerID is a required parameter',16,1)

     ELSE

       UPDATE Customers

          SET ContactName = @contactName

        WHERE CustomerID  = @customerID

          AND ContactName = @oldContact

為了節約空間,這一個存儲過程沒有包含全部的錯誤處理代碼。 不管這個,已經有足夠的代碼來說明怎樣在 CursorAdapter 類中執行更新。

幸運地, 建立 UpdateCustomerContact 存儲過程作為更新指令,可以取代 BeforeUpdate 方法,用下面的代碼:

LPARAMETERS cFldState, lForce, nUpdateType, ;

     cUpdateInsertCmd, cDeleteCmd

   cUpdateInsertCmd = ;

     "EXECUTE UpdateCustomerContact '" + ;

     EVALUATE(this.Alias+".CustomerID") + "','" +;

     ALLTRIM(EVALUATE(this.Alias+'.ContactName'))+ ;

       "','" + ;

     OLDVAL('contactname',this.Alias)+"'"

這裡,代碼放在 cUpdateInsertCmd 參數裡,覆寫了預設的update指令。我使用了evaluate函數,是為了cursor的名字是動态的,說明cursor的名字可以很容易被改變,但是代碼不變。還有,我使用了OLDVAL 函數取回 ContactName 字段被修改前的值。如果在存儲過程的where子句裡需要舊的資料,那麼這個函數是必需的。這有點像自動産生update的情況。

記住,在記錄被實際更新之前 ,通過TableUpdate自動呼叫 BeforeUpdate 方法。 是以, 無論UpdateCmd目前的值是什麼,這個方法程式(指UpdateCmd)被禁用,而且總是使用存儲過程。

注意,你也可以使用已經讨論過的參數設定方法,使用 BeforeUpdate 方法。 這就要求你為 CursorAdapter 提供 UpdateCmd設定,但是, 不要在參數裡用固定的代碼,要用變量或者屬性,并在它們前面加上“?”。

在這裡需要注意的重要的一點是 cUpdateInsertCmd( 或對象的 UpdateCmd屬性) 不能傳回任何值。 進一步說,如果你從存儲過程傳回一個值,它就沒有地方 "去" ,那麼這個值就會永遠丢失。是以,在存儲過程中添加一個RaisError呼叫,以便在更新過程中發生任何錯誤( 例如不正确的參數或一個更新沖突) 的時候,你的代碼能夠作出反應,這樣做是非常重要的。你可以通過測試 TableUpdate 傳回值來捕獲錯誤 ,或者用 AError 方法, 然後分析錯誤。

相似的代碼也應該寫進 BeforeInsert 和 BeforeDelete 方法,以便它們也調用存儲過程,而不是調用設定好的“查詢”。 為了節約空間,我将留下代碼當做 "讀者的練習."

CursorAdapter with OLE DB

我們的下一個任務是看看如何通過 CursorAdapter 類使用OLE DB(對象連接配接與嵌入), 并且把它同我們用過的native和ODBC作一比較。OLE DB技術比ODBC有更多的處理能力, 而且能夠連接配接的資料源類型比ODBC多。 CursorAdapter 通過嵌入ADO對象使用OLE DB,這是OLE DB技術标準的 COM 封裝。 VFP 會自動地把ADO記錄集  轉換成一個 VFP 遊标, 以及處理更新, 正如在早先的例子中講到的一樣。

第一件要做的事,當然還是建立一個新的 CursorAdapter 類,我們用代碼建立一個。

開始建立一個新的程式,取名字 caADO.prg, 把下列代碼添加進去:

PUBLIC goCAADO as CursorAdapter

   goCAADO = CREATEOBJECT('caADO')

   BROWSE

   DEFINE CLASS caADO AS CursorAdapter

     oConn = NULL

     oRS = NULL

     Alias = "cCustADO"

     DataSourceType = "ADO"

     SelectCmd = "SELECT " + ;

       "CustomerID, CompanyName, ContactName, "+;

       "ContactTitle, Address, City, Country "+;

       "FROM Customers WHERE Customerid LIKE 'C%'"

     FUNCTION Init()    

       This.DataSource = this.oRS

       This.oRS.ActiveConnection = this.oConn

       This.CursorFill()

     ENDFUNC

   ENDDEFINE

在這段代碼中,我們将 DataSourceType 設為ADO而且把我們對Customers的查詢加入 SelectCmd 。 當 DataSourceType 是ADO的時候, DataSource 屬性必須包含有效的 RecordSet 或指令對象, 這依賴于你如何使用 CursorAdapter。 如果你不使用參數化查詢 (就象前面例子中講到的用 "?")那麼你可以用記錄集( RecordSet );否則,你必須使用指令對象,因為ADO已經代替了參數選擇。 你的查詢中的任何參數在指令對象中自動地被處理。

在這種情況下,我們使用 RecordSet 對象 , 但是應該注意我們必須提供一個“連接配接”對象。 在這兩種情形中,我使用Access方法建立了關于這些對象的參考。 list3 中是Access方法的代碼。

兩個Access方法首先檢查對象是否已經被建立。 如果沒有,那麼就繼續建立對象。在使用 RecordSet 的情形,你隻需要建立對象,剩下的由 CursorAdapter 來做。 用“連接配接”對象的情況下, 你必須提供連接配接字元串(連接配接句柄)并且打開連接配接,因為CursorAdapter 不為你打開連接配接。這是因為“連接配接”不是 CursorAdapter 的一個屬性, 而是 RecordSet 對象的一個屬性。

下一次發:用OLE DB更新

用OLE  DB更新

如果不另外設定幾個屬性,這麼簡單的 CursorAdapter 是沒有更新能力的。 把下面的代碼放在定義類(define)的代碼中,在init方法之前運作,将會允許自動更新:

KeyFieldList = "CustomerID"

   UpdatableFieldList = ;

     "CompanyName, ContactName, ContactTitle, "+ ;

     "Address, City, Country"

   UpdateNameList = ;

     "CustomerID Customers.CustomerID, " + ;

     "CompanyName Customers.CompanyName, " + ;

     "ContactName Customers.ContactName, "+;

     "ContactTitle Customers.ContactTitle, " + ;

     "Address Customers.Address, "+;

     "City Customers.City, Country Customers.Country"

   Tables = "Customers"

然而, RecordSet 會建立它的預設的 CursorLocation 和 CursorType 屬性。 如果不改變這些屬性設定, RecordSet 最初是隻讀的,是以,你需要按以下方法修正 oRS_Access 方法:

FUNCTION oRS_Access() as ADODB.RecordSet

     LOCAL loRS as ADODB.RecordSet

     IF VARTYPE(this.oRS)<>"O" THEN

       this.oRS = NULL

       loRS = NEWOBJECT("ADODB.Recordset")

       IF VARTYPE(loRS)="O" THEN

         loRS.CursorType= 3  && adOpenStatic

         loRS.CursorLocation = 3  && adUseClient

         loRS.LockType= 3  && adLockOptimistic

         this.oRS = loRS

       ENDIF

     ENDIF

     RETURN this.oRS

   ENDFUNC

為 RecordSet 加上這些代碼以後, CursorAdapter 就能處理自動更新了。

下一次發cursoradapter with XML

CursorAdapter with XML

最後, 讓我們建立使用XML作為資料源的 CursorAdapter。 這一節很有趣,因為XML文本不像通常的資料源。 同樣,當資料源設定為XML的時候, CursorAdapter 不會自動建立 SQL 更新, 插入或删除指令。是以,這種類型的 CursorAdapter 需要大量的代碼接收和更新資料。

在這一個例子中,我将使用 SQL server 2000 的 SQLXML 提供XML文本。 同時,因為SQLXML支援通過XML更新,我們将花一點時間寫必要的代碼來執行更新。 假設你已經配置了SQLXML,允許HTTP資料存取Northwind 資料庫,而且允許用 UpdateGrams 對資料庫進行更新。

在這裡,我在 IIS 上面設定使用一個被稱為 "nwind" 的虛拟目錄存放 HTTP 資料。 是以,我的全部例子将會包含這個網址:

http://localhost/nwind,經由%20IIS%20存取%20SQLXML。

讓我們開始建立一個新的程式,程式名字為caXML.prg,用下列代碼(建立一個類):

PUBLIC oCAXML as CursorAdapter

   SET MULTILOCKS ON && need for table buffering

   oCAXML = CREATEOBJECT('xcXML')

   BROWSE NOWAIT

   DEFINE CLASS xcXML AS CursorAdapter

     DataSourceType = "XML"

     Alias = "xmlCursor"

     UpdateCmdDataSourceType = "XML"

     InsertCmdDataSourceType = "XML"

     DeleteCmdDataSourceType = "XML"

     BufferModeOverride = 5

     *custom properties

     oXMLHTTP = NULL

     oXMLDOM = NULL

     cServer = "localhost"

     cVDir = "nwind"

   ENDDEFINE

不同于通常的 DataSourceType 和Alias(别名)屬性設定,這是我們第一次看見 xxxCmdDataSourceType 屬性。 因為這是一個XML類型的 CursorAdapter,如果你想讓它用來更新源資料,這些屬性就必須進行設定 。推薦在這個類中使用 oXMLHTTP 和 oXMLDOM 屬性,将會在下面做詳細說明。

接收XML資料

在考慮 CursorAdapter 的更新能力之前,我們先研究怎樣從SQLXML 伺服器接收文本。 首先,一個簡單的select指令不能工作,我們必須建立通用的 SelectCmd 。 這在 Init 方法中很容易做到,同時,在INIT中調用 CursorFill 方法,如以下代碼:

FUNCTION INIT() as Boolean

     LOCAL llRetVal, lcMsg, laErr[1]

     this.SelectCmd = "this.GetXml()"

     llRetVal = THIS.CursorFill()

     IF NOT llRetVal THEN

       AERROR(laErr)

       lcMsg = "Cursor was not filled!"

       IF NOT EMPTY(laErr[2]) THEN

         lcMsg = lcMsg + CHR(13) + laErr[2]

       ENDIF

       MESSAGEBOX(lcMsg,16,"XMLCursorAdapter Test")

     ENDIF

     RETURN llRetVal

   ENDFUNC  

這一段代碼建立 SelectCmd 作為本地用的方法,代替 SQL 指令。 以前的例子中沒有這樣做過,對任何 CursorAdapter 類,不管什麼類型,都是合法的。然而,當你使用一個本地的方法作為 SelectCmd 的時候,你必須也提供代碼用來更新,插入和删除, 因為如果不是一條SQL語句的話, VFP 是不會自動處理的。

當我們在 Init() 中使用 CursorFill 的時候,GetXML 方法被調用。 資料源設定為XML的時候,GetXML 方法必須傳回隻包含一個表的最終文本。 如果它包含多個表,你會得到料想不到的結果。 GetXML 方法在list4 中列示。

GetXML開始于送出一個 MSXML2.XMLHTTP COM 對象。 這個對象處理所有 HTTP 通信,包括發送請求到伺服器并且傳回結果。 你可以看到,oXMLHTTP 執行個體對象是被設計好的Access方法控制的,這樣設計是為了防止連續地建立和破壞 COM 伺服器。

然後,你可以看到我們的典型的SELECT描述, 除了LIKE子句有點稍稍不同以外。 HTTP 需要我們當十六位百分比字元顯示到達25%的時候退出顯示(指進度顯示), 在 SQL server接收查詢之前 , 這個值将會變成單一的百分比符号。

然後,代碼用具體的查詢要求來設定URL,并且通過HTTP發送URL到SQL SERVER伺服器。SQL SERVER接收到這個查詢,并且處理它,以XML方式傳回值。這是因為我們已經在“查詢”中包含了FOR XML子句。在這個例子中,XML的根元素叫做 "results" 。你從查詢字元串中可以看出。這是按照你的習慣配置的。

此時, lcRetXML 包含一個來自 SQL server的XML資料流。 既然 GetXML 方法被 VFP 作為 SelectCmd 調用,你可以從 GetXML 方法中傳回這個變量的内容,而且 VFP 将會把資料流轉換成一個 VFP 遊标。 你可以通過執行 caXML 程式測試。 會出現一個包含傳回的XML文本内容的浏覽窗戶。 使用調試工具一步一步檢視  GetXML 方法,可以看到在被轉換到一個 VFP 遊标并釋放之前 , lcRetXML 變量的值是XML文本。

更新XML資料

下一個步驟是決定該如何使這個遊标可更新,以便所作的更改能被發送回 SQLXML 伺服器。 SQLXML 能使用一個特别的XML文本,即 UpdateGram, 利用它直接把更新發送到資料庫。 在 VFP7 中,可以用 XMLUpdateGram 函數建立這個文本。 用VFP 8 的 CursorAdapter , UpdateGram 屬性會自動建立。

第一個步驟是設定 updatable 屬性并且建立一個更新指令。 在類定義的開始設定這些屬性,并在CursorAdapter的init事件中增加一行代碼,提供調用更新指令的方法。

KeyFieldList = 'customerid'

   Tables = 'customers'

   UpdatableFieldList = ;

     "companyname, contactname, contacttitle, "+;

     "address, city, country "

   UpdateNameList= ;

     "customerid customers.customerid, " + ;

     "companyname customers.companyname, " + ;

     "contactname customers.contactname, " + ;

     "contacttitle customers.contacttitle, " + ;

     "address customers.address, " + ;

     "city customers.city, country customers.country"  

   FUNCTION INIT() as Boolean

     LOCAL llRetVal, lcMsg, laErr[1]

     this.UpdateCmd = "this.UpdateXML()"

     this.SelectCmd = "this.GetXML()"

     ** balance of code skipped...

注意,我們已經在init方法中替換了屬性表中的 UpdateCmd 和 SelectCmd 設定,實際上最終結果是一樣的。 不管怎樣, 這一段代碼的第一部份現在看起來很熟悉,在這裡我們設定了 KeyFieldList ,table, UpdatableFieldList 和 UpdateNameList 屬性。 沒有這些屬性設定,就不可能建立 UpdateGram 。

然後,我們建立 UpdateXML 方法作為 CursorAdapter 的 UpdateCmd 。 沒有參數傳遞給 UpdateXML 方法,是以,所有決定更新的工作必須這個方法裡處理。還有,因為XML 類型的 CursorAdapter 沒有預設的更新機制, 你必須寫代碼把更新傳遞給XML資料源。

在這一段代碼中,使用 XMLHTTP 對象傳遞更新到伺服器。 通過LoadXML方法加載 UpdateGram 屬性的内容到 XMLDOM( 在ACCESS方法中) 之内,打開到伺服器的連接配接,設定請求的内容為XML, 然後發送 XMLDOM。所有的結果通過 XMLHTTP 對象的 ResponseText 屬性傳回,接着裝載到 XMLDOM 中,并且分析錯誤資訊。

如果沒有提示錯誤,更新已經成功,而且過程結束。然而,如果有錯誤, 就會産生符合文法的錯誤資訊文本,并且包含在一個自定義的ERROR函數中,這樣 TableUpdate 函數就能找到更新失敗的原因。 如果沒有這些代碼, 即使有問題的時候,TableUpdate 總是傳回成功資訊。

測試一下這段代碼,運作 caXML 程式,在遊标中更改一個字段, 然後在指令視窗中發出 TableUpdate。 如果 TableUpdate 成功, 你應該能在伺服器上看到更新結果。如果 TableUpdate 失敗,你需要用 AError 函數接收 SQL server産生的錯誤資訊。

如果你對 UpdateGram 的内容感到好奇, 你可以一步步檢視類的 UpdateXML 方法,并且檢視 UpdateGram 屬性的内容。 然而,如果你不到有關資料變化的方法中 (如 UpdateCmd , InsertCmd 或 DeleteCmd 屬性内容) ,你也不能完全弄明白 UpdateGram 屬性的内容。

list6顯示當ContactName字段的客戶ID被更改為‘CACTU' 後UpdateGram的内容。

正如你看到的,SQLXML 能夠讀取這個文本并且能夠很容易地建立一個Update-SQL 語句, 然後發送到 SQL server。 updg:sync(同步更新)元素使更新立即發生;是以,如果你有多個表需要更新,你可以把它們關聯起來放到一個 UpdateGram中, 确定把他們封裝進這個元素,執行一次就可以全部更新。

結束語

在這一篇文章中,我們涉及了許多方面,講了新的 CursorAdapter 類的四種表現形式。 你已經學到了如何通過 DataEnvironment 、 CursorAdapter buileder、類設計器、和PRG建立 CursorAdapter 。 你也已經了解了建立本地、ODBC、OLE DB或XML型 CursorAdapter 類的基本方法, 并且怎樣使這些類可更新。

下一個步驟是考慮如何把這些類應用到你的程式中。 對我來說,覺得 CursorAdapter 應用到任何程式的 UI 層中效果很好,同時應用于需要大量過程代碼的許多商業開發。CursorAdapter并不是一個最好的選擇對象用于層來傳遞資料。因為它把所有的來自資料源的資料轉化成一個不便于攜帶的遊标。

然而,在一個使用 CursorAdapter 類進行商業開發的方案中,它能接收來自資料源的資料, 然後用标準的 VFP 指令和函數處理資料,這是因為它産生的是一個 VFP 遊标。 資料可以被轉換為更一個較适當的類型,例如XML。

CursorAdapter 的另一個有利條件是通用的 OOP 接口,不管需要存取的資料是什麼類型。 甚至用XML,需要大量代碼來使類可更新,我們仍然可以用使用 CursorFill取回資料,用 TableUpdate 更新資料, 而且用 AError 傳回錯誤, 像使用其他類型的 CursorAdapter一樣。

基于一個事先的考慮或者計劃,你想建立一個可以重複使用的CursorAdapter類。 這些類可以在程式之間重複使用或者在同一程式内部混合使用,來形成一種你的程式處理資料的标準方法。

有網友問使用cursoradapter是否還需要tableupdate(),下面索性再發一篇文章:

如何通過 CursorAdapter 對象使用 TableUpdate 函數更新資料

這一篇文章适用于:vfp 8.0

摘要:

這一篇文章讨論下列各項主題:

  1、TableUpdate 互動地使用 CursorAdapter 類更新後端資料。

  2、使用vfp 8.0 的新 CursorAdapter 類通過TableUpdate 函數更新資料的時候如何處理更新沖突。

較多的資訊:

可以通過vfp 8.0 的 CursorAdapter 類取回來自本地的或遠端的資料源的資料。 預設情況下,CursorAdapter 産生的遊标不更新後端資料。 為了要更新後端資料,必須設定 CursorAdapter 的下列各項屬性值:

  InsertCmd

  UpdateCmd

  DeleteCmd

如果你不設定這些屬性值,你能通過設定下列各項 CursorAdapter 屬性值自動地産生後端 SQL 更新指令:

  tables

  KeyFieldList

  UpdatableFieldList

  UpdateNameList

  SendUpdates

處理更新沖突

當你使用 CursorAdapter更新後端資料的時候, TableUpdate 函數傳回 CursorAdapter 遊标的更新結果。 如果一個更新沖突發生,後端資料的更新不可能成功。 然而, TableUpdate 可能仍然傳回更新成功,因為在 CursorAdapter 遊标中的資料被更新。  

更新沖突是指,使用者試圖編輯一條取回後已經發生改變的記錄。 下列各項是更新沖突能發生的情況:

1. User1 打開由客戶表建立的一個遊标(cursor)。

2. User2 更新第一條記錄而且送出更新。

3. User1 更新第一條記錄(使用 TableUpdate() 函數).

此時, User1 有一個更新沖突:  User1 正在嘗試更新的後端記錄在被取回之後已經發生改變。

DataSourceType 代碼示例:

下列示例代碼使用vfp 8.0 的native DataSourceType 更新 SQL server Northwind 資料庫的一筆記錄。 查證是否記錄被更新,在更新指令中附加下列代碼:

CRLF+[EXECSCRIPT+(" if _tally=0"+CHR(10)"+error ('update conflit')"+CHR(10)+"ENDIF")]

在這種情況下,Tableupdate() 傳回 (.F。 ) 而且允許你處理更新失敗。

#DEFINE CRLF CHR(13)+CHR(10)

Local loCursor,ovfp

CLEAR

ON ERROR

Set Exclusive Off

Close Databases All

Set Multilocks On

loCursor = Createobject('CA')

* Load loCursor with the data specified in SelectCmd and display error message if error occurs.

loCursor.CursorFill()

GO top

* Display the value of the company name before update.

? "Before:",companyname

?

ovfp=Createobject("visualfoxpro.application.8")

ovfp.DoCmd("set exclusive off")

ovfp.DoCmd("update (_samples+'/northwind/customers') set companyname='Alfreds Futterkisted' where customerid='ALFKI'")

GO top

* Update the data in the cursor.

replace companyname WITH 'Alfreds Futterkiste'

* Update the back end.

retval=TABLEUPDATE(0,.F.,locursor.alias)

Messagebox("Tableupdate="+Transform(retval))

* If update conflict occurs, display the error.

if(retval=.F.)

LOCAL ARRAY errors(1)

AERROR(errors)

* Displays the errors.

IF "Update conflict"$ errors[2]

  MESSAGEBOX("Update Conflict-reverting changes")

  =TABLEREVERT(.T.,locursor.alias)

ENDIF

endif

* Refresh the Cursor to get the updated data.

loCursor.CursorRefresh()  && Get the data again to be sure

GO top

* Display the value of the company name after update.

?

? "After:",companyname

Define Class CA As CursorAdapter

Alias = 'test1'

DataSourceType = 'NATIVE'

SelectCmd = 'select * from (_samples+"/northwind/customers")'

Tables = 'Customers'

KeyFieldList = "customerid"

UpdatableFieldList = "companyname"

UpdateNameList = "customerid customers.customerid,companyname customers.companyname"

WhereType= 3

* This is a custom property, that is added to handle update conflicts. It does not do

* anything by itself. It is added below to the automatically-generated UpdateInsertCmd to

* test whether anything was actually updated.

ConflictCheckCmd =CRLF+[EXECSCRIPT("IF _tally=0" + CHR(10) + "ERROR('Update conflict')" + CHR(10) + "ENDIF")]

Procedure AfterUpdate

  Lparameters cFldState, lForce, nUpdateType, UpdateInsertCmd, DeleteCmd, lResult

  * To see why it will fail on the back end, look at the UpdateInsertCmd that is used

  ? "Update Command sent="+UpdateInsertCmd

* Swap the actual values in the command to see what occurred.

  UpdateInsertCmd=Strtran(UpdateInsertCmd,[OLDVAL('customerid','test1')],Oldval('customerid','test1'))

  UpdateInsertCmd=Strtran(UpdateInsertCmd,[OLDVAL('companyname','test1')],Oldval('companyname','test1'))

  UpdateInsertCmd=Strtran(UpdateInsertCmd,[test1.companyname],test1.companyname)

  ? "With the OLDVAL() and test1.companyname evaluated the update statement is :"+UpdateInsertCmd

  * Check tally.

  ? "Tally="+Transform(_Tally)

Procedure BeforeUpdate

  Lparameters cFldState, lForce, nUpdateType, cUpdateInsertCmd, cDeleteCmd

  cUpdateInsertCmd=cUpdateInsertCmd+this.ConflictCheckCmd

ENDDEFINE

使用SQL server DataSourceType的示例代碼:

下列代碼使用 SQL server DataSourceType 更新 SQL server Northwind 示例資料庫的一筆記錄。 查證記錄是否被更新,把下列代碼加入更新指令:

IF @@ROWCOUNT=0 RAISERROR (' Update

    conflict.', 16, 1)

在這種情況下,Tableupdate() 傳回 (.F。 ) 而且允許你處理更新失敗。

LOCAL loCursor,ovfp,nhnd,lsuccess

CLEAR

SET EXCLUSIVE OFF

CLOSE DATABASES ALL

SET MULTILOCKS ON

loCursor = CREATEOBJECT('CA')

* Load loCursor with the data specified in SelectCmd and display error message if error occurs.

IF !loCursor.CursorFill()

=AERROR(lar)

MESSAGEBOX(lar[2])

ENDIF

* Display the value of the company name before update.

? "Company Name Before Update:",companyname

?

* Create a connection handle for SQL Server so you can set up the update conflict.

nhnd=SQLSTRINGCONNECT([Driver=SQL Server; SERVER=<SQL SERVER NAME>; DATABASE=NORTHWIND])

=SQLEXEC(nhnd,[update customers set companyname='Alfreds Futterkiste' where customerid='ALFKI'])

=SQLDISCONNECT(nhnd)

* Now make a change to the local data, and then try to update it.

GO TOP

REPLACE companyname WITH 'Alfreds Futterkisted'

lsuccess=TABLEUPDATE(0,.F.,locursor.alias)

Messagebox("Tableupdate="+Transform(lsuccess))

* Error handling function. Displaying the error message if update conflict occurs.

IF !lsuccess

=AERROR(lar)

IF "Update conflict"$ lar[2]

  MESSAGEBOX("Update conflict!-Reverting changes")

  =TABLEREVERT(.f.,locursor.alias)

ENDIF

ENDIF  

* Get the current data from the CursorAdapter.

loCursor.CursorRefresh()  

GO TOP

* Displaying the value of the company name after update.

?

?"Company Name After Update:", companyname

DEFINE CLASS CA AS CursorAdapter

Alias = 'test1'

SelectCmd = 'select * from customers'

Tables = 'Customers'

KeyFieldList = "Customerid"

UpdatableFieldList = "companyname"

UpdateNameList = "customerid customers.customerid,companyname customers.companyname"

WhereType= 3 && Key and modified

* This is a custom property that is added to handle update conflicts. It does not do

    * anything by itself. It is added below to the automatically-generated UpdateInsertCmd to

    * test whether anything was actually updated.

ConflictCheckCmd =";IF @@ROWCOUNT=0 RAISERROR (' Update conflict.', 16, 1)"

* Initializing the connectivity to Data source (SQL Server) by using ODBC driver.

PROCEDURE init

  WITH THIS

   .DataSourceType = 'ODBC'

   .DataSource=SQLSTRINGCONNECT([Driver=SQL Server; SERVER=<SQL SERVER NAME>; DATABASE=NORTHWIND])

  ENDWITH

ENDPROC

PROCEDURE BeforeUpdate

  LPARAMETERS cFldState, lForce, nUpdateType, cUpdateInsertCmd, cDeleteCmd

  ? "Entering BeforeUpdate()"

  cUpdateInsertCmd=cUpdateInsertCmd + THIS.ConflictCheckCmd

ENDPROC

PROCEDURE AfterUpdate

  LPARAMETERS cFldState, lForce, nUpdateType, UpdateInsertCmd, DeleteCmd, lResult

  * To see why it will fail on the back-end, look at the UpdateInsertCmd that is used.

  ? "Update Command sent="+UpdateInsertCmd

  * Swap the actual values in the command to see what occurred.

  lcActualCmd =Strtran(UpdateInsertCmd,[OLDVAL('customerid','test1')],Oldval('customerid','test1'))

  lcActualCmd =Strtran(UpdateInsertCmd,[OLDVAL('companyname','test1')],Oldval('companyname','test1'))

  lcActualCmd =Strtran(UpdateInsertCmd,[test1.companyname],test1.companyname)

  ? "With the OLDVAL() and test1.companyname evaluated the update statement is :"+UpdateInsertCmd

  ?

  ? "Leaving AfterUpdate()"

ENDPROC

* Destroying the connection.

PROCEDURE destroy

  =SQLDISCONNECT(THIS.DataSource)

ENDDEFINE

注意:這種更新方式(如這一段代碼所示的)不支援批量更新。 (例如,cursoradapter.batchupdatcount>1) 。使用批量更新的時候,下列各項事件不動作:

  BeforeInsert

  AfterInsert

  BeforeUpdate

  AfterUpdate

  BeforeDelete

  AfterDelete

叁考

更多的資訊,vfp8幫助中的下列各項主題。

“CursorAdapter Object Properties, Methods, and Events"

"Data Access Management Using CursorAdapters"

"Detecting and Resolving Conflicts"

"Locking Data"

"Management of Updates with Views"

"Programming for Shared Access"

vfp8自帶的cursoradapter幫助:

用 Visual FoxPro,可以用cursor adapters接收支援以下資料源類型的本地或遠端資料:

&#8226; 本地的

&#8226; 開放式資料源連接配接(ODBC)

&#8226; ActiveX 資料對象 (ADO)

&#8226; 擴充标記語言 (XML)

Cursoradapter 類為不同的資料源類型作為本地臨時表使用提供支援. Cursoradapter 對象具有以下功能:

&#8226; 靈活地使用不同的資料源.

&#8226; 使用 Cursoradapter對象資料源或者資料環境 .

&#8226; 在資料源限制範圍内分享資料.

&#8226; 随意定義Cursoradapter 對象生成的臨時表的表結構.

&#8226; 控制 Cursoradapter 對象生成的臨時表的資料裝載.

&#8226; 從不同資料源生成基于資料源類型的Visual FoxPro 臨時表.

&#8226; 用 Cursoradapter 屬性和方法控制怎樣添加、更新、删除資料.

&#8226; 除資料環境以外,添加 Cursoradapter 對象到表單、表單集和其他容器.

&#8226; 用 Cursoradapter 類作為單獨的、無資料環境的類.

對 Cursoradapter 對象來說,資料源隻是一個翻譯層,它從資料源擷取資料生成 Visual FoxPro 臨時表.

注意:   Visual FoxPro 不支援Cursoradapter 對象建立連接配接關系. 但是, 可以在生成的臨時表之間建立連接配接關系.

想了解更多 Cursoradapter, 資料環境, 和遊标類的資訊,請看: Cursoradapter Class, DataEnvironment Object, 和 Cursor Object.

用TABLEUPDATE()和TABLEREVERT()函數實作資料互動

TABLEUPDATE( ) 函數能夠識别并且用 Cursoradapter 對象工作. TABLEUPDATE( ) delegates its operations to the cursor adapter associated with the cursor. TABLEREVERT( ) operates on Cursoradapter objects in the same way as other buffered cursors.

想了解更多的 Cursoradapter 對象怎樣影響TABLEUPDATE( ) 和 TABLEREVERT( ) 函數的行為, 請檢視 TABLEUPDATE( ) Function 和

TABLEREVERT( ) Function.

Automatic Updating and Cursoradapters

Visual FoxPro在本地和遠端視圖中自動産生 SQL INSERT, UPDATE, 和 DELETE 指令. 用 Cursoradapter 對象,可以指定、控制Visual FoxPro 怎樣操作SQL INSERT, UPDATE, 和 DELETE 指令.

當 Cursoradapter InsertCmd, UpdateCmd, 和 DeleteCmd 屬性為空的時候, Visual FoxPro 自動産生标準的SQL指令. 你必須确定那些自動産生的指令是否适合使用的資料源.要自動産生SQL INSERT, UPDATE, 和 DELETE 指令,你必須設定下面的 Cursoradapter特定屬性:

&#8226; Tables

&#8226; KeyFieldList

&#8226; UpdatableFieldList

&#8226; UpdateNameList

Tables 和 UpdateNameList 屬性要符合以下規則:

&#8226; Tables

如果允許自動更新,必須按順序列出希望在 SQL INSERT, UPDATE, 和 DELETE 指令中顯示的表的名稱.

&#8226; UpdateNameList

&#8226; 必須指定一個包含成對出現的、本地和完整的遠端資料字段的清單,用分号分隔。 沒一對字段名中,本地的在前,遠端的在後。完整的遠端字段名應該這樣寫:<遠端表名>, <遠端字段名>, <遠端表名> 必須和Tables 屬性中的名字一緻.

如果你設定以下Cursoradapter 的屬性為 True (.T.),必須指定關鍵字段:

&#8226; AllowInsert

&#8226; AllowUpdate

&#8226; AllowDelete

你想了解更多關于SQL 的自動更新指令,請看 INSERT - SQL Command, UPDATE - SQL Command, 和 DELETE - SQL Command.

批量更新(Batch Updates)

如果 Cursoradapter BatchUpdateCount 屬性的值大于1, Cursoradapter 對象使用批量更新 。 同時必須具備以下條件:

&#8226; Cursoradapter 對象設定為所有允許的操作使用相同的ODBC連接配接,包括AllowInsert, AllowUpdate, 和 AllowDelete屬性中設定的INSERT,UPDATE,和DELETE操作。.

&#8226; Cursoradapter 對象設定為所有允許的操作使用相同的ADODB 指令對象。

&#8226; Cursoradapter 對象所有被允許的操作使用 "XML" 作為資料源。

如果使用批量更新, 下列 Cursoradapter 事件不發生:

&#8226; BeforeInsert

&#8226; AfterInsert

&#8226; BeforeUpdate

&#8226; AfterUpdate

&#8226; BeforeDelete

&#8226; AfterDelete

如果批量更新失敗, Visual FoxPro 嘗試在該批資料中的每一行發送一個分散的更新,然而,列舉的事件不會發生。

更多的相關資訊, 參見 BatchUpdateCount Property.

自動更新(Automatic Updating)和 ActiveX Data Objects (ADO)

用 ADO工作的時候, 可以用兩種辦法發送更新:

&#8226; 使用 Cursoradapter 對象,通過在Cursoradapter CursorFill 方法中使用 ADO 記錄集對象發送更新

&#8226; 當使用 ADO記錄集執行自動更新的時候, 設定ursorAdapter InsertCmdDataSource, UpdateCmdDataSource, 和 DeleteCmdDataSource 屬性的時候,必須遵循下列規則:

&#8226; ADO記錄集必須是可讀寫的并且标記為可用。

&#8226; 當使用用戶端記錄集對象的時候,書簽必須總是可用。

&#8226; Bookmarks might be available when using Keyset or Static server-side cursors when supported by the OLE DB Provider.

&#8226; 按照一個字段一個字段的規則進行更新.更新完一條記錄以後, Cursoradapter 對象在ADO記錄集中尋找原始記錄t, 用來改變可更新字段的值, 并且調用ADO記錄集的update方法。 Cursoradapter 對象不調用ado記錄集的updatebatch方法, 是以, 你的程式需要在恰當的時候明确地調用 UpdateBatch方法。

當使用這個方法程式的時候, 需要按以下方法設定 Cursoradapter 對象:

THIS.DataSourceType="ADO"

THIS.UpdateCmdDataSourceType=""

可以用資料環境(DataEnvironment)和 Cursoradapter builders 來建立一個可更新的表單,或者使用資料環境 DataEnvironment 和 Cursoradapter 類庫,或者在Cursoradapter Builder中使用 AutoUpdate tab 制定一個主關鍵字段,更新字段,或者其他要求的資訊。

&#8226; 通過一個自定義的或者自動産生的指令,通過 Cursoradapter 對象向資料庫直接發送一個更新。

&#8226; 當執行自動直接更新, Cursoradapter 對象需要一個 ADO對象,其 ActiveConnection 屬性設定成開放的ADO連接配接模型。

&#8226; 用這種方法的時候, 需要用以下方法設定Cursoradapter對象 :

THIS.UpdateCmdDataSourceType="ADO"

THIS.UpdateCmdDataSource=NewADODBCommandObject

在這個方法裡不推薦使用 THIS.DataSourceType="ADO" 。

自動更新和 XML

Cursoradapter 對象使用XML資料源時,不自動産生SQL INSERT, UPDATE, 和 DELETE指令,因為已經存在好多産生XML的手段。 然而,可以使用Cursoradapter 對象産生一個 XML UpdateGram ,放在 Cursoradapter UpdateGram 屬性.

盡管Cursoradapter 對象能産生XML UpdateGram, 你必須使用恰當的協定來實作最終更新。例如,SQL XML(HTTP), SQL XML OLE DB, 或者 XML Web service to .NET.

使用 XML 資料源和産生 XML UpdateGrams的時候必須遵循以下規則:

&#8226; 必須在 Cursoradapter InsertCmd, UpdateCmd, and DeleteCmd 屬性中指定具體的指令,Visual FoxPro能夠據以進行正确的插入、更新、和删除操作。 如果使用批量更新, Cursoradapter BatchUpdateCount 屬性值必須大于1, UpdateCmd 在批量中隻執行一次。

&#8226; 下列 Cursoradapter 屬性必須設定為 "XML":

&#8226; InsertCmdDataSourceType

&#8226; UpdateCmdDataSourceType

&#8226; DeleteCmdDataSourceType

如果這些屬性不設定為"XML", Visual FoxPro 執行 UpdateCmd, InsertCmd, DeleteCmd 屬性中的指令而且不産生XML UpdateGram.

&#8226; 要想正确的建立XML 更新(UpdateGram), Cursoradapter 需要 Tables, UpdatableFieldList, UpdateNameList 屬性中包含确定的值。 Cursoradapter 可以通過在Tables, UpdatableFieldList,和 UpdateNameList 屬性中指定恰當的名稱來産生一個多表更新。是以, 可以更新用XMLUPDATEGRAM( )函數産生的連接配接多個表的遊标。更多的資訊,請看 XMLUPDATEGRAM( ) Function.

&#8226; Cursoradapter 使用 WhereType 屬性産生XML UpdateGram 的 before 事件。是以, 當你執行一個更新或删除的操作時,   KeyFieldList 和 UpdatableFieldList 屬性必須包含一個主關鍵字段.更多的資訊,參見 WhereType Property.

&#8226; 如果設定了行緩沖, 或者 BatchUpdateCount 的值是1, Visual FoxPro 為每個update, insert, delete 操作建立一個XML UpdateGram 更新。

如果設定為表緩沖, 并且使用批量更新, 即atchUpdateCount 的值大于1, Visual FoxPro 為全部的批量建立一個 XML更新。 此種方式下, 必須使用TABLEUPDATE( ) 函數執行XML更新。

See Also

Working with Tables | Cursoradapter Class | Cursoradapter Object Properties, Methods, and Events