[IM]PostgreSQL利用者のためのガイド

INTER-Mediatorの開発を、MySQLとFileMaker上で行っているので、他にPostgreSQL/SQLiteにも対応しているとは言え、ご不便をおかけしています。なお、サンプルサイトで、PosgreSQLは動かなかったのですが、今は動くようにしました。

まず、PostgreSQLのサンプルを動かすためのデータベースのスキーマファイルは、

dist-docs/sample_schema_pgsql.txt

というファイルにあります。冒頭にコマンド例がありますが、PostgreSQLがどのOSのどのディストリビューションで動いているかによって、いろいろ違いがあると思います。

OS X Serverの場合:PostgreSQLの稼働ユーザはpostgresではなく、_postgresです。-Uの引数を_postgresにします。

INTER-Mediatorとの絡みについては、MySQLとほとんど同じなのですが、1点違いがあります。PostgreSQLでは、主キーフィールドにシリアル値を入力する方法として、SEQUENCEオブジェクトを利用する方法と、SERIAL型を利用する方法があります。どちらの方法も、原則として、定義ファイルのIM_Entry関数の第1引数にあるコンテキストに、’sequence’ => ‘xxxx’ として、SEQUENCEオブジェクトを指定する必要があります。

sequenceキーに対する値がない場合の問題は、ページネーションコントロールを表示して1レコードずつ表示しているとき、「レコード追加」ボタンをクリックしてレコードを作成した場合、新たに作られたレコードが現在のレコードになっておらず、レコードの移動をしないといけません。他は問題ないのですが、これだけの問題ではありますが、使い勝手が変わるので注意が必要です。なお、検索して参照するだけなら、sequenceキーの指定はなくてもいいのかもしれません。

SEQUENCEオブジェクトを使用する場合

SEQUENCEオブジェクトを使用する場合、スキーマは以下のようになると思います。SCHEMAはim_sample、アクセスユーザはwebを想定しています。テーブルとシーケンスの両方のオブジェクトにアクセス権を与えるのを忘れないようにしましょう。

CREATE SEQUENCE serial START 1000;
CREATE TABLE person (
  id INTEGER DEFAULT nextval('serial'),
    :
}
GRANT ALL PRIVILEGES ON im_sample.serial TO web;
GRANT ALL PRIVILEGES ON im_sample.person TO web;

そして、定義ファイルでは、次のように、sequeceキーで、シーケンスオブジェクトの名前を指定します。

array(
 'records' => 1,
 'paging' => true,
 'name' => 'person',
 'view' => 'im_sample.person',
 'table' => 'im_sample.person',
 'key' => 'id',
 'repeat-control' => 'insert delete',
 'sequence' => 'im_sample.serial',
),

SERIAL型を利用する場合

SERIAL型を利用する場合、以下のように、主キーフィールドの型をSERIALにすると思われます。このとき、背後では、「テーブル名_フィールド名_seq」というシーケンスオブジェクトが自動的に作られて、初期値が1になっています。自動的に作られるオブジェクトとは言え、アクセス権の設定は記述する必要があるのが一般的でしょうから、im_sample.person_id_seqに対してwebアカウントのアクセス権も設定しなければなりません。

CREATE TABLE person (
id SERIAL PRIMARY KEY,
}
GRANT ALL PRIVILEGES ON im_sample.serial TO web;
GRANT ALL PRIVILEGES ON im_sample.person_id_seq TO web;

SERIAL型を使った場合、INSERTでレコードを新規に作るときに、ここでのidフィールドへの値を代入はしないようにします。もし、自分で値を設定したい場合は、シーケンスの値と当たらないようにしないといけませんが、そこまでの状況でSERIAL型を使う事はほぼないと思われます。

そして、定義ファイルでは、このときも、次のように、sequeceキーで、シーケンスオブジェクトの名前を指定します。これをしないと、1レコード表示時に新規レコードを作成しても、新規レコードが編集状態で開いてくれません。

 array(
 'records' => 1,
 'paging' => true,
 'name' => 'person',
 'view' => 'im_sample.person',
 'table' => 'im_sample.person',
 'key' => 'id',
 'repeat-control' => 'insert delete',
 'sequence' => 'im_sample.person_id_seq',
),

[IM]JavaScriptコンポーネントのプラグイン

以前から、HTMLエディタのtinyMCE、コードエディタのCodeMirrorや独自作成したファイルアップロードコンポーネントを使えるようにしていたのですが、なんとか「プラグイン」的に使える状態になったので、一度ドキュメントを作成します。JavaScriptで作ったコンポーネントに、データベースにあるフィールドの値を表示し、修正するとそれが書き戻される仕組みを提供します。ただし、コンポーネントごとに、初期化の方法は違うので、その部分を吸収するプラグインを作らないといけません。

JavaScriptコンポーネントの使い方

まず、JavaScriptのコンポーネントを使いたい場合には、次のように、data-im-widget属性にキーワードを書きます。プラグインができていればこれだけです。

<div data-im="testtable@text1" data-im-widget="tinymce"></div>

ここで、tinyMCEを使うにはプラグインが必要ですが、これについては、すでにINTER-Mediatorの中にあります。Samples/Sample_webpage/tinymce_im.jsがそれなので、たとえば、ページファイルのヘッダ部に、次のような記述を行ってtinyMCE自身の読み込みと、プラグインの読み込みを行っておきます。もちろん、パスは適切なものを指定してください。

<script type="text/javascript" src="tinymce/js/tinymce/tinymce.min.js"></script>
<script type="text/javascript" src="tinymce_im.js"></script>

Ver.4.4現在、tinyMCE、CodeMirror、それから独自に作ったファイルアップロードコンポーネント(Samples/Sample_webpage/fileupload_MySQL.htmlがサンプル)が利用可能です。

JavaScriptコンポーネントプラグインの作り方

プラグイン(前記のtinymce_im.jsに相当)は、JavaScriptで記述します。もちろん、tinymce_im.jsもサンプルとして参照する必要があるでしょう。

プラグインのファイルでは、IMParts_Catalog変数のオブジェクトに、プラグインのオブジェクトを追加します。このときのキーが、data-im-widget属性に指定するキーワードとなります。以下はその基本構造です。

IMParts_Catalog["tinymce"] = {
    instanciate: function (parentNode) { },
    ids: [],
    finish: function (update) { }
}

右辺のオブジェクトは、instanciateとfinishという2つのメソッドを持つ事が重要です。INTER-Mediatorは、ページ合成時に、im-data-widget属性があるノードを見つけると、そのノードを引数にとって、instanciateメソッドを呼び出します。

一方、ページ合成の最終段階、つまり、DOMオブジェクトが確定してページ上に存在する状態になった後に、finishメソッドが呼び出されます。結果的に、im-data-widget属性が設定された要素×レコード数の回数instanciateメソッドが呼び出され、最後に1回finishが呼び出されます。

プラグインの作業として必要なこと

この2つのメソッドが行うことは、コンポーネントに対するゲッタおよびセッタメソッドをそれぞれ展開したコンポーネントに対して設定することです。また、対応コンポーネントのid属性値を得るメソッドも実装します。たとえば、instanciateメソッドに記述するとしたら、次のようになります。instanciateメソッドを呼び出されたときの引数parentNodeあるいはコンポーネントのルートの要素に対して、以下の決められた名称のメソッドを実装します。メソッドの中身はtinyMCEの場合の例です。

parentNode._im_getComponentId = function () { // data-im-widgetのある要素に設定
    var theId = newId;
    return theId;
};
parentNode._im_setValue = function (str) { // data-im-widgetのある要素に設定
    var targetNode = newNode;
    targetNode.innerHTML = str;
};
targetNode._im_getValue = (function () { // コンポーネントのルートの要素に設定
    var thisId = targetId;
    return function () {
        return tinymce.EditorManager.get(thisId).getContent();
    }
})();

instanciateメソッドでの作業

通常、JavaScriptのコンポーネントは、特定の要素にidやclass値を適当に与えて、その要素の中に必要なオブジェクトを詰め込むといった動作をします。つまり、起点となる要素を用意しておき、そこに必要なオブジェクトを追加します。tinyMCEだと、手軽な作り方はTEXTAREAタグ要素を用意することですが、ページファイル上に記述した要素がどんな種類のタグ要素でもいいように、data-im-widget属性がある要素の子要素にTEXTAREAタグ要素を作り、その要素をtinyMCEで初期化するようにしています。

そのTEXTAREA要素のid属性は、適当に付けます。targetNodeで参照されるリピーター内の要素は、すでにid属性が設定されているので、そのid値に適当な文字を追加すれば、一意なid属性になります。_im_getComponentIdメソッドは、ここでのTEXTAREA要素に付けたidを返します。

なお、instanticateメソッド中は、まだ、リピーターはエンクロージャーに挿入されておらず、documentからたどれない状態になっています。その場合、初期化をしてもうまく動作しないと思われます。従って、instanciateメソッドでは、元になるTEXTAREA要素を作り、id番号を振り、そのid番号をidsプロパティの配列に追加して、必須のメソッドを定義するところまでしかできないのが一般的かと思います。idsプロパティはなくてもいいのですが、この後のfinalizeメソッドでそれぞれの要素を初期化するために、初期化すべき要素を後から特定できるようにするために、instanciateで作成した要素のidを残します。

instanciateメソッドの段階で実際に利用されるのは、_im_getComponentIdメソッドと_im_setValueメソッドなので、_im_getValueメソッドは実際には設定する必要はありません。_im_setValueメソッドについては、JavaScriptコンポーネントが初期化前であることを考慮して、機能する前の状態での値設定が可能なプログラムを記述する必要があります。

finalizeメソッドでの作業

この段階では、全ての要素がdocument配下にいるので、JavaScriptコンポーネントの初期化を実際に行います。なお、初期化した結果、ゲッタやセッタの動作を変えたい場合は、設定をしなおします。JavaScriptのコンポーネント内で修正した結果をデータベースに書き戻すには、_im_getValueメソッドを、初期化が終わった後の状態でのゲッタとして動作するように設定をする必要があります。単にデータベースに書き戻すだけでいいのなら、ゲッタの設定のみでかまいません。以下は、tinyMCEの例で、idsプロパティにコンポーネントのid属性値が配列として残されています。1つ1つの要素に対して、_im_getValueメソッドを追加しています。

 for (var i = 0; i < this.ids.length; i++) {
     var targetNode = document.getElementById(this.ids[i]);
     var targetId = this.ids[i];
     if (targetNode) {
         targetNode._im_getValue = (function () {
             var thisId = targetId;
             return function () {
                 return tinymce.EditorManager.get(thisId).getContent();
             }
         })();
     }
 }

tinyMCEのプラグインは、ページ上にあるすべてTEXTAREAをHTMLエディタにしてしまう動作で初期化するので、tinyMCE.initメソッドを呼び出すだけです。コンポーネントによっては、個別にオブジェクトをidsプロパティから取得したid値で参照して、それぞれに何らかのメソッドを適用しないといけないかもしれません。

結果的に、それぞれのJavaScriptコンポーネントごとにうまく初期化をしないといけませんが、場合によってはフレームワークの更新も必要になるかもしれません。

自分でJavaScriptコンポーネントを作る場合

自分で1からコンポーネントを作る場合は、以下のプラグインの骨格を作り、後は自由につくっていいでしょう。このクラスにメソッドを集めてもいいですし、他のファイルから参照してもかまいません。

IMParts_Catalog["myjscomponent"] = {
    instanciate: function (parentNode) { },
    ids: [],
    finish: function (update) { }
}

[IM]Postオンリーモードと「確認画面」が不要な理由

INTER-Mediatorでは、Postオンリーモードという動作をするエンクロージャーを定義でき、一般的なフォームのように入力をして、ボタンをクリックすると対応するコンテキストに新しいレコードを作るという動作を行います。ちなみに、INTER-MediatorではFORMタグを使わないで実装をしています。

通常、フォームで入力するときには、入力結果を改めて別の画面に明示し、入力者に確認をさせて、OKかあるいは修正するというユーザインタフェースが一般的です。そのサポートをもちろんPostオンリーモードでやってもいいのですが、私は不要と考えます。

なぜ、確認画面が必要なのか?

検索すると、やはり否定的な意見(例えばこちら)がいくらかある一方で、確認画面の作り方というサイトは大量に出てきます。現状、確認画面は作って当たり前というのがどうも業界の一般常識なのでしょうか? ただし、なぜ、必要なのかという積極的な説明はざっと見た限りでは見つかりませんでした。一方、不要というのは、「出したところで見ている人はほとんどいないだろう」という消極的な側面が理由としてはよく見られる内容です。

まず、一般的なWebフォームではなぜ確認が必要なのかということがあります。当然の理由として、「確認したいから」あるいは「確認する必要があるから」ということになりますが、理由はもう少し掘り下げて考えないといけません。おそらく、過去からのいろいろな経験の積み重ねで次のような理由があるからでしょう。

  1. フォームのページでは入力した情報がすべて見ているとは限らない
  2. 入力した情報以外のものを表示したい(販売サイトの合計金額や送料など)
  3. ReturnキーやEnterキーによって、submitボタンが押されたのと同じになり、意図せずサブミットしてしまうときにやり直しが効かない
  4. 積極的な理由はない、一般にそうだからとか、とにかく確認させたいといった理由

これらを順次検討しましょう。

フォーム上で見えていないのはデザインが悪い

理由1は、大昔の解像度の低いWebページを作っていた時代では確かにあったかもしれません。テキストフィールドが40文字としても、50文字の入力があるような場合、ユーザに対しては全項目を入力した後の状態の画面で全部が見えていないのは確かにあります。ユーザは垂直方向はもちろん、水平方向にもスクロールしないと文字が見えません。そのような状況で、念のため一度全部、ページに見せるということは確かに必要でした。

しかしながら、現在は解像度が高いディスプレイが一般的です。また、スマホでも何でも1ページに押し込めるなんてことはしないようにするという積極的な対応が普通に行われます。現在はデザインが重視されており、入力中に今自分が入力したものが見えないようなレイアウトは、一般にはデザインが悪いと言わざるを得ないでしょう。もし、この理由でフォームの確認画面が必要なら、まず、フォーム自体を見直す必要があり、その結果、確認画面は不要という結論になると言えます。

別の情報の確認は入力結果の確認ではない

理由2です。これは、入力した結果の確認画面の話でしょうか? 違います。これは、システムが生成した結果を利用者が確認する画面のことで、入力の確認画面の議論と混同してはいけません。この件は最後に別の確度でも検討します。

Returnキーの動作はFORMタグ特有の動作

古い時代のフォームでは、入力途中にReturnキーに触れてしまって意図せずサブミットされて困った人も多いかもしれません。理由3のように、テキストフィールドにフォーカスがあるときにReturnキーを押すと、自動的にそれを含むFORMタグのsubmitボタンがクリックしたものとみなされるのは一般的な動作です。

他のフレームワークならともかく、INTER-MediatorはテキストフィールドでReturnを押しても、submit的な動作はもちろん、設定やあるいはプログラムを追加しない限りは何も起こりません。つまり、INTER-Mediatorであれば、Returnで意図せずサブミットされることはありません。逆に、Returnでサブミットしたいのなら、何かしらの記述を追加しなければなりません。

考えましょう

理由4に対してはエクスキューズの必要すらないと考えます。利用者の事を真剣に考えていないサイトの存在理由って何でしょうか? そうか、失礼しました。そういうことを考えないから、個別の動作についても理由がないのですね。

作っているものは「入力フォーム」なのかを考える

以上のように、入力結果を新規レコードとして残すような入力フォームでは、INTER-Mediatorで適切なデザインをしていれば、確認画面が必要な積極的な理由はありません。しかし、理由2も含めて、いわゆる入力フォームにはそういう単純なパターンではない場合もあります。

理由2に記載したように、たとえば、商品発注の入力フォームがあって、サブミットしたら合計金額と送料を表示させたいとします。これは、実は、入力+システム処理結果の確認なのです。こういうものは必要と言えば必要ですが、これは入力の確認というよりも、システム処理結果の確認です(例1)。

また、これと同様なものとして、入力情報を、複数のページで入力する場合です。アンケートなどで1問ずつページが変わるものがあります。これも、過去のページを保持するという意味では、システムの処理が絡みます(例2)。

こうしたページをINTER-Mediatorで作る場合は、2つのアプローチがあると考えます。

  1. 必要なページをすべて1ページに作り、CSSのdisplay属性を利用して順次見せるような仕組みにして、最後にPostオンリーモードで新規レコードを作成する
  2. ページの最初にレコードを作ってしまい、それ以降のページは実際にはそのページの編集状態で提示する

例2のように、ステップで変わるページの結果を覚えるだけなら、1の手法だと比較的楽でしょう。戻るのも、そんなに労せず可能です。ただし、例1のようなECサイトなら、たとえば、選択した商品の単価や在庫情報、合計に対する送料の計算などロジックが絡みます。もちろん、AJAXで必要な情報を取得してクライアントサイドで計算することも実装の1つの手法ですが、いったんデータベースに入力した結果を参照する編集ページを遷移させる方がスキーマに組み込んだロジックが使える点では便利です。ただし、発注情報にステータス管理、つまり、確定前か後かなどの情報をきちんと設定するなど、やや複雑にはなります。

いずれにしても、これらの状況は、利用者としては入力フォームなのかもしれませんが、システムの動作を考えれば、入力だけでなく、システムの動作が絡むものです。そこをきちんと把握しないと、効率的な開発はできません。サービス提供側はユーザの視点も必要ですが、システム開発の視点を都合良く忘れてしまうのは良くありません。

それでも従来手法を求められたら?

INTER-Mediatorでシステムを作る場合、それでも「確認ページ」を求められたどうしましょうか?まず、よくある対処として「ボタンを押したらダイアログボックスで短いメッセージを出して確認」があります。まあ、その程度でいいでしょうという要求のような場合です。そのとき、Postオンリーモードでデータベースへの書き込みに行く前に実行するメソッドを定義して、そこでダイアログボックスを表示します。例えば、こんな感じです。INTERMediator.construct(true)の直前あたりに記述します。falseを返せば、何もしません。

INTERMediatorOnPage.processingBeforePostOnlyContext = function(node) {
    return confirm("本当に入力していいでしょうか? しつこいようですが、やっちゃいますよ");
}

あるいは、「入力フォーム」と「確認ページ」をそれぞれdiv要素で1ページで作っておいて、JavaScriptで切り替えます。入力結果を確認ページ側に転記するなど、地道なプログラムは必要ですが、困難さはほとんどないと思います。

ちなみに、値の確認は、バリデーションの仕組みがPostオンリーモードでも使えるので、それを活用すれば、未入力の確認等は定義ファイルへの記述のみで可能です。

[IM]ローカルコンテキストのキャッシュと悩みポイント

Ver.4.4を目前にして足踏みしている理由について、だいぶんと頭が整理されてきたので、一度ドキュメントを書きます。

INTER-Mediatorでは、コンテキストに対する検索条件を、プログラムから指定する仕組みを持っています。ユーザが入力した条件で再検索するといった仕組みを作りやすくするためものもです。INTERMediatorオブジェクトのadditonalConditionプロパティです。この種のプロパティはいくつかありますが、「additonalConditionプロパティ」で代表して議論します。

今まで、このプロパティは、ページを再合成すると消えてなくなりました。しかしながら、あるページで検索して、別のページに行き、さらに後ほど前のページに戻ったら、同じ検索条件で検索されていて欲しいと思うのではないでしょうか。そこから紆余曲折を端折ると、データベース関連したコンテキストとは別に、クライアント内部だけで利用可能なキーバリューストアの「ローカルコンテキスト」に、additonalConditionプロパティの値も入れて、ローカルコンテキスト自体をキャッシュする仕組みを作りました。キャッシュインやクリア等、APIも用意されています。要約すると、次のような実装になっているはずです(ま、そこもデバッグポイント)。

  1. 定義ファイルの最初の読み出し(SCRIPTタグで読み込む部分)時において、additonalConditionプロパティがnullであれば、{} で初期化する。ただし、ここでは、キャッシュへの書き込みは行わない実装になっている。
  2. INTERMediator.construct(); でページ合成を行うときには、キャッシュの値を取り出して、additonalConditionプロパティにセットする。
  3. additonalConditionプロパティに値を設定すれば、その都度キャッシュをするのが通常動作である。ただし、IE8のみ、1行呼び出しが必要になる。

条件を入力し、検索ボタンを押したときに、上記3のプログラムが動いて、additonalConditionプロパティは設定され、キャッシュもされます。もちろん、検索時に条件として、追加されます。そして、ブラウザを閉じて同じURLを開くと、キャッシュがあるので上記2のメカニズムにより、以前の検索条件が復活して、条件が適用されたクエリー結果が得られます。

そこで、また、別の条件で検索したとします。ここからが問題です。このときに、以前の条件をクリアして、additonalConditionプロパティを {} から構築しなおせば、おそらく問題はないと思います。つまり、キャッシュの内容と、additonalConditionプロパティへの設定プログラムが、同じ意図を持って存在している場合です。検索条件で使うフィールドがいつも同じだと、この条件が成り立つでしょう。

しかしながら、フィールドAで検索するというキャッシュと、フィールドBで検索するというプログラムの両方があったらどうでしょうか? Aか、Bか、はたまたAとBの合成か? 合成といってもどうするか? ここに明確な答えはないと思われます。アプリケーション次第なのではないでしょうか。そうなると、フレームワークとしてはどういう「仕様」を「サポートする」のかというのが非常に決めづらくなります。

additonalConditionプロパティへの設定は、必ずクリアしてから行ってください…というのもありなんですが、残っていることを利用して効率的に設定を適用したい場合もあります。前者は原則で、後者は理解した方はどうぞということになるのでしょうか? あるいは、キャッシュするかしないかというようなフラグを別途作る事で解決するのか、あるいはより混乱するのか。

この辺りを決めかねています。

[IM]JavaScript、プロパティ、セッタ、オブザーバブル?

JavaScript一般のお話ですが、詳細はINTER-Mediatorがらみとなります。

まず、プロパティは、オブジェクトに対して自由に用意できるのですが、Object.definePropertyというメソッドを使えば、セッタ、ゲッタの実装ができます。IEのみ、このメソッドが限定された状況でしか使えないこともありますが、やはり複雑な事をするときにはセッタやゲッタを利用するのはきわめて自然な発送です。

ここで、オブザーバブルです。最近のJavaScriptフロントエンド系フレームワークではもはや実装されていて当たり前の機能です。MVCという点よりも、むしろオブザーバブルな方が重要な仕組みであるとも言えます。これは、フロントエンドなのでユーザインタフェースを構築することが大きな目的であることから来ます。ユーザインタフェース、つまりMVCのViewとModelの連動のために、イベントに対応するプログラムを書くという手法ではなく、イベントが自動的に伝搬されて行く仕組みが欲しい訳で、それをベースにすると、同一のエンティティにバインドした2つの要素が連動するとか、それをさらにはクライアント間で連動させるといったベースになるのです。ここを自動化できれば、アプリケーションの複雑さはそこそこ緩和でき、より手軽にアプリケーションの作成ができるようになると言えばいいでしょうか。

INTER-Mediatorでは、コンテキストに対する検索条件を、INTERMediator.additinalConditionというプロパティに記述することで、条件を追加できます。コンテキスト、つまりはテーブルへのアクセスにおいて、プロパティの値を条件として記述します。しかしながら、条件の記述においてはさまざまなパラメータが必要になるので、結果として、次の様な記述を行うのを基本にしています。

INTERMediator.additinalCondition["コンテキスト名"]
    = [{field: "フィールド名", operator: "演算子", value: "値"}];

連想配列や配列として解釈すると、きわめて階層が深いですが、必要な情報なので記録が必要です。右辺のオブジェクトの配列を指定できるので、単一のコンテキストで複数の条件を指定したり、あるいはもちろんのこと、コンテキストごとに追加条件を指定できます。

ここで、この追加条件を変更したときに、クッキーに記録させることを考え、なるほど、それならば、このプロパティのセッタを作ればいいのではないかと考えました。もちろん、大した実装でもないので、セッタやゲッタは簡単にできたのですが、セッタが呼ばれない事が時々あり、考え込んでしまいました。考えてみれば当たり前のことなのですが、既存のサンプルなどの動きが正しくなくなると、正しい答えに行くのに回り道をしてしまいます。

まず、以下のようにすればセッタが動きます。

INTERMediator.additinalCondition 
    = {"context": [{field: "f1", operator: "=", value: "123"]};

しかし、以下のようにするとセッタは動きません。ようするに、サンプルの多くの書き方では、セッタは動かないのです。

INTERMediator.additinalCondition["context"] 
    = [{field: "f1", operator: "=", value: "123"];

ここをえらく勘違いしていました。なぜ、後者はセッタが動かないのか? これは、additinalConditionプロパティそのものへのに対する代入をしていないからです。左辺の記述により、additinalConditionプロパティにアクセスするのでゲッタは動きます。そこで、オブジェクトがあれば取り出されて、そのオブジェクトに対して、右辺を代入しているので、additinalConditionプロパティ自体への代入はまったくしていないのです。そこから参照されているオブジェクトに対しての変更処理をしているので、additinalConditionプロパティのセッタは稼働しません。

結果的に、ここでの最下層にあるfieldプロパティなどを書き換えてもadditinalConditionプロパティのセッタが動くようにならないといけないわけでして、それを突き詰めれば、オブザーバブルな配列、オブザーバブルなオブジェクトが必要になってくるわけです。残念ながら、INTER-Mediatorではそこの実装はしていません。KnockoutやEmber.jsではそういう仕組みが用意されているのです。モデルに対するプログラミングインタフェースを持つとしたら、確かにオブザーバブルな配列は必要かもしれません。また、これらの代表的フレームワークに関わらず、オブザーバブルな配列の実装はかなりたくさん公開されています。

INTER-Mediatorに汎用的なオブザーバブルな配列やオブジェクトを組み込むのも、ある意味はいいのですが、その前に本来の目的に立ち返られないといけません。INTER-Mediatorはモデルをプログラマが定義しないと使えないのではなく、宣言的な記述で定義したコンテキストを仮想的に「モデル」として見る以上のとは通常はしなくてもかまいません。オブザーバブルな配列があれば、開発している私はいいのですが、利用者の直接的なメリットではないと考えます。

一方、プロパティの値を自動的にクッキーに入れれば、一見すると便利なのですが、アプリケーションで必要なのはむしろ、細かな記録と消去のような気がします。自動的に覚えさせるのは、確かにデモウケは良さそうですが、いくつかのサンプルに実装してみて感じることは「忘れてくれ!」と思う場面も多いことです。このあたり、どういう実装にするか非常に悩みます。今現在、セッタから呼び出されるようにしていますが、もしかすると、クッキーへの記録は明示的に開発者に記述してもらうのがいいのかもしれません。もし、現状のままだと、additinalConditionプロパティへの値の設定の書き方がきわめて限定される点も考慮すべきところです。この点はしばらく考えるつもりですが、ご意見があれば、FacebookのINTER-Mediatorのグループへどうぞ。

[IM]コンテキストの内部実装

コンテキスト定義に従って、実際にページ合成を行うときに、クライアントの内部にコンテキストのオブジェクトを作ります。このオブジェクトは以下の3種類あります。

  • IMLibLocalContext:ローカルコンテキスト
  • IMLibContext:データベースから取り出したコンテキスト
  • IMLibContextPool:コンテキスト群を管理するオブジェクト

このうち、IMLibContextについては、new等、生成をして利用します。他の2つは1つだけでいいので(つまり、シングルトンでいいので)、そのまま使います。IMLibLocalContextについては、別の記事で説明しています。この記事では残りの2つを解説します。

IMLibContextは、エンクロージャ/リピーターの識別があるごとに1つのオブジェクトが生成されます。いくつかプロパティがあり、それぞれ動作上は細かい説明が必要かとは思いますが、まず、いちばん重要なプロパティは、storeとbindingです。いずれも多次元の連想配列、つまりオブジェクトのオブジェクトという形式になっています。1次元目はレコードを示すキーで、主キーフィールド名がid、そのフィールドの値が45なら「id=45」という文字列をキーとして仕様します。2次元目はフィールド名です。storeプロパティは、this.store[‘id=45’][‘birthday’]という要素に対して、その値が代入されています。

一方、bindingプロパティは、this.binding[‘id=45’][‘birthday’]という要素に対して、{id: id属性値, target: ターゲット}形式の配列になっています。階層の深いオブジェクトです。つまり、bindingは、あるレコードのあるフィールドの値を、ページ上のどの要素に設定したかを記録するものであり、id属性値とターゲットが必要です。また、複数の要素に値を設定することもあるので、オブジェクトの配列で記録しなければなりません。

さらに、要素のid属性とターゲットから、コンテキスト、レコード、フィールドを知るために、contextInfoというプロパティを用意しあり、this.contextInfo[id属性値][ターゲット]という要素に対して、{context: this, record: recKey, field: key} といったオブジェクトが保存されています。なお、ターゲットは””もあるので、その場合は”_im_no_target”に置き換えて記録します。

これらに直接アクセスして利用することも可能ですが、IMLibContextPoolでは生成したコンテキストすべてを把握していて、ここからコンテキストに対するさまざまな処理ができるようにメソッドを用意しています。これは一種のMediatorパターンです。

  • IMLibContextPool.getContextInfoFromId(id属性値, ターゲット)
    • 引数に指定したid属性値を持つ要素の指定したターゲットにバインドしているコンテキスト情報(キーとして、context、record、fieldを持つ)を返す
  • IMLibContextPool.contextFromName(コンテキスト名)
    • 引数に指定したコンテキスト名を持つコンテキスト(IMLibContextクラス)のうち、最初に見つかったものを返す
  • 《IMLibContext》.getValue = function (recKey, key)
    • コンテキスト内にある指定したレコード(”id=45″)とフィールド(”birthday”) の値を返す
  • 《IMLibContext》.getContextInfo = function (nodeId, target)
    •  引数に指定したid属性値を持つ要素の指定したターゲットにバインドしているコンテキスト情報(キーとして、context、record、fieldを持つ)を返す
  • 《IMLibContext》.getContextValue = function (nodeId, target)
    •  引数に指定したid属性値を持つ要素の指定したターゲットにバインドしているコンテキスト内での値を返す
  • 《IMLibContext》.setValue = function (recKey, key, value, nodeId, target)
    • コンテキストに値を設定する。nodeIdがnullの場合、最初から3つの引数(レコード、フィールド、値)により、値が記録されると同時に、同じレコードの同じフィールドを持つ他のコンテキストに対しても値の同期を行う。nodeIdとtargetを指定すると、値は自分のコンテキストにのみ設定し、同時にbindingなどの連携情報を設定する。通常はnodeIdはnullを指定してコンテキストへの設定と他のコンテキストへの同期処理をさせることが多いと思われる

 

IMLibContextPoolが記録している全てのコンテキストを得るには、プロパティのIMLibContextPool.poolingContextsを利用します。ここにコンテキストの配列が設定されています。

[IM] コンテキストを実体化する改良

4月からずっと、ちょっとずつ、INTER-Mediatorを改良してきました。内部の構造については別記事にまとめる予定です。

INTER-Mediatorでは「コンテキスト」という概念を、データソースつまりデータベースから得られる結果に当てはめています。単にテーブルということではなく、テーブルから得られた列構造を持つレコードの集合を「コンテキスト」として位置づけています。たとえば、住所録から「会社関係」「親戚関係」といった分類をして取り出すとすると、それは住所録テーブルから得られるものではありますが、列構造を持つレコード群が、意味を持ちます。そうした意味付けされてデータベースから取り出された結果をコンテキストと読んでいます。システムのアーキテクチャとして、DCI(Data, Context and Interaction)という手法が提唱されていたりしますが、そこでのコンテキストと同じ意味です。この考え方は別に新しいものではなく、FileMakerなどでも見られる概念です。ただ、コンテキストと抽象的に説明すると分かりにくくなり、一方で「検索条件を適用したテーブルアクセス結果」というとあまりに陳腐な感じになってしまい、とらえどころのない用語でもあります。

これまでのINTER-Mediatorでは、コンテキストは「定義する」ものでした。定義ファイル(.phpファイル)で、どのテーブルあるいはビューであり、主キーは何で、検索条件やソート条件は何でと言ったいちれんの設定を「コンテキスト」と読んでいました、この定義ファイルにあるコンテキストに名前をつけて、その名前をページファイル(.htmlファイル)側から参照します。シンプルに説明するときには、ページファイルにテーブル名とフィールド名を書けば、その要素とフィールドがバインドして、データを表示し修正すれば書き直しができるという言い方をします。しかしながら、正確には、コンテキストとして意味付けされた一連の列構造を持つレコード群を、ページ上に展開するということです。従って、同じテーブルから一覧表を作る場合と、単一のレコードを示す場合では、目的が違うので、「異なるコンテキストを要求している」と見なして、定義ファイルに別々のコンテキストを記述するというのが原則と考えています。

定義ファイルに記述した、コンテキストの仕様に相当する者は、「コンテキスト定義」と呼ぶ事にします。

一方、このコンテキストに従った動作をするために、内部的には明確な形でオブジェクトを作っていませんでしたが、それを実現しました。内部的なコンテキストは、クライアントのブラウザ内でオブジェクトとして存在し、一つの見方はデータベースの内容のプロキシです。データベースの内容は、コンテキストのオブジェクト内に「再現」されていると考えてください。加えて、コンテキスト内のデータと、ページ上の要素あるいはその属性との間でのバインドが実現されており、たとえばテキストフィールドでデータを修正すれば、コンテキストオブジェクトの関連したデータも更新されることを自動的に行えるようにしました。

ただし、ここで、内部的なコンテキストは、JavaScriptからタッチすることは可能で、プログラマに対して解放されているとも言えますが、一方で、INTER-Mediatorはそうした手続き的なプログラミングを必要としなくても多くの目的をまかなえるように作りたいことがあります。本来、こうしたバインドの実装は、厳密な意味ではオブザーバブルな実装が必要になりますが、まずは、メソッドベースでの実装を行うことにしました。従って、オブザーバとオブザーバブルは明確になっていない実装になり、そうした仕組みの拡張については今後の課題と考えています。現状は、コンテキストのオブジェクトのメソッドを使って値を設定すれば、同じレコードの同じフィールドの結果を表示している他の要素にも変更結果が伝わるという状態にしてあり、スタティックな意味でオブザーバブルになっています。

こうした内部的にコンテキストを持つことに対して、データベースと連動しない「ローカルコンテキスト」も実装しました。ローカルコンテキストは、ターゲット指定でのコンテキスト名に「_」を使い、フィールド名は自由に使います。次のような2つのテキストフィールドを用意します。そして、INTERMediator.construct(true); を実行します。すると、一方に入力してタブキーでchangeイベントを発生させると、他方のテキストフィルドに入力したデータがコピーされます。連動させるためのプログラムの作成は必要ありません。

<input id="tf1" type="text" data-im="_@feeling" />
<input id="tf2" type="text" data-im="_@feeling" />

このローカルコンテキストは、FileMakerで言えば、グローバルフィールドのようなものです。また、ターゲット指定からそのテキストフィールドの値を取り出すメソッドも用意しているので、他の機能との統合を行うときにも気軽に利用できます。以下のように、IMLibLocalContextで参照されるオブジェクトでローカルコンテキストは実現されており、getValueメソッドで引数にターゲット指定のフィールド名のみを指定することで、テキストフィールドの値を取り出すことができます。

var inputValue = IMLibLocalContext.getValue("feeling");

ローカルコンテキストはクッキーに記録されるような動作を考えました。たとえば、検索条件をテキストフィールドに入れていれば、別のページから戻って来たときにも元の検索条件を覚えているような動作を自動化させたいからです。ただし、この動作については、おそらくさまざまな要求が実際の開発では発生すると思われるので、ぜひとも使ってみた意見をいただきたいです。

ページナビゲーションの「更新」ボタンをクリックすると、このローカルコンテキストのクッキーによるキャッシュをクリアします。