Flash Remoting 教學 2 – Pageable RecordSet
◆ 前言
資料分頁(pageable recordset)一直是網頁應用程式中非常重要的一項技巧,通常工程師不會將大量的資料一次傳回給client,而是將資料先預存在server端,等client需要時再送過去。
在flash remoting 中也同樣提供了這種功能,但以往只有使用Macromedia版的remoting adapter才能正確發揮功能,也就是說如果不是 coldfusion, jrun, remoting for .net之類的正牌產品,這部份功能要不就是不完整,要不就是完全爛掉。
Amfphp 是php版的flash remoting, open source並完全免費。過去amfphp在pageable recordset上的表現可說是非常糟糕,大部份人從來都無法真正實作成功;但隨者patrick加入amfphp開發團隊後,最近amfphp的表現可說是突飛猛進,快速逼進商業產品的水準。
目前最新版的amfphp是 pre-v1.0 milestone 2 (MS2),也就是v1.0正式發表前的最後一個beta,在這個版本裏終於開始有非常完整的pageable recordset功能。
有鑑於MS2 版中的pageable recordset實作手法有大改變,而目前網路上所有的教學文件都尚未更新,因此本文將以實際的例子介紹如何使用amfphp 的pageable recordset,並詳細解釋它的運作原理與 flash 端 RecordSet 配合的方式。
◆下載與安裝
請參考第一篇教學。
◆pageable recordset運作原理
在正式開始前我們先花點時間瞭解一下到底pageable recordset是怎麼個運作法。
首先,pageable recordset的用途在於,如果你在server端有100筆資料,通常不會全部傳回client端,而是以分頁的方式,例如每頁20筆資料共分五頁,然後當user按「下一頁」時才依序傳回需要的資料。
一般來說要做到pageable recordset有兩種方式:
1、 先在server端將100筆資料 query出來,放在session中備用,然後傳回需要的部份。
2、只先query出總資料量為100筆,然後用類似 mysql limit的語法一次撈20筆出來回傳。
第1種做法好處是可以減少與database連線的次數,並且資料存放於記憶體中理論上存取速度會非常快,但缺點則是session會吃掉大量資源,如果許多user同時連線並個自保有自已的session data,那 app server往往會撐不住而當掉。
第2種做法的好處則是不會消耗大量server資源,它在有需要時才去跟db撈出資料,但缺點也就是要頻繁的與db連線,此時良好的db connection management (pooling)就很重要。
以afmphp來說,它採取的是第2種方式,當 flash client一連線時,它會先query出資料的總筆數並回傳給flash, 之後就由flash內的RecordSet class來負責決定分頁方式與每次的取回資料量。
因此從這個描述中,大家可以感覺到整個pageable recordset的運作原理是需要下面兩者交互配合才能達成。
1、amfphp的分頁query功能
2、flash RecordSet 的分頁取回功能
有了基本認識後,接下來我們就開始實作吧。
◆flash 製作
首先我們製作好flash client 端的檔案,這個程式很簡單,只要拉一個 DataGrid與button元件到畫面上即可。
程式碼的部份如下:
-
//remoting related
-
import mx.remoting.Service;
-
import mx.remoting.PendingCall;
-
import mx.remoting.RecordSet;
-
import mx.remoting.debug.NetDebug;
-
import mx.rpc.RelayResponder;
-
import mx.rpc.ResultEvent;
-
import mx.rpc.FaultEvent;
-
import mx.events.*;
-
//start debugging so we can see debug info in NCD.
-
NetDebug.initialize();
-
//setting gateway
-
var gateway:String = "http://localhost/pageableRecordSet/gateway.php";
-
respGeneral = new RelayResponder(this, "contactReceived", "contactFailure");
-
_service = new Service(gateway, null, "PRecordSet", null, respGeneral);
-
//
-
var pc:PendingCall = _service.getFullList(["Name", "Region", "Code"]);
-
//
-
dg.columnNames = ["Code", "Name", "Continent"];
-
//
-
function contactReceived(re:ResultEvent):Void{
-
var tmp:RecordSet = RecordSet(re.result);
-
tmp.setDeliveryMode("page", 10, 2);
-
dg.dataProvider = tmp;
-
};
-
//
-
btnRefresh.clickHandler = function(){
-
dg.removeAll();
-
var pc:PendingCall = _service.getFullList(["Name", "Region", "Code"]);
-
}
◆php端製作
這個例子中我們是使用 windows + apache 2 + php 5.04 + mysql 4.1 + amfphp ms2,但基本上在其它平台或程式版本也應該沒問題(注意amfphp至少要是ms1以上才行)
首先我們在apache的web folder (/htdoc)下建一個專案資料夾:
C:\Program Files\Apache Group\Apache2\htdocs\pageableRecordSet
然後在pageableRecordSet下建一個 service 資料夾
C:\Program Files\Apache Group\Apache2\htdocs\pageableRecordSet\services
在這個新建的資料夾內,建立一個新的php程式,名稱為 PrecordSet.php
它的程式碼如下:
-
<?
-
class PRecordSet
-
{
-
var $dbhost = "localhost";
-
var $dbname = "test";
-
var $dbuser = "root";
-
var $dbpass = "";
-
-
function PRecordSet()
-
{
-
-
$this->tablename = "country";
-
-
"description" => "Return column names from table",
-
"access" => "remote",
-
),
-
"description" => "Return a full list of table records, pass as argument an array of columns to be returned",
-
"access" => "remote",
-
"pagesize" => "10",
-
),
-
);
-
-
}
-
-
-
function getColumnNames()
-
{
-
return $query;
-
-
}
-
/*
-
前面有幾個參數都沒關係,但
-
最後兩個參數最重要:$offset, $limit 會被amfphp自動呼叫
-
因此,整個sql語法也要用sprintf來寫。
-
*/
-
function getFullList($ar_col, $cursor, $limit)
-
{
-
//count it first
-
$this->countQuery = "SELECT COUNT(*) AS recordCount FROM country";
-
//
-
return mysql_query(sprintf("select * FROM %s ORDER BY Name ASC Limit %d, %d", $this->tablename,$cursor,$limit));
-
}
-
/*
-
所有需要 分頁 的function, 都要加上這個 _count,讓amfphp自動去呼叫
-
-
*/
-
function getFullList_count(){
-
return $row['recordCount'];
-
}
-
}
-
?>
上面的程式碼中,比較重要的幾點如下:
-amfphp端都是以class的型式存在,因此等於是在寫一個 php class。但一開頭的method table 非常重要,裏面有許多關鍵字可設定這個class的運作行為。
-第26行的pagesize => 10 是告訴amfphp 我希望每頁只傳回10筆資料;這是最關鍵的一行設定,但還需其它地方配合。
-第44行開始的getFullList($ar_col, $cursor, $limit)就是要曝露給client端呼叫的remote method, 名稱為 getFullList,裏面有三個參數。前面的$ar_col 是我們要取回的欄位名稱,實際上這裏要放多少個參數都沒關係,視實際需要而定;比較重要的是最後兩個$cursor與 $limit 參數,這兩個參數是amfphp日後進行分頁取回時會自動填入資料的地方,也就是下面query中limit的兩個關鍵字。
其中$cursor 是指目前所在位置,例如第10筆資料,而$limit則是將下來要取回的資料量,例如假設你設定一次要取回2頁,每頁10筆資料,那麼 $limit就會是 20,而整個function call就會是 getFullList(“some columns”, 10, 20),我們在下面的應用中會再解釋一下這部份。
這裏你只要記住:不管前面有幾個參數(例如$ar_col, 最後兩個一定要是 $cursor, $limit)
-第47行也是必備的項目,從query字串中就可以看出它的功能是在 count()全部的資料量,好在第一次連線時回傳給flash端做為日後分頁計算的基礎。
-第49行的query則是最重要的地方,請注意最兩點。
首先是sprintf的使用,這是一個很傳統的function,主要功能是將 %s, %d這樣的變數動態替換成後面的參數。
第二點則是 limit 的使用,在mysql中limit最常被用來進行分頁處理,它的第一個參數是啟始點(例如要從第10筆資料開始),第二個參數是資料量,例如(20筆),因此透過limit我們就可以從第10筆開始取回20筆資料。
-第55行的getFullList_count()則是amfphp新增的實作手法,這個function基本上就是將前面的getFullList加上 _count,而它主要的功能是用來計算query結果的資料量,這點從56-58行的程式碼中就可看出。它利用前面 COUNT(*) AS recordCount 將計算結果存在 recordCount這個欄位中,然後用$row = mysql_fetch_assoc($q) 與 $row['recordCount'] 來取得計算後的總量。
這支程式是amfphp 內部運作時會自動呼叫的,因此一定要有,但它的名稱不一定要是 functionName_count()這樣,可以在method table內增加一個 countMethod => “my_count”來指定要使用的名稱。
◆實際執行
在實際執行前你需要先放點東西到mysql裏才行,請下載本教學的範例檔案,裏面有個 db.sql 檔,它會在mysql內建的test database內新增一個 country table,這就是我們要用的測試資料。
接者請確定你的web server, php, mysql都正常運作,並且也正確安裝了 amfphp ms2,然後 project folder 名稱正確,service folder裏也有剛寫好的 PrecordSet.php。
最後記得打開 NetConnection Debugger 簡稱NCD (Window > Other Panel > NetConnection Debugger)以方便觀察flash與php間的資料傳輸情況。
當一切就續後就可以執行flash 的 ctrl-enter,此時在NCD中會看到下面的畫面:
這張畫面中顯示當flash第一次與amfphp連線執行 getFullList()後,amfphp傳回了一堆資料,其中最重要的就是:
mTotalCount: 57 這行告訴flash你要求的資料總共有57筆。
mRecordsAvailable: 0 目前我還沒傳回任可一筆資料。
當這些資訊回傳給flash 後,接下來就是flash RecordSet的責任去決定該如何取回這57筆資料,請注意我們在flash裏面寫的程式:
-
function contactReceived(re:ResultEvent):Void{
-
var tmp:RecordSet = RecordSet(re.result);
-
tmp.setDeliveryMode("page", 10, 2);
-
dg.dataProvider = tmp;
-
};
這個contactReceived就是當初設定的 remoting callback, 當amfphp傳回資料時,就是由它去負責接收並做後續處理。在這個function裏我們設定了 setDeliveryMode("page", 10, 2), 這是 RecordSet內建的一個指令,透過這句話我們決定接下來的取回方式是”page”,每次10筆資料,並且預先取回2頁。
然後我們使用 dg.dataProvider = tmp,也就是將recordset餵給 datagrid的dataProivder,就可以順利顯示出來。
而這樣設定後最神奇的地方在於,當user捲動到下一頁時,recordset會知道資料還在server,因此自動跑去跟amfphp要求下面兩頁的資料,請看下圖。
請注意圖中,flash自動去呼叫amfphp中的PageAbleResult.getRecords()這支指令,並且傳回三個參數,第一個參數是session id,這個不重要先不管它,第二個參數則是目前 cursor 所在位置,第3個參數則是flash希望先取回30筆資料備用。
但為什麼是30筆呢?當初我們明明設定是先取回2頁啊!原因是”page”模式下,ReocrdSet永遠會自動取回目前所需的頁面,並且pre-fetch 使用者指定的頁面,因此目前cursor在1,它必需先取回自已所在的第1頁,然後預抓回2頁,也就是總共3頁共30筆資料,這就是 1, 30的由來。
之後RecordSet就會視user的捲動情況而決定何時該向amfphp要求更多的頁面,你可以試試捲動datagrid,此時會發現NCD裏出現一狗票的連線與取回記錄,如下圖
注意裏面有新增了幾個 call指令,這就是RecordSet 在進行自動取回,另外也特別注意倒數第二行的release(),當server 端的所有資料都取回後,ReocrdSet 會透過這指令告訴amfphp可以丟棄存放於server端的分頁記錄順便清空記憶體,等於是某種形式的garbage collection。
透過以上的例子,大家應該可以瞭解amfphp的pageable recodset運作原理,理論上來說只要支援dataProvider與 RecordSet 的元件都會自動支援這個分頁功能,例如combobox, listbox, datagrid 等,但tree就不行,因為它只吃xml data,除非你能替RecordSet 加上即時rs2xml的功能。
◆資料取回模式釋疑
RecordSet在取回資料時共有三種模式,分別解釋如下:
-onDemand: 這是預設的模式,也就是一次取回一筆資料,只有當需要時才連線向amfphp提出請求。好處是每次取回的資料量少,反應速度快,缺點則是會頻繁的app server連線,如果網路速度不快反而會因為lag造成使用上的不變。
-page: 以「頁」為單位取回資料,這是最常見的應用方式,它還可搭配另外兩個參數,分別是pagesize(每頁的資料筆數)與numPrefetchPages(預先取回的頁數),以我們的例子來說,setDeliveryMode(“page”, 10, 2)就是設定取回模式為page, 一頁10筆資料,預先取回2頁。
-fetachAll:這個模式會將所有資料一次取回,但可透過pagesize這個參數來預做分頁,預設值是每頁25筆資料。
一般如果會用到分頁功能,通常不二的選擇就是”page” mode,並且視需要決定每頁的筆數與預先取回量。
◆pageable recordset使用注意事項
amfphp的 pageable recordset 設計與使用上不像coldfusion 那樣方便,但那是因為許多背景因素造成,以open source project來講,能有這樣的表現已經是非常令人讚賞,只要你知道日常操作中該避開的陷井就一切ok。
-amfphp無法處理極大量的page,據各討論list上得到的結論,五千筆是一個上限。
-amfphp使用limit 語法,這是mysql獨有的指令,在mssql上可用select top n 的方式達成,但這牽涉到改寫amfphp的程式。
-php端的 functionName()中要記得加上 $cursor, $limit 兩個參數,這是供amfphp內部分頁使用,少了它們分頁就玩完了。
-php端一定要有 functionName_count()這個指令,也就是每個需要分頁的method都要伴隨者一支count method,才能計算正確的資料量回傳給recordset。
-php端的 method table要有 pagesize => “10”這樣的分頁設定,這是amfphp在第一次回傳資料時要參考的數字,如果這裏沒設定,amfphp就會認定你沒有分頁的需求而將全部資料一次都丟回client端了(這也是目前amfphp example 所犯的錯誤,可能是作者忙者改程式碼還沒空修正這個範例)
◆檔案下載
這個教學的範例檔案可按此下戴。程式碼中已加上大量註解,因此應該很容易理解。
如有任何疑問歡迎隨時提出。

