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のような要素が必要になります。

[続開発プロセス#13] INTER-Mediatorによるアプリケーション

制約のある状況での開発プロセスについて、MVCフレームワーク、FileMakerでそれぞれ簡単ですが、設計をやってみました。そして、本題であるINTER-Mediatorでの設計を今回は説明します。

まず、以下の図は、共通の設計モデルを示すクラス図に、INTER-Mediatorで考慮すべきことをメモとして追加したものです。まず、CategoryModelに紫色のメモで示したように、カテゴリーの全リストはINTER-Mediatorでは不要なので、ここメソッドを削除しています。また、PickingUpクラスやMemoListクラスの属性は、結果的にデータベースのテーブルにあるフィールドが必要になるので、ここではどんなデータがUIのレイヤーに来るのかを明示するため、属性にフィールド名を追加しました。

まず、前の図の下の方にあるステレオタイプがtableの部分をみてください。これは、ドメインモデリングの作業で作ったものと同じであり、もちろん、このままデータベースのスキーマとして定義します。アクセス権の設定なども行、データベースの準備は進めておきます。そして、ステレオタイプがmodelのものは、「データベースから何が抽出されたいか」を操作で示しています。これらの機能を、この後の定義ファイルの設計において、コンテキストとして取り出せるものを用意する段階で改めて調べます。

そして、前の図の上半分に記述されたUIレイヤーについて検討を進めます。まず、UI Componentステレオタイプの2種類のものは、INTER-Mediatorでは実装された機能やプラグインとして用意されているので、それらを呼び出すだけでOKです。ここは機能そのものやプラグインの制限が影響なければ特段の配慮は通常は不要です。

前の図で、緑色に塗ったクラスは、結果的にはある段階で、エンクロージャーとして展開可能なものと対応づけるようにします。INTER-Mediatorでの設計のポイントは、エンクロージャーとリピーターの特定、つまり、どこでこれらの要素を出すのかによります。ここでは設計段階なので、まずは、データベースから得られる1つのリレーションに対して、UIレイヤーでは1つのクラスとして認識するところまでを進めます。つまり、メモの一覧がデータベースにアクセスしてリレーションとして得られます。リレーションなので複数のレコードですが、そのレコードには、緑色のクラスの属性にあるようなフィールドが存在するはずです。その意味で、緑色のクラスがリレーションかあるいはエンクロージャーを示すのであれば、その属性というのは途中をすっ飛ばした不正確な記述になるかと思いますが、ここでは勘弁性を優先してこのように記述します。この記述については、定義ファイルの設計でも同様な考慮をします。

MemoListクラスは、データベースから得られたメモのリストを展開するという意味ではわかりやすいでしょうけども、PickingUpは、大分類と小分類が入り乱れます。一応、1つのクラスPickingUpとして記述しましたが、それは正しいでしょうか? ですが、まずはカテゴリを取り出してボタンと一緒に表示するという仕組みが必要であることを表明するためとしてPickingUpクラスの存在は無駄なものではありません。しかしながら、大分類/小分類というかなりややこしい状況があります。

ここから、HTMLのテンプレートを実際に作ります。もちろん、これはINTER-Mediator用語で言えばページファイルを作成に入ります。ここで解決すべきことは、エンクロージャーとリピーターの特定になります。もちろん、要素の取り出しも重要ですが、INTER-Mediatorではこの「エンクロージャーとリピーターの特定」さえできれば、モデリングは終わったも同然だと考えられます。ただ、今回は設計プロセスを追うのが趣旨なので、詳細に記述します。

エンクロージャーとリピーターの特定に必要なことは、得られるリレーション間の関連です、つまり、リレーションシップの特定に他なりません。まず、前のシステムデザインで記述されたMemoListについては、メモが複数あるので、リスト全体とメモ単体の間に1対多の関係があり、ここで1に対してエンクロージャー、多に対してリピータを割り当てれば良いので、ここでのエンクロージャーとリピーターの識別は容易です。なお、結果的に1:*になる部分は*の数が一定しない、つまりデータに応じて変わる部分であり、結果的にデータベースからのリレーションを展開することになるでしょう。ボタンが10個並ぶなら、1:10になるので、これらはスタティックなものとも言えます。つまり、HTMLのテンプレートをモデリングした場合に出てきた1:*の部分がエンクロージャーとリピーターとして記述すべき箇所になるということが言えます。

しかしながら、ここで、ステレオタイプがtableの部分をみてください。memoテーブルのtop_category_id、sub_category_idは、それぞれcategoryテーブルのcategory_idと対応づけていて、ここで各メモがどのカテゴリーに分類されているのかを記録できるようになっています。また、category同士のリレーションシップも設定されており、これにより、大分類と小分類のレコードの関連づけが行われています。このようなスキーマにみられるリレーションシップは、エンクロージャーとリピーターの内部にさらにエンクロージャーとリピーターが存在することを示唆します。そのことを考慮して、ページファイルの中心部分をモデル化したものは以下の通りです。

まず、HTMLテンプレートの設計の上部を説明しましょう。ここは、システム設計でのPickingUpの部分です。ここでは、大分類と小分類が一覧されているという見え方になっていますが、もう少し詳細に見ると、1つの大分類に対するその小分類が複数リストになっています。つまり、大分類と小分類が1対多の関係になっているものが、さらに、大分類の数だけ並んでいるということになり、全体から見れば、多数の大分類があり、さらに、その大分類に対応した多数の小分類があるという階層構造になっています。これらは、ネストしたエンクロージャー/リピーターで定義します。外側のエンクロージャー/リピーターは次のように定義するとします。class属性は、HTMLテンプレートでのクラス名をつけておきます。

<div data-im-control="enclosure" class="CategoryBox">
    <div data-im-control="repeater" class="MajorCategoryBox">
        <div data-im="****@category_name"></div>
    </div>
</div>

このリピーターは、大分類の項目1つに対応します。なので、リピーターの中でcategory_nameフィールドを表示すると、このエンクロージャーに割り当てるコンテキストが大分類の一覧にしておくことで、ページ上には大分類だけの一覧が表示されます。コンテキスト名はまだ決めていないので、****にしておきます。

そして、このリピーターの中に、対応する小分類の一覧を表示するエンクロージャー/リピーターを定義します。次のようになります。

<div data-im-control="enclosure" class="CategoryBox">
    <div data-im-control="repeater" class="MajorCategoryBox">
        <div data-im="****@category_name"></div>
        <div data-im-control="enclosure" class="MinorCategoryContainer">
            <div data-im-control="repeater" class="MinorCategoryBox">
                <button onclick="****">選択</button>
                <div data-im="****@category_name"></div>
            </div>
        </div>
    </div>
</div>

ここで、内側のエンクロージャー/リピーターの表示を行うためにリレーションの取得を行う場合、その時の外側のリピーターで展開している現在のレコードと照合する、つまりリレーションシップを考慮することが、定義ファイルでの設定で可能です。その仕組みを利用して、内側のエンクロージャー/リピーターでは特定の大分類に対する小分類だけが表示されるようになります。

引き続いてMemoLineのリピーターから続く2つのエンクロージャーについて説明します。ここではまず、memoテーブルだけだと、大分類や小分類は主キー値を記録していることで、どの分類なのかは記録されていますが、単なる数字なので、人間が目で見て判別するのは困難ということがあります。つまり、memoテーブルの値に加えて、選択した分類の名前を知りたい分けです。名前が別テーブルにあるので、つまりはそこでリレーションシップに応じたテーブル連結をするのがSQLでの定番ですが、UIレイヤーではそういう用途にselectタグによりポップアップメニューが使われてます。ポップアップメニューは、ここでは、top_category_id値を対応させることができますが、そのcategory_idに対応する値を内部で記録しつつ、ページ上には別の文字列を表示することもできます。この場合、categoryとmemoが1対多の関係になりますが、逆にメモの一覧にある大分類のポップアップメニューには、選択された大分類項目だけでなく、選択肢として表示されるように全ての大分類の項目が必要であり、それぞれoptionタグでselectタグの内部に記述しなければなりません。つまり、selectは選択肢の提供と、選択したものの名称表記の両方の機能を持っているのです。逆に言えば、selectを適切に使えば、選択肢の用意だけで選択したものの名前はそこにあるものを使うので、一石二鳥であるとも言えます。INTER-Mediatorでは、selectとoptionの関係をエンクロージャー/リピーターとして扱うので、その関係をモデル図の右下のあたりに2箇所記述しました。

INTER-Mediatorで一番難しいのは、このエンクロージャー/リピーターの特定、そして、その階層関係での定義ではないかと思われます。エンクロージャー/リピーターを特定するには、そこにリレーションが得られるという事実と関連づければ良いのですが、階層関係になると非常に難しくなるというのはあるでしょう。やはり、動作原理を知るのがしっかりとした設計を行うためには必要なことは言うまでもありません。

ここまでのところで5つのエンクロージャー/リピーターが出てきました。よって5つのコンテキスト定義が必要という見方もできます。ここで、コンテキストを実際に定義して行く作業に入ります。実際には4つの定義で問題ないことがわかります。

ここで、コンテキスト定義は、以下の図のように、Contextクラスを元にインスタンス化したオブジェクトとして記述することにします。現実には連想配列なのでコンテキスト定義は言語の意味でのクラスを元にはしていませんが、認識可能なキーが決まっているのは、属性が定義されていることと同義ですので、この点は問題ないと思われます。実際には、コンテキスト定義を利用して、コンテキストオブジェクトが生成されます。正確に記載すると以下の図の左側、Precise wayと書かれた方のクラス図になります。つまり、コンテキストオブジェクトConcreteContextObjectはテーブルConcreteTableを継承しているか、あるいは参照することになり、その結果、ConcreteEntityの配列がstore属性から参照できるようになっています。ここで、ConcreteContextObjectをインスタンス化した時、当然ながら継承元のフィールドがそのまま所有します。ここで、テンプレートバインディングやあれこれとUMLの仕組みを試したのですが、モデルとしてしっくりくるものがありません。そこで、右側のように、Concise wayと書かれたように、コンテキストで利用する実テーブルであるTableを継承して、ContextObjectを定義します。名前は任意にして、このような目的のクラスであることをステレオタイプのcontext objectで示すことにします。こうすれば、右下のようなオブジェクトが記述でき、テーブルのフィールドをそのまま持ちます。もちろん、この表現は不正確であり、コンテキストオブジェクトはTableによるレコードの配列を持つのではありますが、そこを正確に全部記述すると図が煩雑になるだけなので、「実際にはそういうことだけども勘弁性を重視してこのように書く」という合意ができたとして、Concise wayのような表記をします。

ここでHTMLテンプレートの設計を見ながら、コンテキスト定義のオブジェクトを検討します。ここでは代表的な属性だけを記述することにします。結果をまとめると以下の図になります。

まず、カテゴリの一覧部分、つまりHTMLテンプレートの設計でいえば、CategoryBoxクラスとMinorCategoryContainerクラスを取り出します。これらはエンクロージャーなので、リレーションと対応させないといけません。ここでは図で示せるように、tableステレオタイプのcategoryクラスを継承して、top_category_listとsub_category_listのコンテキストオブジェクトクラスを定義し、それらのインスタンスとして、緑色のボックスのオブジェクトを記述しましたが、これは実際にここまでの記述は不要でしょう。記述すべきは山吹色のボックスのコンテキスト定義のオブジェクトのみです。コンテキスト定義のtop_category_listでは、viewあるいはtable属性を見ることで、リレーションの元になっているテーブルが判別できるので、結果的に薄黄色と緑色のボックスは、コンテキスト定義の情報や、スキーマの情報からすぐに入手可能であるということです。

同様にして、memo_list、minor_category_list、major_category_listのコンテキストを定義します。memoテーブル由来のコンテキストはmemo_listのみですが、残りの4つのコンテキストはcategoryテーブルを由来としています。このように、同一テーブルからであっても場面によって(つまりコンテキストによって)必要とするデータは異なり、その結果定義は別々に必要ということになります。ここで、major_category_listとyop_category_listの定義は同一なので、これは一方だけを使うことでも構いません。もちろん、名前だけが違う同一の定義が2つあっても構いません。そこは好みの問題です。定義をよく見ると、parent_category_idがnullのレコードが大分類の項目であり、このリレーションはコンテキストには強く依存せず、どこでも同じ条件で取り出しができることになります。minor_category_listとsub_category_listはどうでしょうか? relationshipの定義が微妙に違います。カテゴリのリストの方は、大分類そのものと関連があるのに対して、メモの方はメモで選択されている大分類項目と関連があります。つまり、これら2つのコンテキストは、別々のものです。つまり、場面が違うものであり別々に定義しなければならないことになります。

ここまで設計すれば、あとは手を動かして実装するだけになります。必要な属性などを追加してページファイルを作り、コンテキスト定義をまとめて定義ファイルを作ります。

ここで、HTMLテンプレートのAllSelectButtonやMinorCategoryButtonを見てください。ここで、ボタンを押して、メモの一覧に検索をかけて絞り込んだり解除したりということが必要になっていますが、INTER-Mediatorの機能ではそれは含まれていません。そういう仕組みのボタンは提供されていないのですなので、ここはどうしてもJavaScriptで実装をしなければなりません。それぞれのボタンは次のように定義しましょう。

<button onclick="redrawMemoList()"
        class="AllSelectButton">全選択</button>
    :
<button onclick="redrawMemoList($)"
        class="MinorCategoryButton"
        data-im="sub_category_list@category_id@$onclick">選択</button>
<div data-im="sub_category_list@category_name"></div>

対応するプログラムは例えば次のように作成します。つまり、memo_listコンテキストに対する検索条件を、状況に応じて切り替えれば良いということです。

function redrawMemoList(id) {
  let idValue = parseInt(id)
  INTERMediator.clearCondition('memo_list')
  if(idValue) {
    INTERMediator.addCondition('memo_list',
        {field:'sub_category_id',operator:'=', value:idValue)
  }
  const context = IMLibContextPool.contextFromName('memo_list')
  INTERMediator.constructMain(context)
}

以上がINTER-Mediatorでの実装設計になりますが、実質的には、ページファイルや定義ファイルがモデルそのものであるとも言えます。一度実装に近いクラス図などを記述するのも良いのですが、結果的にHTMLや定義ファイルを書いてしまった方が早いとも言えます。つまり、デザイン時の作成物と、目的とする作成物が近いというのがこうしたツール(いわゆるローコードツール)の特徴なのではないかと思われます。

そして、INTER-Mediatorでは、エンクロージャー/リピーターの理解は不可欠です。このようなツールごとに理解しておかないといけない特有の概念は必ずあり、それが最も注目すべき制約ということになるのではないかと思われます。

[続開発プロセス#12] FileMakerによるアプリケーション

ここまでに設計を進めてきたアプリケーションの設計図は以下の通りですが、途中でモックアップを作るときにHTMLを使ったので、それをいきなりFileMakerに切り替えるのは若干無理があると思われるかもしません。しかしながら、モックアップを作る意義は、何をしたいのかを明確にすることであり、ここまでの設計で必要な仕組みが概ね抽出できているということが重要です。クラス図にあるPickingUpとMemoListを見ることで、必要な仕組みを思い出せるはずです。ですが、やはりFileMakerは大きく違う環境です。実際にFileMakerで開発をする方々は早い段階で、FileMakerでどうすればうまく実現できるか、言い換えればFileMakerにおける制約を考慮し実現可能な設計を模索するはずです。もちろん、最初からやればいいのではと思われるところですが、以前にも書いたように、環境を固定してしまうことで、本来やりたい要求を制限し、視野の狭い発想になりがちであるため、まずは必要な仕組みや機能を抽出した上で、どのように実装環境に適応するのかを考えるべきです。

FileMakerで実装する場合、以下の設計図だと、データベースはもちろん問題なく実現可能なところですが、レイアウトに2つのTOからの展開をする方法を検討する必要があります。FileMakerではウインドウ全体を占めるレイアウトには、1つのTOしか設定できません。そのため、同一ウインドウ内に2つの「リスト」を展開するには、それぞれをポータルで展開するか、設計をもう少し変更する必要があります。今回のアプリケーションではポータルで展開する方法でもできそうですが、実際にはポータルでは制約が多くあることから避けたいと考えます。後からの設計変更に対応できない場合が出そうな気がするからです。そこで、今回は、メモのリストをレイアウトに割り当てることにします。そうなると、カテゴリの選択は、別のウインドウに出すとか、別のレイアウトにして切り替えるなどのアイデアが思いつきますが、ここではメモのリストのヘッダ部分で、分類をポップアップメニューで選択して、その選択結果を検索条件としてメモのリストを絞り込むと言うことにしました。カテゴリの一覧は制限がありますが、ポップアップメニューであればそれに近いと言うことで、このような実装にします。また、FileMakerの場合、ページネーションを入れる必要は薄いと思われるのでページネーションは不要とします。日付時刻の入力は、FileMakerの機能が利用できるので、コンポーネント群の利用は不要と考えます。こうした決定をまずはメモとして書き込んでおきました。

FileMakerでの一番大きな制約は、開発言語で使われるようなスタイルのクラス定義ができないことです。そして、レイアウトをベースに画面を構築することになりますが、リレーションシップでのTOや値一覧など、FileMaker特有のオブジェクトがあり、それらを適切に設計する必要があります。

ここでは1つのレイアウトにメモを一覧することを基調としているので、メモの内容をボディパートに配置し、リスト表示にして一覧できるようにすることが基本です。以下の図はレイアウトの設計をしたところです。LayoutからBodyPart以下の内容は特に説明は不要でしょう。これらは、MemoのTOとバインドすることで、表示と編集が可能になります。自動化されるところはこれ以上は記述する必要はないでしょう。ここで、メモの中にある大分類のポップアップメニュー(MajorCategorySelect)と、小分類のポップアップメニュー(MinorCategorySelect)です。ポップアップメニューの設定結果はフィールドに保存されるのでMemoと言うTOと結びつきますが、一方、選択肢を構築しなければなりません。そのために値一覧(ステレオタイプがValue List)をそれぞれアサインする必要があります。大小どちらの分類も、categoryテーブルを元にしたTOを割り当てますが、大分類はもちろん小分類を省いたものですし、小分類は現在選択されている大分類に応じた選択肢が出ている必要があります。ただし、レイアウトを検討している段階では、そこのTOにどんなデータが出て欲しいかと言うことをまずは明確にすることが必要です。よってTOとValu List、そしてコメントだけを記載しました。ヘッダパートはボタンが2つとポップアップメニューが2つです。このポップアップメニューは、メモの検索条件を与えるためのものであり、Memoの1レコードとは独立しています。従って、Value ListやTOも別途与える必要があります。なお、実際には何かフィールドをアサインしないとポップアップメニューは構築できないので、こちらはグローバルフィールドを定義することにします。

レイアウトとしてのモデルが作成できれば、それとデータベースを合体させて、さらに必要な定義を検討します。以下の図はこれまでの作業を合わせたものですが、クラス図で記述する場合、テーブルとTOはスーパークラス/サブクラスの関係で記述するのが良いでしょう。そのデータベースですが、いくつかフィールドを追加しています。memoテーブルのalways1は文字通り、計算結果で常に1を値として持ちます。また、categoryテーブルでは大分類なら1を返す計算フィールドを定義しておきます。他にmemoテーブルではヘッダ部のポップアップの値を受けるためのグローバルフィールドを定義しました。ここで、TO同士のリレーションシップも線で結んでおき、その時の条件をメモで記述しました。大分類は、is_parentが1のものに絞り込む必要がありますし、小分類は大分類で選択されている項目の値とparent_idが同じものに絞り込むことで現在の大分類に応じた小分類の一覧が表示されます。TOのMemoはレイアウトに割り当てられ、レイアウト上のオブジェクトとバインドされるので、それらも線を引いて表現をしました。

ここまでくれば、あとは実装です。ただ、本当にこれでいいのかと思うところもあるかと思いますので、完全ではありませんが、作ったデータベースをダウンロードできるようにしておきます。

今回のアプリケーションだと、メモの側に見える分類は、ポップアップメニューによって、項目名が見えているので、テーブル同士のmemoとcategory間に参照関係は、TOの中には明確には見えていません。値一覧を通じて間接的な関係しかありません。ER図での関係と、TOの関係すなわちFileMakerデータベースのリレーションシップは、微妙に違うと言うか、視点の違いが読み取れます。言い換えれば、FileMakerでは値一覧をリレーションシップを用いて定義をしているのですが、汎用的な設計には値一覧のような存在は顕在化していないと言うことにもなります。

実際、FileMakerで、このようなモデルベースの開発をするかと言うと、おそらくほとんどの方はやっていないとおっしゃるでしょう。最後の図のような関係は、FileMakerの様々な画面で確認は可能ですが、それらの画面で定義した様々なオブジェクトの間の関係は、頭の中で統合しているのではないでしょうか。そうして実際の開発できる方、あるいは迷いながら開発する方も含めて、FileMakerでは「試行錯誤しながら完成に近く」と言うことが可能です。結果的にこのような設計を示しましたが、ざっくりと設計してあとは動きを見ながら調整すると言うことになるでしょう。これは、FileMakerが汎用言語による開発との違いでよく語られる点ではありますが、それが可能なのは、FileMaker自体、設定できるものに制約が大きくあり、自由度が低いことから、限られた要素のコントロールしかできず、それによって逆に見通しがよくなると言うことがあるのです。もちろん、限られたコントロールでもアプリケーションが作れるほどの仕組みがあるから実用に耐えられるのです。

汎用言語で散々作ったものが設計が悪いとなって修正するのは非常に骨が折れる仕事であることはよく言われていますが、FileMakerではどうでしょうか? 実は複雑なスキーマ、大量のスクリプトによる処理が絡んでくると、FileMakerでも見通しは悪くなります。汎用言語での開発だと、設計をあらかじめしておくことで問題点を先に洗い出すことが行われます。一方、FileMakerの場合はあまりこうした設計を重視せず、むしろ試行錯誤ができる点を頼りに進めることはよくありますが、結果としてある程度作ったあたりから特定の機能が得られなかったり、ちょっとした改変が難しかったりと言うことにもなりがちです。FileMakerだからと言って設計をきちんと行わないと、汎用言語で開発する場合と同じようなことは起こると言えるでしょう。

それでもFileMakerでは試行錯誤はやってもいいとは思います。しかしながら、試行錯誤をしっぱなしでは良くありません。試行錯誤した結果を設計という俯瞰した見方の情報として、ドキュメントなり、あるいはリレーションシップの構造やスクリプトへのコメントなどをで残すことによって、改善させる方法はいくらでもあります。そしてもちろん、要求を最初にきちんと揃えることの重要性は言うまでもなく、その点からもFileMakerで作る場合でもドメインモデルから一般的な設計を作って、そこからFileMakerの制約と向き合うという設計方法は意味があると考えます。

次回は、INTER-Mediatorを使った場合の設計を考えます。