C Sharpens you up

http://qiita.com/yuba に移しつつあります

顧客ログイン後にEBeanのアクセスDBを切り替える

Play framework 2.x Java and 1.x Advent Calendar 2013*1の11日目(5日ぶり2回目)です。

さて、法人向けクラウドサービスを提供するときに問題となる、顧客ごとのデータの分離についてが今日のトピックです。

個人向けのWebサービスであればすべての情報は1個のスキーマに放り込まれて、データアイテムは外部キー関連でログインIDと結びついているものです。ところが法人向けではそうはいかない。何かの間違いである顧客企業のデータが他社の画面に表示されてしまう事態は万が一にもあってはならないし、そもそものセキュリティ内規とか外部団体の規約とかで「独立保存されていること」が要件となっていたりします。

そこでデータの分離の話。

顧客データ分離レベル

顧客データの分離の仕方については、ここで説明するよりマイクロソフトの出してる白書を読んでしまいましょう。これで100点満点の解説ですので。

ものすごい要約をすると、顧客データの分離には

  • 全顧客を1個のスキーマに放り込む
  • 顧客ごとにスキーマを切り替える
  • 顧客ごとにデータベースを切り替える

というレベルがあって、上に行くほど開発コストが上がり、下に行くほど運用コストと安全性が上がりますよと言っています。

スキーマ分離やデータベース分離の運用コストが高いのはわかります。顧客と新規契約するごとにDBメンテナンスが必要になるし、バージョンアップのときにすべてのスキーマ、すべてのデータベースにもれなく変更を適用する必要があります。
さて、データベース切替方式なら開発コストは低いの? あれ、でもどうやってそんなプログラミングするの? JDBC直叩きでならわかるけどEBeanを使ってだったらどうするのか。

EBeanServerの動的作成

通常、EBeanの接続設定はすべてPlay!のapplication.confに記述します。サーバ名はdefaultとしていますよね。
でも今回のようにログインユーザー名を認識してから接続設定が決まるというシチュエーションでは、接続設定をコードで作成します。

まず準備として、エンティティクラスを用意します。

  • models.commonBeans.Tenantsというエンティティクラス。顧客のログイン情報を扱う、システム共通のDBにアクセスするクラスです。
  • models.tenantBeans.Itemというエンティティクラス。これは各顧客用のデータを扱うクラスです。だから対応するテーブルは各DBにそれぞれ存在します。

次に、application.confで静的接続設定をしましょう。

db.default.driver=org.postgresql.Driver
db.default.url="postgres://postgres:postgres@localhost/common"
evolutionplugin=disabled
ebean.default="models.commonBeans.*,models.tenantBeans.*"

default接続がtenantBeansパッケージも扱うかのように書いちゃってますね。これは仕方なくて、設定ファイルの段階でこうしておかないとフレームワークがItemクラスをエンティティクラスだと認識してくれないのです。

そしていよいよ、動的にサーバ接続を生成するJavaコードです。

DataSourceConfig dsConfig = new DataSourceConfig(){{
	setDriver("org.postgresql.Driver");	// これはPostgreSQLの場合
	setUrl("jdbc:postgresql://【顧客専用のデータベース】");
	setUsername("【顧客専用のDBログインID】");
	setPassword("【顧客専用のDBログインPW】");
}};

EbeanServer server = EbeanServerFactory.create(new ServerConfig() {{
	setName("【顧客専用のDB接続名】");	// この設定は必須なので他とかぶらない名前を何か
	setDataSourceConfig(dsConfig);
	setClasses(Arrays.<Class<?>>asList(models.tenantBeans.Item.class));
	setRegister(false);	// ここをtrueにするとグローバル接続マップに登録されてしまいます
	setDdlRun(false);
	setDdlGenerate(false);
	setDefaultServer(false);
}});

中括弧を二重にした変な書き方は真似しなくてもいいです*2

こうして作成したEbeanServerオブジェクトは、いつものEBeanと同じように使えます。

final List<Item> items = server.find(Item.class)
	.where()
	.eq("price", 1000)
	.findList();

サーバ接続を生成するところと使うところがコード的に離れている場合は、HTTPコンテキストに格納して引き回しましょう。
コンテキストオブジェクトはコントローラの中ではctx()メソッドで取れるし、認証クラスのgetUsernameメソッドの中でなら引数としてもらっていますね。

// コンテキストへの格納
ctx().args.put("server", server);
// コンテキストからの取り出し
EbeanServer server = (EbeanServer)ctx().args.get("server");
というわけで

複数DBの使い分けはEBeanでも簡単ですよ!
各テーブルから顧客IDカラムを消去できちゃうからいろいろ楽になりますよ!

ちなみに、顧客専用DBのログインPWはどう管理する?

上に書いたコードでは適当にしてしまったのですが、顧客専用DBのログイン認証情報はどこにどうやって持たせるか?
システム共通の暗号鍵で暗号化して顧客テーブルに保管する、がまあありそうな落としどころかなと思っていますがこれが模範解答かはよくわかりません。

いかがお考えになりますか。

明日は

@wm3さんが誕生日記念にTypesafe Configについて書いてくれるそうです。

*1:Advent Calendarとはクリスマスまでのカウントダウン日めくりのことで、それになぞらえて12/1から12/25まで日替わりで参加者がブログ記事を寄稿するイベントです。

*2:なんのことだか興味がある方は以前の記事をご参考に。