[DBデザイン#2] データベースのモデル

データベースの話を進める上で、基本的な概念として何をベースに話をスタートさせるかをきちんと決めておきましょう。用語の整理というよりも、基本概念の整理がまず必要です。以下の図は、どこかで見たことがあるようなと思われる方もいらっしゃいますが、大して難しい概念はないと思います。まず、これを見て、直感的に、住所録、つまり複数の人の名前、住所、電話番号を「表にしている」と見てもらえると思います。

この表がデータベースのごく基本的なモデルとなります。表にまとめることで、人間がデータを参照したときに理解、あるいは活用がしやすくなるということがあります。データがを混沌と記録していたとして、記録はできているとしても、そこから特定のデータを探したり、データの傾向を求めるということは大変なことだったり失敗したりということになります。一方、データをきれいに整理しておけば、容易に検索したり、確実に集計したりということが期待できます。その整理された形式が表です。

ここで、Excelなどの表計算ソフトを引き合いに出して、「表は良い」ということを議論することもできるでしょう。表にしておくことで、オーバーな言い方ではありますが、多くの人が成功体験を積んでいるはずです。色々な利点が見出されている表というのものをモデルとして考えるということが出発点になります。もっとも、それは鶏と卵の議論ではないかということも言えるかもしれません。リレーショナルデータベースの理論が最初に発表されたような時期だと、表というモデルを考えること自体は単に1つのアイデアであり有用性までは実証されていないとの見方もあったようです。後々、ソフトウエアとして実装された上で、リレーショナルデータベースの有用性が証明され、理論と実際が相互に発達してきたという経緯もあります。

結果的に「まず表で始めます」となると、表が便利なのは常識だろうということで理由が吹っ飛んでしまいますが、表が便利なことを突き詰めて考えるのはやはりかなり難しいとも言えます。感覚的に、理解しやすく、しかも、大量のデータを扱えそうだというあたりの考えが出発点にあるということは、それほど不自然ではないと考えます。

さて、表とは言ってももう少しその構造を細かく見ましょう。非常に重要なことは「1人の人が1行」ということです。つまり、この行のことを「レコード」と呼ぶとして、1レコードが何を表しているのかということが非常に重要です。実は、これが決まればデータベースは実装可能ですが、1レコードを示すものが揺らいだり、間違っていると、データの記録がうまくできません。このことは改めて説明しますが、ここでは住所録は1レコードが1人であるということに注目します。そして、1人のレコードには、複数の「フィールド」があります。フィールドには「フィールド名」がつけられています。

1人の人の住所録を管理する上で、ひたすら名前や住所を書き並べても良いのですが、よく整理された状態というのは、一定の基準で情報が分離され、整頓されているということであると言えます。基本的に1人の人は、1つの名前があって、1つの住所があるということにここではしてください。だとしたら、名前と住所をごちゃ混ぜに記録するよりも、名前の枠、住所の枠で分離しておく方が、整理されたように見えます。つまり、ある特定の「属性」の情報「だけ」が取り出したりあるいは入力しやすくなっている方が都合が良いということが言えるはずです。もし、ごちゃ混ぜだったら、その混沌としたデータから一部分のデータ(例えば、名前だけ)を取り出すことが大変だったり、あるいは不可能だったりします。よって、フィールドに分離分割され、フィールドに実際のデータを入れておくという仕組みが作られたわけです。そして、フィールドという単位で処理をすることにより、うまくいけばデータの種類に関わらず、必要なデータだけがポンと出てくるということができるはずです。

ちなみに、レコードについては、行、ロー、タプルなどの言い方があり、フィールドについては、列、カラム、属性、アトリビュート、プロパティなどの言い方もありますます。一部のコミュニティではその言い方を間違えると白い目で見られるということも以前はあったようですが、表形式でデータ全体を考えるという概念上は概ね同じものを指しています。一連の記事では、レコード/フィールドという呼び方で進めます。

表計算ソフトだと、縦横にセルが並んでいるのですが、見かけは同じ表でも、データベースでの表は、まず、表をレコードごとに分割されていて、そのレコードにフィールドが存在するという見方をするのが良いでしょう。表の一部分を自由に扱うということもある意味はできますが、階層的に記述したら、以下の図のように、レコードの方が上位概念になります。この図の線は、正確には「導出可能」という意味です。ある表である「住所録」があるとき、その住所録からレコードに辿り着き、特定のレコードからフィールドのデータに辿り着けるというのが導出の具体例です。実際にそういうプログラムを記述できると言っても良いでしょう。視覚的にいえば、表をまず横方向にスライスして、そのスライス1つ1つについて、縦方向にスライスすることで実データに辿り着くという感じです。

階層の図(「ツリー」と呼びましょう)ではフィールド名は省略していますが、最初の表と、この階層図は、概念的には同じものを示しています。同じとは何か? ここでは「同一データ」と言っても良いでしょう。「表とツリーは同一である」ということは理解しづらいことかもしれませんが、一方で当たり前すぎて意識していない人も多いかもしれません。住所録は表で記述できますが、ツリーでも記述できるので、同一なのです。もちろん、表のほうがコンパクトで扱いやすそうです。ツリーが優れている理由はここでは明確にはありませんが、ツリーは関連性を明示可能です。つまり、表/レコード/フィールドという階層を明確に示すことができています。

ツリーを見ていると、フィールドの集合がレコードであり、レコードの集合が表であるということが明確にわかります。ここで、フィールドだけが具体的なデータを記録できていて、レコード自体はそのフィールドの集合という位置づけになります。なので、もちろん最初のレコードを「鈴木一郎」という名前で呼んでもいいのですが、レコードを特定する情報はここでは明確ではないので、便宜的に「レコード1」と記述しています。なお、レコードの特定の問題については、今後、色々な視点で説明をすることになります。

集合(set)といえば数学、数学といえば「嫌い」という人も多いかもしれませんが、集合論の真面目な議論に入らなくても、日常的な感覚の範囲では頑張って理解しましょう。ただ、集合と言うと深い議論があって、その1つは「集合には要素に重複はない」と言う定義です。集合の基本的な定義は「お互いに区別できるものの集まり」となっています。住所録の場合、重複したデータ、つまり全く同一の2つの行があった場合、用途上それは2つ存在している必要は全くなく一方は消したりないものとして扱うことは問題ありません。なので、レコードの「集合」が表なのです。また、フィールドについても、1つしかない名前を2つ以上のフィールドに入れたところでそのうちの1つしか必要ないので、フィールドの「集合」がレコードになるのです。この辺り、現実と数学の定義がよく合います。集合の考え方がリレーショナルデータベースの理論の基礎になっているという点は決して無視できない時事です。ここでは細かく議論しませんが、集合の要素は順序は問わないという定義もやっぱりデータベースの表といろんな意味でよくマッチングします。ちなみに、今回は数式は出しませんでしたが、先々では数式も出ると思います。その方がわかりやすいこともありますので、その時はなるべくわかりやすく説明するように心がけますが、数式があっても驚かないでください。

数学としての集合は有名ですが、その仲間のようなものも数学で定義されています。要素の重複を許すものをバグ(bag)あるいは多重集合(multiset)と呼ばれています。また、重複を許しつつ順序も情報として持つものを列(sequence、list)と呼ばれています。さらに重複は許さないが順序も情報として持つ順序集合(ordered set)もあります。これらも集合を基にした数学の理論は展開されています。ですが、プログラムができる方は、むしろ配列などとして理解されており、物理的な意味でデータを実際にこの形式で扱っているとも言えるので、抽象数学よりも具体的な世界での理解はされているのではないかと思います。数学に興味のある方はその辺りも書籍などを当たってみると良いでしょう・・・と言いつつ、この種の数学の書籍はかなり気合いと時間をかけないと理解に至らないのは確かですね。

次回は、表をツリーにしたことで、ER図の一部の定義に到達できることを説明しましょう。

[DBデザイン#1] データベースデザインについて書く

データベースデザインを、ここでは、「リレーショナルデータベースでシステム開発等を行う場合に、データベースのスキーマ、つまりどんなテーブルやフィールドを用意すれば良いのか」と言う問題であると定義します。データベースそのものの設計ではなく、データベースを利用する場合の問題です。RDBで開発をしている人は、どのように設計をすればいいのかと言うことが大変難しい問題であることはよくご存知でしょう。FileMakerを使って「簡単にテーブル定義ができます」となっていても、操作が簡単なことで設計が適切にできるかどうかは定かではありません。単純なシステムならともかく、今時は手元でやっている複雑怪奇でどうしようもない作業があるからコンピュータにさせようと言うのが動機としては一般的です。したがって、目的とするシステムを実現可能なデータ構造も複雑化しがちです。そう言うことで開発をしようというと、ベテラン、エースあるいはアーキテクトなどと呼ばれる専門家などがスキーマ設計を行うことになります。彼らは、多くの事例にこれまで取り扱ったという経験があり、複雑でどうしようもなさそうな要求をうまくシステムが飲み込んでしまうようなスキーマを作ってしまいます。

そうしたスキルを得るにはどうすればいいか? 経験を積むと言うこともあります。しかし、経験を積んでもそれができない人もいます。理解をするには理由を知ることが重要です。簡単な問題には「なぜ?」に対する答えは言えるのですが、非常に複雑な問題や、要件が怪奇すぎて微妙な問題に対するうまい解決策などをスキーマ設計者が出してきたとき、なぜそうなのかと言うことは設計者もうまく説明できないかもしれません。もちろん、理由があるのですが、知識を知らない人に説明できるかというとなかなか難しいと言うのが実情でしょう。もちろん、そう言う場合に頼りになるという意味では悪くはないとも言えますが、「自分もそうしたスキルをつけたい」と思ったらどうすればいいのでしょうか?

そう言うわけで、いろんな書籍があり、セミナーなどもやっていて、それらで勉強すればなんとかなっているという人もいるでしょうし、やっぱりわからなかったという人もいるでしょう。私も、ある有名なデータベース設計者のセミナーを参加して気づきました。そのかたの示す設計はもちろん、なるほどと思うところもあるのですが、理由を仔細におっしゃらない場面も多々あり、要するに「常識的にはこうなりますね」的な説明で終わってしまう場面もあったからです。その理由を知りたい、理由を知って応用したいと思うのが通常かと思いますが、やはりデータベース設計の世界は、どうもまずは経験を積んだ人の知見が主なナレッジということになってしまうのでしょう。

私自身はその答えはもう持っていて、クローズドなセミナー等で少しはお話ししています。また、そういうコンテンツを持っているのなら、それをビジネス展開してはどうかと思われるところかもしれません。少しはそう思っていたのですが、いろんな状況が重なり、「そうだ、淡々とブログに書こう!」と思った次第です。不定期にこの話題を積み重ねて行こうと思っています。まず、近々は、以前に所属した会社の社内セミナーで話そうとしていた内容を文章にまとめようと思っています。大まかに説明すると、「同じ名前のものが部署によって異なる対象になる」という事例から、それらはテーブルを分離しないと管理できないという話です。ドメイン駆動開発の世界でのユビキタス言語の考え方を、データベース設計のある側面に適用したお話しです。

そういうわけで、データベースデザインについて書くことにしました。

FileMaker Server 19.4(Ubuntu)の稼働するLinuxでPHPも稼働させる

FileMaker Server 19.4.2.204が稼働するUbuntu Server 18では、デフォルトではPHPは入っていません。自分で入れれば動くのはわかっているのですが、いわゆる「インストーラ」はないので、色々うまく設定しないといけなくなります。一応、動かす方法はわかったので、忘れないようにここに書いておきます。

まず、PHPのインストールですが、Ubuntuの普通のパッケージだと7.2です。せめて7.4にしたいので、こちらのサイトを参考に以下のようにコマンドを入れました。レポジトリを追加して、PHP 7.4をインストールします。

sudo apt-get update
sudo apt -y install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt-get update
sudo apt -y install php7.4

これだけだと、UbuntuでのサービスとしてのApache2を稼働させるとおそらくPHPは稼働しますが、FileMaker ServerのApacheは、独自の方法で起動しているので、そちらでPHPを認識させないといけません。

そこで、以下のようにコマンドを入れて、メインの設定ファイルを修正します。最初のcdコマンドで移動するパスが、FileMaker Serverをインストールした場合のApacheのルートディレクトリです。

cd /opt/FileMaker/FileMaker\ Server/HTTPServer/
sudo nano conf/httpd.conf

以下のような場所があるので、まずここを検索します。

#
# Disable PHP document
#
<FilesMatch "\.php$">
    Require all denied
</FilesMatch>

この部分を、以下のように変更します。php.confはこれから作ります。

#
# Disable PHP document
#
#<FilesMatch "\.php$">
#    Require all denied
#</FilesMatch>

Include conf/extra/php.conf

これだけでよさそうに思うのですが、これだけだと次のようなメッセージが出てしまいます。

[Wed Jan 19 11:11:29.516483 2022] [php7:crit] [pid 30487:tid 140490306792384] Apache is running a threaded MPM, but your PHP Module is not compiled to be threadsafe.  You need to recompile PHP.
AH00013: Pre-configuration failed

Apacheはマルチスレッド対応だけど、PHPは違うから、PHPを再コンパイルしろとあります。再コンパイルできるなら、レポジトリから取って来ません。そこで、若干問題が生じる可能性もあるのですが、Apacheをマルチスレッドではない状態で動かします。こちらのサイトを参考にしましたが、手順は異なります。

この点を解決するためにconf/httpd.confの編集を続けます。以下のような部分を検索します。「mpm」で検索すると良いでしょう。

LoadModule mime_module /usr/lib/apache2/modules/mod_mime.so
LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
LoadModule negotiation_module /usr/lib/apache2/modules/mod_negotiation.so

これを、次のように変更します。mpm_event_moduleをコメントにして読み込みません。新たに、mpm_prefork_moduleを読み込みます。記載されていないモジュールの読み込みが増えていますが、そのモジュールはもともとありました。

LoadModule mime_module /usr/lib/apache2/modules/mod_mime.so
#LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
LoadModule mpm_prefork_module /usr/lib/apache2/modules/mod_mpm_prefork.so
LoadModule negotiation_module /usr/lib/apache2/modules/mod_negotiation.so

これで、httpd.confを保存しておきます。

続いて、php.confを作ります。php7.4をインストールしたときに作られるファイルから適当に記述を持ってきて作ったものです。エディタを起動するために、次のようにコマンドを入れます。

sudo nano extra/php.conf

もちろん、存在しないファイルなので、エディタの画面は真っ白です。ファイルの中身は次のようにします。

LoadModule php7_module /usr/lib/apache2/modules/libphp7.4.so

<FilesMatch ".+\.ph(ar|p|tml)$">
    SetHandler application/x-httpd-php
</FilesMatch>
<FilesMatch ".+\.phps$">
    SetHandler application/x-httpd-php-source
    # Deny access to raw php sources by default
    # To re-enable it's recommended to enable access to the files
    # only in specific virtual host or directory
    Require all denied
</FilesMatch>
# Deny access to files without filename (e.g. '.php')
<FilesMatch "^\.ph(ar|p|ps|tml)$">
    Require all denied
</FilesMatch>

冒頭のLoadModuleは必須として、最初のSetHandlerだけでも多分動くと思いますが、とりあえず、他のものも入れておきました。なお、元のファイルにあったユーザのホームで公開した場合にスクリプトを動かさないようにする設定は省きました。

ここで、Apacheサーバだけを再起動とかして動くようにするのがお作法なのですが、サーバ自体を再起動します。それが確実なようです。すると、PHPが動きました。ちなみに、php.iniファイルのパスは、/etc/php/7.4/apache2/php.ini です。

こうしてPHPが稼働する状態になっているなら、PHPのモジュールはインストールするだけでOKです。例えば、初期状態ではcurlが稼働しませんが、次のようにコマンドを入れます。

sudo apt-get install -y php7.4-curl

すると、/etc/php/7.4/apache2/conf.dに、curl.iniファイルへのショートカットが作られて、認識します。あとは、サーバー再起動でもいいのですが、httpdctl gracefulでも設定は反映されます。

ちなみに、この方法でPHPを動かすのはまたまたアップデートの時などに手順やファイル名が違ったりしたりと色々面倒な気がするので、実稼働するシステムでは、FileMaker Serverとは別にPHPを稼働させるサーバを立てるのが、トラブルは少ないのではないでしょうか。

JavaScriptのasync/awaitを再理解してみた

半年ぶりの投稿になりますが、表題のことをやってみました。ちょっと時間の余裕ができたので、改めて勉強しようとしていろんなサイトを見たのですが、「async関数はPromiseを返す」ということが前提だけに、まずはPromiseを勉強するという体裁になっています。一方、「async/awaitは結局のところ、非同期呼び出しを同期呼び出しのように書いてよし」というシンプルに考えていいのかどうか、非常に迷うところでした。ということで、忙しい世の中ですので先に言ってしまうと、そこまでシンプルには残念ながら行かないことがわかります。INTER-Mediator Ver.6の実装で結構苦労したことが、本当にその苦労が必要だったのかということの検証でもあるのですが、これは個人的なところですね。

まず、次のようなプログラムがあるとします。以下は、適当な.jsファイルに入れて、nodeコマンドで実行します。ただし、「npm install xmlhttprequest」あるいはsudo付きで、XMLHttpRequestモジュールのインストールをお忘れなく。ブラウザで適当に動くようにされているのなら不要です。単にWebサイトのデータをAJAXで取り出すプログラムですが、どんな順序で実行されたかわかるように、コンソールへ出力を残しています。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const r = new XMLHttpRequest();
  r.open('GET', 'https://msyk.net')
  r.onreadystatechange = () => {
    if(r.readyState == 4) {
      console.log('receive aFunc');
    }
  }
  console.log('setup aFunc');
  r.send()
  console.log('send aFunc');
}

console.log('before aFunc');
let a = aFunc(); // async関数を普通に呼び出してみた
a.then(console.log('act1')) // thenが呼び出せるということはPromiseが返っている
console.log('after aFunc');

これを実行すると、次のようにコンソールに出てきます。ポイントは「after aFunc」より後に「recieve aFunc」が出るということです。AJAXによるダウンロード処理は非同期で行われているので、関数aFunc内に記述したコードが実行し終わる前にaFuncを呼び出しているステートメントの次に行くのです。これが非同期呼び出しの動作です。JavaScriptは「プログラムとして書いたコード」は、並列では動きませんが、「書いた順序」では実行しないというよくあるプログラムです。thenについてはPromiseを勉強すれば必ず説明されていますので、省略します。

before aFunc
start aFunc
setup aFunc
send aFunc
act1
after aFunc
receive aFunc

さて、ここで、やりたいことは「通信が終わったら次に行く」と言う動作です。要するに、「aFuncを同期的に呼び出したいのでしょ。だったら、awaitを入れればいいじゃん」と思うところですね。やってみましょう。ここで、単にaFunc()の前にawaitをつけると文法エラーになります。awaitはasync関数内部でしか使えません。グローバルエリアはasyncではないので、エラーになるので、新たにbFunc関数を作り、そこで実行します。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const r = new XMLHttpRequest();
  r.open('GET', 'https://msyk.net')
  r.onreadystatechange = () => {
    if(r.readyState == 4) {
      console.log('receive aFunc');
    }
  }
  console.log('setup aFunc');
  r.send()
  console.log('send aFunc');
}

async function bFunc() {
  console.log('before aFunc');
  let a = await aFunc(); // async関数を普通に呼び出してみた
  //a.then(console.log('act1')) // thenはエラーになり呼び出せない
  console.log('after aFunc');
}

bFunc()

おっと、thenの呼び出しでエラーになるので、コメントします。そして、コンソールの出力結果を見ると、次のようになっていました。前と変わりません。after aFuncより後にreceive aFuncがあるということは、aFuncのコールが終わってから実行したコードよりも後に、aFuncの中身が処理されています。つまり、async/awaitを使っても同期呼び出しされていないということです。おやおや、おかしいですね。

before aFunc
start aFunc
setup aFunc
send aFunc
after aFunc
receive aFunc

ここで、thenの呼び出しでエラーを出してみると、This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().などと書かれています。はい、ここでPromiseの知識が必要です。Promiseでは、thenやcatchメソッドを呼び出すことで、「先に進める」的な動作をするというか、それを期待しているとも言えます。thenメソッドの2つの引数にresolveやrejectメソッド(もちろん、その名前でなくてもいいのですが、たぶん、皆さんは常にそう書いていると思います)が乗ってくるので、そちらの方がお馴染みかもしれません。ともかく、Promiseを書かないで、asyncにしたからと言って、そのままawaitできるのかというと、この例で示すようにできないということになります。

では、どう書けばいいかというと、こういうことです。非同期処理部分をPromiseで包みます。既存の処理をPromise化するのに適した方法として、コンストラクターの引数に、処理を記述する方法です。処理が終わったらresolve()を呼び出します。reject()は省略します。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const p = new Promise((resolve, reject) => {
    const r = new XMLHttpRequest();
    r.open('GET', 'https://msyk.net')
    r.onreadystatechange = () => {
      if(r.readyState == 4) {
        console.log('receive aFunc');
        resolve()
        console.log('resolve aFunc');
      }
    }
    console.log('setup aFunc');
    r.send()
    console.log('send aFunc');
  })
}

async function bFunc() {
  console.log('start bFunc');
  await aFunc()
  console.log('end bFunc');
}

bFunc()

結果はこのようになります。おや?receive aFuncが、end bFuncより後にあります。これじゃあ、全然同期的に呼び出しているんじゃないですね。なんででしょうね?どこが間違っているのでしょう? これは難しいクイズですね。実はブログを執筆しながら、偶然発見してしまったトラブルです。

start bFunc
start aFunc
setup aFunc
send aFunc
end bFunc
receive aFunc
resolve aFunc

はい、答えは、aFunc関数の最後に「return p」つまり、生成したPromiseを返すというステートメントが必要になります。async内にPromise生成を記述しないと、自動的に生成されるけど、書いてしまったら、ちゃんと自分で返すようにしないといけないと言うことですね。そうすれば、コンソールには次のように出力されます。無事、非同期通信処理の結果を受け取ってから、await aFunc()の次のステートメントを実行しています。つまり、aFunc()は非同期処理が記述されているけども関数呼び出しは同期的に処理が行われたと言えます。

start bFunc
start aFunc
setup aFunc
send aFunc
receive aFunc
resolve aFunc
end bFunc

ところで、前のプログラムで、bFunc()の部分の次にconsoleを記述したら、どの順序で実行されるでしょう。一番最後? それは違いますね。receive aFuncの前にbFunc()の次に記述したステートメントが実行されます。つまり、bFunc()によって呼び出されるのは非同期関数なのです。当たり前ですね、asyncと頭に書いているから。なので、さらにbFunc()呼び出しの部分を同期的にやりたいとなると、ちょっとしつこいですが、こんな風に書かないといけません。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const p = new Promise((resolve, reject) => {
    const r = new XMLHttpRequest();
    r.open('GET', 'https://msyk.net')
    r.onreadystatechange = () => {
      if(r.readyState == 4) {
        console.log('receive aFunc');
        resolve()
        console.log('resolve aFunc');
      }
    }
    console.log('setup aFunc');
    r.send()
    console.log('send aFunc');
  })
  return p
}

async function bFunc() {
  console.log('start bFunc');
  await aFunc()
  console.log('end bFunc');
}

async function cFunc() {
  console.log('# before bFunc');
  await bFunc()
  console.log('# after bFunc');
}

cFunc()

コンソールへの出力結果は次のようになり、期待通り、awaitで非同期処理が同期処理として呼び出されています。

# before bFunc
start bFunc
start aFunc
setup aFunc
send aFunc
receive aFunc
resolve aFunc
end bFunc
# after bFunc

最終的に、ここでは、cFunc()という非同期呼び出しがありますが、他の部分のコードは全部、同期的に動いていると言える状態かと思います。ですが、全部の関数にasyncがつけられていて、非同期に動く関数であることを明確に示しています。INTER-Mediatorでの実装では、通信処理は何段階か呼び出した低いレイヤーで行なっています。そこをasync/await対応にしたら、その1つ上をやっぱりasync/await対応にしてということになり、要するに全階層がasync/await対応、つまり、全レイヤーが非同期処理するという状況にまずはなりました。asyncで非同期にして、awaitで同期的に呼び出すと言うことをあちらこちらに適用しないといけなくなりました。なんだか、非同期処理を同期的に記述してプログラムしやすくするという目的があるのに、全体的に非同期処理になってしまうではないかというジレンマに陥りました。結局、全体的に見直しが必要になったのは言うまでもありませんが、AJAXを非同期にするために、なんとか頑張ったのでした。実は、それから数年経過し、その頑張りに意味があったのか、ちょっと検証したくなったので、今日、こんなテストをやっているうちに、この記事を書こうと考えた次第です。まあ、方針としては間違いなかったのかと思います。お付き合いありがとうございます。

FileMakerの&演算子

さるDBを分析していると、計算式にすごいものを見つけた。2つの論理式のAND条件を取るためなのか、なんと、

A = B & C = D

という式が記載されている。前後が条件式なので、AND条件なんだろうか? なんじゃこらと思った。普通はC由来のプログラマ脳だと&&とやってエラーになって怒られるが、この式は当然ながら文字列結合の&演算子なのでエラーにならない。

FileMakerではfalseは数値の0、trueは数値の1だ。論理式A、Bについて、A & Bの結果は、どうなるかといえば、AがfalseでBもfalseなら “00” となりこれを論理式として評価すると数値の0でありfalseになる。A、Bが一方が1の場合、”01″ あるいは “10”となって数値化すると0でないのtrueになる。A、Bともにtrueなら、”11″を数値化して11はtrueだ。すると、&は、ORなのか! これはすごい。すごすぎる。

こんなものを納品するデータベースに書いたやつの顔が見たいね…

Amazon LightsailにFileMaker Server 19をインストール

FileMaker Server 19 for Linuxが登場したときに、VM上で動かす方法をブログに記載しました。今回は、Lightsail上で動かす方法をまとめておきましょう。Amazon Lightsailは簡単に言えば、Amazon版のVPSサービスです。コア数やメモリなどの設定が何段階かあるのですけど、FileMaker Serverの場合はメモリは4GB以上は欲しいところです。2コアで4GBメモリ、80GB SSDの設定で毎月20ドルです。EC2の同等なものがt3.mediumくらいだとすると、月に30ドルくらいになるので、それよりも安いということと、管理やセットアップが楽というのがLightsailでのマシン利用のメリットでしょう。なお、FileMaker Serverは8GB以上がメモリ推奨値です。4GBで動く保証はもちろんできませんが、大体、2GBだとサーバーは動くけどWeb系が怪しい感じ、4GBだとWebDirectも含めてとりあえず全機能は動くけど負荷増大には弱そう…という印象です。数人で使うサーバーなので、4GBで運用することにしました。

Lightsailによるクラウドコンピューターを用意

実際のセットアップを追っていきましょう。まず、Amazon Web Servicesのコンソールに入り、Lightsailのコンソールに移動します。そして、インスタンスを作成します。以下は、通常は画面を見ながら作業するところですので、設定のポイントだけを説明します。まず、ロケーションは東京を選択してあります。イメージは、Linux/Unixのうち「OSのみ」の「CentOS」を選択します。アプリが入っているものでない方が良いでしょう。

さらにスクロールしてプランを選択します。インスタンス名については、以下は既定値通りにしておきます。これで、画面下にある「インスタンスの作成」ボタンをクリックするだけです。

作成できれば、インスタンスのところにグレーのボックスで表示されます。そのボックスの右上にある点々の部分をクリックすると、ポップアップメニューが出てくるので、そこで「接続」を選択すると、新たなウインドウにコンソールが出てきます。コマンドはあとでまとめて紹介しましょう。ここで、スタティックなIPと、ファイアウォールの設定をしたいので、「管理」を選択します。

静的IPつまりは固定したIPアドレスは、いくつかは無料です。「管理」を選択した後にはそのコンピューターの設定がでできます。「ネットワーキング」のところに静的IPの設定があるので、それをクリックし、どのコンピュータのIPなのかを指定すれば、基本的にIPは割り当てられます。

また、ファイアウォールについても、同様に「ネットワーキング」のところにあるIPv4 Firewallのところで設定します。初期値は22と80番ポートだけが開いています。「ルールを追加」の部分をクリックして、TCPの443、5003、16000を指定して、ルールを追加しておきます。

ターミナル等のSSHから接続したい場合は、ログインのためのキーが必要です。こちらについては、画面上部の「アカウント」と書かれた部分をクリックして、ポップアップメニューからさらに「アカウント」を選択します。そしてページ中央のナビゲーション部分で「SSHキー」を選択します。ここでキーを指定したりもできますが、デフォルトで作られているキーをダウンロードして利用するのも良いでしょう。なお、ダウンロードした場合、そのファイルのアクセス権設定ではグループや全員に対しては何もできなくなっている必要があります。sshコマンドでは、-iオプションで、キーファイルを指定できます。ログインする時のユーザ名は「centos」です。このユーザーはパスワードなしでsudoが可能です。

コンピューターの設定は以上です。

FileMaker Serverのインストール

続いて、FileMaker Serverをインストールします。まずは、OSのアップデートを済ませておきます。

sudo yum update -y

FileMaker Serverのインストールに必要なコマンドのうち、最初から入っていないのはunzipです。unzipコマンドもインストールしておきます。

sudo yum install -y unzip

FileMaker Server 19.2.1のセットアップでは、http24というパッケージに依存しますが、こちらも最初は入っていません。以下のコマンドでインストールをします。これは、ClarisのKnowlege Baseにも記載されています。このcentos-release-sclはいろいろなソフトウエアが含まれています。その中に含まれているApacheとSSLモジュールを利用する模様です。

sudo yum install centos-release-scl -y

これで準備は完了です。あとはダウンロードしてインストールです。ダウンロードのURLは、ライセンスのページからコピペしてください。以下、XXXで一部を隠しますが、ライセンスを所有されていればわかるはずです。このまま以下のコマンドをコピペしてもダウンロードはされません。その後、unzipで展開しておきます。

curl -O https://downloads.claris.com/XXX/fms_19.2.1.23 .zip
unzip fms_19.2.1.23.zip

なお、この方法での入手だと、ダウンロードが途中で止まる場合もあります。その場合は再度トライする、あるいは一度PC/Macにダウンロードして、scp等でクラウドのコンピューターに転送しましょう。

rpmファイルが展開されれば、それを指定してyumでインストールをします。こちらの詳細は、VM上で動かす方法をご覧ください。

sudo yum install -y filemaker_server-19.2.1-23.x86_64.rpm

証明書を取得してインポート

続いて、Let’s Encryptで証明書をインストールします。設定方法は、こちらの記事を参考にさせてもらいました。このコンピューターのIPアドレスには、fms.msyk.netというFQDNと関連づけてありますので、以下はこの名前をそのまま使います。

まずはcertbotのインストールですが、いきなりはインストールできず、次の2つのコマンドを入れます。apacheプラグインは不要かもしれませんが、一応入れておきます。

sudo yum install epel-release
sudo yum install certbot python-certbot-apache

続いて証明書を発行します。

sudo certbot certonly --manual -d fms.msyk.net --agree-tos

途中で処理が止まって入力待ちになります。以下のように、通知が送られるメールアドレスを指定します。

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Enter email address (used for urgent renewal and security notices)
(Enter 'c' to cancel): msyk@msyk.net
Starting new HTTPS connection (1): acme-v02.api.letsencrypt.org

メールアドレスを共有するかどうかを、YかNで指定します。

Would you be willing, once your first certificate is successfully issued, to share your email address with the Electronic Frontier Foundation, a founding partner of the Let's Encrypt project and the non-profit organization that develops Certbot? We'd like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom.

(Y)es/(N)o: Y
Account registered.

続いて、チャレンジ、つまり一種の認証設定を行います。以下のようにターミナルで出ている状態になったら、リターンキーを押さずに、別のウインドウでログインします。

別のウインドウで、同じコンピュータにログインしたら、前述のメッセージのように、Web公開された場所に指定された名前のファイルを作り、その内容を指定通りにします。まず、FileMaker Serverは、/opt/FileMaker/FileMaker Serverディレクトリにあります。そこにある、HTTPServer/htdocsが、Webサーバーのドキュメントルートです。例えば、以下のように作業を行います。途中でエディタで作業をするのが手軽だと思うので、最初にnanoを入れていますが、適当なエディタを使ってOKです。パスを掘り、URLの最後の長い名前のファイルを作ります。もちろん、ファイル名は、ウインドウからコピペしてください。

sudo yum install nano -y
cd /opt/FileMaker/FileMaker\ Server/HTTPServer/htdocs
sudo mkdir -r .well-known/acme-challenge
cd .well-known/acme-challenge
sudo nano q3mLUhDwx-9PFQ4PihyvUJfQAlQ-PVqE0SV3KBTxx4g 

エディタでは、ターミナルに見えていた「Create a file containing just this data:」の次の行の文字列(q3mLUhDwx-9P…Vnu6zbvyJgTsの文字列)を入れてファイルとして保存します。このファイルの中身も、もちろん、ウインドウからコピペします。上記のような方法で作ったファイルはrootユーザー&rootグループになりますが、読み込み権限だけがあればいいので、全員に対するrが効いて問題なく処理できます。

ここまで準備ができれば、ターミナルの「Press Enter to Continue」と見えているウインドウに戻り、リターンキーなどを押して先に進めます。これで、通信とチャレンジが行われて、証明書が作成されます。以下のようにメッセージが出ますが、Congratulations!と出ていれば成功でしょう。その次の行以降に生成された証明書のパスが見えています。

 Waiting for verification...
 Cleaning up challenges
 Subscribe to the EFF mailing list (email: msyk@msyk.net).
 Starting new HTTPS connection (1): supporters.eff.org
 

 IMPORTANT NOTES:
  - Congratulations! Your certificate and chain have been saved at:
    /etc/letsencrypt/live/fms.msyk.net/fullchain.pem
    Your key file has been saved at:
    /etc/letsencrypt/live/fms.msyk.net/privkey.pem
    Your cert will expire on 2021-03-30. To obtain a new or tweaked
    version of this certificate in the future, simply run certbot
    again. To non-interactively renew *all* of your certificates, run
    "certbot renew"
  - If you like Certbot, please consider supporting our work by:
 

    Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
    Donating to EFF:                    https://eff.org/donate-le 

証明書をFileMaker Serverに読み込みます。ここで生成した証明書へのパスの途中にある/etc/letsencrypt/liveがrootだけに読み書きができるディレクトリなので、このままだとFileMaker Serverはファイルの読み込みができません。そこで、以下のように、FileMaker Serverの中のCStoreディレクトリに一度証明書ファイルをコピーしてfmsadminコマンドで読み込むことにします。

cd /opt/FileMaker/FileMaker\ Server/CStore
sudo cp /etc/letsencrypt/live/fms.msyk.net/*.pem .

これで、証明書などのファイル4つ(cert.pem、chain.pem、fullchain.pem、privkey.pem)がCStoreに読み込まれます。通常は同名のファイルは最初からはありませんので、コピーして問題はないでしょう。また、ファイルの所有者とグループはrootになりますが、全員に対して読み込みができるので、アクセス権については問題ありません。そして、CStoreディレクトリがカレントであることを確認して、以下のコマンドで、FileMaker Serverに証明書を読み込みます。以下のコマンドは1行です。

fmsadmin certificate import cert.pem  --intermediateCA chain.pem --keyfile privkey.pem

これで証明書がセットアップされました。3ヶ月後に失効しますが、その時に対処を考えるとして、お疲れ様でした。

FileMaker Server 19のWebテクノロジーベンチマーク

以前からやろうやろうと思っていたベンチマークを必要に迫られてやらないと行けなくなったので、やりました。昔、2008年や2009年にFileMakerのイベントでお話しさせていただいた通り、当時のFMSのカスタムWebは同時にアクセスすると、なぜか倍以上の時間がかかるみたいな状態だったのですが、そんな実装はいつの間にかなくなってそこそこちゃんと動くようになっています。今の焦点は、Custom WebとData APIの違いがどの程度なのかというところかと思います。CloudやLinux版ではCustom Webがなくなっているため、いずれData APIに移行するのか、はたまたFileMakerを諦めるのかはそろそろ心算が必要な時期ではないでしょうか?

では先に結果を書きます。評価条件などは後から記載します。以下のグラフは、レコードの検索にかかる時間を100回行った場合の平均値です。横軸は1回の検索で取り出すレコード数で、それぞれのレコード数の検索を100回ずつ行った結果です。FileMaker Serverはこの作業以外は行わない状態にしました。FXはCustom WebのXML共有を使ったものです。APIはFileMaker APIを意味します。いずれも、INTER-MediatorのサーバーサイドのAPIを使って、検索を行いました。ベンチマークというかPHPで作ったプログラムがあるMacで動いていて、別のMacでFileMaker Server 19が動いているという状況での計測値です。

レコード数が少ない状況では、圧倒的にFXが早いです。200レコードの場合でもAPIの方は3.5倍も時間がかかっています。1つには、FXが1回のネットワークアクセスで処理が終わるのに対して、APIは認証、検索、ログアウトの3つのアクセスがあるので、ある程度時間がかかるのは仕方ないと思われます。しかしながら、ほとんどデータのやりとりがない1レコードの検索でも、200msほどかかっており、これがAPIのベースの処理に必要な時間と思って良いかと思います。100レコードでも253msとあまり変わらずです。ちなみに1レコードの場合は、FXは24msほどでした。

ところが、レコードの件数が2000件を超えると、FXよりAPIの方が処理が早く終わります。つまりデータが増えても処理時間の伸びは抑えられています。大量のデータに向くかどうかはさておき、FXよりもAPIの方が、比較の上ではデータが増えてもパフォーマンスの低下は少ないと言えるでしょう。同じデータを散布図で描くと次のように、かなり直線上に乗る感じです。傾きが違うのが見て取れるかと思います。

FXとAPIの違いの原因はなんでしょうか? もちろん、アルゴリズムが違うのは違うのでしょうけど、FXはXML、APIはJSONであり、これらの処理ライブラリを考えれば、JSONの方がパフォーマンスは上げやすいのではないかと考えられます(竹内さん、アドバイスありがとうございます)。

一方、FileMaker Serverに対して並列にアクセスをかけた場合の結果を以下に示します。500件のレコードを検索する処理を100回順番に実行するのが1つのタスクとします。並行実行処理数が1の場合は、そのタスクが1つだけ動く場合なので、前述のグラフの500の場合と同じ測定をしています。並行実行処理数が2、4、8は、そのタスクを同時にこの数字だけ稼働して、全てのタスクが終わるまでの時間を測定しました。従って、2の場合は検索数は200回行っているということになります。

Custom WebのFXは、並列数が1も2も時間に違いがありません。しかし、4、8となると線形的に増加しています。どうやら、2つくらいの並列処理くらいまでは現実に並列に処理をしているのではないかと思われます。サーバー側のプロセスであるfmdcwpcは1タスクだけだとCPUの利用率は30%くらいなのに、2タスク以上だと80〜90%と本当に仕事を頑張っている様子です。ところが、APIは2並列で稼働すると時間が2倍かかっています。つまり、サーバー側で並行的に効率良く処理する仕組みが稼働してはないかと思われます。APIの処理プロセスはfmwipdのようで1タスクだと3-〜50%くらいですが、タスクを増やしてもあまりCPUの利用効率は上がりませんでした。

また、次のグラフは、前述の時間を検索数で割ったものです。FXは、このように1の場合の半分の検索時間の値が2以上8まで推移しているので、どうやら1検索の2倍のパフォーマンスを上限として、順次処理を進めることができる模様です。ところが、APIは1検索のパフォーマンス以上は出せていません。その結果、2並列では2倍の時間がかかるということが分かります。

以上のことから、APIはやはりWebアプリケーションに使うとしてもアクセスが集中しないような用途に限定すべきでしょう。FXでもある意味そうなのですが、まだ、性能が高い面があります。また、Webアプリケーションでは小さなレコード数のアクセスが多いだけにFXで運用する方が有利ではないでしょうか。FileMaker Data APIはもちろん汎用的に使えるのですが、レコード数の増大に強い面があるとしたら、やはり大量のデータ交換に耐えうる設計がなされていると見るべきでしょう。

ちなみにこの並列処理の実験で気付いたのですが、FXは、8つのプロセスであってもほぼ同時に8つのプロセスが終了します。ところが、APIは、事実上、最初に入ったプロセスの検索を終えてから、2つ目の処理に移行しています。同時にプロセスは上がっているのですが、どうやら片方が優先的に処理されるようです。キープしたコネクションを優先的に使用するのかもしれません。実際の測定値は、1つ目のプロセスが34.5秒で終わり、2つ目は69.8秒で終わります。この場合、測定値は、69.8秒としました。同時に開始し、最後に終わったプロセスの経過時間を採用しました。

さて、なぜ、APIは4や8の測定値がないのでしょう。並列度をそこまで上げれば、812のエラー(Exceed host’s capacity)が出ます。ちなみに、Developer版なので、3ユーザですね。Data APIはデータ量の制限はあるのは知っていますが、ユーザ数による並列処理制限があるのでしょうか? これは始めて知りました。ちなみに、2並列ではエラーは出ないのですが、3並列ではうち1つの並列プロセスで812のエラーになってしまうため、測定不可能としました。ちなみに、この一連のベンチマークのために、3.3Gの転送量が計上されていました。

ベンチマーク測定においては、FileMaker Server 19側は特に設定は変更していません。コマンドで、XML共有をオンにしただけです。ベンチマークのプログラムは別のPHP 7.4が稼働するMacで動かしました。その2つのMac間は、Gigabit Ethernetなので、ネットワークのパフォーマンスは非常に高い状態です。なお、並列処理を行うために、php -Sによるサーバープロセスを最大8つまで異なるポート番号で起動して、ブラウザからそれぞれのポートのphpによるWebサーバーに接続し、ほぼ並列に動く状態を作りました。このマックはQuad CoreのCore i7です。ベンチマークのプログラムはこちらです。lib/src以下にINTER-Mediatorをクローンし、INTER-Mediatorをカレントにしてcomposer updateを行った後に、dist-docs/buildup.shを稼働させて(3)を選択して、lib/INTER-Mediatorを生成します。なお、INTER-Mediatorは、Pull Reuqest #1487が含まれたものである必要があります。fms-benchのディレクトリをルートにして「php -S localhost:9000」などで起動して、「http://localhost/do_bench.php」でベンチマークを動かしました。

ここでのベンチマークの検索プログラムは、レコード数12万件余りの郵便番号データベースを使っています。検索ごとに、乱数を使って異なるスタートポジションを指定して、そこから決められた数のレコードを取り出しています。フィールド検索には入っていませんが、データベース内部をランダムに探る必要がある点からデータベース自体の入出力により近い結果が得られることを期待しています。検索やリレーションシップが絡むと、データそのものによる要因が発生するので、今回のベンチマークではスタート位置のランダム化で測定を行いました。

いずれ機会があれば、更新系の処理もやってみたいですが、機会があるかどうか定かではありません。

2022/5/5追記:サーバ復旧時に画像が復活していなかったので、画像だけ埋め込みました。

FileMaker Server 19 Linux版のインストール手順

2020/10/28に、これまでプレビュー版だったFileMaker Server 19 Linux版の正式版が出ました。インストール方法や、インストール結果などをまとめておきます。

原則として、FileMakerのなんらかのライセンスがあることと、Linuxを稼働する環境があることを前提としています。以下の手順はVirtual Boxで、CentOS 7.8をインストールして動かすところから説明していますが、クラウド、オンプロミスにしても、原則同一かと思います。それぞれの細かい点は違っていると思うので、適時読み替えてください。CentOSは、こちらのサイトから「7 (2003)」のタブを選択してダウンロードします。Virtual BoxでMacあるいはWindowsだと、x86_64のISOイメージをダウンロードすれば良いでしょう。MacではもうすぐARM64を選ぶ場合も出てきそうです。その後、サーバーのリストが出るので適当なサーバーに移動し、「CentOS-7-x86_64-Minimal-2003.iso」というファイルを選択しました。サーバーなのでミニマルを利用します。こちらをダウンロードして利用しました。

Virtual PCでの準備

Virtual PCで、ツールバーの「新規」をクリックするなどして、新たにVMを追加します。名前などは適当につけますが、最初の選択肢は次のように、タイプは「Linux」、バージョンは「Red Hat (64-bit)」を選択しておきます。FAQですが、「CentOS」という選択肢はなく、内容はLed Hatと基本部分が同じなので、『CentOSはRed Hadを選ぶ』ことで大丈夫です。

メモリを最初2GB、ディスクは8GBにしていたのですが、2GBだと、Webパブリッシングが動きませんので、4GB以上にするのが良いようです。FileMakerの推奨環境だとメモリは8GBですので、負荷が多いあるいは安定性が必要なら8GBは確保しましょう。

VM作成後、左側のリストに項目が出てくるので、それを選択して、「設定」をクリックして設定パネルを出します。ここで、メモリなどの変更はできますが、ネットワークの設定を次のようにしておきます。つまり、アダプター1を有効化して割り当てはNAT、アダプター2も有効化してこちらは「ホストオンリーアダプター」にします。おそらく、vboxnet0が選択されていると思いますが、ここの設定がいくつもある方は適切に選択してください。アダプター2は、VMの外部からのネットワーク接続の確保ですので、「ブリッジアダプター」でも構いません。

ここで先の設定のために、ホストオンリーアダプタで選択したvboxnet0の設定を確認しておきます。左上の「ツール」をクリックすると右側に表示されます。通常はvboxnet0が自動的に作成されており、192.168.56.1/24のIPアドレスになっています。この後、VMをこの範囲の固定IPに設定しますので、この設定と、後から設定するIPは矛盾がないようにする必要があります。

続いて設定の「ストレージ」を参照します。ここで、コントローラー:IDEのしたの「空」を選択し、右側の属性にある光学ドライブの右の、CDマークをクリックしてメニューを表示して、「ディスクファイルを選択」を選択します。この後、ダイアログボックスが出てくるので、ダウンロードしておいた「CentOS-7-x86_64-Minimal-2003.iso」ファイルを選択します。すると、左側で「空」の部分がファイル名に置き換わります。これで、通常はCentOSのインストールディスクから起動するようになります。

OKボタンをクリックして、設定を確定します。左側で該当する項目が選択されているのを確認して、ツールバーの起動ボタンをクリックすると、起動が始まります。(以下のVMの設定は変更前のものです)

CentOSのインストール作業

ここからはCentOSのインストール作業です。画面はVirtual Boxのものですが、他の環境でも基本、同じだと思います。なお、AWSなどのクラウド環境では事実上、インストール作業は不要ですし、サクラVPSなどのVPS環境ではそのサービスでのインストール方法がサポートページにありますので、そちらをチェックしましょう。

インストーラの最初画面では、このままリターンキーを押します。実際には「Install CentOS 7」が選択された状態がデフォルトなので、それを選択することになります。

インストーラの画面が出てきます。ここで、デフォルトは英語なのでそのままでも良い方はそのまま右下のボタンで進めます。日本語にしたい方は、左下の検索枠のjapなどと入れれば、自動的に日本語が選択された状態になるので、「続行」をクリックします。

次は「インストールの概要」が出ます。ここは少し待つと、次の図のようになり、「インストール先」に黄色いアイコンが見えていて、これはここの設定がなされていないことを示しています。ここでネットワーク名とホスト名を設定してもいいのですが、ネットワークは後からコマンドで設定することにします。「インストール先」をクリックします。なお、下まで見えない場合は、右端の部分にスクロールバーが出るので、それをドラッグして表示範囲を変更します。画面上のポインタがウインドウの外に出ないのでパニックになるかもしれませんが、ウインドウの右下に、ポインタを外に出すキー操作(この場合は左側のコマンドキー)が書かれています。

インストール先の設定画面では、「ローカルの標準ディスク」の1つの項目を選択すればOKです。2回クリックが必要な気がしますが、ともかく選択して、左上の「完了」をクリックすればOKです。

もとの「インストール概要」の画面に戻り「インストールの開始」ボタンをクリックして、インストールを進めます。

次にこのような画面になります。右側の「ユーザーの作成」をクリックします。「ROOTパスワード」の方は放置で構いません。ルートのパスワードを設定しない運用方法が現状では安全と思われるので、管理者ユーザーを作ってルートはログイン不可能にしておきます。

ユーザーの作成では、自由にユーザーを定義してください。なお、ユーザー名とパスワードは絶対に忘れないようにしてください。そして、「このユーザーを管理者にする」のチェックには絶対に忘れないようにしてください。入力後「完了」ボタンをクリックすると設定されます。

設定されました。画面下に見えているように、インストールはその間もどんどんと続きます。

「再起動」ボタンが見えればインストールの完了です。

CentOSの最初起動時にネットワーク設定する

しばらく待つと起動します。login: が見えるまで待ちます。これで起動しました。ここで、インストール時に作成したユーザー名とパスワードを入力してログインをします。このコンソールでの作業はやりにくいので最低限にしたいのですが、ネットワーク設定まではここでやってしまうのが良いと思います。

ログイン後、以下コマンドで、ネットワークの状況をみてみます。この2つのコマンドにより、enp0s3とenp0s8の2つのネットワークアダプタがあることが分かります。Virtual Boxの場合は前者がアダプター1で、後者がアダプター2です。ちなみに、NATつまりアダプター1は、このVMがクライアントになってインターネット接続するために設定されたものです。アダプター2は前に説明したように、VMへの接続ができるようにするためのものです。なお、いずれのコマンドを見ても、IPv4のアドレスは見えず、まだネットワーク接続されていない状態になっています。

ip a
nmcli connection

次のようにコマンド入力をして、NAT側(アダプター1)はDHCPによるIP設定、ホストオンリーアダプター側(アダプター2)は固定IPに設定します。最初の4行は固定IPの設定です。コマンドはややこしいですが、何を設定しているのかは容易に想像できると思います。ここでは、192.168.56.19を固定IPにしています。6, 7は、起動時に自動的にアクティブになるようにするための設定です。なお、DHCP設定は何も指定しない場合にその方法でIPアドレスが設定されます。最後は、ホスト名の設定です。これはsudoが必要ですので、自分のパスワードで認証して続けます。ホスト名の確認はhostnameコマンドを利用します。

nmcli connection modify enp0s8 ipv4.address 192.168.56.19
nmcli connection modify enp0s8 ipv4.gateway 192.168.56.1
nmcli connection modify enp0s8 ipv4.method manual
nmcli connection modify enp0s8 ipv4.dns 8.8.8.8
nmcli connection up enp0s8
nmcli connection modify enp0s3 connection.autoconnect yes
nmcli connection modify enp0s8 connection.autoconnect yes
sudo nmcli general hostname centos.msyk.net
hostname

これでネットワーク設定ができたので「sudo reboot」コマンドで再起動します。

再起動後は、Macだと普通にターミナルで接続します。その方が、画面が見やすいなど作業効率が良いからです。要するにターミナルのウインドウでsshコマンドで接続するのですが、例えば「ssh-copy-id msyk@192.168.56.19」で、デフォルトの鍵ファイルをサーバーに登録すれば、以後は「ssh msyk@192.168.56.19」でパスワードを入れなくても接続は可能です。もちろん、ユーザー名とIPは指定したものです。ssh-copy-idコマンドの実行時にはサーバーのフィンガープリントの登録確認や、アカウントのパスワード入力も必要になります。この辺りの情報は他のサイトをご覧ください。以下は、ターミナルで通常通り接続できた状態であるとします。

ログインできれば、IPアドレスを確認しておきます。このコマンドはいろんな意味で覚えやすいのですが、出力結果はコンソールではもう少し見やすハズです。enp0s3とenp0s8に、それぞれ10.0.2.15、192.168.56.19が設定されていることが分かります。

$ ip a
1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp0s3: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:50:61:b7 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic enp0s3
valid_lft 83583sec preferred_lft 83583sec
inet6 fe80::ca47:2b2f:8c68:439b/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: enp0s8: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:a4:c7:ae brd ff:ff:ff:ff:ff:ff
inet 192.168.56.19/32 brd 192.168.56.19 scope global noprefixroute enp0s8
valid_lft forever preferred_lft forever
inet6 fe80::20fd:6331:59b8:1ec0/64 scope link noprefixroute
valid_lft forever preferred_lft forever

また、以下のコマンドを入れて、CentOSのアップデートをしておきましょう。少し時間がかかります。

sudo yum update -y

FileMaker Serverのインストール

FileMakerのドキュメントでは、wgetとunzipのインストールをせよと書かれています。unzipは確かに存在しないので、以下のようなコマンドでインストールしますが、wgetについては既存のcurlコマンドが使えるので必ずしも必要ではありません。

sudo yum install -y unzip

Apache2のインストールについては、あとで結果を書きますが、FileMaker Serverのインストール時に自動的にインストールされます。ここでは、CentOSのMinimal版だからかもしれませんが、「sudo yum list installed|grep httpd」とコマンドを入れても何も出力されず、Apache2は入っていません。従ってそのまま進めます。もし、「systemctl status httpd」により、httpdプロセスが既にアクティブになっている場合には、それは止める必要があると考えられます。「sudo systemctl stop httpd」で停止できますし、起動時に自動的に起動していたら「sudo systemctl disable httpd」で自動起動できないようにしておきます。Apache2自体は使うのですが、サービスの起動はFileMaker Serverに任せないと、動かない機能が出るのはプレビュー版で経験したことがあります。

では、FileMaker Serverダウンロードです。私はFDS会員なので以下のようなページが供給されていますが、そこにあるFileMaker Server 19のCentoOS Linuxの部分でコンテキストメニューをだし、「リンクアドレスをコピー」を選んで、URLをクリップボードにコピーします。

そして、「curl -O 」と手入力し、そしてペーストすることで以下のようなコマンドになるので、これで、カレントディレクトにダウンロードすることができます。URLは汎用的なもののように思えるのですが、一部は「XXX」に変えてあります。ここでは「fms_19.1.2.234.zip」というファイルがダウンロードされるので、unzipコマンドで展開すると、同じディレクトリにドキュメントやインストーラファイルが展開されます。ドキュメントは、設定支援インストール構成ファイル「Assisted Install.txt」を利用する場合には一読しましょう。そうでない場合は、追加の情報はありません。

$ curl -O https://downloads.claris.com/XXX/fms_19.1.2.234.zip
$ unzip fms_19.1.2.234.zip
$ ls -l
合計 682052
-rw-rw-r--. 1 msyk msyk 155 10月 17 10:04 Assisted Install.txt
-rw-rw-r--. 1 msyk msyk 24950 10月 17 10:44 FMS License (English).rtf
-rw-rw-r--. 1 msyk msyk 30689 10月 17 10:44 FMS License (French).rtf
-rw-rw-r--. 1 msyk msyk 26969 10月 17 10:44 FMS License (German).rtf
-rw-rw-r--. 1 msyk msyk 28616 10月 17 10:44 FMS License (Italian).rtf
-rw-rw-r--. 1 msyk msyk 79026 10月 17 10:44 FMS License (Japanese).rtf
-rw-rw-r--. 1 msyk msyk 27934 10月 17 10:44 FMS License (Spanish).rtf
-rw-rw-r--. 1 msyk msyk 5398 10月 17 10:04 README_Installation_English.txt
-rw-rw-r--. 1 msyk msyk 6225 10月 17 10:04 README_Installation_French.txt
-rw-rw-r--. 1 msyk msyk 6062 10月 17 10:04 README_Installation_German.txt
-rw-rw-r--. 1 msyk msyk 5906 10月 17 10:04 README_Installation_Italian.txt
-rw-rw-r--. 1 msyk msyk 6327 10月 17 10:04 README_Installation_Japanese.txt
-rw-rw-r--. 1 msyk msyk 6074 10月 17 10:04 README_Installation_Spanish.txt
-rw-rw-r--. 1 msyk msyk 350402832 10月 17 10:50 filemaker_server-19.1.2-234.x86_64.rpm
-rw-rw-r--. 1 msyk msyk 347731241 10月 30 10:06 fms_19.1.2.234.zip

続いて、ディレクトリにあるrpmファイルを特定し、以下のようにインストーラのコマンドを入力してインストールを行います。そこそこ時間がかかります。「Perform pre-installation…」と出てくるまで待ちます。このメッセージが出れば、質問に答える必要が出てきます。

$ sudo yum install -y filemaker_server-19.1.2-234.x86_64.rpm
[sudo] msyk のパスワード: xxxxxxx
読み込んだプラグイン:fastestmirror
filemaker_server-19.1.2-234.x86_64.rpm を調べています: filemaker_server-19.1.2-234.x86_64
filemaker_server-19.1.2-234.x86_64.rpm をインストール済みとして設定しています
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ filemaker_server.x86_64 0:19.1.2-234 を インストール
:
インストール中 : 1:java-1.8.0-openjdk-1.8.0.262.b10-0.el7_8.x86_64 115/141
インストール中 : ImageMagick-6.9.10.68-3.el7.x86_64 116/141
インストール中 : libfontenc-1.1.3-3.el7.x86_64 117/141
=== Perform pre-installation…

最初の質問は、使用許諾に従うかどうかです。使用許諾はzipファイルに入っていますが、lここでは「y」を選択するしかないでしょう。

I confirm that I have read and agree to the terms of the Claris FileMaker Server Software License Agreement included with the software.
Agree (y) Decline (n) [y/n] y

続いては、FileMaker Serverとして起動するか、WebDirect Workerとして起動するかを選択します。最初は前者なので、「0」と入力します。

0 ) Claris FileMaker Server
1 ) Claris FileMaker WebDirect Worker
Choose 0 to install Claris FileMaker Server or 1 to install Claris FileMaker WebDirect Worker. [0/1] 0

続いて、管理者のユーザー名、パスワード、パスワードリセット用の4桁数字のPINナンバーをそれぞれ入力して、リターンを押します。メッセージをよく見ると、fmsadminグループが作成され、現在ログインしているユーザーがそのグループのメンバーに登録されています。

Perform installation for Claris FileMaker Server…
Set up the Claris FileMaker Server Admin Console account for Claris FileMaker Server.
Use this account when you sign into Claris FileMaker Server Admin Console.
Enter User Name: admin
Create a password to sign into Claris FileMaker Server Admin Console.
Enter password:
Confirm password:
Create a 4-digit PIN needed to reset Claris FileMaker Server Admin Console account password via the command line interface.
Enter PIN:
Confirm PIN:
Set Claris FileMaker Server Admin Console account information.
Claris FileMaker Server is being installed by msyk to run as fmserver of fmsadmin group…
Create fmsadmin group…
Create fmserver user in fmsadmin group…
Add msyk user to fmsadmin group…

その後、インストール作業が再開されて、いろんなメッセージが見えますが、ここはしばらく傍観します。

インストール中 : filemaker_server-19.1.2-234.x86_64 118/141
=== Perform post-installation…
Set up core dump location at /var/crash…
Deployment type: Claris FileMaker Server
Retrieved Claris FileMaker Server Admin Console account information from cache.
Install default license certificate.
Create a default Claris FileMaker Server configuration with Japanese locale.
Open HTTP connection port 80…
Open HTTPS connection port 443…
Open Claris FileMaker Server connection port 5003…
Open ODBC connection port 2399…
Open Claris FileMaker Server Admin Console connection port 16000…
Enable and start HTTP server service…
Enable Claris FileMaker Server service…
Reload system daemons…
Check for Avahi daemon…
Avahi daemon has not started yet, wait for 2 seconds…
Avahi daemon has not started yet, wait for 2 seconds…
Avahi daemon has not started yet, wait for 2 seconds…
Avahi daemon has not started yet, wait for 2 seconds…
Avahi daemon has not started yet, wait for 2 seconds…
Start Claris FileMaker Server service…
Claris FileMaker Server service has started…
Waiting for connection session…
Sending Claris FileMaker Server Admin Console account information to Claris FileMaker Server…
Claris FileMaker Server Admin Console account is set up successfully.
HTTP Server has not started yet, wait for 2 seconds…
HTTP Server has not started yet, wait for 2 seconds…
HTTP Server has not started yet, wait for 2 seconds…
HTTP Server has not started yet, wait for 2 seconds…
HTTP Server has not started yet, wait for 2 seconds…
Warning! Failed to start HTTP server, please reboot the system.
インストール中 : 1:xorg-x11-font-utils-7.5-21.el7.x86_64 119/141
インストール中 : 1:cups-libs-1.6.3-43.el7.x86_64 120/141
インストール中 : libtiff-4.0.3-32.el7.x86_64 121/141
:

次のインストール項目に移る前に「Warning! Failed to start HTTP server, please reboot the system.」と見えています。これはインストール作業直後に「sudo reboot」をしなさいということです。しばらく待ってプロンプトが」出れば、「sudo reboot」とコマンドを打ち込んで再起動します。

インストール後のFileMakerサーバーのセットアップ

VMが再起動したら、ブラウザから、以下のURLで接続をします。もちろん、IPアドレスは実際のIPアドレスにします。そして、httpsであること、16000であることを確実に設定してください。これで、Admin Consoleが出てきて、あとはGUIでの設定になります。もちろん、ログインアカウントは、FileMaker Serverのインストール中に指定したユーザー名とパスワードになります。なお、おそらくは自己署名証明書で当初は運用するので、ブラウザはすんなりと画面は出してくれないと思いますが、いくつかの操作をすればページは開きます。

https://192.168.56.19:16000

セットアップ状態の確認

まず、httpdつまりApache2がインストールされたかを見てみると、このように元々入っていなくても、FileMaker Serverのインストーラによって自動的にセットアップされていることが確認できました。

$ yum list installed | grep http
httpd.x86_64 2.4.6-93.el7.centos @base
httpd-tools.x86_64 2.4.6-93.el7.centos @base

ですが、以下のコマンドの結果のように、httpdサービスはinactiveになっています。これは、httpdサービスとしては起動していないことを意味します。httpdデーモンの起動はFileMaker Serverが行います。

$ systemctl status httpd
● httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled)
Active: inactive (dead)
Docs: man:httpd(8)
man:apachectl(8)

ファイアウォールの設定も自動的に行われています。以下のコマンドで確認できますが、portsに設定が見えています。servicesじゃないのかよと思ってしまいますが、ともかくポートは開いています。

$ sudo firewall-cmd --list-all --zone=public
public (active)
target: default
icmp-block-inversion: no
interfaces: enp0s3 enp0s8
sources:
services: dhcpv6-client ssh
ports: 80/tcp 443/tcp 5003/tcp 2399/tcp 16000/tcp
protocols:
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:

Systemd配下で動くFileMaker Serverってどうなっているのだろうと思って/etc/systemdを探ったら、例えば、以下のようにすることで、サービスとして動いていることの確認はできそうです。psコマンドよりかは分かりやすい気がします。ただ、「sudo systemctl stop com.filemaker.httpd.start」とやって止まるかと言えば、おかしなメッセージが出てきて、startでは元に戻らない状況になってしまったので、起動や停止は、fmsadminコマンドなどを利用する方が確実かと思います。

$ sudo systemctl status com.filemaker.httpd.start
● com.filemaker.httpd.start.service - Filemaker.com monitor system and start httpd
Loaded: loaded (/etc/systemd/system/com.filemaker.httpd.start.service; enabled; vendor preset: disabled)
Active: active (running) since 金 2020-10-30 10:35:40 JST; 19min ago
Process: 1207 ExecStart=/usr/bin/env /opt/FileMaker/FileMaker Server/HTTPServer/bin/httpdctl start -d (code=exited, status=0/SUCCESS)
Main PID: 1417 (httpd)
CGroup: /system.slice/com.filemaker.httpd.start.service
├─1417 /usr/sbin/httpd -k start -D FILEMAKER -f /opt/FileMaker/FileMaker Server/HTTPServer/conf/httpd.conf
├─1420 /usr/sbin/rotatelogs /opt/FileMaker/FileMaker Server/HTTPServer/logs/error_log.%Y-%m-%d-%H_%M_%S 10M
├─1421 /usr/sbin/rotatelogs /opt/FileMaker/FileMaker Server/HTTPServer/logs/fmsadminserver_error_log.%Y-%m-%d-%H_…
├─1422 /usr/sbin/rotatelogs /opt/FileMaker/FileMaker Server/HTTPServer/logs/ssl_error_log.%Y-%m-%d-%H_%M_%S 10M
├─1423 /usr/sbin/rotatelogs /opt/FileMaker/FileMaker Server/HTTPServer/logs/access_log.%Y-%m-%d-%H_%M_%S 10M
├─1424 /usr/sbin/rotatelogs /opt/FileMaker/FileMaker Server/HTTPServer/logs/fmsadminserver_access_log.%Y-%m-%d-%H…
├─1425 /usr/sbin/rotatelogs /opt/FileMaker/FileMaker Server/HTTPServer/logs/fmsadminserver_ssl_request_log.%Y-%m-…
├─1426 /usr/sbin/rotatelogs /opt/FileMaker/FileMaker Server/HTTPServer/logs/ssl_access_log.%Y-%m-%d-%H_%M_%S 10M
├─1427 /usr/sbin/rotatelogs /opt/FileMaker/FileMaker Server/HTTPServer/logs/ssl_request_log.%Y-%m-%d-%H_%M_%S 10M…
├─1431 /usr/sbin/httpd -k start -D FILEMAKER -f /opt/FileMaker/FileMaker Server/HTTPServer/conf/httpd.conf
├─1432 /usr/sbin/httpd -k start -D FILEMAKER -f /opt/FileMaker/FileMaker Server/HTTPServer/conf/httpd.conf
├─1433 /usr/sbin/httpd -k start -D FILEMAKER -f /opt/FileMaker/FileMaker Server/HTTPServer/conf/httpd.conf
└─2131 /usr/sbin/httpd -k start -D FILEMAKER -f /opt/FileMaker/FileMaker Server/HTTPServer/conf/httpd.conf

10月 30 10:35:39 centos.msyk.net systemd[1]: Starting Filemaker.com monitor system and start httpd…
10月 30 10:35:39 centos.msyk.net env[1207]: [Fri Oct 30 10:35:39.773253 2020] [proxy:warn] [pid 1257:tid 139647227140…haring
10月 30 10:35:40 centos.msyk.net systemd[1]: Can't open PID file /var/run/httpd.pid (yet?) after start: No such file …ectory
10月 30 10:35:40 centos.msyk.net systemd[1]: Started Filemaker.com monitor system and start httpd.
Hint: Some lines were ellipsized, use -l to show in full.

カスタムWebパブリッシングは、リリースノートにあるように、非サポートとなっています。SETコマンドで、CWPCONFIGの属性であるENABLEXMLへの書き込みはできませんし、そもそも以下のようにCWPCONFIGの設定の読み取りすらできません。機能がないので、設定がないのは当然ということになりますね。

$ fmsadmin GET CWPCONFIG
username (msyk):admin
password:
Error: 21 (Not Supported)

とりあえず、正式版初インストールのレポートです。何かあれば、書きたします。

FileMaker 19のJavaScriptはES6で書けるのか?

FileMaker 19が発表されました。1番の目玉は、JavaScriptということでチェックしてみました。Webビューアは便利な機能である一方、MacとWindowsでHTMLのレンダリングエンジンが違うことが非常に不便なところであり、結果的に機能制約の多いWindows版で開発して、Macでチェックするということになっていたかと思います。また、JavaScriptに関する追加機能である、JavaScriptからFileMakerのスクリプトを呼び出すことも試してみました。

まず、以下のようなレイアウトを作りました。下半分のテキストを、上半分にあるWebビューアに表示するものです。WebビューアのURLとして「data:text/html,下半分のテキスト」となるような実験環境を作りました。まず、navigator.userAgentの結果をみてわかるように、Internet Explorer Ver.11相当です。Trident 7.0という文字列がそれを示しています。手元で調べた結果だと、FM17がTrident 7.0でした。それ以前のものは残っていないので、わからないのですが、いずれにしても、レンダリングエンジンはFM19では変わっていません。

JavaScriptの関数のなかに、「FileMaker.PerformScript()」という記述があります。このように、JavaScriptからスクリプトを呼び出すことができます。引数は、スクリプト名とスクリプトを引数を指定します。ただし、この関数が使えるようにするには、Webビューアのオプション「JavaScriptによるFileMakerスクリプトの実行を許可」にチェックを入れておく必要があります。最初はオフになっているので、入れ忘れないようにしないといけません。

ここでは次のような、単に1行だけのTestスクリプトを定義しておきました。

Webビューア上に見えているボタンをクリックして、JavaScriptを実行してみます。すると、隣のテキストフィールドをquerySelectorで参照して、そこに入力した文字列をvalueプロパティで取り出し、それをFileMaker.PerformScriptの引数に渡してTestスクリプトを実行し、ダイアログボックスが表示されています。なるほど、JavaScriptとの連携がだいぶんとやりやすくなりました。

なお、もう1つの新機能は「WebビューアでJavaScriptを実行」というスクリプトステップです。このスクリプトステップは1つの関数を呼び出すものです。これにより便利そうなのは、正規表現での文字列処理あたりがまず浮かびます。

ここで、コメントにしている行を有効にしてみます。バッククォートで囲むと、文字列のテンプレート処理が可能になり、${変数}で、変数やあるいは式の値を文字列に埋め込みができます。しかし、残念ながら何も表示されなくなります。つまり、JavaScriptのコンパイルで失敗していて、JavaScriptのプログラムが動かない状態になっています。もちろん、ボタンを押しても何も起こりません。なお、変数定義のlet、constについては、IE11がサポートする数少ないES6機能の1つです。

Mac版のFM19だとこのように、文字のテンプレート処理もきちんと動いています。userAgentで出てくる結果は、OSに入っているSafariと同一でした。ChromeやEdgeは「AppleWebKit/537.36」と出るので、FileMakerはOSに入っているものを利用していると思われます。

そういうわけで、FM19でJavaScriptがどの程度ジャンプするのか気になったところですが、Windows版がIE11相当なエンジンであるので、残念ながらES6(正しくはES2015というべき?)でのプログラミングができる状況ではありません。Babel使うくらいなら、ES5というか、IE11互換で記述した方が良さそうですね。

ちなみに、現在、JavaScriptで作られた様々なライブラリは「ES6のみ」という限定がされているものはまだ少数派ですが、Reactなんかは最初からES6で作られていますし(ReactアプリをIE11で動かす方法はある)、INTER-Mediatorの最新版はIE非対応としています。一方、メジャーなライブラリはIE11対応しているので、IE11エンジンだからといって、世間のライブラリが使えないということはほとんどないと思います。いずれにしても、Claris社がすっきりと「ES6対応です」と言わないのは何故だろうかと思っていたのですが、そういうことだったわけです。

[続開発プロセス#14] エンクロージャー/リピーターの制約

前回の記事で、HTMLのテンプレートの設計、つまりページファイルの設計を出すときに、非常に根深いところにある制約の話が必要であることに気付きました。ですが、前の記事が長いだけに、別途記述するということで、連投となりました。

話を少しすっ飛ばしたのは、ページのモックアップにある左側の、カテゴリ一覧の設計です。まず、オブジェクト指向的に正しくモデリングをするということを進めます。以下の図は、ともかく見えるオブジェクトとをクラスとして記述して、全体をまとめるであろうCategoryBox配下に並べたところです。オレンジ色のメモに記述したように、ここには矛盾があります。ここでは、「小分類は必ず何かの大分類に所属している」前提があるとしているので、このように、大分類に所属しない小分類が存在しえるモデルは、モデル自体が間違っているので、正しい設計とは言えません。

ここで、小分類は大分類に所属することをモデルとして表現してみます。小分類の名前とボタンがあり、それぞれ、MinorCategoryLabel、MinorCategoryButtonクラスですが、既に存在する大分類の名前であるMajorCategoryLabelに関連づけてみました。これだと、小分類の存在は大分類の存在を前提になっている状況は示しています。しかしながら、オレンジ色のメモに記述したように、小分類の中でラベルとボタンがバラバラであり、ラベルが4つでボタン3つでも成り立ちそうなモデルです。ここで、1つの小分類は、何らかの1つの対象が保持する形にであるべきではないかと考えます。

そこで、次のように、小分類の項目をまとめるMinorCaterogyBoxクラスを間に入れてみました。この辺りで分類そのものをデータベースのテーブルに入れていることを考えれば、MinorCategoryLabelなどフィールドに相当するものが、レコードに相当するMinorCategoryBoxにまとめられて、それが1つの大分類に所属するという状況をだいぶんと正確に示してきていることが伺えます。ここで、改めて大分類に目を向けます。ここでの大分類の名前を示すMajorCategoryLabelは大分類の1項目の中の1つのフィールドであり、属性の1つにすぎません。これ自体が大分類項目1つを代表するのはちょっと無理があるのではと考えられますし、小分類の類するから、「大分類の1つの項目をまとめるものがあれば良い」ことが考えられます。

そして、次の図のように、大分類の1レコードに対応するMajorCategoryBoxが存在し、大分類の1つのフィールドは、このMajorCategoryBoxから1対1で表現しています。また、1つの大分類から複数の小分類が関連づけられることも、MajorCategoryBoxとMinorCategoryBoxの1対多の関係として表現できています。ここで、エンクロージャー/リピーターの関係を1対多の関係に持ち込み、データベースのデータをバインドしたいと考えます。これはINTER-Mediatorの肝になる機能です。

ここで改めて、エンクロージャー/リピーターをモデルとして示します。HTMLのテンプレートは何がクラスで何がオブジェクトかというのは視点あるいは場面によって変わると思われますが、HTMLの記述可能な範囲において、エンクロージャーがリピーターを含み、リピーターにはターゲットノードが含まれる階層構造になっています。オブジェクト図で記述すると明白なように、これらはクエリー結果のリレーション/レコード/フィールドと言った階層に一致しています。

INTER-MediatorはDOMの領域でテンプレート処理を行っています。テキストのレベルで行うより高い粒度になりますが、一方でそのための制約が発生しています。このリレーションとテンプレートを合成するアルゴリズムでは、テンプレートにあるリピーターを一度複製を取って削除します。複製は所属するノードも含めるので、複製はテンプレートそのままにターゲットノードを含めて保存されています。そして、リレーションの中にレコードがあればその保存したリピーターを複製して、エンクロージャーの子要素とします。この作業を繰り返します。この流れによってリピーターがレコードの数だけ繰り返されて、一覧表示が完成します。

このとき、必須ではないのですが、エンクロージャーの子要素は全てリピーターである方が最終結果は予測付きやすいです。仮にエンクロージャーにリピーターでない子要素がある場合、現状は結果的にリピーターが繰り返す前に集まってしまいます。それでいいのかもしれませんが、そうなら、エンクロージャーの前に記述すればいいことなので、リピーター以外はエンクロージャーの子要素にはしないというルールは問題ありません。ところが、前要素→リピーター→後要素のように並んでいた場合、気持ちは前要素→展開したリピーター→後要素のようになって欲しいかもしれませんが、そうなりません。そこで、header、footerとなるリピーターやseparatorとなるリピーターも定義しています。とにかく、テンプレートで並べたリピーター以外の要素が展開後どうなるかを意識しなくてもいいように、リピーター以外は子要素にないのが良いと考えます。これは、INTER-Mediatorの大きな制約なります。

ここで、エンクロージャーとリピーターの展開は、1対多の箇所に対応づけられるのがわかったので、以下の図のように、ステレオタイプで、enclosure/repeater/tareget nodeを割り当てました。しかしながら、ここで、MinorCategoryBoxがリピーターなのですから、その上位であるMajorCategoryBoxがエンクロージャーになる必要があります。もちろん、そうすると、MajorCategoryBoxはエンクロージャー件、さらに上位のコンテキストに対するリピーターでもあります。この状態でINTER-Mediatorは稼働するのではありますが、こうなると、MajorCategoryBoxの子要素に、MinorCategoryBoxとMajorCategoryLabelの2つの要素が割り当てられます。前者はリピーターですが、後者はリピーターではなく、INTER-Mediatorの制約に反します。そこでどうするかを考えないといけません。

ここで、新たに、MinorCategoryConteinerクラスを導入します。つまり、レコードの数に応じて複数存在するMinorCategoryBoxクラスのオブジェクトをまとめる存在のものを用意します。HTMLではこの記述は、汎用タグのdivやspanで簡単にできます。すると、MajorCategoryBoxはMajorCategoryLabelとMinorCategoryContainerを含み、リピーターを含まないことになります。(なお、AllSelectButtonは本来はエンクロージャーであるCategoryBoxの外に出すべきです。この要素は、リピーターではないからです。)

このような、エンクロージャーとリピーターの間の制約があり、そのために、ここでは4段階のdivタグで囲まれるようなHTMLコード例を示したわけです。

この制約は一般には考えにくい事実かもしれません。HTMLは独特の柔軟性を持っていることを利用しているということと、やはりデータベースの検索結果であるリレーションという表形式のデータを自動的に展開するという仕組みが微妙にコンフリクトする場面でもあります。リレーションの結果を調整するのはもちろんですが、一方で、HTMLのテンプレートも、リレーションをマッピングできるように調整しないといけないということになります。

元々、INTER-Mediatorは、trタグの要素がtbodyタグ要素の子要素として繰り返すことで、レコードの展開を「繰り返し」の拡張命令的な処理をしなくても実現できることを発見したのが始まりです。tbodyタグの子要素にはtrタグ要素しか登録できません。それ以外のものはテーブルの外にはみ出ますし、定義上、trしか存在できないはずです。また、同時にselectとoptionについても同様な関係があるとして、これらの2組は自動的にエンクロージャー/リピータとして認識するようにしました。前述のカテゴリ一覧も、tableの中にtableを作る方法でできると説明してしまえが一言で終わってしまい、あとは試行錯誤してコンテキスト定義を行えば動くでしょう。しかしながら、data-im-control=”enclosure” / data-im-control=”repeater”により、どんなタグでもエンクロージャーやリピーターになりうるようにしたのですが、その結果、明示的にエンクロージャーとリピーターのタグを記述しないといけなくなってしまっています。デザイン的には不要でも、INTER-Mediatorのアルゴリズムに合わせるために、ここでのMinorCategoryContainerのような要素が必要になります。