[DBデザイン#5] 実例から考える: “商品”に注目する

前の記事では前提となる会社の業務を紹介しましたが、どの部署にも「商品」という単語が出てきました。データベースに慣れているみなさんは「はいはい、『商品マスター』でしょ」と思われるところかもしれません。もちろん、商品マスターは作るのですが、なぜ商品マスターが必要なのでしょうか、そして、それだけでいいのでしょうか?ここで、「商品」というキーワードをより詳細に分析してみます。

次の図では、それぞれの部門に対して、「商品とはなんでしょうか?」と質問した結果だと考えてください。実際、要求分析をするときに、そんなダイレクトな質問はいろんな意味で(機会やあるいは適切な担当者と話ができるのかなどを含めて)無理でしょうし、しかもちゃんと答えてくれるとも限りません。通常は、いろんなインタビューをもとに、分析者がこうではないだろうかと考えるのが一般的かと思います。なお、前回は経営者が出てきましたが、言っていることが曖昧なので、ここから先は3部門だけに絞りましょう。

営業部門にとって、商品は売っているものです。製造部門は作っているものです。この辺りは明確でしょうけど、どうもマーケティング部門にとっては、商品というのは何なのかということがどうも曖昧で、具体的に言いづらい感じです。おそらく、状況によって変わるのでしょう。

ここで、部署ごとに、商品をどのように扱っているのかをみてみましょう。それを示したのが次の図です。

まず、製造部門は、製造した商品1つ1つをどうやら管理したいようです。つまり、商品の箱1つ1つが対象になります。一方、営業部門は、商品の箱があることは理解しているでしょうけど、商品そのものを集合的に扱います。しかも、製造部門は、商品の物理的なことを考えなければなりませんので、在庫ということまでもしかして考えているかもしれませんが、営業部門はおそらく商品という存在を移動させているという考えが支配的ではないでしょうか。つまり、営業担当者は、実際に自分で箱を手に取って顧客に届けているというよりも、伝票を書いて、ロジスティックスに依頼をするわけで、実際に商品そのものを目に見ていることは滅多にないようなのがいわゆる営業という活動ではないかと思われます。マーケティングに至ってはなんだかわかりませんが、インタビューでどうやら商品カタログの管理くらいは必要なことがわかったという程度です。

同じ商品という呼び方をしているのに、このように部署によって実は扱う概念が大きく変わってきます。当然、それらを一緒くたにするのは問題があると考えます。部署ごとに違いがある一方、共通の概念もあります。例えば、商品名は社内で統一的に扱われているはずです。一方、商品の単価が業務上必要でしょうか? 販売部門は伝票を作成するのに必要そうです。一方、製造部門は単価が存在しても、業務上は通常は使わないでしょう。一方、マーケティングはおそらく単価情報は、業績等をチェックするような場合に使うかもしれません。商品1つずつにつけるシリアル番号はどうでしょう? 製造部門には必須かもしれませんが、営業部門には興味のないデータです。このように、共通のものと共通でないものが存在するのです。

ドメイン駆動開発の世界では、こうした概念を「ユビキタス言語」として捉えます。ある特定のコミュニティで通用する言語なのですが、つまり、ここでは「営業部門言語の商品」と「製造部門言語の商品」に若干の違いがあると考えて、これらの部門間では異なるユビキタス言語が使われていると考えます。同じ言葉で違う概念があるということは、ドメインつまり「業務そのものの定義」が異なるという考え方です。実は、商品というよく似た言葉でも、このような違いがあるものの、社内で働いていると、なんとなくそういうものだということでドメインが違うことは気づきにくいものです。まさに、第三者が設計するときに、そうしたドメインの違いを見出すことができ、業務の整理が進むということが期待できます。もちろん、分析者や開発者は、顧客のドメインを理解し、ユビキタス言語を使いこなせるくらいになっていることが理想です。

ユビキタス言語は、そのドメインのモデルの基礎になるものです。言語を構成する概念を整理したものがモデルとして利用できるでしょう。ユビキタス言語はモデルが表面に出てきたものでもあります。私たちは、ドメインのモデルを知りたいのです。そのモデルをデータベースに実装すれば、システムは成功するはずです。モデルを探るのが要求分析であり設計です。その手がかりがユビキタス言語にもあるということです。

ちなみに、1つの言葉が異なる概念を示すだけでなく、異なる言葉が同じものを指す場合もありますし、1つの単語が状況によって異なるものを示す場合もあります。最後の例は1つの単語が同一職場の同一業務で状況によって違っていたのですが、よくそれでトラブル発生しないもんだと思ってしまいます。たまたま問題にならなかったのでしょう。言葉というのはコンテキストを引き連れて登場するので、区別は可能なのかもしれませんが、外部からその世界に入ってきた人には面食らってしまう事象です。こういう事例があった場合は、ドメインをまたがって用語の整理をするのが理想なのですが、なかなか業務の上で使い続けている言葉は変えてもらえないのが実情です。

ドメイン駆動開発といえば、現在はマイクロサービシズで話題になっていますが、ドメイン駆動開発の方が先に提唱されていた概念です。マイクロサービシズでは、どんなサービスに分割をするのかというのが設計上の大きな問題です。ここでサービスの分割を考えるとき、ユビキタス言語の通用する範囲を重視するという考え方があります。これを、非常に雑に考えると、要するに部署ごとに別システムであり、部署間の動きが必要ならそれはシステム連携だろう、そしてシステムは1つのマイクロサービスであるとも言えるとも言えます。実は、マイクロサービスシズの勉強を一時期頑張っていた時期があり、いろんな資料に当たったのですが、あっさりと「マイクロサービシズは、部署ごとに分ます、以上」と説明されているものがありました。その資料ではドメイン駆動開発やユビキタス言語といった抽象概念はすっ飛ばしていました。あらあらと思いつつ、非常に大まかに言えばそういうことで、間違いはないかもしれませんが、会社の部門構成が最適化されていないと色々苦しいだろうなと思ったりもします。しかし、最適化されていない会社ってきっと破綻するでしょうから、システム作っても早晩使われなくなる? ということで、部署でサービス分けるというのはあながち悪いサジェスチョンではないと思った次第です。そこで、ここでの実例も、部署を単位にしてみました。

商品のような基本的な単語でも、そのドメインによって役割が違うようです。実際、みなさんの会社やあるいは扱った案件ではどうでしょう? 他には「在庫」なんてどうでしょう。在庫と言ってもいろんな概念があります。会計的には在庫は金額ですが、倉庫で働く人にとってはそこにあるたくさんの段ボールだったりします。商品や在庫などはどの会社でもあるようなものなので、ある意味では扱い方はある程度はわかっているようなものでもあり、経験則も適用できるかもしれませんが、むしろ、わかったつもりになるのは危険で、そのドメイン特有のルールや扱い方がないかを常にアンテナを張るくらいの気持ちがないと、後から気づいて大きく出戻りが発生するということにもなりかねません。商品や在庫だけでなく、部署ごとに違う、あるいは担当者ごとに違うような概念がないかを見つけてみるのも良いでしょう。また、部署で違うだけでなく、部署を横断するようなドメインの存在も見えてくるかもしれません。要求分析以降は常にドメインやそこでのモデルを意識する必要があります。

次回は、異なる概念を表を作って捉えるということを説明しましょう。

[DBデザイン#4] 実例から考える: 前提となる業務

今回から、数回に分けて、データベース設計をするときにどこに注目するのかを考えることにします。一応、「分かりやすい」業務を架空の会社でおこなっているという想定です。もっとも、そういう架空の設定そのものが、いかにも記事をそれらしく見せるためのわざとらしい前提に見えるかもしれませんが、やはり現実の世界そのままはなかなか複雑で理解しづらいですし、現実には「要求が不明あるいは曖昧である」という問題との対峙が非常に難しいところになります。なので、問題を単純化したいと思います。

データベース設計の1つの肝は、「1レコードに相当する単位あるいは概念の特定」だと考えます。そのために、どんなフィールドが必要かを集めて分類などを行うと思いますが、1レコードの特定が設計の1つの目標になります。そこに至るまでに、表にして考えるということをまずはこの実例で見ていっていただきたいのですが、表が出てくるのは次回以降になります。まずは、ロボット製造販売を行うような会社がありますという前提の説明をしましょう。あえて非現実的にするために、おもちゃロボットがなぜかヒットしたという会社を考えます。画像素材は誰が見てもご存知のイラストやさんです。ありがとうございます。

まず、この会社には営業部門があります。製造したロボットを、卸売や大手小売業者に販売します。営業部門では、「ロボットさんま1号 x 24個」のように、この会社の商品を、通常は複数個まとめて出荷するという作業を行います。営業部門が考える関心ごととは、ともかくたくさん販売して、売上を伸ばすことです。また、季節性のある商品や何かイベントなどでの売上の波が来そうな場合に、別の部門との折衝を行なって在庫確保などもしているかもしれません。

一方、この会社には製造部門も持っています。製造はどうやら外注していないようです。製造部門は、当然ながら、商品を1つずつ作ります。まとめてどこかに在庫しますが、どうやら出荷計画を作ってそれに基づいて製造をするという大きな流れを持っているようです。この部門の関心事は不良品がないなどの製品品質を高めることが大きいようです。

さらにマーケティング部門もあります。マーケティング部門なので、現状の商品を訴求するためのさまざまな活動が行われていますが、一番大きなミッションは新たな市場を作るということを経営側から命じられています。この部門にとっては、商品1つ1つは関心ごとではなく、商品という大きな概念そのものを扱っていると言えるでしょう。

もちろん、この会社には経営陣がいます。もちろん、経営陣は会社の収益を確保し高めることが一番の関心事になりますが、どうやらこの会社はハイテクな商品を作っているように見えて、情報システムは部門ごとに作ってしまっているようで、連携はしていないようです。

このような会社で、部門にまたがって使える業務システムを設計するということが発生しました。ということで、設計を進める上で何を考えるかということを考えてみたいと思います。これくらいの内容なら、すぐに作れるよとおっしゃる方もいらっしゃるでしょうけど、そういう方が頭に即思い付くような「テーブル構成」がどのように導き出されるのかを、「表を作ってみる」というアクションから考えてみたいと思います。

[DBデザイン#3] 表のモデル

昨日このポストを書いた後、サーバーのメンテを長くやってないなと思ってアップデートをかけたら、一気に起動しなくなってしまってデータが消えてしまいました。ある程度はバックアップしてあるので、メッセージは一昨日分まで、画像などは2019年までしかなかったです。まあ、でも、Ubuntu Server 20.04に入れ替えて心機一転です。ただ、同じ内容を書いても、日が変わると内容が違って来そうで、いいのか悪いのか、ともかく昨日書いた内容を思い出しつつ、変化しながら書きます。

データベースの設計を行う上で、まず、データをどのように見るのかという基本的なことで、表を出しました。表に対して、レコードやフィールドといった名称を定義しました。多くの方が表計算ソフトを使っているので、表は便利かつ有用であるというのは理解はしていると思いますが、それが「なぜ?」を突き詰めると実はなかなか難しいということも説明しました。ほとんどの方が、表にまとめて、あるいは表にまとまったものを参照して、何か目的を達したり、より効果的に作業ができるなどの成功体験があるということがあるでしょう。そうした体験から有用性を理解しているということが、ともかくの理由でした。

なお、数学的な世界では、集合の仕組みを利用して、表そのものをきちんと定義するということは可能であり、いわゆるデータベースの教科書ではそうした集合論をベースにした表の定義が最初の方で説明されています。いきなりその辺りで挫折する方も多いかもしれません。また、そうした高度に抽象化された世界が実際のデータベース設計にどこまで役に立つのと問われると、最初のとっかかりとしてはやはりきついと思います。データベースの設計をある程度理解した時、そうか、基本は集合だったよなということを思い出すことで理解が深まるということもあります。

いずれにしても、まずはデータを表として記述することで、ある程度コンパクトに、そして存在するデータを余すことなく記述できます。つまり、Excelでなんでもできるでしょうというあたりの考えに近いかもしれません。つまり、表はそれで完結した世界です。しかしながら、私たちは書ききれないほどの大量のデータを扱いたいのでデータベースを使おうということがあります。この時、データベースは表形式にデータを扱いものの表の縦横を自由に使うのではなく、まずは行に相当するレコードに区切り、レコードはフィールドとして分割され、フィールドにデータを入力できるということを説明しました。その状況をツリーのように記述できます。ここでの線は、導出可能であるということを意味しています。つまり、あるレコードが決まれば、そのレコードからフィールドの値が取り出せるということです。プログラミング言語で処理をすると、実際にそうした側面が強く出ますが、ここではモデルの段階から表はレコードの集まり、レコードはフィールドの集まりであるということを意識したいと思います。

このように、表は階層的なツリー図で表現できるということであれば、表とツリーは同一のデータを示していると言えます。ただ、ツリーの方が実際に描画するのは大変そうですが、概念としては同一のものを示しています。

ここで、具体的にデータが存在するのはフィールドです。レコードもデータの集まりではあり、要するにフィールドの集合ではありますが、単一のデータを持つわけではありません。その意味では、レコード、そして表自体も存在がちょっと抽象的になります。それをまずはあえて楕円形のオブジェクトで書きます。レコードには名前等がないので、便宜的にレコード1などと数字付きで書きますが、表そのものには「住所録」といった名前があります。

ここで、表自体の設計情報を記述する場合、もちろん、全データを書くというのは設計図としては複雑すぎて意味がありません。設計、つまりモデルは、興味のある対象に絞った情報を集めたものというのが原則です。それを、必要な情報に捨象するといった言い方をします。つまり、「どんな表なのか」ということを如実に示す情報が欲しいわけです。

ここでの表を見てみると、まず、どのレコードも同じフィールドを持っています。そのフィールドの実データではなく、フィールドの名前を使って、どんなフィールドが存在するのかを記述してみると、次の図のようになります。どのレコードからも、同一のフィールドが存在するので、全レコードについて記述する必要はなく、レコードは1つだけで良いでしょう。

ここでさらに、表はレコードの集合であるということが決められているということでああるのなら、レコードが存在しますということをモデル上に表現する必要はないとも言えます。ただ、「しない」わけではなく、そういう表現をすることもありますが、ここではシンプルな表はみんな同じ考えで見ているとして、レコードの存在はあえて書かなくても同じフィールドを持つものが複数存在するということを前提とします。すると、残ったものは、表の名前と、フィールド名です。これらを、ちょっとわざとらしく、ボックスに書いてみると次のようになります。

これは、ER図のエンティティのようでもあり、クラス図のクラスのようでもあります。いずれにしても、こうした記述が住所録を管理する表の設計として認識できます。元は表ですが、それを階層的に見た上で、必要な情報だけを残すと、このようなものができます。データベースの設計者は、このボックスを見ることで、実データが表として展開されて、たくさんのデータが入るという状況を想定し、それが実際のシステムの用途に合致したものかどうかを検討しながら設計を進めるということができるのです。

ここで、命名が「住所録」といった集合的なものになっている点にちょっと気に掛かる人もいるかもしれません。ここはある意味好みの問題なのかもしれませんが、表に対して名前をつけているのか、レコード1つに対する名前なのかで分かれるところだと思います。データベースの設計では、レコードとして実現されるので、表とレコードは最初から組み合わされているので、ER図のような世界に入り込めるのです。これがクラス図であれば、「住所録」ではなく、「顧客」などの単一レコードを示す名称をつけ、さらに「顧客リスト」といったクラスを考えて、顧客リストは顧客の集合、つまり表とレコードの関係のような記述をする方が落ち着くと思われかもしれません。クラス図の方が抽象度が高いので、いろいろな記述のバリエーションは考えられます。これは、結果的には設計者がどう決めるかの問題です。少なくとも、1つの設計の中で、ボックスの粒度をどう扱うかを統一した上で、そのルールが他の人にわかるようなコミュニュケーションをしているかどうかということです。

私自身は、クラス図で書くことが多いのですが、いきなりデータベース設計するのではなく、まずはドメイン分析からスタートして詳細化してデータベース設計に持ち込むので、その結果クラス図で記述する異なってしまいます。もっとも、使っているツールのastah* professionalは、クラス図からER図を作成できるので、単にツールで書き換えればER図になります。また、クラス図で記述すれば、オブジェクト図を書くことができます。オブジェクト図はクラスに基づくオブジェクトであり、つまり実データが入ったボックスを記述できます。その仕組みにより実際のデータを当てはめてみて、それが意図した状態を表現できているかということを、検証することが可能です。複雑なデータであれば、そうした設計から実データ展開した検証をしたくなりますが、クラス図とオブジェクト図で、作業が可能です。クラス図でスタートしたら、結果的にクラス図で記述したデータベース設計になり、自分としては違和感ないのですが、怪訝な顔をされることもありますね。

次回からは、架空の会社の業務を想定した上で、同じ名前の対象物に対して、部署ごとに管理したい内容が違い、結果的に違う表が必要になるというお話を何回かに分けて行います。

[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追記:サーバ復旧時に画像が復活していなかったので、画像だけ埋め込みました。