Archive for 7月, 2009

Flexのlogging機能はログの書き出し命令と、実際の書き出し実装を分けられる仕組みです。

ログを書き込むときには

package com.coltware.fxmail.pop3
{
  import mx.logging.ILogger;
  import mx.logging.Log;

  public class POP3Client extends UIComponent{
  	private static var log:ILogger = Log.getLogger("com.coltware.fxmail.pop3.POP3Client");

	public function POP3Client() {
  		log.debug("instance created ..");
	}
  }
}

のようにでき、また、ログの実際の書き出しをどのようにやるかは、

var logTarget:TraceTarget = new TraceTarget();
logTarget.level = LogEventLevel.DEBUG;
logTarget.includeCategory = true;
logTarget.includeDate = true;
logTarget.includeTime = true;
logTarget.includeLevel = true;
Log.addTarget(logTarget);

のようにできます。
複数指定する場合には、同様に必要なLogTargetクラスをnewして、addTargetしてやればOKです。
(ただし、ログの書き出しはアプリケーションの初期化処理など初めする必要があります。)

このlogging機能の良いところは、ログの書き出し命令と、実際の書き出し処理が分離されているので、
各プログラムを書いている人は、ログをどこにいつ出すのか?ということを気にして実装する必要がないのです。
(現実問題としては多少はあるでしょうが・・・・)

この辺りを図にすると以下のような感じになるのではないでしょうか?
logger

また、命令と書き出し処理が分かれていることに付随してもうひとついいところが、書き出し命令1つに付き、書き出し処理を1つにする必要がないのです。
たとえば、通常の動作ログはファイルに書き出し、重大なエラーが発生したときにネットワークで、エラー収集サーバにとばすなどということもできますし、
開発時には、デバッグ情報をtrace関数で表示するが、実際の運用時には表示しない(つまり、書き出し処理を使わない)ということもできます。
さて、このように書き出し命令と書き出し処理がわかれているといいことだらけのようですが、ひとつ問題となることがあります。
意図しないログも書き出されてしまうという問題です。

たとえば、ライブラリ開発者はデバッグなどの情報をログに書き出すようにプログラムを作っていることが多々あります。
しかし、これを使ったプログラマがデバッグ情報を出力すると、ライブラリのログも表示されてしまうという問題です。

ただし、この問題を回避するためにFlexのlogging機能にはfilterという概念があります。

ログを書き出すときに

Log.getLogger("xxxx.yyyy.zzzz.ABC");

のように書いた部分を使ってフィルタがかけられるのです。

logTarget.filters=["xxxx.yyyy.zzzz.*"];

ってな感じです。

というよう、かなり使える機能です。

しかーし、残念ながら、Flexで標準で用意されている書き出し実装はtrace出力のみなのです。
もうひとつ残念なこととして、ログレベルを1つ1つ指定できないのです。つまり、debugだけ書き出すということ。
ログレベルが上だったら出力されてしまいます。

でも、TraceTargetのソースを見るとすごーく単純なのがわかると思います。

override mx_internal function internalLog(message:String):void
{
        trace(message);
}

実装はこれしかありません。
ここで、traceの部分でメッセージを出力しているで、その部分をファイルに書き出すなり、ネットワークなりに飛ばすなりを書けばOKです。

私はFileに書き出す処理と、ブログでは紹介していませんがsyslogサーバ(実際はsyslog-ng)に飛ばすようなものを使っています。
単に、syslogに飛ばしているのはLinux上のほうがtailなどでメッセージを見やすいという理由と、syslogサーバでさらにこのログをどのように扱うかを指定できるからです。
(といってもこのあたりからは運用テクに入るので記述しませんが・・・・・)

addEventListenerの最後の引数にはuseWeakReferenceというオプションがあり、これをtrueにするとメモリリークが発生しにくいということですが、デフォルトではこれはfalse。つまり、自分で参照はすべて制御するというものです。

しかし、これをデフォルトでtrueにしましょう!とオライリーの本などでも書いてあったので、そうしてみるか。と書いてみたら、思い通り動かない!という状況になってしまいました。

そんなソースを紹介します。
[AS]
package {

import flash.events.*;
import flash.utils.*;
import flash.net.*;

public class SMTPConn {

private var sock:Socket;

// 弱参照を使うか、強参照を使うか?
private var useWeakReference:Boolean = true;

public function SMTPConn() {
sock = new Socket();

}

public function connect():void{

sock.addEventListener(Event.CONNECT,connectHandler,false,0,useWeakReference);
sock.addEventListener(ProgressEvent.SOCKET_DATA,readBytes,false,0,useWeakReference);
sock.connect(“127.0.0.1″,25);
trace(“open …”);
}

private function connectHandler(e:Event):void{
trace(“connected …”);
}

private function readBytes(e:ProgressEvent):void{
trace(“read bytes ” + sock.bytesAvailable);
trace(sock.readUTFBytes(sock.bytesAvailable));
}
}
}
[/AS]

具体的には、Socketで接続が完了してもconnectHandlerが動かないのです。
実は、上のソース自体には別に問題がありません。
このクラスの呼び方というか、オブジェクトのスコープにあるのだとおもいます。
(理屈的にも、動作的にもそのようなうごきになったので)

このソースをあるボタンを押したときのclick=”test_smtp()”のような感じで以下のようにしました。

[AS]
private function test_smtp():void{
var smtp:SMTPConn = new SMTPConn();
smtp.connect();
}
[/AS]
このソースが、うまくうごくときと動かないときがあるのです。
最初、相当なやみましたが、スコープの問題だとわかりました。

たぶん、connectHandlerが動く前に、test_smtp()内の処理が終了し、smtpオブジェクトがいらなくなったので、ガベージ対象になったのでしょう。
これで、弱参照をつかっているとconnectHandlerが動かないと。

ただし、useWeakReference = false にしたり、smtpのオブジェクトのスコープを1つうえ。つまり、
[AS]
var smtp:SMTPConn;
private function test_smtp():void{
smtp = new SMTPConn();
smtp.connect();
}
[/AS]
のようにすればうまく動きます。
UIがあれば、さすがにそのUIが消えた時点でほとんどの処理が必要なくなり、このようなことは発生するケースが思い浮かびませんが、バックグラウンドで処理する場合にはこのような使い方がありえるのではないかなと。

あらためて、addEventListenerの勉強というか、強参照と弱参照の勉強をさせてもらいました。

Zipファイルの読み込みについてのサンプルがアドビのAIRのマニュアル(?)にあった。

ただし、このサンプルはHTMLに記述して使うもので、FlexのなかでActionScriptとしてのサンプルでなかったので、
それを参考にFlex & ActionScript用に書き直してみた。

といっても、クラスやらなんやらの名前(パッケージ名)が違う程度ですが。

AIRでZipファイルを簡単に解凍できるということをしって私にはとっても役にたちました。
これで、MS-Officeのファイル(2007以降のXML形式)はzipファイルですから・・・・、このファイルを簡単に操作できるようなものも作れるというわけです。
まあ、最初はファイルのメタ情報をとるような感じになるとおもいますが・・・・

後で本格的につくって、MS-Officeのファイルを操作しようかな・・・と野心が芽生えました・・・

というわけで、以下私がZipの動きをみるために書いたソースです。
zipファイルの指定は書き直してください。

[AS]
import flash.utils.*;
import flash.filesystem.*;

private var stream:FileStream;
private var zfile:File;

private var h_signature:uint; // シグネチャ( 4バイト )
private var h_version:uint; // バージョン
private var h_compMethod:uint; // 圧縮方式 (2bytes)
private var h_filenameLength:uint;
private var h_xfieldLength:uint; // 拡張フィールドの長さ

public function unzip():void
{
zfile = File.desktopDirectory.resolvePath(“ここにデスクトップにあるファイルを指定してください”);
trace(zfile.nativePath);
stream = new FileStream();
stream.addEventListener(Event.COMPLETE, readBytes);
stream.addEventListener(IOErrorEvent.IO_ERROR,ioError);
stream.openAsync(zfile,FileMode.READ);
trace(“unzip end”);
}

private function readBytes(e:Event):void{
trace(“FILE SIZE IS ” + zfile.size);
trace(“STR SIZE IS ” + stream.bytesAvailable);

var bytes:ByteArray = new ByteArray();
stream.endian = Endian.LITTLE_ENDIAN;
bytes.endian = Endian.LITTLE_ENDIAN;

while(stream.position < zfile.size){
stream.readBytes(bytes,0,30);
bytes.position = 0;
h_signature = bytes.readInt();
if(h_signature != 0x04034b50){
trace("sig is " + h_signature);
break;
}
else{
trace("sig is OK : " + h_signature);
}

bytes.position = 4;
h_version = bytes.readByte();
trace("versioin is " + h_version);

bytes.position = 8;
h_compMethod = bytes.readByte();

trace("compmethod is " + h_compMethod);

// ファイル名の長さ
bytes.position = 26;
h_filenameLength = bytes.readShort();

// 拡張領域のサイズ
bytes.position = 28;
h_xfieldLength = bytes.readShort();

var offset:uint = h_filenameLength + h_xfieldLength;

stream.readBytes(bytes,30,offset);

// ファイル名を読み取る
bytes.position = 30;
var filename:String = bytes.readUTFBytes(h_filenameLength);

trace("filename is [" + filename + "]");

bytes.position = 18;
var compSize:uint = bytes.readUnsignedInt();
bytes.position = 22;
var uncompSize:uint = bytes.readUnsignedInt();

trace("org size is " + uncompSize);
if(uncompSize > 0 ){
stream.readBytes(bytes,0,compSize);
if(h_compMethod == 8 ){
bytes.uncompress(CompressionAlgorithm.DEFLATE);

// ここでbytesの中身を見ればOK
}
}
}

}

private function ioError(e:Event):void{
trace(“IO Error ” + e);
}

[/AS]

今回はFlexとか関係なくちょっとメールについてのお話です。

Postfixとなっていますが、基本的なネタはPostfixとは関係がありませんし、電子メールを知らない人には「へー」そんなふうにしてメルマガとかっておくられているんだー。ということを知っていただければと思います。

ただ、私はそんな手法に1つに名前がついているとは知りませんでした。
それがVERPというらしいです。

ちなみに、PostfixのVERP Howtoはこちらです。

さて、皆さんが受信しているメルマガとかのメールヘッダを見たことをがありますか?

今回はメールヘッダの部分で、Return-Pathという項目についてです。こちら、メールを配信して、エラーとなった場合などにそのメールアドレスに返事を返すためのメールアドレスです。ではメールを書くときにFromなどでもエラーが帰ってくるのですが、これはFromとこのReturn-Pathが同じようにしているだけなのです。基本的にエラーはこのReturn-Pathに帰るのです。

メルマガなどでメールを送るときには、このReturn-PathのメールアドレスとFromのメールアドレスを違ったものにします。

つまり、機械的なエラーはReturn-Pathに帰り、返事はFromというようにするのです。
こうするのは、機械的なエラーを自動的に処理したいからなのです。

このPostfixのVERPの機能で説明すれば、

user@domain へメールを送りたいときに、このReturn-Pathには owner-listname+user=domain@origin のようなメールアドレスにすると説明には書いてあります。
そう、Return-Pathに送り先の情報が隠れているのです。

これで、user@domainにメールが届かない場合には、owner-listname+user=domain@origin がメールを受け取るので、そのメールアドレスを見れば、だれ宛のメールがエラーなのかわかるというわけです。

ちなみに、通常のメルマガではこんな簡単なふうにはしないでしょう。
たいてい、何やら暗号らしきアルファベットやら、数字やらがあるとおもいますが、これが送り主のみ知っている内部のユーザ識別IDというわけです。

ただ、こういった機能がPostfixにはデフォルトであるのですね。びっくりです。

ただし、この機能は使わないほうがいいですよ。
携帯とかのメールアドレスで最大までつける人がいるんですよ。
そしたら、そのメールアドレスにほかの情報をつけるんで、最大をこえますよね。
そのせいでメールが送れなくなることがあるんです。

エラーを追跡するための情報が原因でエラーになるなんてしゃれにならないですからね。

Flexも、ActionScriptもはじめてはや半年。

別に仕事で使っているわけでもないが、今後のためとおもって勉強中。

今の仕事には直接関係がないし、会社じゃ勉強はできないので一日一時間をめどに勉強をしてきたわけですが、さすがに半年もたつとそれなりに理解ができてくるものです。時間でいえば、1×20×6 = 120(時間)も使っているよ。

だいたい1カ月ですね。これって、結構使っていますね。びっくり。

そう、それだけつかってきて感想。

FlexというかAIRはかなり使えると思う。今 ブラウザでいろいろなことができるようになっているが、
実は、ネットワーク越しのサービスってできることが多くなれば多くなるほど、それを過剰に制限するという流れがあることも事実である。

うちなんかも、事前に申請したドメインしか見えないとか。(さすがにそれはなくなったが・・・)

もう、携帯のキッズフィルターと同じレベルですよ。
いままで、ネットカンパニーにいたからそんな現実理解できなかったけど、そうじゃない世界に踏み込めば、ネットの世界の住人はどこの世界の住人?

という印象がある感じである。
そうそう、スクリーンセイバーだって、2分だよ。(これ以上、ポリシーでながくできない)

そしたら、電話中だってパソコン以外見ていたら、消えちゃうわけよ。画面が。
しかも、パスワード入れなきゃ開けないし。もう毎日、何度パスワードをいれてるかわからないよ。
別に俺はパンチャーじゃないからキーボードをずっとうっていないっていうの。画面を読むときだってそりゃあるでしょ。最近いろいろPDFだし。
(だれか、スクリーンセイバー防止の動き続けるマウスパッドとか作ってくれって。光学式だったらできそうな気がするが・・)

そうなってくると、「カメラなし携帯」がビジネス携帯で売り文句になっていたけど、それと同様、ネットにつなぐ必要なしサービスが必要になるんじゃないかな?完全なクライアントアプリじゃ同じだから、ネットにもつなげるサービスね。オフラインにもできるじゃなく。
(最新、オフラインでも使えるってうたい文句が多いけど、おれにとってはオンラインにもできるで、オンラインはあくまでオプション。オフラインがオプションじゃなく。)

だって、Googleカレンダーとか便利かもしれないけど、さすがにお客さんの情報載せられないでしょ。
そしたら、便利かもしれないけど使えないよ。
でも、ネットの住人じゃない人は、ネットに書いているとかわかっていないから書いちゃうんですが・・・
(だから、制限しちゃえって話にもなるのですが・・・)

でもAIRであれば、自分のローカルのカレンダー情報と、Googleカレンダー、Yahooカレンダーってどこのサービスだって理論的にはくつけられるし。会社の情報はローカルで、自分のプライベートはGoogle、Yahooでってね。

あともうひとつ、ネットサービスの問題点。
世の中がGoogleカレンダーだけってのも変だし、そのほかのサービスを使うって人もいるわけだから、それらの人が混在しちゃったら、結局すべてつかわなければならず、それこそ大迷惑。
GoogleカレンダーとYahooカレンダーとMicrosoftカレンダーはすべてつながりどのサービスを使っていても、お好きなドメインからすべて見えますってなるわけないんだから、結局のところネットだからオープンってのは建前で、現実的にはロックインされてるよね。
場が変わっただけで、MicrosoftのOffice問題がネットで起きるだけ。
Powerpointである必要性はありませんが、周りがそれなので、Powerpointである必要があるのです。ってのが、Googleである必要はありませんが、周りがGoogleなのでGoogleである必要があるのです。って。

考え方によっては、データ自体もサービス提供側に取られているのだから、インストール型よりたちがわるいともいえる。

こんな感じで現場ではネット社会と非ネット社会の隔たりは大きくなるばかり。

そういったところで、もうちょっとバランスを保ちながらサービスをAIRだったらつくれる気がするんだけど、Flexも含めてあんまり広まっているようには見えないけど、広まっているのかな?

ただ、Eclipse RCPのアプリケーションを作ってきたけど、それよりはだいぶ簡単にアプリケーションが作れるとおもうけど、従来のWEBアプリに比べたらだいぶ複雑だよね。
だから、単なるネットサービスならば、ビジネスって点では別にFlexでもなくていいのかなという気はする。
RIAもSOAと同じで、目指す目標がいいが、誰がつくれるの?使えるの?ってレベルなのかな。

それとも、オンラインサービスはさらに進みGoogleカレンダーからYahooカレンダーにドラッグ&ドロップできる時代がくるのだろうか・・・・

いやいや、早く、出てこいAIRのお手本アプリ・・・・

私はテーブルを作成するときには、いつもXMLで自分流に定義して、それを使ってテーブルをcreateする。
こうするといいことがいくつかある。

  1. ドキュメントをxsltなどで自動的につくれる。

    いろいろなライブラリがすでにあるテーブルからDBにアクセス用に自動的にオブジェクトを作るが、私はXMLから作成している。
    それは、結局ドキュメント化されなければいけないのであれば、やっぱりドキュメントが自動のほうが嬉しい。

  2. テーブルの変化(バージョン管理)もXML(ドキュメント)に含められる。

    だいたい、規定していない属性、項目は処理しない。したがってそれを使ってドキュメントに使える。
    また、テーブル追加や削除( alter table )もそこでやったこともあったなー。これはいらなかったかな。

  3. データベースの違いを吸収できる。

    まあ、AIRの場合にはSQLiteしかないから関係ないが・・・
    でも、サーバでMySQLを使って、そのデータをAIRのSQLiteでキャッシュするというときなんか、同じようなフォーマットで共通化できるから便利じゃないかな。
    たとえば、booleanとか。MySQLのむかーしのバージョンではなかったんだよね。(4.1から導入されたらしい)
    だからこれで定義があると、tinyint(1)と書き換えていた。

でも、いいことばかりじゃない。
複雑なテーブルは作りにくい。でも、逆にいえばこれはいいことともいえる。
そして、これが一番の難題。自分で”create table foo(….. ” あれ?つぎなんだっけ?と、文法を忘れる。
したがって、自分で手で作成するときにはリファレンスがないとわからない。
あと、3でも書いたが、違いを吸収してしまうので、新しくできた機能、変化を気にしなくなる。

さて、では実際にはどんな感じでXMLを書くのか?

< ?xml version="1.0" encoding="UTF-8" ?>
<table name="status" dbName="its">
    <field name="sysid" 	type="integer" primary="true" auto_increment="true" >
        <title>システム用ID</title>
    </field>
    <field name="server_id" type="integer" required="true" >
        <title>サーバID</title>
        <description>どのサーバのデータか?</description>
    </field>
    <field name="uid" 		type="integer" required="true" >
        <title>ユーザID</title>
        <description>サーバ上のユニークIDが入る。ただし、ローカルの場合にはsequenceを使う</description>
    </field>
    <field name="createdAt" type="timestamp" 	required="true" >
        <title>作成日時</title>
    </field>
    <field name="updatedAt" type="timestamp" 	required="true" />
    <field name="createdBy" type="integer" 		required="true" />
    <field name="updatedBy" type="integer" 		required="true" />
    <field name="sync"		type="integer" 		required="true" />
    <field name="syncAt"	type="timestamp" 	/>

    <!--  人が管理するためのフィールド -->
    <field name="title" 	type="varchar" size="255" required="true" />
    <field name="type"      type="integer"      default="-1" />
    <field name="memo"		type="text"	/>
    <field name="enableNew" type="boolean"      required="true" />
    <field name="isRoot"    type="boolean"      required="true" />
    <field name="weight"	type="integer"		default="0">
        <title>表示する際の重み</title>
    </field>
    <sequence name="uid" />
</table>

ってな感じです。
titleとか、descriptionとかはドキュメント用。
あと、sequence はcreate sequenceなんだけど、SQLiteにはないから代替の処理。
MySQLにもないけど、PostgreSQLにはあるからこの場合にはふつうにcreate sequenceにする。

/**
 *  Copyright (c)  2009 coltware@gmail.com
 *  http://www.coltware.com
 *
 * XMLからテーブルを作成する
 *
 */
package com.coltware.cise.db
{
	import flash.data.SQLConnection;
	import flash.data.SQLResult;
	import flash.data.SQLStatement;
	import flash.events.SQLErrorEvent;
	import flash.events.SQLEvent;

	import mx.logging.ILogger;
	import mx.logging.Log;

	public class TableFactory {

		private var _conn:SQLConnection;
		private var _xml:XML;

		private static const _log:ILogger = Log.getLogger("com.coltware.cise.db.TableFactory");

		public var lastSql:String = "";

		public function TableFactory() {

		}

		/**
		*
		*/
		public function set connection(conn:SQLConnection):void{
			this._conn = conn;
		}
		/**
		*  定義XMLの設定
		*/
		public function set xml(defxml:XML):void{
			this._xml = defxml;
		}

		public function dropIfExist():String{
			var tableName:String = this._xml.@name;
			lastSql = "DROP TABLE IF EXISTS " + tableName;
			_log.debug("SQL : " + lastSql);
			return lastSql;
		}

		public function drop(resultFunc:Function = null,errorFunc:Function = null):SQLStatement{
			var stmt:SQLStatement = new SQLStatement();
			stmt.sqlConnection = this._conn;
			stmt.text = dropIfExist();
			if(resultFunc != null){
				stmt.addEventListener(SQLEvent.RESULT,resultFunc);
			}
			if(errorFunc != null){
				stmt.addEventListener(SQLErrorEvent.ERROR,errorFunc);
			}
			stmt.execute();
			return stmt;
		}

		public function handleCreate(e:SQLEvent):void{
			var stmt:SQLStatement = e.target as SQLStatement;
			var result:SQLResult = stmt.getResult();
			_log.debug("handleCreate " + result.lastInsertRowID);
		}

		public function create(resultFunc:Function = null,errorFunc:Function = null,ifNotExists:Boolean = false):SQLStatement{
			var stmt:SQLStatement = new SQLStatement();
			stmt.sqlConnection = this._conn;
			stmt.text = createSQL(ifNotExists);
			if(resultFunc != null){
				stmt.addEventListener(SQLEvent.RESULT,resultFunc);
			}
			if(errorFunc != null){
				stmt.addEventListener(SQLErrorEvent.ERROR,errorFunc);
			}
			stmt.execute();
			return stmt;
		}

		public function createSQL(ifNot:Boolean = false):String{
			var sql:String;

			var tableName:String = this._xml.@name;

			var ifNotStr:String = "";
			if(ifNot == true){
				ifNotStr = " IF NOT EXISTS ";
			}

			sql = "CREATE TABLE " + ifNotStr + tableName + "( \n";

			var pkey:Boolean  = false;
			var cnt:int = 0;

			var pkeyList:Array = new Array();

			for each(var child:XML in this._xml.field){
				var name:String = child.@name;
				var type:String = child.@type;
				type = type.toUpperCase();

				if(cnt > 0 ){
					sql = sql + ",\n";
				}
				cnt++;

				var primary:String = child.@primary;
				var ai:String = child.@auto_increment;

				var primaryKey:String = "";
				if((primary == "true" || primary == "TRUE" )){
					if(ai == "true" || ai == "TRUE"){
						primaryKey = " PRIMARY KEY AUTOINCREMENT ";
						pkey = true;
					}
					else{
						pkeyList.push(name);
					}
				}
				var nn:String = child.@required;
				var notNull:String = "";
				if(nn == "true" || nn == "TRUE"){
					notNull = " NOT NULL ";
				}

				switch(type){
					case "INTEGER":
					case "INT":
						sql = sql + name + " INTEGER " +primaryKey + notNull;
						break;
					case "VARCHAR":
					case "CAHR":
						sql = sql + name + " VARCHAR(" + child.@size + ")" + notNull;
						break;
					case "TEXT":
						sql = sql + name + " TEXT " + notNull;
						break;
					case "TIMESTAMP":
						sql = sql + name + " DATE " + notNull;
						break;
					case "BOOL":
					case "BOOLEAN":
						sql = sql + name + " BOOLEAN " + notNull;
						break;
				}

				var def:String = child.@default;
				if(def != null && def.length > 0 ){
					sql = sql + " DEFALUT '" + def + "'";
				}

			}

			if(pkey == false && pkeyList.length > 0){
				sql = sql + ",\n PRIMARY KEY (" + pkeyList.join(",") + ")";
			}

			sql = sql + ");";

			// シーケンスがある場合
			for each(var c:XML in this._xml.sequence){
				// TODO シーケンスの作成で、エラー処理がない
				this.createSeq(tableName,c.@name);
			}

			_log.debug("sql : " + sql);
			lastSql = sql;
			return sql;
		}

		private function createSeq(tableName:String,fieldName:String):void{
			var n:String = "seq_" + tableName + "_" + fieldName;
			var sql:String = "CREATE TABLE IF NOT EXISTS " + n + "( seq INTEGER PRIMARY KEY AUTOINCREMENT );";
			var stmt:SQLStatement = new SQLStatement();
			stmt.text = sql;
			stmt.sqlConnection = this._conn;
			stmt.execute();
		}
	}
}

ってな感じです。

さて、DataGridのdataProviderを自作する件のpart2であるが、
そもそもDataGridとdataProviderはどのようなやりとりをしてデータを表示しているのだろうか?ということを書きたいと思います。

というのも、DataGridに関する情報をみていると雑誌(たぶん)などでも大きなデータでスクロールバーを動きをみて、ページング対応が必要とか記述してある。
ほんとにこんなに難しいのだろうか?と疑問に思ったのがきっかけだ。

(今回は sqlは関係ありませんが、中身がわかればどのようなクエリを発行すればいいか分かるので、参考になると思います。)

確かに、データ取得(バックエンドの都合)によるページングは必要かもしれないが、UIの動きはまったく関係なく解決できるはずだ。
Flex側の都合ではいらないのではないだろうか?ということを思った。

まあ、私もJavaのSwingや、EclipseのRCPを作った時にこのへんは勉強したなー。

(利用者がそれを知った上で、ページングをUIに望んでいるなら別ですが・・・
たまにいるんですよね。俺がほしいデータは500ページあたりにあるんだよってことが分かるくらいにつかいこんじゃう職人が・・・)

話はもどって、DataGridとdataProviderではどのようなやりとりが行われているのか、わかりやすいように会話風にしてみました。

登場人物は
UI君       :DataGridそのもの
プロバイダさん  :ICollecitonViewを実装したクラス(例:ArrayCollection)
下請け      :IListを実装したクラス ( 例: ArrayList ※クラスはpublicですが、ドキュメントは@private なので見えません)
神(ユーザ)   :利用する人。UI君を雲の上から操作し、すごくわがままなこともできる。

では・・・

[UI君] : さっそく、データを表示したいんだけど、全部で何件あるの?
[プロバイダさん]: 下請けさん、何件なの?
[下請け] : プロバイダ様、1000万件です。( get length() )
[プロバイダさん]: 全部で1000万件だよ。 ( get length() )

[UI君] : 1件目ちょうだい!
[プロバイダさん]: はいよ、下請けさんお願いね。
[下請け] : できました。お願いします。( getItemAt() )
[プロバイダさん]: できましたよ。UI君。
[UI君] : じゃ、次2件目!
[プロバイダさん]: はいよ、例のごとくよろしく。
[下請け] : できました。お願いします。
(これが続く。。。)
[UI君] : じゃ、15件目!
[プロバイダさん]: はいよ、またお願い。
[下請け] : できました。お願いします。
[プロバイダさん]: できましたよ。UI君。
[UI君] : おっと、表示するのは15件だからこれで終わりだな。

[神(ユーザ)] : スクロールバーを下げるから、もっと下を見せなさい。
[UI君] : おー、次の仕事を頼むぞ。プロバイダさん。次16件目
[プロバイダさん]: はいよ
[下請け] : できました。お願いします。
[UI君] : 次17件目
[プロバイダさん]: はいよ
[下請け] : できました。お願いします。

[神(ユーザ)] : こりゃ、だめだ。みつからん。逆から見てみよう。ソート!
[UI君] : あちゃー、今までデータを順番にもらっていたけど。無駄だったよ。今までのお願いはなしね。
[プロバイダさん]: りふれーしゅ!!
 ※ ここは自作が必要ですが・・・ 、どうやらこの処理で IList のtoArray()が動くようです。
 このメソッドがインターフェースに入っていなければ、IListもっと良くなると思うのですが。
 これじゃ、データをすべてArrayで返さなければいけないみたいですよね。上の例だと1000万件も。
 でも、実際はこの処理をすべて自作してしまえば、このメソッドは使われない(今のところ)ので、実装ではエラーでも返しちゃいましょ。
 (これでほかの場所でも実は使っているのですが、そこがわかりますよ。)
  
[UI君] : そうそう、ソートの条件はこれね。これからもよろしく。
[プロバイダさん]: 了解!( set sort )
 ※ プロバイダを自作した場合にいは、ソートの概念の実装も自作が必要です。
フィールド名と順番(ASC,DES)が来るから、DBを使っていればその条件を渡してあげればいいだけですが・・・
 このあたりは、下請けさんにもソートの条件を伝える必要がありそうです。
 というのも、下請けさんは公式にはソート条件をもらうすべを持っていません。こっそり教えてあげましょう。
  
[UI君] : じゃ、1件目ちょうだい。
[プロバイダさん]: はいよ。
[下請け] : できました。お願いします。

だけど、プロバイダさんとUI君は実はもっと愛情を注げば(拡張すれば)、もっと賢くなれるようなのです。
こんな感じです。
[UI君] : 次の18件目ちょうだい。
[プロバイダさん]: 了解。じゃ、下請けよろしく。
[下請け] : ただいま、データが用意されていませんので、少々お待ちいただけないでしょうか?
[プロバイダさん]: あちゃー、それは今用意できていないよ。そんな急に言われても困るよ。UI君。( throw ItemPendingError )
[UI君] : んー、それじゃデータが用意できたら教えてよ。
時間がながれる・・・・
[下請け] : データができましたので、お知らせ致しました。
[プロバイダさん]: UI君、待たせたね。できたよ。
[UI君] : おー、ありがとう。じゃ、表示させてもらうよ。

でも、このあたりはインターフェースだけあるようで、実装はありません。
私も今のところ必要性を感じないので見ていませんのでご了承ください。

さてさて、このようにUIとプロバイダではデータが大きくなっても対応できるようなつくりになっています。
あとは、プロバイダがどのようにデータを取得するかは 下請け(IList)の実装にかかっているのです。

下請け(IList)が必要な時に1件、1件取ってきてもsqliteを使っていれば問題ないようにも思いますし、
ネットワーク越しに前もってまとめてデータを取得しておいて1件、1件渡せば、処理はページング処理(キャッシュ処理)が必要になります。
ただ、ページング処理をしてしまうと、ページアウト処理(というか、作ったデータをいらないので消す処理)が必要になってしまいます。
(このあたりは、調べきっていませんが、IViewCursorあたりがそんな役目をするためのもののような気がします。)

でも、DataGridとしてUI側の対応は何も変わらなくても問題ないということがわかっていただけるのではないでしょうか?

追記
それでも、DataGridで大きなデータを扱うときにバックエンド側や、データ自体の問題ではなくUI側の問題で対応が必要な情報をおもちでしたら、
教えていただけないでしょうか?
いずれ、私もその問題にぶつかると思いますので助かります。

ソフトウェア&ライブラリ



ライブラリ
airxmail(en)
AIR版メール送受信ライブラリ
airxzip
AIR版ZIP圧縮・解凍ライブラリ
カレンダー
2009年7月
« 6月   8月 »
 12345
6789101112
13141516171819
20212223242526
2728293031  

カスタム検索
RSS
Add to Google
にほんブログ村 IT技術ブログへ