[IM]バリデーションの実装を再考する

INTER-Mediatorではかなり以前にバリデーションの実装はしたものの、完全ではない状態だったのですが、このところ、手を入れるに従って、いろいろ不具合…というか、「考慮が薄い」ポイントが目立ち始めましたので、改めて、議論を進めたいと考えています。

まず、バリデーションは、「入力チェック」とも言われます。いちばん、根本的なことは何かというと、「正しくない値をデータベースに保持しないようにしたい」という要求があると考えています。「正しくない」の基準は、アプリケーションによって変わりますが、「NULLである」ということかもしれませんし、「正しいメールアドレスのフォーマットではない」ということかもしれません。その場合に、データベースに記録しないようにするということがあります。データベース絡みとなると、いろいろ複雑な問題が出てきますので、これを後回しにして、まずは、アプリケーション利用者レベルからの見方を考えてみます。

開発者や管理者がバリデーションを必要とし、実装しますが、Webアプリケーションフレームワークは、バリデーションに関連したユーザーインタフェースを構築し、ユーザーを惑わせない仕組みの提供が望まれます。そもそも、アプリケーションで、どようにバリデーションが絡み合うのか、5W1H的にまず考えます。

  • When:正しくない値が入力されたとき
  • What:開発者や管理者が望ましくないと考えるデータを検知する仕組み
  • Where:入力可能なコンポーネント
  • Who:ユーザーが生成すると考える
  • Why:単純なミス、さまざまな誤認、テストあるいはインスペクション
  • How:キータイプ、あるいはコピー&ペーストなどのユーザの操作

以上の分析からは、バリデーションの検知と通知が大きな目的であることが出てきます。しかし、一部に例外があって、フィールドの初期値がバリデーション違反という場合もあります。そのとき、上記のWhoは「システム」ということになってしまいます。この初期値が違反している問題は、厳密にはデータベースの定義が正しくないで終わってしまいますが、非常に複雑な事情が絡むので、後ほど議論します。

バリデーションの検知は、INTER-Mediatorではすでに実装しています。onchangeイベントが発生したときに、フィールド単位でのチェックを行います。フィールドをまたがった判定用にも、コールバックされるメソッドの定義があります。最近になって、初期値が違反しているときでもバリデーションが働くように、onblurイベントでも判定をするようにしました。これら、さまざまなニーズはあると思われますが、タイミングと仕様が確定していれば、対処できる範囲かと思います。

一方、バリデーション違反の通知は、さまざまなバリエーションが考えられます。そういうニーズも状況も多様な状況では、プログラムを組んで対処というのはもちろん柔軟な対応ができていい部分ですが、一定の範囲を宣言的な記述でまかなうことで、プログラムを書くことを減らす意図のあるINTER-Mediatorではなんとかしたかったところです。そこで、フィールド単位のバリデーションが違反したら、「ダイアログボックスを表示して促す」「近辺に赤字等でメッセージを出す」という仕組みを定義ファイルの設定だけで実現しました。そして、イベント発生時に違反が検知されれば、雇用な動作をして、フィールドからフォーカスがはずれないようにしました。

しかし、ここまででも、すでに議論のポイントはいくつもあります。

  • バリデーションはいつ行うのか?
    • キータイプごと?
    • カット&ペーストするごと?
    • データベース更新前?
    • 複数のフィールドに対して「書き込み」ボタンがあれば、それを押したとき?
    • ポストOnlyモードとデータベース更新時は動作を違う必要があるのか?
    • 現状は、定義ファイルで指定可能なのはデータベース更新前のみ。
  • 違反通知をどのように行うのか?
    • 現在は、ダイアログボックスとページ上への文字の追加
    • 新しいページを表示したい?
    • 何が間違えたのかをもっと詳しく表示したい?
  • バリデーションに違反したら、その後にどのような操作を期待するのか?あるいは期待されるのか?
    • そのままでいいのか?
    • どこまでロールバックするのか?
    • どこまで既定値的な値を設定するのか?
    • 違反したレコードは削除しなくていいのか?
    • 違反してもレコードは作るのがいいのか?
    • 現在は、そのままにしつつ、正しい値を入力しないとそのページの他の作業をできなくしている。

あらゆるバリエーションに対する答えを用意するのか、それとも、主要な手法以外はプログラミングをしてもらうのか、その辺りが議論のポイントになると思います。いずれにしても、問題を書き出すことは必要でしょう。

データベースエンジンには通常バリデーションの仕組みは含まているので、本来はそちらを使うべきという議論があるでしょう。SQL言語での定義時に記述するため、「難しいから敬遠している」という向きもあるかもしれません。一方、なぜ、フレームワークがバリデーションをサポートするかというと、データベース側でのバリデーション処理は、違反時の状況の取得や、そこからの適切なユーザーインタフェースの構築、さらにやり直しなどの処理の組み立てなど、単純ではないプログラミングを要求されます。また、その対処方法も、データベースごとに違う可能性もあるので、むしろ、フレームワークの内部でバリデーションをサポートした方が、動作上作りやすいということもあるわけです。データベース側のエラー検知は、レイヤーを上下するワークフローをうまく組み立てるようなプログラミングを必要としますし、その結果、アプリケーションサーバーとデータベースということなるソフトウエア間の連携ということも必要になります。結果として、データベースに頼らない方が、フレームワーク内部で完結するため複雑さの一部を回避でき、加えてデータベースごとの事情に左右されないというメリットを生みます。

その結果、データベースのスキーマ定義にバリデーションルールが入らないことになり、それによる大きな問題は、初期値がバリデーション違反という状況で、ユーザーの操作に入ることがあります。これをどこの段階で、不正とみなし、どのような方法で排除するか、これが定まらないと、実装が揺らぎます。

ということで、とりあえず、頭にあることを書き出しておいて、議論を進める手掛かりにしたいと思います。

[IM] 2015年に向けての組織化を考える

INTER-Mediatorの開発を始めてから、ちょうど丸5年くらいが経過しました。いろいろあったものの、一貫して、新居雅行個人のプロジェクトのような形態だったのですが、エミックさんをはじめとしていろいろな展開が始まっており、そろそろ個人プロジェクトではなくそうかと思っています。かといって会社作るということではありません。こうしたプロジェクトは組織そのものも、プロダクツに見合ったものをコミュニティプロセスで発見的に開発しないといけないと考えています。

まず、INTER-MediatorがMIT Licenseであり、オープンソースである点はそのままにしますが、クレジットは「INTER-Mediator Development Project」にしようと考えています。加えて、その開発そのものをマネージメントしている「INTER-Mediator Directive Committee」が存在するという形態にしたいと思います。人数少ない中、組織ばっかり作るかという話もありますが(笑)、先々の方向性を決めるINTER-Mediator Directive Committeeがあって、実際の開発はINTER-Mediator Development Projectがやっているという状況とみなすのです。本来は、さらにINTER-Mediator利用者としての開発者やシステム開発を行うエンドユーザもいるのですが、今のところはこれらのみなさんに対する「組織」は作らないで、時機が来るのを待ちたいと思っています。

「INTER-Mediator Development Project」のミッションは、INTER-Mediatorの継続的な開発を進めることです。これは明確ですね。コードに書くクレジットはこれになります。また、GitHubの主体も「INTER-Mediator Development Project」にするつもりです。GitHubは現在新居のアカウント上にありますが、「組織」という仕組みがあるので、それに移行するつもりです。「INTER-Mediator Development Project」のメンバーは、Contributor、Special Thanks、そしてcloneやforkした皆さんというゆるい定義にしたいと思います。つまり、集まっている方々はプロジェクトに関わっているというみなしをするという組織であり、言い方を変えれば求心力はないものの影響力はあるというところでしょうか。今の所、動いている唯一のコミュニティであるFacebookのグループは、「INTER-Mediator Development Project」の活動の一貫であるということにします。

「INTER-Mediator Directive Committee」のミッションは、INTER-Mediatorの継続的な発展を促す活動を行うということです。こちらは、立候補や推薦と同意などを含めて、メンバーが誰であるのかということを明確にします。したがって、入会と退会を明確にする必要があります。「INTER-Mediator Directive Committee」は役割を明確に定めることと、必要に応じて担当者を設定するものとします。役割は、次のようなものです。

  1. INTER-Mediatorの今後の開発計画を立てて、方針を示す(例えば、半年に1度、ドキュメントをリリースする)
  2. Webサイトを運用する
  3. GitHubでのpull requestに対処する
  4. 普及のための活動(勉強会やもくもく会など)を主催、あるいはリードする
  5. トレーニングマテリアルを整備し、提供する

5についてはすでに私自身がお金をいただきながらやっていることですが、位置付けは普及活動になるかと思います。2についても私がやっていますね。1については、Project側の意見も吸い上げないといけませんが、決定して声明を出すのはCommiteeの役目と考えています。

現在のWebサイトは3つのドメインで同じものを表示していますが、以前に松尾さんから想定されていたように、.orgはCommitee、.infoはマニュアル、.comは総合的な紹介サイトということにしようと考えています。人や作業が増えた時に、分割されていると、作業分担がしやすいのではないかと思っています。この作業は、さっそく、2015年前半の作業プランに入れるべきかもしれません。

そういうことで、大きな意味では「Committeeを作り、名乗ります」ということになりますし、それだけといえばそれだけですね。会則とかややこしいことは初期段階では不要かと思います。私がChairmanですということでもいいのですが、役割分担ではないroleの割り当ては、Committeeの人数が増えてからにしようと思います。

Committeeメンバーについては、松尾さんには入ってもらいたいと思っています。エミックさんではFM Publisherを開発して提供しており、INTER-Mediatorの動向は自社ビジネスに少なからず影響するわけですから、言い換えればコントロールする側に立つ意味もあるかと思います。また、立候補、推薦等も受け付けますが、本人が嫌がった場合には当然ながらメンバーとは言えないかと思います。みなさんの中でも立候補される方はご連絡ください。もちろん、開発関係の方の参加が想定されますが、開発に関係ないもののこうしたコミュニティにより深く関わってみたいという方の参加も歓迎します。

[IM] MySQLでエンコードにはまったとき

INTER-Mediatorの話題です。こういうことがあるかどうは微妙かもしれませんが、私の郵便番号検索のサイトで、以下のような状況になりました。

  • 郵便番号検索のアプリケーション(A)はmysql_Connectで稼働している(あー、5.5ではついに書き直さないといかんなー)
  • UTF-8で動いているつもりだったが、MySQLで、エンコーディングの指定をしないままに動かしてしまっていた(これは失敗、でも、UTF-8でなんとなく動いている)
  • その上に、INTER-Mediatorで作った、書籍販売サイト(B)を動かす。これも、なんとなく動いている。郵便番号とは別のデータベースを作ったので、独立しているから問題なかった。
  • INTER-Mediatorでの郵便番号検索ページ(C)をいくつかの理由で立ち上げて、郵便番号データを読み込んでいるデータベースを利用して検索しようとしたら、文字化けた。

とまあ、要するに、2項目目が失敗なのですが、なんとなく動いているという状況下気づかなかったものの、最後の項目ではちゃんとやろうとして失敗に気づいたということです。my.cnfに、エンコードとしてUTF-8をつけると、(C)はきちんと動きました。それから、(A)もきちんと動きました。はいおしまいと思っていたら、(B)が文字化けます。つまり、(A)(B)(C)、同時に動かなくなったのです。なお、(C)の後追加した「character_set_client=utf8」が問題(かどうかも問題)のようで、これはmy.cnfから削除しました。(C)の発覚後、「character_set_server=utf8」も追加しましたが、最終的にはこれは残してあります。

結局どうしたか? (A)は、実は、「SET NAMES utf8」というSQLコマンドを発行して、うまく動かしていたのです。つまり、これを動かさないとうまく行かない大昔の状況のまま運用していて、エンコードの指定が適切でなかったことに気づいたわけです。(A)(B)(C)全部を動かすために結局何をしたかというと、INTER-Mediatorのアプリケーションで「SET NAMES utf8」を実行するようにしました。クエリーするたびにまず、このコマンドを実施して、SELECTを出すようにしました。これも、定義ファイルの設定でできます。こんな感じの定義ファイルです。scriptというキーワードは、FileMakerではよく使いますが、すべてのデータベースで利用可能です。

IM_Entry(
    array(
        array(
            'name' => 'postalcode',
            'view' => 'pcode',
            'records' => 50,
            'maxrecords' => 50,
            'paging' => true,
            'script' => array(
                array(
                    "db-operation"=>"load",
                    "situation"=>"pre",
                    "definition"=>"SET NAMES utf8"
                ),
            ),
        ),
    ),
    null,
    array('db-class' => 'PDO'),
    false
);

そういうことで、あっさりトラブル解決できました。まあ、でも、郵便番号サイトは、今シーズン終わったら、PHP 5.5か5.6で動くように、全面改修だな。

[IM]ページ上のオブジェクトに機能を割り付ける

データベースの内容を一覧するときに、検索結果を適用するという仕組みを一切プログラムを書かずに実現するために、ボタンやテキストフィールド、あるいは一般的なノードに対して機能を割り当てるという考え方を導入しました。要素のdata-im属性を利用して、ローカルコンテキスト(名前は「_」)に特別なキー名でバインドすることで、機能が割り当てられます。今の所、検索ページを作るための以下のものが利用できます。抽象的に記述してもわかりにくいと思いますので、具体的なタグのテキストともに紹介します。

検索条件の付加

テキストフィールドを「_@condition:….」のローカルコンテキストへバインドさせると、そのテキストフィールドに入れた文字列が検索条件になります。また、Enterキーで、コンテキストの更新が実行されます。

 <input type="text" data-im="_@condition:postalcode:f3,f7,f8,f9:*match*">

@以降は、コロン(:)で4つのセクションに分かれます。

  • 第1セクション(例:condition):この文字列
  • 第2セクション(例:postalcode):コンテキスト名
  • 第3セクション(例:f3,f7):フィールド名。複数の指定も可能
  • 第4セクション(例:*match*):演算子

最初の2つはいいとして、3つ目のフィールドは複数指定も可能です。複数指定をすると、それぞれのフィールドに対して同じ値の検索条件をORで与えます。演算子は、一般的なものに加えて「*match」「match*」「*match*」の3つが用意されています。データベースエンジンに関わらずに、部分一致や前方一致などをこの演算子で記述します。

上記のテキストフィールドに、例えば「新宿」と入れてEnterキーを押すと、以下の検索条件がコンテキストに付加されて、再度検索を行い、そのコンテキストのエンクロージャー内が更新されます。

(f3 LIKE '%新宿%' OR f7 LIKE '%新宿%' OR f8 LIKE '%新宿%' OR f9 LIKE '%新宿%')

表示件数の制御

以下のポップアップメニューを選択すると、レコードの表示件数をポップアップの選択肢で指定でき、選択と同時にコンテキストが更新されます。「limitnumber」が決められた名前で、コロンより後にはコンテキスト名を記述します。changeイベントにより、コンテキストの更新します。

<select type="text" data-im="_@limitnumber:postalcode">...</select>

コンテキストの更新ボタン(検索ボタン)

以下のボタンをクリックすると、指定したコンテキストが更新されます。つまり、「検索」ボタンとして機能するということです。「update」が決められた名前で、コロンより後にはコンテキスト名を記述します。clickイベントにより、コンテキストの更新します。

<button data-im="_@update:postalcode">search</button>

並べ替えフィールドの指定

以下のSPANタグ内の▲をクリックすると、f3フィールドの昇順で並べ替えを行います。同一のコンテキストに対する「addorder」の機能を持った要素は連動します。たとえば「f3で昇順」の後に「f9の降順」を選択すると、「f9の降順」を最優先とし、続くキーとして「f3で昇順」を設定します。最後に設定した条件が最優先になるようになっています。このバインドはclickイベントに対応しており、クリックすれば指定したコンテキストが更新されます。

<span style="cursor: pointer" data-im="_@addorder:postalcode:f3:asc">▲</span>

@以降は、コロン(:)で4つのセクションに分かれます。

  • 第1セクション(例:addorder):この文字列
  • 第2セクション(例:postalcode):コンテキスト名
  • 第3セクション(例:f3):フィールド名。1つのみ
  • 第4セクション(例:asc):昇順ならasc、降順ならdesc

[IM]ContributorとSpecial Thanksの定義

ReadmeなどにINTER-MediatorのContributorとSpecial Thanksの項目があって、ともかく英語で書いてありますが、この区分をいちおうきちんと定義しておきます。もちろん、状況に応じてルールの増減はあります。

Contributor

  • コア部分に関連するソースコードをコミットあるいは提供した
  • レポジトリにまとまったサンプルを提供した
  • 開発進行において重要な決定を下し、それを遂行した
  • Webサイトを主体的に編集した。あるいはWebサイトにまとまったページを作成した
  • 貢献順。たとえば、コードの行数など。判断はとりあえず新居が行う

Special Thanks

  • バグレポートを具体的にコミュニティに公開した
  • イベントやサイト運用などのコミュニティ活動を支えた
  • 順番は姓のアルファベット順、Contributorになればこちらのリストからは落とす
  • 通常は氏名のみ、つまり組織名は書かないが、希望があれば書きますよ

こんなところかと思いますが、ご意見ありますでしょうか?

[IM]日付時刻関数の実装

INTER-Mediatorに日付時刻関数を実装しようとしています。というか、少し実装しました。とりあえずの目標はマニュアルに書いた関数のサポートです。クライアントサイドで動かすので、JavaScriptの仕組みとうまく連動させないといけません。しかし、そのままのスペックはちょっとどうかと思い、このような仕組みを考えました。

日付や時刻は原則として整数あるいは小数点数を取るにしても、連続した数値になります。この点は問題ないのですが、JavaScriptなら1ミリ秒が「1」、Excelだと1日が「1」というように、そのルールを知らないといけませんし、処理系によっては1秒が1の場合もあります。覚えておけばいいといえばいいのですが、決まっているから書かないという状況になると、つどつど調べないといけません。これは不便です。

この1単位の問題に加えて、データベースでは、DATE、TIME、DATETIMEという3つの型を併用します。もちろん、1ms=1にまとめるメリットはあるかと思いますが、あえて、INTER-Mediatorの中では、2つのスタンダードを作ることにしました。

  • 1日=1、つまり、DATE型フィールドに対応(date関数で生成=日付データ)
  • 1秒=1、細かい点はさておいて、TIME、DATETIMEに対応(datetime関数で生成=日時データ)

とします。日付の計算は日単位、日付時刻の計算は秒単位とするわけです。つまり、

  • date(‘2014-10-8’) – date(‘2014-10-6’) → 2
  • datetime(‘2014-10-8 09:00:00’) – datetime(‘2014-10-6 21:00:00’) → 129,600(36時間)

とすることで、データベースの型と、日付時刻関数の結果の対応付けを考えました。

ちなみに、JavaScriptではなぜgetMonth関数だけが0〜11になって実際の月の数値を得るには+1しないといけないのかとか、見方を変えればgetDate関数が0スタートになっているべきなのではなど、疑問は多々わきます。日付計算のしやすさなどの理由はあるかもしれませんが、ここがけっこうJavaScriptのはまりどころだったりします。

そこで、日付時刻の要素取得(つまり月や日を数値として取り出す)関数と、特定の要素に対する計算を行う関数を用意すれば、結果的には「見える通りの数値」として扱えるのではないかと考えました。たとえば、月を得る関数がmonth()だとして、11月なら11という数字が得られ、加えて3ヶ月後などの日付を求めるaddmonth(d, x)があれば用途は足りると考えたわけです。しかし、日付と日時というダブルスタンダードを持ち込むデメリットがここで発生します。整数化された数値を見て、どちらの型なのかがわかりません。結果的に、

  • 日付データの月を返す:monthd(x)
  • 日時データの月を返す:monthdt(x)
  • 日付データに指定した月数を加えた値を返す:addmonthd(x)
  • 日時データに指定した月数を加えた値を返す:addmonthdt(x)

という手法にならざるを得ないと考えました。関数が増えるだけ面倒もあるかもしれませんが、要素取得や計算の関数の末尾が「d」なのか「dt」なのかという点を注意すればいいので、さほどややこしいルールとも考えられません。

しかし、このままだと、month関数やaddmonth関数の存在が気になります。「なし」でもいいのですが、名前的にはもったいないです。考えたのは、なんらかの設定で、日付計算するか、日時計算するのかということを、定義ファイル上のオプション指定で決められるようにするという手法です。たとえば、デフォルとは日付としても、何かの指定をすれば日時になるという具合です。キーワードなどはまだ考えていません。つまりこういう関数があるということです。

  • 月を返す(単位は設定依存):month(x)
  • 指定した月数を加えた値を返す(単位は設定依存):addmonth(x)

 

関数の数は増えますが、把握できる内容ではないかと思います。

タイムゾーンもまともに考えれば頭が痛いですが、日時の解釈の問題と考え、そしてデータベース内ではタイムゾーンは一定にする、あるいはしたいぞと思うことが普通なので、日付あるいは日時データの書式化の問題かとも考えていますが、ここはまだじっくり考えていません。

以上のような方針で実装を考えていますが、どうでしょうか?ちなみに、今現在レポジトリにあるものは、date関数とdatetime関数は実装されています。

[IM]コンテキストの共有化とPusherの利用

INTER-Mediatorでは、「コンテキスト」は、データベースに対するデータの出入り口的なイメージのものであり、検索条件などでの意味づけされたデータソースを意味します。その「共有化」とは、同一エンティティが複数のページ上のオブジェクトに展開されているとき、1つのエンティティを変更すると、その結果が他のオブジェクトにも反映される仕組みと定義します。Ver.4.4までに、単一ページ内のコンテキストの共有化が実現しています。つまり、あるページ上に、同一フィールドとバインドした要素があるとすると、一方を変更すると、もう一方は自動的に更新します。この動作を実現するためのプログラミングは必要なく、バインドの設定(ターゲット指定の付与)だけで可能です。

Ver.4.5に向けて、コンテキストの共有化をマルチクライアントで実現する仕組みを開発しており、概ね動くところまできました。つまり、同一のページを複数のクライアントで参照しているとき、誰かがデータを変更すると、その結果は他のユーザのページにも反映されるという動作が典型的です。従って、1つのフィールドを単一の要素にバインドしている場合でも、マルチユーザつまり複数のブラウザで同一のエンティティをバインドしているという点で「共有化」されていると言えるわけです。

コンテキストの共有化を実現するために、ページファイル上でのターゲット指定や、定義ファイルでのコンテキスト定義以外に何をしなければならないかをこの文書にまとめておきます。単一ページ内のコンテキストの共有化は特別な仕掛けは不要です。しかしながら、マルチクライアントでのコンテキストの共有化では、WebRTCを利用したPusherというサービスを利用することにしました。試用程度なら無償ですが、実運用には有償となってしまうものの、開発の効率化のために利用することにしました。

Pusherアカウントの取得とアプリケーション登録

Pusherのサイトでアカウントを取得します。Pusherでは「App」という単位で管理ができるので、たとえばINTER-Mediatorで作る1つのソリューションを、1つのPusherのAppとして登録するという方法もありますし、複数のソリューションで共有してもいいかもしれません。いずれにしても、アカウントを作成し、New Appというボタンなどで新たに1つのAppを作成します。ページ上に表示されるapp_id、key、secretの3つの情報がこの後に必要となります。

Pusherのサーバプログラムのインストール

PusherのサーバモジュールはPHP版を利用します。こちらのレポジトリをダウンロードし、そこから得られるlibディレクトリにあるPusher.phpという1つのファイルだけをサーバにインストールします。他は使用しません。ファイルはPHPの設定ファイル(php.iniが代表的)で、include_pathの設定で参照できるディレクトリにあればかまいません。もっとも安直な方法は、INTER-Mediatorフォルダに入れて、サーバにコピーしておくことです。もし、設定が以下のようなものであれば、例えば/usr/lib/phpディレクトリにPusher.phpをコピーしておけば良いでしょう。

include_path = ".:/usr/lib/php/pear:/usr/lib/php"

ページファイルへの追加

Pusherのクライアントソフトウエアを、ページファイルで組み込む必要があります。たとえば、以下のように、ヘッダ部で定義ファイル(include_MySQL.php)の読み込みの前に読み込みます。この方法だと、Pusherのサイトから直接取り出すので、ファイルを自分のサーバにコピーする必要はありません。ソースはこの通りコピペで大丈夫ですが、Pusherのバージョンが変わった時などはそれに合わせてください。

<html>
<head>
    :
    <script src="http://js.pusher.com/2.2/pusher.min.js" type="text/javascript"></script>
    <script type="text/javascript" src="include_MySQL.php"></script>
</head>

定義ファイルあるいはparams.phpへの追加

Pusherで定義したAppに関する指定は、定義ファイルのオプション部あるいはparams.phpで指定をします。原則的にはどちらか一方で定義をしてください。両方指定すると、定義ファイルの方が優先されます。定義ファイルでは、pusherをキーにした配列を定義し、さらにPusherのAppで示された3つの値を配列の各要素の値とします。以下は、定義ファイルでの定義例です。

IM_Entry(
    array( 
              /* コンテキストの定義 */ 
       ),
    array(
        :
        'pusher' => array(
            'app_id' => '1234',
            'key' => '9876543210',
            'secret' => '9876543210',
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

params.phpファイルに記述するときには、以下のように、$pusherParameters変数に同様な配列として定義をします。

$pusherParameters = array(
 'app_id' => '1234',
 'key' => '9876543210',
 'secret' => '9876543210',
);

上記のいずれかがあると、マルチクライアントのコンテキストの共有化がオンになります。定義ファイルあるいはparams.phpの指定の有無だけで、共有化の利用/不使用が決まります。指定がないと一切何も行いません。指定があるのに、Pusherのサーバあるいはクライアントソフトウエアが利用できない状態になると、なんらかのエラーが発生します。

現状での制約

レコードの追加においては、そのコンテキストの検索条件を加味して、検索条件に合わないレコードの追加は行いません。しかしながら、別のクライアントで作成したレコードが当初はコンテキストに合わないものの、フィールドの値を変更してコンテキストの検索条件に合うようになっても、現状ではそのレコードが見えるようにはなりません。

さらに、コンテキストのソート条件は現状では加味されておらず、一連の表示リストのサイトに常に追加されます。

[IM]コンテキストに対応したモデルの実装

以前に「コンテキストの内部実装」という記事を書きましたが、コンテキストというか、正確には、コンテキストに対応したモデルの実装について、書いておきます。内部のソースを読んでいない人にはなんのことかさっぱり分からないと思います。

この仕組みを実装しているのは、INTER-Mediator-Context.jsファイルにあるIMLibContextクラスです。そこにあるいくつかのプロパティのデータ構造は、コードを読む場合や改造では絶対に必要な知識なので、一度まとめておこうと思っていました。

以下、「recKey」は、レコードを特定する文字列で、「キーフィールド=その値」という文字列です。たとえば、”id=3″のような文字列に相当します。そして、以下の「key」はすべてフィールド名の文字列が入ります。「portal」はFileMaker Serverを使う場合に、定義ファイルで’portal’=>trueにしている場合の、関連レコードに対応する外部キーの値です。FMS以外ではportal引数は渡されません。以下、portalが有る場合とない場合のデータ形式を記述します。

まず、IMLibContextクラスのプロパティstoreには、実際のフィールドのデータを記録します。store自体がオブジェクトで、以下のようにアクセスすることで、フィールドの値Valueにアクセス可能です。

this.store[recKey][key]→Value
this.store[recKey][key][portal]→Value

次に、プロパティbindingです。これは、データベースのフィールドが、どの要素に結びついているのかを記録するオブジェクトです。データベースから、バインドしているノードを求めるために用意しています。右辺は、オブジェクトの配列になっています。IdValueはバインドしたノードのid属性値です。Targetは、そこでのターゲット指定です。なお、Target指定は “” の場合もありますが、そのときにはTargetの値は “_im_no_target” という文字列にしています。

this.binding[recKey][field]→[{id: IdValue, target: Target}, ...]
this.binding[recKey][field][portal]→[{id: IdValue, target:Target}, ...]

次はcontextInfoプロパティです。こちらは、バインドしたノードから、データベースのどのレコードの、どのフィールドなのかを求めるためのものです。要するに逆テーブルです。thisと、.contextは同じものですが、thisが使えない状況を考慮してプロパティを作ってあります。

this.contextInfo[nodeId][target].context→コンテキスト自身
this.contextInfo[nodeId][target].record→recKeyの値
this.contextInfo[nodeId][target].field→keyの値
this.contextInfo[nodeId][target].portal→portalの値

これらの3つが主要なデータ構造となります。

[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) { }
}