2008-04-24

dojo.io.bind ではない?

現時点での最新版 Dojo Toolkit 1.1.0 を使ってます。
情報は少なくないのですが実際にやってみると、どうもかみ合わないと思ったら、

Dojo Porting Guide: 0.4.x to 0.9

0.9 以降と 0.4 以前のリリース間では、後方非互換とのこと。 dojo.io.bind については、 IO Transports [AJAX] にあるように dojo.xhrXXX に変更になったみたいです。

では、HTML フォームの submit を抑止し、XmlHttpRequest する実験の続き

以下のようなコードになりました。

    function receiveData(response, ioArg) {
}

function onSubmitTestForm() {
var testFormNode = dojo.byId('TestForm');
var deffered = dojo.xhrPost({
url: testFormNode.action + '.json',
handleAs: 'json',
load: receiveData,
form: testFormNode});
}

function init() {
var formWidget = dijit.byId('TestForm');
dojo.connect(formWidget , 'onSubmit', 'onSubmitTestForm');
}

dojo.addOnLoad(init);
...
...
<form id="TestForm" onsubmit="return false;" action="..." method="post" dojotype="dijit.form.Form">



  1. form タグの onsubmit で return false し、送信抑止。


  2. ページの onLoad 時に init 関数内でフォームの onSubmit イベントに onSubmitTestForm を接続


  3. フォームの送信時に onSubmitTestForm が実行され、フォームの内容を伴った Post リクエストが行われる。


  4. レスポンスを受け取ると、receiveData が実行される。



と、いう動作を firebug と デバッガ で確認できました。



嵌ったところが一箇所あって、




  • dojo.connect() では、第1引数に dijit.byId() で取得したオブジェクト


  • dojo.xhrPost() の form 引数には、dojo.byId() で取得した DOM node



でした。dojo.xhrPost() に、dijit.byId() を使うと、form.getAttributeNode is not a function なんてのが firebug のエラーに捕捉されてました。

2008-04-22

dijit.form.Form submit 抑止はどうするの?

Dojo Toolkit 1.1.0 でのお話。

dojo の connect 機構を使ってやってみた。

    function onSubmitTestForm() {
return false;
}

function init() {
var formTest = dijit.byId('TestForm');
dojo.connect(formTest, 'onSubmit', 'onSubmitTestForm');
}

dojo.addOnLoad(init);


これでは、フォームは送信されてしまう。



オフィシャルの trac に Broken Backwards compatibility for dijit.form.Form と、いうチケットを見つけた。まさにこのことについてのやり取り。 接続からのリターン値は、無視される。オーバーライドする必要がある。とのこと。オーバーライド?


他にも、いくつか同様のチケット(#6280 とか)上がってて、斜め読みした感じでは、



<form dojoType="dijit.form.Form" onSubmit="return false;" id="TestForm" method="post" action="...">


こうすると、リクエストは送信されず、onSubmitTestForm が実行されました。onSubmit="dojo.stopEvent(arguments[0]);" でも、同じ。何が違うんだろう?



とりあえず onSubmitTestForm から dojo.io.bind してみます。

2008-04-16

PHP で Excel 2003 の SummaryInformation を取得したい。

SummaryInformation は、エクスプローラで xls ファイルを右クリックしてコンテキストメニューの「プロパティ」を開き、「概要」タブなどに表示されている情報です。ワークシートの内容にアクセスするものは、いくつかヒットしました。

  • 茶漬けブログさんの Excel_Peruser
    ワークシートが読めます。ちょっと使ってみた範囲では完璧でした。素晴らしい。 Excel_Reviser と組合わせて既存の Excel ファイルをテンプレートとして使えます。
  • PHPExcel
    Excel 2007 (.xslx)の読み込みは、できるらしい。PHPExcel_DocumentProperties->getTitle など、それらしい実装があります。Excel 2007 持ってないから、チェックできない。
  • PHP-ExcelReader
    ワークシートが読める。
  • PEAR::Spreadsheet_Excel_Writer
    ワークシートの書き込みができるらしい。

ワークシートがやっぱりメインだよな。その他の資料としては、

1日やって、だめだったらあきらめるつもりで PHP-ExcelReader を使ってダンプを出して眺めたりしてました。それらしい、箇所はあるのですがたどり着く方法がなかなかわかりませんでした。そろそろ、見切りをつけようと思っていたところで、jakarta になんかあったことを思い出して、探してみると The Apache POI Project になってました。いつの間にかトッププロジェクトに昇格してる。

で、PropertySet 辺りの実装を参考に改めてダンプを見てみると、

「読めるっ!!読めるぞぉ」(ラピュタのムスカ風に)

甚だ、中途半端ですが必要な部分のみ parse できるだけの PHP 実装をしてみました。

    function parseProperty($data){
$offset = 0;
$result = array();
$result['byteOrder'] = GetInt2d($data, $offset);
$offset += 2;
// 以下は、Intel little endian きめうち
$result['format'] = GetInt2d($data, $offset);
$offset += 2;
$result['osVersion'] = GetInt4d($data, $offset);
$offset += 4;
$result['classID'] = unpack('C*', substr($data, $offset, 16));
$offset += 16;
$result['numSections'] = GetInt4d($data, $offset);
$offset += 4;
// section を取得
for ($secidx = 0 ; $secidx < $result['numSections'] ; $secidx++) {
$section = array();
$section['formatID'] = unpack('C*', substr($data, $offset, 16));
$offset += 16;
$section['offset'] = GetInt4d($data, $offset);
$offset += 4;
$propTop = $propTblOffset = $section['offset'];
$section['size'] = GetInt4d($data, $propTblOffset);
$propTblOffset += 4;
$section['numProperties'] = GetInt4d($data, $propTblOffset);
$propTblOffset += 4;
// UTF-16 の可能性もあるようだが今のところ SHIFT-JIS きめうち
// 以下の propertyID = 1 がコードページなので対応は可能じゃないかな。
$srcenc = 'SJIS-win';
for($propidx = 0 ; $propidx < $section['numProperties'] ; $propidx++) {
$property = array();
$property['propertyID'] = GetInt4d($data, $propTblOffset);
$propTblOffset += 4;
$property['propertyOffset'] = GetInt4d($data, $propTblOffset);
$propTblOffset += 4;
$propOffset = $propTop + $property['propertyOffset'];
$property['propertyType'] = GetInt4d($data, $propOffset);
$propOffset += 4;
// ここで、propertyType (Variant型)ごとの値取得
switch($property['propertyType']) {
case 2: // VT_I2 = 2 byte signed int.
$property['propertyLength'] = 2;
$property['propertyValue'] = GetInt2d($data, $propOffset);
$propOffset += $property['propertyLength'];
break;
case 30: // VT_LPSTR = null terminated string.
$property['propertyLength'] = GetInt4d($data, $propOffset);
$propOffset += 4;
$strval = mb_convert_encoding(
substr($data, $propOffset, $property['propertyLength']),
"UTF-8", $srcenc);
$property['propertyValue'] = str_replace("\x00", "", $strval);
$propOffset += $property['propertyLength'];
break;
case 64: // VT_LPSTR = null terminated string.
$property['propertyLength'] = 8;
$lowWord = GetInt4d($data, $propOffset);
$propOffset += 4;
$highWord = GetInt4d($data, $propOffset);
$propOffset += 4;
// EPOC の違いを変換。
// 1601-01-01 00:00:00 (Windows EPOC/100nsec)
// -> 1970-01-01 00:00:00 (unix EPOC/1sec) に変換
$unixTime = $highWord * 4294967296 + $lowWord;
$unixTime = floor($unixTime / (1000 * 1000 * 10));
$unixTime -= 11644473600;
$property['propertyValue'] = date('Y/m/d H:i:s', $unixTime);
break;
}
$section['properties'][$propidx] = $property;
}
$result['sections'][$secidx] = $section;
}
return $result;
}

これで、propertyID = 2 の propertyValue にタイトルの文字列が取得できます。日付関係も問題ないよう。手持ちのファイルでは、概ね OK でした。

もっと、効率的に書けるでしょうし、エンコーディングのきめうちや未対応の Variant 型など突込みどころも多いですが、後で必要になったらがんばるかもしれません。

2008-04-11

こんな model はこうする(ことができるかも?) 空振り

こんな model はこうする(ことができるかも?) の続きです。

185 個の hasMany を bindModel する為に、 memory_limit と max_execution_time を適宜調整して、debug を 1 に設定し、なんとか完走しました。しかし、サーバ TA が3~5分かかっていたのでそのままではちょっと使えない。ボタン何回も押されちゃったりするし。

で、必要なテーブルに絞って hasMany することに。この時点で、不整合が発生する危険を作りこんでしまいました。最終的に、アソシエーション定義の dependent = true とすることで、頭の Model->del によって3段のアソシエーションを辿って削除することが出来ました。

がっ!! del も deleteAll もトランザクションが実装されてない...orz (rev.6596)

saveAll ではトランザクションされてたのにな。仕方ないから query 使うかな。

2008-04-10

こんな model はこうする(ことができるかも?)

こんな model はどうする? の続きです。

Oyatbl - hasMany - Kotbl - belongsTo - Betsutbl

のような関係の kotbls_xxx が 200 個近くあります。
アクセスする kotbls_xxx は、oyatbls のフィールドの値で決まります。kotbls_xxx は、別の masters テーブルの id に対応(xxx = id)した個数あります。

$this->Oyatbl->Kotbl->setSource('kotbls_' . $id);

で、すんなり動きました。ただし、Kotbl モデルの定義で、あとで setSource するからといって Kotbl->useTable = false としてしまうと belongsTo Betsutbl アソシエーションが効きません。setSource では、アソシエーションの再構築が行われないようです。
Kotbl->useTable = 'kotbls_xxxどれか' と定義しておくか、setSource のあとで bindModel/unbindModel する必要がありました。

また、上の記述とアソシエーション定義を見て気付いたのですが

var $hasMany = array(
'assocalias' = array(
'className' => 'Kotbl',
...

この場合は、className Kotbl モデルのインスタンス(オブジェクトだっけ?)が assocalias という名前で生成されるということらしいです。
なので、こんな荒業も

$ids = $this->Master->find('list', array('id'));
foreach ($ids as $id) {
$assocKey = 'Kotbl' . $id;
$tblName = 'kotbls_' . $id;
$this->Oyatbl->bindModel(array(
'hasMany' => array(
$assocKey => array(
'className' => 'Kotbl',
'foreignKey' => 'fkey',
'dependent' => false
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
)
)
, false
)
);
$this->Oyatbl->{$assocKey}->setSource($tblName);
}

デバッガで覗いた限りでは、model オブジェクトの中身はそれらしくなっていました。動作の確認は、また明日。しかし、かなりヘビー。大量の SQL 発行とかなりメモリも消費します。実用になるのか?

delete をトランザクションで行いたいのですが、なんか、苦労の方向性が間違っている気がする。DB 設計を見直してリレーションを設定して delete cascade で良いんじゃないか? ただ、既存 DB なので影響範囲にすべて手を入れるとなると...

2008-04-09

アソシエーションで集約関数を使いたい

CakePHP 1.2(6598)。 PostgreSQL 8.1

はまった。親テーブルに対して、子テーブルを集約関数(COUNT,SUM など)でサマった結果を結合したい場合です。

例えば、 User hasMany Post という関係について User hasOne (user_id 毎のPost 数) と、いうアソシエーションを設定できないか。理想は、

    Users
LEFT JOIN
(SELECT user_id, COUNT(id) AS cnt FROM Posts GROUP BY user_id) postcounts
ON
Users.id = postcounts.user_id


こんな問合せが組立てられること。

conditions に GROUP BY を記述する方法は、使えませんでした。GROUP BY が結合条件(ON 節)に入ってしまって、 syntax error です。

とりあえず、2点考えてみましたが、何れも搦め手ですっきりしません。

1. データベースビューを使う


PostgreSQL なのでデータベースビューを作成することができます。

CREATE OR REPLACE VIEW postcounts AS
SELECT user_id, COUNT(id) AS cnt
FROM Posts
GROUP BY user_id

CakePHP でも、テーブルと同様に扱えるようです。データベースビュー (postcounts) に対する model (Postcount) を bake することができました。あとは、通常の model 同様、hasOne に設定します。
長所
  • hasOne アソシエーションなので model->recursive = 0 での問合せに含まれます。検索条件に使えるので PaginatorHelper を使いやすいです。
短所
  • 集約する条件を動的に指定できない。「ある期間内の投稿数」のような条件でサマることは、よくあるんじゃないかな。


2. hasMany の結果をカウントする



hasOne アソシエーションではないのですが、User hasMany Post が設定されているので、model->recursive = 1 で問合せて count(kekka['Post']) で件数は得ることができます。大量の問合せが発行されることを危惧していたのですが、WHERE 節に IN 句が使用された1文が発行されていました。

長所
  • hasMany の conditions に指定することで集約条件を動的に設定できる。

短所
  • 別の文なので、検索条件に含めることはできない。
  • 親テーブルの件数が多い場合には、IN 句が大量となり遅くなる(と、思う)。

CakePHP 的には、hasMany をカウントの方が素直でしょうか。勝手に思っていることですが、model->query を使うと、負けてしまった気分になると思います(^_^;

2008-04-07

こんな model はどうする?

CakePHP 1.2 でのお話。PostgreSQL 8.1。

同一構造で関連ID毎に185個のテーブルがある。2つのマスタテーブルに対して belongsTo でマスタテーブルからは、 hasMany。残念(?)ながら、パーティショニングされてはいないし、するには移行手順が必要。

さぁ、どうする? 185個モデル作るべきなのか? まさかね。

全てのテーブルを同時に参照する必要はなさそうなので参照するテーブルを動的に切り替えるロジックは作れそう。

model->setSource と model->bindModel/unbindModel で行けるのかなぁ? 続きは、明日。

2008-04-03

CakePHP で Dojo toolkit

CakePHP 1.2 で Dojo Toolkit を使ってみた。


全てのページで必要ではないので、layout の scripts_for_cake 変数を使います。

CakePHP Users in Japan フォーラム scripts_for_layoutとは?


試しに Dijit のポップアップカレンダー付き日付入力テキストボックス(dijit.form.DateTextBox)を使ってみます。


Dojo 配置


Dojo を app/webroot/js 以下に配置します。今回は、dojo というディレクトリを掘りました。


app
└─webroot
└─js
└─dojo
├─dijit
├─dojo
├─dojox
└─util


dojo.js と CSS 読み込み



テンプレートに以下を追加します。全てのページで Dojo を必要とはしていないので、 layout ではなくテンプレートに記述して、scripts_for_cake メカニズムを使います。



<?php
$html->css('../js/dojo/dijit/themes/tundra/tundra', 'import', array(), false);
$html->css('../js/dojo/dojo/resources/dojo', 'import', array(), false);
$this->addScript('<script type="text/javascript" src="/絶対URL/js/dojo/dojo/dojo.js" djConfig="isDebug: true, parseOnLoad: true"></script>');
$javascript->codeBlock(' dojo.require("dijit.form.DateTextBox");
dojo.require("dojo.date.locale");
dojo.require("dojo.parser"); // scan page for widgets and instantiate them', array('inline' => false));
?>


Javascript ヘルパの link では、大事な大事な djConfig が設定できなかったので addScript を使いました。弊害として、src 属性に絶対 URL を書かなければいけなくて、なんともかっこ悪い。



フォームにインプット要素を記述



他の form ヘルパの要素と混在可能です。



<?php echo $form->label('created.min', '登録日'); ?>
<input name="data[Moderu][created][min]" type="text" value="<?php echo $this->data['Moderu']['created']['min'] ?>" id="UsrpramCreatedMin"
dojoType="dijit.form.DateTextBox"
constraints="{min:'2003-01-01',max:'2013-12-31',datePattern:'yyyy/MM/dd'}"
promptMessage="登録日の検索範囲"
trim="true" />


注意点



これで、そこそこ表示されますが、落し穴が2点ほど




  1. layout の body タグに dijit テーマの class 属性が必要。

    今回は、tundra を使っているので <body class="tundra"> としないとかっこよくならない。

  2. CSS の基本セレクタが衝突。 cake.generic.css を一部変更して使っていると妙な表示(今回の場合、input ボックスの高さが異常にでかかった)になりました。

    form div セレクタを削除して対処しました。



djConfig や constraints などをうまく扱うためには、やはりヘルパが欲しいところです。