Chapter 3
データベースの更新を行うページ
この章は、INTER-Mediator Ver.11をもとに記載しました。
前の章ではデータベースの内容をページに表示する方法を説明しました。続いて、ページ上で入力したり、あるいは新たなレコードを作成する方法について説明をします。バインドの仕組みを利用することで、手続き的なプログラムを記述しなくても、ページ上のデータを修正した結果がデータベースに反映されます。
3-1更新可能なテキストフィールド
Webページ上にテキストフィールドを配置すると、通常はそのままで更新可能な状態になります。もちろん、データベースのアクセス権等の動作上の状況を満たしている必要がありますが、INTER-Mediaotrはテキストフィールド上のデータを修正したときに、そのデータをデータベースに書き戻すことができるようになっています。
更新が可能なタグ要素
INTER-Mediatorで作成したWebページでは、INPUT、TEXTAREA、SELECTの各タグで記述されたリンクノード(data-im属性があるタグ要素)については、更新すなわちそこに見えているデータを変更することによってデータベースへ変更結果を反映させることが可能です。それぞれのタグ要素は、INTER-Mediatorによって、自動的にid属性が設定されます。元からid属性がある場合にはそれを利用しますが、重複の可能性があれば書き換えるといった動作になります。そして、自動設定されたid属性をもとにして、それぞれの要素が、どのコンテキストの、どのレコードの、どのフィールドから得られたものなのかを内部で記録します。
タグ要素に表示された値が変更されたときに、逆にそれらの情報を利用して、データベースを更新します。そのため、元のテーブルのどのレコードなのかを特定するために、テーブルのキーフィールドの指定が必要です。つまり、定義ファイルのコンテキストの中で、keyに対応した値が正しく指定されていなければなりません。INTER-Mediatorではクライアントサイドで、例えばkeyの値が「id」だとしたら、「id=23」のような形式で内部的ではレコードを特定する情報を記録します。ここでの「23」はidフィールドの値の例です。つまり、検索条件式のような記録をするということです。
定義ファイルのコンテキストでは、tableキーによる値が、更新時のエンティティとなります。tableキーを指定しないと、nameキーが更新するエンティティになります。テーブルに対して、読出しと書込みをそのまま行うなら、nameキーのみを指定することでも読出しと更新はできます。しかしながら、ビューに対しては書込みができない場合もあります。そのため、読み込み時と書込み時で異なるエンティティを指定できるようにしました。また、同一のテーブルでも用途によって異なるnameキーの値を付けるという考え方もあるので、読出しはview、書込みはtableキーで指定したエンティティに対して処理を行うというルールで構築します。
テキストフィールドやテキストエリアの更新の動作
valueChangeイベントを利用して更新処理が行われます。例えばチェックボックスならクリックしたときに更新が行われます。一方、テキストフィールドやテキストエリアは、Tabキー等でフォーカスが移動したときに、データベースへの更新が行われます。これらのテキスト表示コンポーネントに関しては、自動的に更新する処理も組み込まれています。最後に入力をしてから、約5秒後に、自動的にデータベースへの更新が行われます。つまり、フォーカスを移動するか5秒程度待つことで、データベースには反映されるという動作になります。これらの動作は、「何もしなくても自動的に保存される」という動作を目指したものです。
テキストフィールドとテキストエリアに関しては、INTER-Mediator独自のアンドゥ機能が実装されています。通常は、Command+ZあるいはCtrl+Z等で、ブラウザが提供するアンドゥが稼働しますが、リンクノードについては、フィールド内にフォーカスがある状態でCtrl+Shift+Zを押すことで、そのフィールドの値はページを表示した直後の値に戻ります。フィールドが書き換えられている場合には、データベース側の変更も行います。
楽観的ロックの実装
データベースの更新処理においては、複数のユーザーが同時に参照して書き込むような場合の競合の問題が発生します。通常、データの更新は、読み出した結果を、時間が経過して変更されるという流れになります。2人のユーザーAとBがそれぞれ、同一のテーブルの同一のフィールドを変更しようとした場合、Aの読み込み→Bの読み込み→Bの変更→Aの変更といった順序で処理がなされたとします。すると、Bが変更した結果は、最後のAの変更によって上書きされてしまいます。もちろん、これで問題がないという用途もあるかもしれませんが、一般には「先に保存した方が消えて無くなる」のは問題のひとつです。
FileMakerを通常のアプリケーションとして利用した場合、Aが読み込み編集中の処理になった段階で、Bは編集作業に入ることができなくなります。こうした動作を「悲観的ロック」と呼びます。つまり、誰か1人だけがひとつの対象(この場合はフィールド)を編集可能にするという制御をするという手法です。この方法は有効ですが、インターネットを経由した接続の場合、突然の切断の可能性はそこそこ高く、ステートレスなHTTPの場合、切断の検出は一概にはうまく行きません。誰かが編集作業を始めた後、ネットワークが切断してしまったらデータベースはロックされたままになってしまい、以降の共同作業ができなくなってしまうこともあります。そこで、Webアプリケーションでは「楽観的ロック」という手法を実装するのが一般的です。楽観的ロックは、「読み出したデータに変更がなければ更新する」というルールです。
AとBが同時に同じレコードを編集しようとしているとします。最初は「data」だったデータを、その後Bによって「changeb」に変更しようとしたとします。このとき、フィールドの値はまだ「data」なので、Bの更新作業は成功します。しかしながら、次にAが更新しようとするときに、データベース側の値を得ると「changeb」に変わっているので、ユーザーA側では元々のデータの「data」と、現在の値の「changeb」を比べて違うことを手掛かりにして、「他のユーザーが変更した」ことが検知できるのです。ここで、INTER-Mediatorは「データが変わっており本当に変更していいのか?」と問い合わせるので、ユーザーAは、そこでのボタン選択で、キャンセルも上書きもできるようにしています。なお、もちろん、この場合に、ユーザーBが、同じ値「data」に変更したとしても、それはユーザーAは検知できませんが、これは変更していないのも同然とみなすこともできます。厳密な意味での排他制御ではないとも言えますが、一般的なアプリケーションではなんの排他制御をしないよりも遥かに安全であり、実用的な意味では十分なことが多いと思われます。
INTER-Mediatorでは、更新処理の前に、再度データベースへのクエリーを発行して、対象とするレコードのフィールドの値に変更がないかどうかを確認します。この処理は自動的に行われて、他のユーザーが変更している形跡があれば、ダイアログボックスでそのまま上書きするか、更新をキャンセルするかをたずねます。
なお、この楽観的ロックを行わないように、確認なく更新をしたい場合には、INTERMediatorクラスのignoreOptimisticLockingプロパティを設定して、ページを構築します。例えば、ページ合成を行う前に呼び出されるメソッドをリスト3-1-1のように記述します。INTERMediatorOnPage.doBeforeConstructについては、『8-5 ブラウザーを判断するページ』の最後に解説があります。
INTERMediatorOnPage.doBeforeConstruct = function () {
INTERMediator.ignoreOptimisticLocking = true;
}
演習Webページのテキストフィールドで更新する
INTER-Mediatorの演習環境を利用して、データの書き戻しを伴うページの作成を行います。新たなページを作成して、更新の動作を検証します。
テキストフィールドとテキストエリアのあるページ
table、viewともに「person」と入力します。personは定義されているテーブルです。
table、viewともに「person_layout」と入力します。FileMakerではテーブル名やTO名ではなく、レイアウト名を指定します。
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
<!DOCTYPE html>
<!--
/*
* INTER-Mediator Ver.@@@@2@@@@ Released @@@@1@@@@
*
* Copyright (c) 2010-2015 INTER-Mediator Directive Committee, All rights reserved.
*
* This project started at the end of 2009 by Masayuki Nii msyk@msyk.net.
* INTER-Mediator is supplied under MIT License.
*/ -->
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def02.php"></script>
</head>
<body>
<table border="1">
<thead>
<tr>
<th>name</th><th>mail</th><th>category</th><th>ckecking</th>
<th>location</th><th>memo</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text" data-im="person@name" /></td>
<td><input type="text" data-im="person@mail" /></td>
<td></td>
<td></td>
<td></td>
<td><textarea data-im="person@memo"></textarea></td>
</tr>
</tbody>
</table>
</body>
</html>
ページ上から更新できることを確認する
楽観的ロックの動作を確認する
テキストエリアの動作を確認する
mysql -u web -h 127.0.0.1 -P 13306 --password=password test_db
select name,memo from person;
$ mysql -u web -p -r -h 127.0.0.1 -P 13306 --password=password -D test_db -e 'select memo from person where id=1'|xxd
mysql: [Warning] Using a password on the command line interface can be insecure.
00000000: 6d65 6d6f 0ae6 9c80 e588 9de3 81ae e8a1 memo............
00000010: 8c0a e694 b9e8 a18c e381 97e3 819f e382 ................
00000020: 880a ..
演習のまとめ
- テキストフィールドやテキストエリアにdata-im属性を記述してリンクノードとすれば、データの表示だけでなくデータベースの更新も行えます。
- コンテキストには、keyキーによるキーフィールドの指定が必要になります。
- データの更新時には楽観的ロックが自動的に稼動します。
時刻の扱いについて
単一の時間帯で使う場合にはあまり問題にならないかもしれませんが、時刻の問題は非常に複雑です。つまり、時差をどのように扱うのかということが大きな問題になる場合もあります。最近は日本だけで使うアプリケーションでも、サーバの設定がUTCであったりすることもあり、その時間差ということが問題になるかもしれません。例えば、予定表のアプリケーションで、11:00amに開始となっていたとします。日本ではそのまま見えることが期待する一方、イギリスでは2:00amと表示されるべきなのか、それとも11:00amと表示させるべきなのかはアプリケーションの要求次第ということもあります。そのため、時差が発生した、あるいは発生しなかったという場合の対処については知っておく必要があります。
時間に関する設定は、params.phpにあります。$defaultTimezoneは、サーバ側のタイムゾーンを指定します。日本だと 'Asia/Tokyo'、世界標準時だと'UTC'を設定します。右辺の文字列は、PHPのマニュアルの「サポートされるタイムゾーンのリスト」のページに記載されています。サーバOSのタイムゾーンがUTCの場合は、この値を'UTC'にすることがひとつの解決策です。つまり、この設定をサーバの時間帯に合わせるということです。
$defaultTimezone = 'Asia/Tokyo';
$follwingTimezones = true; // 既定値はtrue
$follwingTimezonesをオンにすると、タイムゾーンとローカル時間を考慮した次のような動作になります。まず、データベースから得れた時刻を含むデータは、$defaultTimezoneの値を手がかりにUTCの時刻あるいは日付時刻に変換して、クライアントに送付します。クライアントは、サーバから送り込まれる時刻や日付時刻はUTCであると仮定します。そして、それを表示するとき、data-im-format属性を指定すると、ブラウザのローカル時間に合わせて変換した日付時刻を画面上に表示します。書き込む時は逆の処理を行います。こうして、サーバの時間帯と、ブラウザの時間帯がそれぞれ考慮した日付時刻表示となります。もし、inputタグで、type属性に「datetime-local」を指定している場合には、data-im-format属性の値は「datetimelocal」としてください。この変数は、Ver.11の途中から、trueにしています。falseにして生のままの動きにするとかえって混乱するのではないかと思われ、trueを既定値にしました。
まとめて更新処理を行う
最初にまず説明したいことは、通常はまとめて処理をする必要はないと考えています。テキストフィールドを更新するたびにデータベースアクセスするのは、待たされる時間が長くなるのではないかという心配はあるかもしれません。しかしながら、単一レコードの更新なので、よほど低速通信中でない限りはそんなに遅くはなりません。また、更新のデータベース処理は非同期処理をしているので、一般的なWebページのような「待ち」にはならないはずです。
しかしながら、どうしても、まとめて保存をしたい方は、まとめて保存という仕組みを限定的ながらサポートをしています。まず、この仕組みを利用する場合は、ページネーションのコントロールを表示します。コンテキストの定義の中でpagingキーの値をtrueにします。ページファイルには、id属性が「IM_NAVIGATOR」である、ページネーションのコントロールを生成する場所を指定するタグを作っておきます。そして、IM_Entryの2つ目の引数の配列内に、transactionキーで値が「none」の要素を追加します。定義ファイルエディターでこの項目を設定する場合は、ページ最上部の「Show All」ボタンをクリックしてください。リスト3-1-3は定義ファイルでの設定場所を要約したものです。
<?php
//todo ## Set the valid path to the file 'INTER-Mediator.php'
require_once('INTER-Mediator/INTER-Mediator.php');
IM_Entry(array (
array (
'name' => 'person',
'key' => 'id',
'view' => 'person',
'table' => 'person',
'paging' => true,
),
),
array (
'transaction' => 'none',
),
array (
'db-class' => 'PDO', ....
),
false);
この設定により、ページネーションのコントロールに、「保存」ボタンが表示されます。修正のたびにデータベースへの保存は行わず、ボタンを押したときだけに保存が行われます。なお、この方法を使うとまとめて保存されますが、保存の処理は最適化されていません。単純に1フィールドずつ更新処理のためのサーバーアクセスを実行し、一連の処理をまとめてやるだけのものです。変更フィールドが大量になると、それなりに時間がかかるものと思われます。
更新を行わないようにする
ここまでのところで、INTER-Mediatorではテキストフィールドに表示した値を変更すると自動的にデータベース更新がかかるバインドの処理が利用できるということが大きな特徴であることを説明してきました。一方で、データベースの内容は表示させたいものの、更新処理をしたくない場合もあるでしょう。その場合、タグ内に「data-im-control="unbind"」と記述することで、データベースへの更新処理をしなくなります。
このセクションのまとめ
テキストフィールド、テキストエリアについては、リンクノードとして定義していれば、フィールドのデータを表示するだけでなく、ユーザーの編集作業により、書き換えたデータを元のレコードのフィールドに更新をかけます。このとき、楽観的ロックの仕組みも利用できるため、マルチユーザー動作の基本的な処理も、ページ上のタグ要素に単にdata-im属性を記述するだけで実現できています。テキストエリアもサポートし、改行の入力にも対応しています。
3-2チェックボックス、ラジオボタン、ポップアップメニュー
テキスト以外のフォームで使うコントロールについて解説をします。特に、チェックボックスとポップアップメニューはよく利用されるものです。データベースの値とこれらのコントロールの状態を対応づけることや、更新した状態、選択肢を用意する方法などを解説します。
チェックボックス
HTMLのルールでは、チェックボックスは、type属性が「checkbox」のINPUTタグ要素です。INTER-Mediatorでは、そのタグに、data-im属性でコンテキストとフィールドを指定してバインドすることで、データベースの内容に応じたチェックボックスを作成することができます。INTER-Mediatorで利用する場合には、value属性を指定することが必須です。チェックボックスをオンにしたら、value属性に指定した値をフィールドに書き込みます。チェックボックスをオフにしたら、そのフィールドの値をSQLデータベースの場合はNULL、FileMakerの場合は""にします。例えば、チェックボックスの名前が「チェック欄」などとなっていても、value属性値を「1」にしておけば、フィールドには1かNULLのどちらかしか設定されません。したがって、チェックボックス名とは関係なく数値フィールドにすることもできます。もちろん、チェックボックスの名前と同じvalue属性を設定してもかまいませんが、その場合は、フィールドは文字列型にする必要が出てくるでしょう。チェックボックスのフィールドを表示するときには、フィールドのデータとvalue属性が一致していれば、チェックが入るようになります。そのため、value属性が「1」で、データベースのフィールドにあるデータが「2」の場合にはチェックは入りません。
ラジオボタン
HTMLのルールでは、ラジオボタンは、type属性が「radio」のINPUTタグ要素です。一般にはラジオボタンがひとつということはあり得ないので、複数のボタンが並び、どれかひとつをオンにすると、他はオフになるのが一般的です。そうした動作をさせるために、ラジオボタンのグループをまとめる属性としてname属性があり、同一のname属性のラジオボタンがセットとして動作する仕組みになっています。
INTER-Mediatorでは、そのタグに、data-im属性でコンテキストとフィールドを指定してバインドすることで、チェックボックスと同様にデータベースの内容に応じたラジオボタンを作成することができます。同一のフィールドにバインドしたラジオボタンには、自動的に同一のname属性が設定されるので、グループとして動作します。複数のレコードを含む一覧の中にラジオボタンがあれば、レコードごとに、同一フィールドにバインドした複数のラジオボタンに同一のname属性がつけられます。そして、レコードごとに異なるname属性値を持ちますので、レコードごとに選択されるボタンは分離されます。
その他の動作は、チェックボックスと同様です。ラジオボタンもvalue属性を設定しますが、選択したラジオボタンのvalue属性値がフィールドに書き込まれます。一連のセットのラジオボタンのvalue属性値を異なるものにしておくことで、フィールドのデータに応じた値のボタンが選択されるようになります。ラジオボタンも、実際にデータベースに保存されるデータは、value属性の値になるので、もちろん、文字列でもいいのですが、数値に対応付けておいて数値型のフィールドとバインドしてもいいでしょう。
ポップアップメニューおよびリスト
HTMLのルールでは、SELECTタグによってポップアップメニューやリストを構成でき、選択肢はOPTIONタグを利用して記述をします。INTER-Mediatorの場合は、SELECTタグにdata-im属性を指定して、指定したコンテキストのフィールドとのバインドを行います。その結果、フィールドの値に応じてポップアップメニューが選択され、ユーザーがメニュー選択すると、その項目の値にフィールドの値が書き換わります。
フィールドに設定する値は、OPTIONタグのvalue属性に記述します。OPTIONタグのテキストが実際に見える文字列になりますが、value属性はそれと同じでもいいですし、整数と対応付けてもかまいません。したがって、バインドするフィールドはどんなデータを記録するのかに応じて、文字列型かあるいは数値型を選択することになります。
ポップアップメニューは未選択の状態がないため、データがないときのポップアップメニューの選択項目の処理として、状況に応じた処理が必要になりますが、INTER-MediatorではデータがないNULLの状態のときには何も表示しないという動作を行います。なお、後からフィールドのクリアをするような場合には、OPTIONタグでクリアに相当する項目を用意しなければなりません。
選択肢自体をテーブルから取り出すようなことももちろん可能です。これについては、『4-3 複数のコンテキストとリレーションシップ』にあるリレーションシップについての知識が必要ですので、そちらで説明をします。
リストの場合、複数の項目を選択するリストも動作上は可能ですが、その場合のバインド処理については正しく行われないので、リストの場合は単独項目の選択でご利用ください。
演習テキスト以外のコントロールを設定する
チェックボックス、ラジオボタン、ポップアップメニューを実際にページ上に表示して、フィールドのデータを更新できるところを確認してみます。
チェックボックスをページに用意する
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def02.php"></script>
</head>
<body>
<table border="1">
<thead>
<tr>
<th>name</th><th>mail</th><th>category</th><th>ckecking</th>
<th>location</th><th>memo</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text" data-im="person@name" /></td>
<td><input type="text" data-im="person@mail" /></td>
<td>
<select data-im="person@category">
<option value="1">Family</option>
<option value="2">Class Mate</option>
<option value="3">Collegue</option>
</select>
</td>
<td>
<input type="checkbox" data-im="person@checking" value="1"/>
</td>
<td>
<input type="radio" data-im="person@location" value="1"/>Domestic
<input type="radio" data-im="person@location" value="2"/>International
<input type="radio" data-im="person@location" value="3"/>Neighbor
<input type="radio" data-im="person@location" value="4"/>Space
</td>
<td><textarea data-im="person@memo"></textarea></td>
</tr>
</tbody>
</table>
</body>
</html>
フィールドに設定される値を確認する
:
<td><textarea data-im="person@memo"></textarea></td>
</tr>
</tbody>
</table>
<table border="1">
<thead>
<tr>
<th>name</th><th>mail</th><th>category</th><th>ckecking</th>
<th>location</th><th>memo</th>
</tr>
</thead>
<tbody>
<tr>
<td data-im="personlist@name"></td>
<td data-im="personlist@mail"></td>
<td data-im="personlist@category"></td>
<td data-im="personlist@checking"></td>
<td data-im="personlist@location"></td>
<td data-im="personlist@memo"></td>
</tr>
</tbody>
</table>
</body>
</html>
演習のまとめ
- チェックボックスとラジオボタンは、data-im属性とvalue属性を指定してタグを定義することで、バインドしつつ、値の更新が可能です。
- ポップアップメニューはSELECTタグにdata-im属性を指定してバインドし、選択肢を記述するOPTIONタグのvalue属性でフィールドに保存する値と対応付けます。
- 同一ページ上で、同じレコードの同じフィールドの値をバインドによって表示されていれば、一方を変更するともう一方も自動的に変更されます。
このセクションのまとめ
このセクションでは、チェックボックス、ラジオボタン、ポップアップメニューの構築方法を説明しました。INPUT、SELECT、OPTIONタグを利用し、いずれもdata-im属性を指定してバインドすることで、データベースの値とユーザーインターフェースが連動します。value属性の値が、実際にフィールドに記録される値になります。
3-3レコードの追加・削除・複製
ここまで、データの表示と更新を見てきましたが、残る処理は新規作成と削除です。これらを行うために、定義ファイルで設定を加えます。すると、その定義に関連した適切な場所に、削除と挿入のためのボタンが自動的に作られます。ただし、新規にレコードを作成するというのは単に空欄を作ればいいだけではすまないこともあります。その場合は、JavaScriptによるプログラムと併用することを検討する必要があります。Ver.5.2以降でサポートする「複製」についても説明しましょう。
挿入と削除が可能なコンテキスト
定義ファイルに指定するコンテキストでは、tableキーで指定したエンティティに対して、レコードの挿入や削除の処理が実施されます。tableキーが記述されていない場合には、nameキーの値をエンティティとして利用します。同時に、keyキーにより、そのエンティティでの主キーあるいはそれに準じるフィールド名を指定する必要があります。レコード削除の場合は、どのレコードを削除するのかを特定するために、主キーのフィールドの指定は必須です。一方、レコードの挿入だけなら特に主キーの扱いは不要ではありますが、INTER-Mediatorでは、新たにレコードを作った時にそのレコードの主キー値を得て、そのレコードを表示します。1レコードだけを表示させるように設定してあれば、編集可能な状態で開くことができます。つまり、新たに挿入されたレコードの主キー値を得るために、キーフィールド名をkeyキーに指定する必要があります。
ここで、PostgreSQLについては、連番を主キーフィールドに設定する仕組みを「シーケンス」と呼ばれるオブジェクトを使って実現しています。テーブル自体に自動連番の機能はなく、連動して稼働するシーケンスオブジェクトが「次の値」を返すという仕様になっています。したがって、レコード挿入時に新たに作られたレコードにおけるkeyキーで指定したフィールドの値は、シーケンスの値から取り出す必要があります。そのため、PostgreSQLについては、シーケンスオブジェクト名を明示的にコンテキスト内で指定できるようになっています。定義ファイルエディターでも、「Sequence」のラベルで記入欄があります。以下、PostgreSQLの場合のみ、コンテキストへの追加の指定が必要になります。手法としては、自分で定義したシーケンスオブジェクトを使う場合と、自動で用意するシーケンスオブジェクトを使用する場合があります。なお、SERIAL型を利用する場合は、シーケンスオブジェクトの認識を自動的に行うため、sequenceキーの指定は不要です。
挿入と削除のコントロール
定義ファイルに記述する手法として、コンテキスト内のrepeat-controlキーに、表3-3-1のような値を与える方法があります。これにより、適当な場所を選んで、「追加」ボタンや「削除」ボタン、「複製」ボタンを追加することができます。複数の値を空白で区切って指定することもできるので、それぞれのボタンをいずれも表示することができます。
repeat-controlキーの値 | 動作 |
---|---|
insert | 「追加」ボタンをリピーターの後に追加する |
insert-top | 「追加」ボタンをリピーターの前に追加する |
delete | 「削除」ボタンを追加する |
confirm-insert | 「挿入」ボタンをリピーターの後に追加する。ボタンを押した後、挿入していいかどうかを確認する |
confirm-insert-top | 「挿入」ボタンをリピーターの前に追加する。ボタンを押した後、挿入していいかどうかを確認する |
confirm-delete | 「削除」ボタンを追加する。ボタンを押した後、削除していいかどうかを確認する |
copy | 「複製」ボタンを追加する |
confirm-copy | 「複製」ボタンを追加する。ボタンを押した後、複製していいかどうかを確認する |
copy-CCCC | 「複製」ボタンを追加する。複製時にリレーションシップで関連付けられたコンテキストCCCCについても複製を行う |
confirm-copy-CCCC | 「複製」ボタンを追加する。複製時にリレーションシップで関連付けられたコンテキストCCCCについても複製を行う。ボタンを押した後、複製していいかどうかを確認する |
「削除」ボタンは、レコードごとに付与されます。言い換えれば、ひとつのリピーターに対して、ひとつのボタンが付与されます。ボタンは、リピーターの末尾に付与されます。もし、リピーターがTRタグ要素であるのなら、その内部のTDタグが並んでいるはずですが、最後にひとつ、中身が空のTDタグ要素(つまり「<td></td>」)を追加しておけば、そのセルにボタンが挿入されます。「削除」ボタンは、IM_Button_Deleteというclass属性が付与されるので、ボタンのスタイルのカスタマイズは、このクラス名をセレクタとして使用したCSSの定義を記述します。
「追加」ボタンは、コンテキストに対してひとつ付与されます。こちらも、言い換えれば、ひとつのエンクロージャーに対して、新たなレコードを作成するボタンが付与されます。挿入ボタンの挿入は位置は、エンクロジャーがTBODYの場合は、TFOOT領域に新たにTRタグ要素を作り、その中の最初のTDタグ要素の中にボタンを生成します。キーワードに「-top」が含まれていれば逆にTHEAD領域に新たなTRタグ要素を作り、その中のTD要素の中にボタンを生成します。テーブルではないエンクロージャーの場合には、最後のリピーターの後に追加します。「-top」があれば、リピーターの前に追加します。「追加」ボタンは、IM_Button_Insertというclass属性が付与されるので、ボタンのスタイルのカスタマイズは、このクラス名をセレクタとして使用したCSSの定義を記述します。
「複製」ボタンは、「削除」ボタンと同様にレコードごとに付与され、配置の方法は「削除」ボタンと同様です。クリックすると、対応するレコードの複製が行われ、それぞれのフィールドの値が複製された新しいレコードが作成されます。ただし、定義ファイルのコンテキスト定義にあるkeyキーで指定したフィールド、および、データベースでのテーブル定義において既定値が設定されているフィールドについてはコピーを行いません。keyキーで指定したフィールドは自動的に連番を設定することを想定して、複製対象のフィールドから除外しています。「削除」ボタンは、IM_Button_Copyというclass属性が付与されるので、ボタンのスタイルのカスタマイズは、このクラス名をセレクタとして使用したCSSの定義を記述します。「copy-」に続いて別のコンテキストのnameキーで指定した名前を指定した場合を考えてみましょう。現在のコンテキストとの間でリレーションシップが定義されていれば、関連するレコードのコピーも同時に行います。これについては、『4-3 複数のコンテキストとリレーションシップ』の演習で説明します。
これらのボタン名称は、ブラウザーの言語に応じて、「削除」や「Delete」と変化しますが、独自に名前をつけたい場合には、定義ファイルのコンテキスト定義において、"button-names"キーに指定する連想配列で、そのコンテキストに登場するボタン名のカスタマイズが可能です。リスト3-3-1や図3-3-1のように、コンテキストの定義に設定を行えば、例えば削除のためのボタンの名称は「レコード削除」ボタンとなります。定義ファイルでは、ページ冒頭のShow Allボタンをクリックして、設定項目を表示して設定してください。
IM_Entry(array (
array (
'name' => 'person',
'key' => 'id',
'view' => 'person',
'table' => 'person',
'repeat-control' => 'confirm-insert confirm-delete copy',
'records' => 10,
'maxrecords' => 100,
'button-names' => array (
'insert' => 'レコード追加',
'delete' => 'レコード削除',
'copy' => 'レコード複製',
),
),
演習挿入と削除のコントロールを追加する
ページネーションに「追加」ボタンを表示する
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def02.php"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table border="1">
<thead>
<tr>
<th>name</th><th>mail</th><th>category</th><th>ckecking</th>
<th>location</th><th>memo</th>
</tr>
</thead>
:
1レコードごとに表示する場合のレコード追加
レコードの「複製」ボタンを設置する
レコードの複製の機能は、Ver.5.2より搭載していますが、MySQLでの搭載のみです。FileMaker等は将来の実装予定項目となっています。
<body>
<!-- <div id="IM_NAVIGATOR"></div> -->
<table border="1">
<body>
<div id="IM_NAVIGATOR"></div>
<table border="1">
演習のまとめ
- コンテキストにrepeat-controlキーに対する値を記述すれば、レコードの削除や新規レコード作成、レコード複製のためのボタンが自動的に挿入されます。
- 単に削除やレコード追加するだけでなく、確認をしたのちに削除や追加を行うユーザーインターフェースも指定できます。
- ページネーションがあれば、レコード追加ボタンはページネーション上に追加されます。
- 1レコードずつ表示しているコンテキストの場合、レコード追加したり複製をすると、新しいレコードだけが検索された状態でページは表示されます。
新規レコードのフィールドの初期値
レコードを新規に作成したとき、データベースエンジン側に設定された既定値はもちろん機能して、指定に従ってフィールドに値が設定されます。加えて、定義ファイルに指定するコンテキストの中にdefault-valuesキーによる値を追加することで、コンテキスト単位でレコードを作成したときにフィールドに設定される値を指定できます。fieldキーとvalueキーを持つ連想配列のさらに配列にしたものが、default-valuesキーの値として設定可能です。
リスト3-3-2は定義ファイルをPHPのコードで見た場合での設定例です。同一の設定を定義ファイルで行う場合には、図3-3-2になります。定義ファイルエディターのページの冒頭にあるShow Allボタンをクリックすることで、Default Valuesの設定項目が表示されるので、その下にある「追加」ボタンをクリックして項目を追加し、テキストフィールドの値を書き直します。
コンテキストがこの状態で新しいレコードを作成すると、nameフィールドには「Jon Doe」、mailフィールドには「msyk@msyk.net」という値がレコード作成時には自動的に設定されます。もし、コンテキストにqueryキーによる検索条件が設定されているとき、場合によっては新規レコードを作成しても、条件に合わないで、コンテキストとして取り出されない可能性があります。その場合は、default-valuesの指定により、新規レコードでもqueryの条件に合うようにレコードを作るということで対処できるでしょう。
IM_Entry(array (
array (
'name' => 'person',
'key' => 'id',
'view' => 'person',
'table' => 'person',
'repeat-control' => 'confirm-insert confirm-delete',
'paging' => true,
'records' => 10,
'maxrecords' => 100,
'default-values' => array (
array ( 'field' => 'name', 'value' => 'Jon Doe', ),
array ( 'field' => 'mail', 'value' => 'msyk@msyk.net', ),
),
),
さらに、フィールドの既定値をJavaScriptのプログラムで動的に設定する方法もあります。INTERMediatorオブジェクトにあるadditionalFieldValueOnNewRecordプロパティを利用します。このプロパティは、コンテキスト名をプロパティ名としたオブジェクトを参照します。例えば、ページファイルのヘッダー部にあるSCRIPTタグ要素のプログラムをリスト3-3-3のように記述したとします。ここでは、historyというコンテキストで新しいレコードを作成すると、startdateフィールドに今日の日付を初期値として設定します。additionalFieldValueOnNewRecordに続いて、コンテキスト名を [ ] で指定します。右辺の値は、fieldキーとvalueキーを持つオブジェクトです。fieldキーにはフィールド名を指定し、valueキーには値を指定します。この場合は、正確には、ページを開いた日付になります。なお、FileMakerの場合は、日付の文字列を「月/日/年」形式にしなければなりません。
INTERMediator.additionalFieldValueOnNewRecord
["history"] = {
field: "startdate",
value: new Date().toISOString().substr(0, 10)
};
もし、新規レコード時に値を設定するフィールドが複数ある場合、fieldおよびvalueキーを持つオブジェクトの配列で記述をします。リスト3-3-4はその例です。新しくhistoryコンテキストにレコードを作ったとき、startdateおよびenddateフィールドに、今日の日付が入力されます。
INTERMediator.additionalFieldValueOnNewRecord
["history"] = [
{ field: "startdate",
value: new Date().toISOString().substr(0, 10) },
{ field: "enddate",
value: new Date().toISOString().substr(0, 10) },
];
なお、これらフィールドの初期値に関する設定は新規レコード作成時だけでなく、レコード複製時にも適用されます。
論理削除に対応する
定義ファイルのコンテキスト定義に、soft-deleteキーによる値を追加すれば、論理削除(Soft Delete)に対応します。論理削除とは、レコードの削除操作においては実際にレコードを削除するのではなく、フラグとなるフィールドになんらかの値を代入して、削除したことにする手法です。soft-deleteキーの値を「true」にするのが基本です。そのときは、コンテキストのもとになっているテーブルにあるdeleteフィールドを、削除したかどうかのフラグに使います。soft-deleteキーの値を文字列にすると、その文字列がフラグとなるフィールド名になります。
soft-deleteキーが設定されるとフラグとなるフィールドが、数値の1のものが削除されたレコードであると判断します。それ以外のものは削除されていないものと判断します。repeat-controlキー等で付与した「削除」ボタンは、実際にはレコード削除はしないで、フィールドに1を設定するだけにします。削除とみなさないレコードでは、そのフィールドの値はNULLあるいは""でかまいません。したがって、初期値を特に設定はする必要はありません。
論理削除が設定されたコンテキストに対してクエリーを行うとき、「delete != 1」という検索条件が必ず付与されるので、検索結果からは論理的に削除されたレコードは排除される仕組みになっています。
なお、論理削除されたレコードを参照したい場合は、別のコンテキストを定義してください。もちろん、そのコンテキストでは、soft-deleteキーは設定しないでおきますし、場合によってはqueryキーの値を利用して、deleteが1のレコードに最初から絞り込んでも良いでしょう。そして、復活させるためのユーザーインターフェースを、例えばポップアップメニューで作成するなどしてください。
実際に、INTER-Mediatorの演習環境を利用して、論理削除の動作の確認をしてみます。以下のページは、page06.html、def06.phpを利用しましたが、すでにこのファイルを使っている場合には、異なる番号のファイルを利用してください。定義ファイルは、図3-3-3のように作成しました。personテーブルを使用したコンテキストを2つ「person」と「personall」定義します。図はMySQLに対応するもので、FileMakerの場合は、tableおよびviewキーの値を「person_layout」と設定してください。personコンテキストはsoft-deleteキーの値を「checking」としています。これは、テーブルに定義された数値型のフィールドです。このフィールドを、削除のフラグとして利用するということをこれで宣言しています。personallコンテキストではsoft-deleteキーの値はなく、queryキーも設定されていないので、原則としてすべてのレコードが出力されます。
ページファイルはリスト3-3-5のように作成しました。それぞれのコンテキストをTABLEタグで一覧表示しています。テーブルが左右に並ぶように、style属性を設定してあります。その他はこれまでに説明してきたことと特に違いはありません。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def06.php"></script>
</head>
<body>
<table style="float:left" border="2">
<tbody>
<tr>
<td data-im="personall@name"></td>
<td data-im="personall@checking"></td>
</tr>
</tbody>
</table>
<table style="float:left" border="1">
<tbody>
<tr>
<td><input type="text" data-im="person@name"/></td>
</tr>
</tbody>
</table>
<br clear="all"/>
</body>
</html>
実際にページを表示してみます。図3-3-4では、左側のテーブルに、全レコードが表示されていて、1レコード目のcheckingフィールドが「1」であることが分かります。右側は、論理削除の設定が機能しているため、checkingフィールドの値が「1」のレコードは当初から見えていません。最初からcheckingが「1」のレコードがない場合は、以降の説明を参考にレコードを増やして確認してみてください。
ここで右側のテーブルの下にある「追加」ボタンを押して、レコードを追加します。追加後は、ブラウザーのページの更新を行って、再度ページの構築を行ってください。そして、右側のテーブルで新規レコードに対するnameフィールドのテキストフィールドがあるので、そこに適当な名前を入力しておきます。
この状態で、右側のテーブルの新しいレコードに対応する「削除」ボタンをクリックします。すると、右側のテーブルでレコードが消えますが、フィールドの反映された値を確認するために、ブラウザーのページの更新を行って、再度ページの構築を行ってください。そうすれば、後から追加したレコードのcheckingフィールドの値が「1」になっていることが分かります。つまり、「削除」ボタンでは削除は行われず、データベースに対して行う処理はchekingフィールドに「1」を設定するだけになります。
論理削除を加味したコンテキストと加味しないコンテキストを同時に表示するようなことは、実際の業務で作られることはまずないと思われます。ここで、確認のための更新作業を行うことは、実用上は大きな問題にはならないと考えます。
PostgreSQLでのシーケンスオブジェクト
このセクションの最初にも説明したように、PostgreSQLではレコード挿入操作の動作を他のデータベースと揃えるために、シーケンスオブジェクトの名前をコンテキストにsequenceキーで指定する必要があります。
まず、シーケンスオブジェクトを自分で定義する場合を考えます。リスト3-3-6は、スキーマ定義の一部分です。まず、CREATE SEQUENCEでserialというシーケンスオブジェクトを定義しています。そして、personテーブルのidフィールドでは、シーケンスオブジェクトのserialより新たな値を得て、それを初期値にしています。最後の方には、GRANTステートメントで、この場合のユーザー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;
定義ファイルの記述方法として、PHPでの一部分を示します。リスト3-3-7のように、sequeceキーで、シーケンスオブジェクトの名前を指定します。なお、ここでの「im_sample」はスキーマ名です。定義にスキーマ名を含めるかどうかは、データベースの設計次第でもありますが、INTER-MediatorのPostgreSQLのサンプルでは、データベースのエンティティは「スキーマ名.エンティティ名」という形式で記述しました。
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型で定義して、自動的にシーケンスオブジェクトを用意するという手法もあります。このとき、CREATE SEQUENCEによるオブジェクトの定義は不要ですが、「テーブル名_フィールド名_seq」というシーケンスオブジェクトが自動的に作られて、初期値が1になっています。例えば、im_sampleスキーマのpersonテーブルのidフィールドがSERIAL型だったとします。すると、person_id_seqというシーケンスオブジェクトが自動的に作られます。自動的に作られるオブジェクトにもアクセス権の設定を記述する必要があります。ただし、この場合は、sequenceキーの指定は不要です。
このセクションのまとめ
レコードの削除やレコード作成は、コンテキストにrepeat-controlキーで値を指定することで、自動的にボタンを生成することができます。ボタンの位置や、あるいは確認をするかどうかなどは、指定値によりある程度はカスタマイズが必要です。ページネーションが表示されていれば、ページネーション上にレコード作成ボタンが表示されます。コンテキストのrecordsキーの値が1の場合に新規レコードを作成すると、新規レコードだけが検索された状態になり、ボタンを押せば新規レコードだけが表示されるといった動作になります。
3-4入力専用のPost Onlyモード
アンケートの記入などのような、入力専用のページを作成することもできます。ボタンをクリックすると、コンテキストに新しいレコードを作ることができます。
ページファイルでの違い
入力専用モード(あるいは「Post Onlyモード」)で動作させる場合には、定義ファイルは通常通り作成し、新規レコード作成できる状態になっていればOKです。具体的には、tableキーあるいはnameキーで指定される書き込み時のテーブルが、実際に存在して書き込み可能になっていれば動作します。定義ファイルでは、動作のバリエーションのための指定を加えることができますが、これはこの後に説明します。
Post Onlyモードのページも、通常のページファイルと同様に作成します。これまでに説明した部分でもFORMタグは使っていませんが、Post Onlyモードでも同様です。ただし、エンクロージャーに相当するタグ要素にdata-im-control属性を設定して、値は「post」とします。また、レコード作成のボタン(以下、「登録ボタン」と記述します)が必要ですが、BUTTONタグの要素に対して、data-im-control属性を設定して、こちらも値は「post」とします。この2つの点が通常のデータベースの内容を表示するページと異なりますが、後は同様です。なお、原則として、ひとつのエンクロージャーに1セットのリピーターという構成になります。リピーター内部のリンクノードは、通常通り、data-im属性に「コンテキスト名@フィールド名」の値を設定します。コンテキスト定義に指定したvalidationキーの設定についても適用されます。
チェックボックスとラジオボタンをグループとして動作させることもできます。その場合、data-im属性ではなく、data-im-group属性に「コンテキスト名@フィールド名」の値を設定します。ラジオボタンであればどれかひとつの要素だけが選択されますが、チェックボックスは複数の要素をオンにすることで、それらの要素のvalue属性値を改行で区切った値をデータベースに送り込みます。FileMakerのチェックボックスセットに似た機能です。
Post Onlyモードでは、data-im-control属性に「post」を指定したエンクロージャーの外部にあるエンクロージャーに対しては、普通にページ合成ができるので、データベースアクセスの結果と、入力フォームを混在させることもできます。また、入力フォームの内部のエンクロージャーも同様に通常通り処理されるので、ポップアップメニューの選択肢をデータベースから取り出して配置することなど、通常のページと同様な処理ができます。つまり、ページファイルの中にあるひとつのコンテキストだけをPost Onlyモードで動作させることができます。このとき、データベースの新規レコードを作成に使うコンテキストの範囲はそのコンテキストに加えて、コンテキストの中にある別のコンテキストの領域も探します。
階層化したコンテキストの動作を説明するために、具体的に説明しましょう。まず、商品申し込み用紙のようなページを作るとします。申し込み用紙に相当するPost Onlyモードのコンテキストを「request」とします。その中で、商品一覧を「products」コンテキストから取り出して表示します。商品一覧の中に商品を選択するチェックボックスを配置し、そのチェックボックスはrequestコンテキストに含むようにして選択結果を新しいレコードに加えたいとします。ページファイルをリスト3-4-1のように記述します。
<table>
<tbody data-im-control="post">
<tr><th>名前</th><td><input type="text" data-im="request@name"></td></tr>
<tr><th>住所</th><td><input type="text" data-im="request@addr"></td></tr>
<tr><th>メール</th><td><input type="text" data-im="request@mail"></td></tr>
<tr><th>商品</th><td>
<table>
<tbody>
<tr>
<td><input type="checkbox" data-im-group="request@selection"
data-im-control="unbind"
data-im="products@product_id@value"></td>
<td data-im="products@name"></td>
</tr>
</tbody>
</table>
</td>
<tr><th></th><td><button data-im-control="post">送信</button></td>
</tbody>
</table>
実際にページを表示するとき、外側のテーブルのrequestコンテキストはPost Onlyモードで動作するので、基本的にそのまま展開されるだけです。しかしながら、内側のテーブルではproductsコンテキストがあるので、複数のレコードを取り出していくつかの行に展開します。各行には、チェックボックスと、おそらく商品名がそれぞれセル内に表示されているでしょう。チェックボックスのvalue属性は、product_idフィールドの値が設定されます。
ここで、「送信」ボタンをクリックすれば、外側のテーブルの内部にあるdata-imおよびdata-im-group属性がrequestコンテキストの要素だけでなく、内部のコンテキスト、つまり内側のテーブルの中のdata-imおよびdata-im-group属性がrequestコンテキストの要素も収集して、データベースに送り込みます。このときのデータベースに送られるINSERT文を模式的にMySQLの文法で記述すると、リスト3-4-2のようになります。productsコンテキストでは、いくつかの商品がチェックボックスとともに見えていて、そのうち、product_idが21と44と32のチェックボックスを選択していたとします。selectionフィールドは改行で区切られます。このフィールドを保存するには、SQLデータベースの場合は文字列型にしておく必要があります。なお、この結果を持って、さらに別のテーブルにレコードを作りたい場合には、『8-1 サーバーサイドでの処理の追加』以降で説明するアドバイス定義クラスをPHP言語でのプログラミングで記述する必要があります。
INSERT INTO request
SET name='Someone', addr='Anywhere', mail='foo@bar.com', selection='21\n44\n32'
ただし、このように、データベースの値を適用したチェックボックスを用意しつつ、その結果をPost Onlyモードで利用する場合でも、チェックボックスは通常のコンテキストであるproductsコンテキストで展開しているため、product_idフィールドとバインドしてしまいます。ここでは、単に表示だけよく、値の収集はrequestコンテキストとして稼働するので、チェックボックスのオン/オフで、データベース更新する必要はありません。そこでタグの中に「data-im-control="unbind"」と記載してバインドをしないようにして、データベースへの更新処理を行わないようにしています。
登録ボタンを押した後の動作の設定
ここまで設定したPost Onlyモードのコンテキストの場合は、登録ボタンを押した後、ページはそのままなので、何度もボタンを押されてしまう可能性があります。その都度レコードが作成されてしまい、意図しない動作を引き起こします。そこで、表3-4-1のような設定を、コンテキストに加えます。これにより、ボタンを押したら、ボタンが即座に消えて、2回押されることはほぼなくなります。また、その後にメッセージを出したり、別のURLに移動できるので、入力者に対する適切なユーザーインターフェースを構築することができます。なお、これらの動作の前に、データベースに新しいレコードを作成します。
コンテキストで使用できるキー | 動作 |
---|---|
post-reconstruct | この設定があれば、登録ボタンを押した後にページの再合成を行う。値はなんでも良いが論理値として定義されているので、trueを指定する。post-dismiss-messageが指定されていれば、4秒後に再合成する |
post-dismiss-message | 入力型フォームにおいて、登録ボタンを押した後に、ボタンを消してここに記述したメッセージの文字列を表示する。文字列はSPANタグでclass属性が「IM_POSTMESSAGE」のタグ要素に含まれている |
post-move-url | 入力型フォームにおいて、登録ボタンを押した後に、ここで指定したURLにページを移動させる。指定しない場合にはページ移動はない。post-dismiss-messageが指定されていれば、4秒後に移動する |
いずれも、画面更新や別のURLへの移動までの動作時間は4秒としましたが、その時間を変更したい場合には、INTER-Mediatorで参照されるオブジェクトのwaitSecondsAfterPostMessageプロパティの値を設定してください。単位は秒です。リスト3-4-3は指定例で、例えば、ページ合成前に実行されるメソッドに記述をします。INTERMediatorOnPage.doBeforeConstructについては、『8-5 ブラウザーを判断するページ』の最後に解説があります。
INTERMediatorOnPage.doBeforeConstruct = function () {
INTERMediator.waitSecondsAfterPostMessage = 10;
}
登録ボタンをクリックした後に独自のプログラムを追加したい場合は、リスト3-4-4のような記述でメソッドを定義します。例えば、データベースに新規レコードを作成する際に、独自の判定ルールに合致している場合だけ新規レコード作成を認めるといった運用が考えられます。表3-4-1に示したメッセージ表示、ページ再合成、ページ移動が定義ファイルに設定されていた場合、それらよりも前に、以下に定義した関数を実行します。processingBeforePostOnlyContextは、データベースへの書き込み前に実施され、返り値がfalseだとデータベース処理を行わず、ページ移動なども行いません。processingAfterPostOnlyContextはデータベース処理後に実行され、返り値は指定しません。
INTERMediatorOnPage.processingBeforePostOnlyContext
= function(node){
// any program here.
return true;
};
INTERMediatorOnPage.processingAfterPostOnlyContext
= function(node){
// any program here.
};
演習Post Onlyモードのページを作成する
実際にPost Onlyモードのページを作成して、その動作を見てみましょう。ここでは、新しくページを作りますが、これまでに使ってきたpersonテーブルに入力するので、入力した結果はこの章のこれまでの演習で作ってきたページを利用します。
Post Onlyモードのページの作成
table、viewともに「person」と入力します。personは定義されているテーブルです。
table、viewともに「person_layout」と入力します。FileMakerではテーブル名やTOC名ではなく、レイアウト名を指定します。
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def03.php"></script>
</head>
<body>
<table>
<tbody data-im-control="post">
<tr>
<th>name</th>
<td><input type="text" data-im="person@name"/></td>
</tr>
<tr>
<th>mail</th>
<td><input type="text" data-im="person@mail"/></td>
</tr>
<tr>
<th>category</th>
<td>
<select data-im="person@category">
<option value="1">Family</option>
<option value="2">Class Mate</option>
<option value="3">Collegue</option>
</select>
</td>
</tr>
<tr>
<th></th>
<td><button data-im-control="post">Register</button>
</tr>
</tbody>
</table>
</body>
</html>
登録後に状態を表示するページにする
演習のまとめ
- エンクロージャーと登録ボタンにdata-im-control属性を設定し、値を「post」にすれば、データベースの内容を合成しない、Post Onlyモードのコンテキストがページ上に構築されます。
- 登録ボタンをクリックすると、テキストフィールドなどに入力した内容をフィールドの値とした新しいレコードが作成されます。
- ボタンを押した後にメッセージを表示したり、あるいはページの再合成が可能なので、二重登録を避けることができます。
Post Onlyモードと「確認画面」が不要な理由
INTER-Mediatorでは、Post Onlyモードによる新規レコード作成に対応しています。通常、フォームで入力するときには、入力結果を改めて別の画面に明示し、入力者に確認をさせて、OKかあるいは修正するというユーザーインターフェースが一般的です。もちろん、そのような動作をPost Onlyモードで実現してもよいのですが、開発者としてはその機能は不要と考えます。
Web情報を検索してみると、確認画面に対する否定的な意見がいくらかある一方で、確認画面の作り方というサイトは大量に出てきます。現状、確認画面は作って当たり前というのがどうも業界の一般常識なのでしょうか? ただし、なぜ、必要なのかという積極的な説明を明確にしたサイトは筆者が探した中にはありませんでした。一方、不要であるという主張に対する理由には「出したところで見ている人はほとんどいないだろう」という消極的な理由を挙げているものがほとんどです。
確認フォームが必要な理由
まず、一般的なWebフォームではなぜ確認が必要なのかということがあります。当然の理由として、入力したものを「確認したいから」、あるいは「確認する必要があるから」ということになりますが、理由はもう少し掘り下げて考えないといけません。おそらく、過去からのいろいろな経験の積み重ねで次のような理由があるからでしょう。
- フォームのページでは入力した情報がすべて見えているとは限らない
- 入力した情報以外のものを表示したい(販売サイトの合計金額や送料など)
- ReturnキーやEnterキーによって、submitボタンが押されたのと同じになり、意図せずサブミットしてしまうときにやり直しが効かない
- 積極的な理由はない、一般にそうだからとか、とにかく確認させたいといった理由
これらを順次検討しましょう。まず、最初の理由、つまりすべての要素が見えていないというのは、大昔の解像度の低いWebページを作っていた時代では確かにあったかもしれません。テキストフィールド枠が40文字しかない画面で、それ以上の文字入力を許している場合、入力後に全体を確認する必要があります。ユーザーは垂直方向はもちろん、水平方向にもスクロールしないと文字が見えません。そのような状況で、念のため一度全部、ページに見せるということは確かに必要でした。
しかしながら、現在は解像度が高いディスプレイが一般的になり、一覧性が高まりました。また、スマホのように画面表示領域が小さいデバイスの場合には、1ページにすべてを詰め込むような設計は避けられています。現在はデザインが重視されており、入力中に今自分が入力したものが見えないようなレイアウトは、一般にはデザインが悪いと言わざるを得ないでしょう。もし、この理由でフォームの確認画面が必要なら、まず、フォーム自体を見直す必要があります。入力中に全体像が分かるようにしてあれば、わざわざ確認画面を出す必要はなくなるでしょう。項目が多くなると、スクロールしないとすべてが見えないから、確認画面が必要という議論もあるかもしれません。しかしながら、その場合は、確認画面自体もスクロールが必要になることが一般的ではないでしょうか? とにかく、入力フォームの段階で、確認ができるレベルでページを作っておくのが最善策であることを目指すのが適切な対処だと考えます。
2つ目は、入力したもの以外の情報を確認したいという理由です。例えば、販売サイトでの合計や送料等の表示があります。これは、入力した結果の確認画面の話でしょうか? 違います。これは、システムが生成した結果を利用者が確認する画面のことで、入力の確認画面の議論と混同してはいけません。
3つ目の理由は、古い時代のフォームでは、入力途中にReturnキーに触れてしまって意図せずサブミットされることがありました。テキストフィールドにフォーカスがあるときにReturnキーを押すと、自動的にそれを含むFORMタグのsubmitボタンがクリックしたものとみなされるのは一般的な動作です。他のフレームワークならともかく、INTER-MediatorはテキストフィールドでReturnを押しても、設定やあるいはプログラムを追加しない限りsubmit的な動作は起こりません。つまり、INTER-Mediatorであれば、Returnで意図せずサブミットされることはありません。逆に、Returnでサブミットしたいのなら、何かしらの記述を追加しなければなりません。
4つ目の理由は、積極的な理由がなく、そう言われているからやるというものです。積極的な理由もなく、何かをするのでしょうか? 理由がないのが理由ということに対しては、エクスキューズの必要すらないと考えます。本当に利用者のことを真剣に考えているのであれば、何か理由があるはずです。ユーザーにとって特別なメリットがないのに機能を組み込むのは開発者側の勝手な思い込みではないでしょうか?
それでも“従来手法”を求められたら?
INTER-Mediatorでシステムを作る場合、それでも「確認」ページの作成を求められたらどうしましょうか? 確認の実装方法として、「ボタンを押したらダイアログボックスで短いメッセージを出して確認」があります。新たなページを作るのではなく、単に入力フォームの内容が書き込みされるということを促して、人間の手による応答が入ればいい程度のような場合です。
この場合は、Post Onlyモードで入力ページを作成します。そして、データベースへの書き込みに行く前に実行するメソッドを定義して、そこでダイアログボックスを表示します。例えば、ページファイルのヘッダー部にあるSCRIPTタグ要素のプログラムをリスト3-4-5のように記述したとします。ページファイル内でPost Onlyモードのリピーターがあり、そこでボタンをクリックすると、ここに定義したprocessingBeforePostOnlyContextメソッドが呼び出されます。このメソッドがfalseを返せば、データベース処理等は何も行いません。confirm関数はダイアログボックスを表示し、OKをクリックすればtrue、キャンセルをクリックすればfalseを返します。
<script type="text/javascript" src="def03.php"></script>
<script type="text/javascript">
INTERMediatorOnPage.processingBeforePostOnlyContext
= function(node) {
return confirm("本当に入力していいでしょうか? "
+ "しつこいようですが、やっちゃいますよ");
}
</script>
これ以上の仕組み、例えば、確認用の別のページを見せるような手法については、少し複雑になります。次にそのヒントについて説明をします。
確認付きの入力フォームの作成方法
以上のように、入力結果を新規レコードとして残すような入力フォームでは、INTER-Mediatorで適切なデザインをしていれば、確認画面が必要な積極的な理由はありません。しかしながら、発注側の要求は常に論理的とは限りません。ここまでに記載したような説明をしたとしても、「やっぱり確認ページが欲しいです」という場合もあるでしょう。その場合の一般的な対処法についてまとめておきます。
まず、いわゆる「確認ページ」が欲しいという場合には、入力および確認のための要素をすべて1ページに作り、CSSのdisplay属性を利用していずれか一方を表示するような仕組みにして、Post Onlyモードで新規レコードを作成するという方法があります。この方法は、複数のページを経由して書き込みをするような場合にも応用できます。
確認ページ付きのPost Onlyモードのページの例を示します。INTER-Mediatorの演習環境の適当なWebページのセット(例えば、def21.phpとpage21.html)等に入力すれば確認できます。定義ファイルは図3-4-1のように、入力結果を受け付けるsurveyという名前のコンテキストが定義されているとします。Database Settingsのセクションは、これまでの演習で紹介したように指定します。このコンテキストには、フィールドにはQ1とQ2が少なくともあるとします。ページファイルはリスト3-4-6に示します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<style>
.input {
display: table-row;
}
.confirm {
display: none;
}
</style>
<script type="text/javascript" src="def21.php"></script>
<script type="text/javascript">
function toConfirm() {
var textNode, value, textNode;
value = document.getElementById("Q1Input").value
textNode = document.createTextNode(value);
document.getElementById("Q1Confirm").appendChild(textNode);
value = document.getElementById("Q2Input").value
textNode = document.createTextNode(value);
document.getElementById("Q2Confirm").appendChild(textNode);
toggle("none", "table-row");
}
function toInput() {
document.getElementById("Q1Confirm").innerHTML = "";
document.getElementById("Q2Confirm").innerHTML = "";
toggle("table-row", "none");
}
function toggle(inputDisplay, confirmDisplay) {
var body, nodes, i;
body = document.getElementsByTagName("BODY")[0];
nodes = INTERMediatorLib.getElementsByClassName(body, "input");
for (i = 0 ; i < nodes.length ; i ++) {
nodes[i].style.display = inputDisplay;
}
nodes = INTERMediatorLib.getElementsByClassName(body, "confirm");
for (i = 0 ; i < nodes.length ; i ++) {
nodes[i].style.display = confirmDisplay;
}
}
</script>
</head>
<body>
<table>
<tbody data-im-control="post">
<tr class="input">
<th>Q1</th>
<td><input type="text" data-im="survey@Q1" id="Q1Input" /></td>
</tr>
<tr class="input">
<th>Q2</th>
<td><input type="text" data-im="survey@Q2" id="Q2Input"/></td>
</tr>
<tr class="input">
<th></th><td><button onclick="toConfirm()">確認</button></td>
</tr>
<tr class="confirm">
<th>Q1</th><td id="Q1Confirm"></td>
</tr>
<tr class="confirm">
<th>Q2</th><td id="Q2Confirm"></td>
</tr>
<tr class="confirm">
<th></th>
<td>
<button onclick="toInput()">戻る</button>
<button data-im-control="post">書き込み</button>
</td>
</tr>
</tbody>
</table>
</body>
</html>
実際にページを表示した結果が、図3-4-2です。最初、2つのフィールドに対するテキストフィールドが見えているので、適当にキータイプをします。そして、「確認」ボタンをクリックすると、確認のための表示に切り替わります。ここで、「戻る」ボタンだと、テキストフィールドの表示に戻り、「書き込み」ボタンをクリックすると、Post Onlyモードの動作として、レコードを新たに作成し、Q1およびQ2フィールドは、それぞれテキストフィールドに入力した値になります。
プログラム部分を解説します。それぞれのボタンから呼び出されるJavaScriptのプログラムにあるtoggle関数は、classの値に応じて引数の値をCSSのdisplay属性に設定します。display属性が「table-row」なら行として表示、「none」であれば非表示になります。class属性がconfirmの行は、ヘッダー部のSTYLEタグの定義により、ページを表示したときには非表示になります。INTERMediatorLib.getElementsByClassNameは、INTER-Mediatorによって提供されるメソッドで、最初の引数で指定したタグ要素(この場合は、BODYタグ要素)以下の要素(ノード)を検索し、2つ目の引数で指定した名前のclass属性を持った要素の配列を得るものです。これ以外は、JavaScriptの標準的なメソッドや関数を利用したものです。
toConfirm関数では、テキストフィールドに入力した文字列を、確認用のセルの値に設定し、表示する行を切り替えています。Post Onlyモードでは、data-im属性があっても、id属性は変更されずに定義通りに残ります。id属性を手掛かりにして、テキストフィールドを参照しvalue属性で入力した文字列を取得しています。なお、DOMのAPIでは、プログラムにあるようにテキストノードを生成して、appendChildで子要素として設定するといったややこしいことを行う必要があります。jQueryを使えばもっと短く記述できますので、そちらに慣れているのならjQueryも併用しましょう。原則、INTER-MediatorとjQueryは共存できるはずです。toInput関数は表示する行を切り替えればいいのですが、何度も行き来すると、確認用のセルに、つどつどテキストフィールドの値が追加されることになるので、入力用のセルを表示する前にいったん確認用のセルはクリアしています。
なお、このままでは、バリデーションの実装に悩むところです。バリデーションのルールに合わなくても、toConfirm関数を実行して、確認ページに遷移してしまいます。確認ページではバリデーションの結果は得られません。
解決方法として、INTER-Mediatorのバリデーション機能を統合するのもひとつの方法ですが、もっと明確な方法もあります。toConfirm関数では、実質的には入力したデータをひとつひとつ変数に取り出しています。であるならば、かえって、JavaScript上で確認処理を実装したほうがより明確ですし、正しくないときの処理も自由に組み込めます。発展させるなら、そちらの方向がより良い方法と考えられます。
処理結果を伴う場合の対処
ECサイトにあるような、商品申し込みをした後に、送料が計算されて出てくるようなアプリケーションを考えてみましょう。こうした確認用のページは発注者から見れば「入力フォーム」かもしれませんが、状況に応じて(あるいは一定の)送料が付加される点では、単なる入力ページではなく、何らかの処理を行うページであり、入力フォームの確認ということとは動作上は意味合いが違うということはこれまでに説明しました。
では、そういうページはどう作ればいいのでしょうか? これらについて、画一的な方法はありません。そのアプリケーションに応じた作り方をしなければならないと考えます。もし、一定の「送料」を付加するだけなら、直前の『確認付きの入力フォームの作成方法』で紹介した方法の応用でできるでしょう。確認ページでは、送料を追加で表示します。データベース側に送料の金額をフィールドに入れたい場合には、typeがhiddenのINPUTタグを用意してフィールドにバインドしておき、JavaScriptのプログラムでそこに設定を行います。
さらに複雑なロジックが絡む場合の対処法については、さまざまなプログラミングテクニックが絡むので、『6-5 Post Onlyモードと連動した処理』で実例として示します。
このセクションのまとめ
Post Onlyモードという仕組みで、入力専用のページの作成が可能です。エンクロージャーおよび内部のボタンに、data-im-control属性がpostのものを用意するのが基本で、これにより、テキストフィールド等に入力したデータを、新規レコードとして指定したコンテキストに追加できます。ボタンを押した後にメッセージを表示したり、別のページに移動する仕組みも定義ファイルで実装できます。バリデーションや、リピーター内部にエンクロージャーを指定したマスター参照による選択肢の構築なども可能です。なお、一般には、入力後の処理をする仕組みも含めて「入力フォーム」と呼ばれることが多いのですが、INTER-Mediatorでは処理を含む場合、Post Onlyモードだけの仕組みでは思った通りの動作ができないかもしれません。その場合は、Post Onlyモードを使わないという選択肢も考慮すべきです。
3-5バリデーション
データの入力や更新時には、ユーザーの入力した結果を検証し、間違いであることが分かればデータベースへの入力をしないで、ユーザーに修正を求めたいことがあります。こうした一連の機能をバリデーションと呼びます。INTER-Mediatorではコンテキストへの定義の追加でバリデーションの設定が可能です。
コンテキスト定義に設定するバリデーション
コンテキストの定義では、validationキーによって指定が可能です。値は、field、rule、message、notifyをキーに持つ連想配列を要素とした配列です。これは、queryキーなどと同様な形式です。定義ファイルエディターでは、ページの冒頭にある「show all」ボタンを押すことで、設定項目が表示されます。連想配列に指定する内容は表3-5-1の通りです。
キー | 指定する値 | 動作 |
---|---|---|
field | フィールド名のみ | データ確認を行うフィールド名 |
rule | JavaScriptの式 | 入力値は変数value、ノードへの参照は変数targetに入っているものとして、式を組み立てることができる。式の結果がtrueならそのまま処理を進める |
message | 文字列 | 正しくない場合に表示するメッセージ。ruleで指定した式の結果がfalseなら、messageの文字列を表示して値を元に戻す |
notify | (省略、以下のもの以外) | ダイアログボックスでメッセージを表示する |
inline | ノードの直後に文字列としてmessageをSPANタグ(クラスは_im_alertmessage)で追加する | |
end-of-sibling | 兄弟ノードの最後にmessageをSPANタグ(クラスは_im_alertmessage)で追加する |
fieldでは、コンテキスト内に現れるフィールド名を指定します。ruleには式を記述しますが、変数valueとtargetをいきなり記述できます。例えば、リスト3-5-1のようなvalidationキーの値を記述した場合、フィールドnameとバインドしたテキストフィールド全てで、データの変更を行ったときに、「length(value) > 1」という式を計算します。式の記述については『4-4 計算プロパティの設定』で解説をしますが、lengthは文字列の長さを求める関数です。つまり、テキストフィールドに入力された文字列を調べて、1より大きい場合はtrueになり、そのままフィールドの値が更新されて何もメッセージは出しません。しかしながら、文字列が1文字だけの場合ではruleの式はfalseになります。すると、messageやnotifyで指定されたメッセージが表示されます。
IM_Entry( array (
array (
'name' => 'person',
'key' => 'id',
'view' => 'person',
'table' => 'person',
'validation' => array (
array (
'field' => 'name',
'rule' => 'length(value) > 1',
'message' => '2文字以上入力してください',
),
),
),
ダイアログボックス以外のメッセージの表示方法として、ページ上にテキストノードを追加する方法があります。テキストフィールドの右側などに、「文字を入力してください」といったようなメッセージが表示されます。このときは、notifyキーの値を指定してください。テキストを赤文字にしたいなどのスタイルを指定したい場合には、テキスト自体のclass属性が_im_alertmessageとなっているので、このクラス名に対するセレクタをCSSで記述すれば良いでしょう。
演習バリデーションを組み込む
Post Onlyモードのページの作成
table、viewともに「person」と入力します。personは定義されているテーブルです。
table、viewともに「person_layout」と入力します。FileMakerではテーブル名やTOC名ではなく、レイアウト名を指定します。
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def04.php"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<tbody>
<tr><th>id</th><td data-im="person@id"></td></tr>
<tr>
<th>name</th>
<td><input type="text" data-im="person@name"></td>
</tr>
<tr>
<th>mail</th>
<td><input type="text" data-im="person@mail"></td>
</tr>
</tbody>
</table>
</body>
</html>
データを判定してエラーを表示する
インラインでエラー表示
Post Onlyモードでのバリデーション
<table>
<tbody data-im-control="post">
<tr>
<th>name</th>
<td><input type="text" data-im="person@name"></td>
</tr>
<tr>
<th>mail</th>
<td><input type="text" data-im="person@mail"></td>
</tr>
<tr>
<th></th>
<td><button data-im-control="post">入力</button></td>
</tr>
</tbody>
演習のまとめ
- Validationsの設定により、指定したフィールドの値が一定の条件を満たさない場合にはエラーメッセージを表示することができます。
- 条件には式を記述します。式では、フィールドの値を示すvalue変数や、HTML要素を参照するtarget変数を利用できます。
- 標準ではダイアログボックスでエラーメッセージを表示しますが、設定により、HTMLページ内にメッセージを表示することができます。
バリデーション機能の利用と注意点
バリデーションの仕組みは、ユーザーインターフェースの応答を重視して、クライアントサイドのみで実施されるようになっています。しかしながら、JavaScriptを利用してサーバーへ直接データを投入することにより、定義したバリデーションを無視したデータ入力ができることになります。この点は問題としては捉えていますが、実現方法に懸念すべき点が多く、Ver.5.7の開発を行っている2017年後半における計画では、先のメジャーアップデートであるVer.6系列で実現する考えです。
Post Onlyモードの場合、複数のフィールドの値をもとに判断をするようなバリデーションを実装したいときには、JavaScriptを利用する必要があります。JavaScript関連の章の『6-5 Post Onlyモードと連動した処理』『Post Onlyモードで利用できるAPI』で解説します。
演習でもありましたが、バリデーションの機能はレコード編集時には、そのテキストフィールドにフォーカスが入らないと実施されません。「確定」ボタン的なものを持たないユーザーインターフェースなので、そのような動作になります。しかしながら、空欄のままになっては困るということもあるかと思います。その場合、エンクロージャーの処理を終了したときに呼び出されるメソッド(『6-4 ページ合成に割り込む処理の追加』で説明)に、特定のフィールドをフォーカスするようなメソッドを組み込むことで実現できるでしょう。
このセクションのまとめ
入力したデータが正しいかどうかを判定して、正しくない場合にはエラーメッセージを出す機能が組み込まれています。一般にはバリデーションと呼ばれる機能で、定義ファイルには、特定のフィールドの値を元に判断する設定を記述することができます。一方、複数のフィールドを元にバリデーションを行うには、JavaScriptのプログラミングが必要です。