スキーマを定義すべきかどうかの論争は結構古くからあり、私もときどきブログなどで取り上げてきました。【Reflex】 貴族になって楽をしよう
スキーマや型がなくても処理できるようにすべきという「ボヘミアン」に対し、型情報を最重要視し、スキーマがなくてはデータに意味はないとする「貴族」の論争です。
これはかつてはXMLのスキーマ言語の話でしたが、XMLに限った話ではなく、実はJSON全盛の今の時代でも論点は変わっていないと思います。
今日はこれに関連する話について述べたいと思います。
#そもそもなぜスキーマが必要か
NoSQLの強みの一つはスキーマレスであるといわれます。
スキーマレスであれば、ログ分析などで投入したデータに合わせてデータカラムの形式を自由に変更できるため、スキーマ設計に関わる手間を減らすことが可能といわれます。
しかし、実際にはスキーマレスであることが有効なのはログ分析ぐらいで、業務アプリケーションを作るならスキーマは必須です。なぜなら、スキーマレスと言っても、どんなデータがどうやって入ってるかを知らないと取り出しも更新もできないからです。
スキーマレスであるなら尚更、スキーマ管理に気を配っておかないといけません。わからなくなってから後で調べるのは非常に大変です。
例えば、SPAとサーバサイドについてで紹介した事例では、MongoDBを使ってECを構築したものの、スキーマレスという性質が品質面で悪い影響を与えているというものでした。
また、Node.jsと連携する、MongooseJSでは以下の理由から独自のスキーマを導入しています。
働くプログラマ - MEAN あれこれ: MongooseJS による強力な検証
ただし、大きな欠点もあります。MongoDB は「スキーマレス」データベースで、スキーマはあらかじめ定義されています。つまり、データベースはコレクションを保持し、コレクションはドキュメントを保持し、ドキュメントは基本的に単なる JSON オブジェクトと定められています。これが大きな問題の種をはらんでいます
まず、MongoDB のクエリは、本質的には、クエリ ドキュメント (find 呼び出しの最初のパラメーター) です。このドキュメントには検索対象のフィールドを含み、このフィールドを使ってコレクションをスキャンします。したがって、「{'fristName': 'Ted'}」というクエリを既存のデータベースの「persons」コレクションに対して実行しても何も返されません。クエリ ドキュメントのフィールド名のスペルが誤っているため (「fristName」ではなく正しくは「firstName」です)、コレクション内のどのドキュメントとも一致しません (もちろん、コレクション内のドキュメントにもスペルミスがあれば、話は別です)。これはスキーマレス データベースの最大のデメリットの 1 つです。コードやユーザーの入力にちょっとした誤りがあるだけで、非常に困った性質を持つバグが思いがけなく発生します。このため、コンパイラからでもインタープリタからでも、なんらかの言語サポートを受けるのが適切です。
このように、スキーマレス・データベースの筆頭であるMongoDBでさえもスキーマを導入するようになっているのが現状です。
データベースだけではありません。PHPやRailsといった動的型付け言語を使った開発は疲弊するといわれるようになる一方で、JavaScript界隈では静的型付けのTypeScriptが注目されています。
つまり、業務アプリケーション開発では「貴族」的な立場が主流になっているのです。
#自由に追加変更できるスキーマにすべき理由
しかし、ひとたび「貴族」的な立場でシステムを構築すると、簡単には変更できない硬直したシステムになりがちです。
例えば、項目を追加・変更したい場合、データベースでは、alter tableを使ってカラム変更する必要があります。また、ソースコードの変更もしなければなりません。そして、変更した後はテストも必要でしょう。
既に運用が始まっているシステムではもっと大変です。データの洗い替えが必要かもしれません。あげくには、後から変更したくないという理由から、あらかじめ予備のカラムをいくつか用意しておこうといったバッドノウハウが蔓延ったりします。
一度作成したスキーマを運用後で変更したいことはよくあります。
ここは「ボメミアン」的な発想で、後でスキーマに項目を追加変更しても、データストアやプログラムなどに影響しないような柔軟なスキーマ管理にすべきです。
#vte.cxにおけるソフトスキーマ管理
私はスキーマを導入する「貴族」の立場を取るとしたら、「ボヘミアン」の主張、つまり動的な項目の追加変更は諦めるしかないと思いこんでいました。
vte.cxはJavaで作られていますが、Javaのクラスを動的に追加することは言語仕様としてはできません。
しかし、それを可能にしたのがJavassistというライブラリです。これはJavaバイトコードを変換するもので、動的にクラスを生成することができます。
Javassistによって「ボメミアン」の主張も可能になったわけです。
vte.cxはNoSQLですがスキーマが存在します。
ただ、これはソフトスキーマであり、運用中であっても項目の追加や変更が自由にできます。
ちなみに、Javassistを知ったのはMessagePack対応をしたのがきっかけでした。
MessagePackはバイナリデータを動的にJavaクラスに変換します。
内部ではJavassistが使われています。
#スキーマレスでデータモデルが正しくないと破綻する例
firebaseは今最も人気のあるBaaSで、リアルタイムに同期するストレージ機能など優れた機能が豊富です。
ただ、個人的にイケてないなと思っているところが一つあります。
それは、スキーマレスに起因するところで、パフォーマンス劣化が起きやすいところです。
Firebaseのデータベースは任意のJSONオブジェクトをツリー状に保持できる柔軟なNoSQLです。一般的なリレーショナルデータベースのような厳格なスキーマ定義等は存在せず,自由な発想でデータを格納することができます。
しかしながら,必要なデータを何でも1つのツリーの中に含めてしまうと,思いもよらない無駄な大量のデータ転送やパフォーマンスの低下を招くことがあります。
第6回 Firebaseデータベースの効率的なデータ構造と高速化のポイント
結局、「データのネストを避ける」という回避策が推奨されていますが、これは本末転倒なように思います。なぜなら、そもそも階層構造のデータを扱えるのがJSONのメリットのはずなのにネストを避けなければならないからです。
Firebaseの表現はJSONPointerに近いものですが、データ構造を1つのJSONとURLで表現しようとしているところに無理があるように思います。
例えば、以下のオブジェクトが/d/masterに入っていたとします。
{
"customer" :[
{ "name":"foo" ,"id": "1"},
{ "name":"bar" ,"id": "2"},
・・・
],・・・
}
JSONPonterでアドレスを/customer/0/idとすれば、customer配列の0番目のidという意味になります。これを、URL(/d/master)にアドレスを直結して、/d/master/customer/0/id と表現するのはあまりうまくありません。これが巨大なJSONデータ表現をもたらし、パフォーマンス劣化の原因にもなっています。
むしろ、HTMLの同一文書内のフラグメント#を使って、/d/master#customer[0].id と表現する方がJSONっぽくて自然でしょう。
また、こうすることで、URLに紐付くデータはあくまで/d/masterとすることができ、エンティティ内のcustomer配下のデータを区別できるというメリットが生まれます。URLごとに異なるエンティティにすることでJSONは小さくなりパフォーマンス問題は解決できるはずです。
vte.cxはスキーマは1つですが、エンティティはURLごとに分かれており、エンティティ内部のデータ構造がURLに紐づくことはありません。
なので、階層構造にしてもパフォーマンスが劣化することはありません。
詳しくは、RESTとJSON、スキーマ定義について思うところ を参照してください。
#実際に運用してみてどうだったか
vte.cxではこれまで複数の業務アプリケーションをリリースして運用してきましたが、それらはほとんどがリリース後の変更を必要とするものでした。
前述したように、vte.cxではソフトスキーマを採用しており、運用中であっても項目の追加・変更が可能になっています。
このソフトスキーマの仕組みのおかげで、お客様のシステムの変更要求に柔軟に迅速に対応できたのは大変よかったと思います。
逆にソフトスキーマじゃなければ今頃どうなっていたか、想像するだけで恐ろしい感じです。
それでは、また。