Flash Remoting 教學 2 – Pageable RecordSet

In tutorials   July 31, 2005 - 4:23 pm

◆ 前言

資料分頁(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元件到畫面上即可。

程式碼的部份如下:

Actionscript:
  1. //remoting related
  2. import mx.remoting.Service;
  3. import mx.remoting.PendingCall;
  4. import mx.remoting.RecordSet;
  5. import mx.remoting.debug.NetDebug;
  6. import mx.rpc.RelayResponder;
  7. import mx.rpc.ResultEvent;
  8. import mx.rpc.FaultEvent;
  9. import mx.events.*;
  10. //start debugging so we can see debug info in NCD.
  11. NetDebug.initialize();
  12. //setting gateway
  13. var gateway:String = "http://localhost/pageableRecordSet/gateway.php";
  14. respGeneral = new RelayResponder(this, "contactReceived", "contactFailure");
  15. _service = new Service(gateway, null, "PRecordSet", null, respGeneral);
  16. //
  17. var pc:PendingCall = _service.getFullList(["Name", "Region", "Code"]);
  18. //
  19. dg.columnNames = ["Code", "Name", "Continent"];
  20. //
  21. function contactReceived(re:ResultEvent):Void{
  22.     var tmp:RecordSet = RecordSet(re.result);
  23.     tmp.setDeliveryMode("page", 10, 2);
  24.     dg.dataProvider = tmp;
  25. };
  26. //
  27. btnRefresh.clickHandler = function(){
  28.     dg.removeAll();
  29.     var pc:PendingCall = _service.getFullList(["Name", "Region", "Code"]);
  30. }

◆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
它的程式碼如下:

PHP:
  1. <?
  2. class PRecordSet
  3. {
  4.     var $dbhost = "localhost";
  5.     var $dbname = "test";
  6.     var $dbuser = "root";
  7.     var $dbpass = "";
  8.  
  9.     function PRecordSet()
  10.     {
  11.         $this->conn = mysql_pconnect($this->dbhost, $this->dbuser, $this->dbpass);
  12.         mysql_select_db( $this->dbname );
  13.  
  14.         $this->tablename = "country";
  15.  
  16.         $this->methodTable = array(
  17.             "getColumnNames" => array(
  18.                 "description" => "Return column names from table",
  19.                 "access"      => "remote",
  20.                 "arguments"   => array("")
  21.             ),
  22.             "getFullList" => array(
  23.                 "description" => "Return a full list of table records, pass as argument an array of columns to be returned",
  24.                 "access"      => "remote",
  25.                 "arguments"   => array("arrayOfColumns"),
  26.                 "pagesize"    => "10",
  27.             ),
  28.         );
  29.  
  30.     }
  31.  
  32.  
  33.     function getColumnNames()
  34.     {
  35.         $query = mysql_query(sprintf('SHOW COLUMNS FROM %s', $this->tablename));
  36.         return $query;
  37.  
  38.     }
  39.     /*
  40.     前面有幾個參數都沒關係,但
  41.     最後兩個參數最重要:$offset, $limit 會被amfphp自動呼叫
  42.     因此,整個sql語法也要用sprintf來寫。
  43.     */
  44.     function getFullList($ar_col, $cursor, $limit)
  45.     {
  46.         //count it first
  47.         $this->countQuery = "SELECT COUNT(*) AS recordCount FROM country";
  48.         //
  49.         return mysql_query(sprintf("select * FROM %s ORDER BY Name ASC Limit %d, %d", $this->tablename,$cursor,$limit));
  50.     }
  51.     /*
  52.     所有需要 分頁 的function, 都要加上這個 _count,讓amfphp自動去呼叫
  53.    
  54.     */
  55.     function getFullList_count(){
  56.         $q = mysql_query($this->countQuery);
  57.         $row = mysql_fetch_assoc($q);
  58.         return $row['recordCount'];
  59.     }
  60. }
  61. ?>

上面的程式碼中,比較重要的幾點如下:
-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裏面寫的程式:

Actionscript:
  1. function contactReceived(re:ResultEvent):Void{
  2.     var tmp:RecordSet = RecordSet(re.result);
  3.     tmp.setDeliveryMode("page", 10, 2);
  4.     dg.dataProvider = tmp;
  5. };

這個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 所犯的錯誤,可能是作者忙者改程式碼還沒空修正這個範例)

◆檔案下載

這個教學的範例檔案可按此下戴。程式碼中已加上大量註解,因此應該很容易理解。

如有任何疑問歡迎隨時提出。

by admin

15 Comments Add your own

  • 1. jeremy&hellip  |  August 29th, 2005 at 12:06 am

    今天看log時發現下面的網址有位aday網友提到關於這個範例的問題:

    http://www.mmug.com.tw/
    forum/viewtopic.php?t=6653

    問題主要是第18行:
    var pc:PendingCall = _service.getFullList(["Name", "Region", "Code"],1,5);

    aday提到加上最後兩個參數,1, 5後才能順利執行,不然php會回傳下列錯誤:

    description: "Missing argument 2 for getfulllist()"

    為此我特別把程式挖出來再compile測試一次,結果還是正確的,由於沒看到aday完整client/server端的程式碼,所以我只能就目前的推測說明一下:

    1、php端的 getFullList($ar_col, $cursor, $limit) 最後兩個參數是amfphp會自動去填入並使用的,理論上不需要user自已寫

    2、那amfphp怎麼知道該填什麼數字進去呢?很簡單,一開始的 method table裏有設定 pagesize=10,而client端的pagemode會傳回一次要幾頁,這些資訊就是 $cursor與$limit

    3、那如果像aday加了 1, 5兩個參數會發生什麼事?這代表flash第一次向amfphp發出request時,就會撈回五筆資料,而不是像範例中那樣,第一次跟amfphp溝通時只會取回總資料筆數(57筆),然後控制權交給 recordset 配合 datagrid的顯示需要動態去跟amfphp請求剩下的資料。

    4、那為何 aday 不加 1,5 就會錯,加了就沒事呢?呃,這我也想知道啊~ 請把php程式碼寄來或貼出來讓大家看看吧。

  • 2.  |  October 26th, 2005 at 3:59 am

    理論上不後控制權交下的就沒貼出來讓裏沒設

    資料好處是每次取回的資料量少

  • 3. jeremy&hellip  |  October 26th, 2005 at 11:55 am

    ???
    可以翻成中文再說一次嗎?

  • 4. jason Chang&hellip  |  October 22nd, 2006 at 9:14 pm

    你好 我在作您的測試黨的時候 netdebug都跑步出來
    我在作您第一個測試黨hello時結果有跑出來但是netdebug也是沒有任何動作
    而且第二個測試黨測試影片時跑步出來請您可以幫我看看問題出在哪裡嗎??
    我的軟體版本為
    flash 8
    amfphp 1.2.5
    appserver 2.5.5
    phpMyAdmin 2.6.4-pl4
    php 5.1.1
    mysql 5.0.16-nt

  • 5. jason Chang&hellip  |  October 22nd, 2006 at 9:16 pm

    以下是我的.as
    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/yoyo/flashservices/amfphp/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"]);
    }

  • 6. jason Chang&hellip  |  October 22nd, 2006 at 9:17 pm

    以及我的php檔
    conn = mysql_pconnect($this->dbhost, $this->dbuser, $this->dbpass);
    mysql_select_db( $this->dbname );

    $this->tablename = "country";

    $this->methodTable = array(
    "getColumnNames" => array(
    "description" => "Return column names from table",
    "access" => "remote",
    "arguments" => array("")
    ),
    "getFullList" => array(
    "description" => "Return a full list of table records, pass as argument an array of columns to be returned",
    "access" => "remote",
    "arguments" => array("arrayOfColumns"),
    "pagesize" => "10",
    ),
    );

    }

    function getColumnNames()
    {
    $query = mysql_query(sprintf('SHOW COLUMNS FROM %s', $this->tablename));
    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(){
    $q = mysql_query($this->countQuery);
    $row = mysql_fetch_assoc($q);
    return $row['recordCount'];
    }
    /*
    @author: jeremy@richtechmedia.com
    @website: http://ria.richtechmedia.com
    released under CC 2.5.
    */
    }
    ?>

  • 7. jason Chang&hellip  |  October 22nd, 2006 at 9:22 pm

    您好 若是我跟andy 一樣 改成
    問題主要是第18行:
    var pc:PendingCall = _service.getFullList([”Name”, “Region”, “Code”],1,5);
    這樣 結果就可以跑出來了!!
    到底為何壓??
    懇請您幫我解答 謝謝

  • 8. jason Chang&hellip  |  October 22nd, 2006 at 9:36 pm

    請問您那如果我要作成 排名的話 我如何
    排序 還有顯示名次??

    感激不盡

  • 9. AXIS&hellip  |  November 1st, 2006 at 3:15 pm

    回報!!
    這個程式在php5+mysql 5.1.18的平台的遠端主機上

    確定無法執行!!
    (有在取回資料的語法前加上
    mysql_query("SET NAMES 'utf8'");
    mysql_query("SET CHARACTER_SET_CLIENT=utf8");
    mysql_query("SET CHARACTER_SET_RESULTS=utf8");
    )

    最後會顯示
    result--___amfphp_error

  • 10. luke&hellip  |  January 7th, 2007 at 9:51 pm

    你好,我按照你上面的分页程序做的时候,出现

    Result: "__amfphp_error"

    我检查了过程,好像都对的,不知道是什么问题

    方便的话希望能告诉我一下,非常感谢

  • 11. milkmidi&hellip  |  February 13th, 2007 at 4:35 pm

    我也是之直載你的原始檔,直接執行
    但一樣會出現錯誤
    加了那二個參數,就可以

  • 12. milkmidi&hellip  |  February 13th, 2007 at 5:00 pm

    站長,請問您一下
    我已成功連線,並將資回傳
    但回傳的應該是RecordSet
    但RecordSet的屬性,都會抓不到值
    都需要用_rs.items[0].名字才抓的到
    像_rs.getLength()都會是0
    _rs.getNumberAvailable()才抓的到正確的值
    所以在dataProvider,值都出來來
    請問我有那兒設定錯誤了嗎

  • 13. jeremy&hellip  |  February 14th, 2007 at 12:11 am

    你有試過看看 NetConnection Debugger 裏顯示傳回值的結構嗎?

    不過我很多年沒用 flash remoting + as2了,所以印象也有點模糊 Orz

  • 14. AMFphp 的简介&hellip  |  November 16th, 2009 at 2:00 pm

    [...] pageable recordset working examples [...]

  • 15. Flash Remoting类 包 功&hellip  |  March 26th, 2010 at 1:16 am

    [...] pageable recordset working examples [...]

留言回應

hidden

您的留言會先經過站長認証後才刊登在網站上。
your comments will be approved by Administrator before appearing on the page.

Trackback this post  |  Subscribe to the comments via RSS Feed

mobile phone