2012年8月9日木曜日

クラウド温泉3.0 (17) / SQL

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その17です。

「Monadicプログラミング・マニアックス」のネタを要素技術のボトムアップで積み上げてきましたが、なかなか本丸に到達しないままクラウド温泉も来週末に迫って来ました。

来週はお盆休みでブログはお休みする予定なので、今日と明日の二回は応用からのトップダウンでネタを整理したいと思います。

SQLと関数型言語

関数型プログラミンをやっていて感じるのは、SQLなどの問い合わせ言語と関数型プログラミングの相性がとてもよいことです。現状はScalaとSQLが直接接続できるというわけではありませんが、データフローの実現で関数型プログラムとSQLを組合せる場合、同じセマンティクスの土俵の上で一気通貫に処理を考えることができます。これは一般的に問い合わせ言語が一種の関数型言語であるのが理由だと思います。その上で、プラットフォーム上の制約を考えて実装をプログラムとSQLとに振り分けるという形になります。

このアーキテクチャ上ではORMはちょっと中途半端な感じで、問題を複雑化させる要因になるかと思います。

現状のORMの問題の一つは、実現がER図的な共通ドメイン・モデル側に寄りすぎていて、ユースケース・スコープのドメイン・モデルはうまくハンドリングできない点です。この問題も関数型プログラミング&SQLでうまくさばくことができます。

またORMの実現方式にもよりますが以下のような問題もあります。

  • 性能チューニングや処理の記述でSQLの力を十分に活かせない。
  • データベーススキーマの変更や業務の変更に弱い。

こういう問題があるので、今までもプロ筋はiBatis/MyBatisを好んで使う傾向があったと思います。

問い合わせ言語はSQLだけではなく、各種のNoSQLにもそれぞれの問い合わせ言語がありますし、XMLではXQueryだけでなくXSLT、XPathも一種の問い合わせ言語と考えることができます。いずれも関数型言語としてとらえなおしてみると、関数型プログラミングとの併用で面白いユースケースが見つかりそうです。

SQLと関数型言語の接続

問い合わせ言語としてのSQLの特徴の一つは、問い合わせ結果が表形式になるということです。問い合わせの結果得られる情報は色々な構造を持つわけですが、一つの表にそういった構造がエンコードされて入ってきます。これをデコードするのが、プログラム側の最初の処理になります。

エンコードされる情報は木構造だったり、グラフ構造だったりするわけですが、これをデコードするコーディングはイディオム化できます。このイディオムを覚えておけば、SQLと関数型プログラミングを自由自在に接続することができるわけです。

組織階層図

例題としてRDBMSの上の部門マスタから組織ツリーを生成するプログラムを考えてみましょう。以下のようなSQLで部門マスタから部門情報を取り出すとします。

select C.部門ID as 部門ID, C.部門名 as 部門名,
       P.部門ID as 親部門ID, P.部門名 as 親部門名
  from 部門マスタ as C
    left outer join 部門マスタ as P on C.親部門ID=P.部門ID

プログラム

プログラムはSQLのアクセスは省略して、SQLの結果がMapのListとして返ってきた所から始めることにします。PlayのAnormがこういったアクセス法の代表例です。

変数recordsにデータが格納されています。

package tryout

import scalaz._, Scalaz._

object SqlSample {
  val records = List(Map('部門ID -> 1,
                         '部門名 -> "営業統括",
                         '親部門ID -> None,
                         '親部門名 -> None),
                     Map('部門ID -> 11,
                         '部門名 -> "東日本統括",
                         '親部門ID -> Some(1),
                         '親部門名 -> "営業統括"),
                     Map('部門ID -> 12,
                         '部門名 -> "西日本統括",
                         '親部門ID -> Some(1),
                         '親部門名 -> "営業統括"),
                     Map('部門ID -> 13,
                         '部門名 -> "首都圏統括",
                         '親部門ID -> Some(1),
                         '親部門名 -> "営業統括"),
                     Map('部門ID -> 111,
                         '部門名 -> "北海道支店",
                         '親部門ID -> Some(11),
                         '親部門名 -> "東日本統括"),
                     Map('部門ID -> 112,
                         '部門名 -> "東北支店",
                         '親部門ID -> Some(11),
                         '親部門名 -> "東日本統括"),
                     Map('部門ID -> 113,
                         '部門名 -> "北陸支店",
                         '親部門ID -> Some(11),
                         '親部門名 -> "東日本統括"),
                     Map('部門ID -> 114,
                         '部門名 -> "中部支店",
                         '親部門ID -> Some(11),
                         '親部門名 -> "東日本統括"),
                     Map('部門ID -> 121,
                         '部門名 -> "近畿支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 122,
                         '部門名 -> "中国支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 123,
                         '部門名 -> "四国支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 124,
                         '部門名 -> "九州支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 125,
                         '部門名 -> "沖縄支店",
                         '親部門ID -> Some(12),
                         '親部門名 -> "西日本統括"),
                     Map('部門ID -> 131,
                         '部門名 -> "東京支店",
                         '親部門ID -> Some(13),
                         '親部門名 -> "首都圏統括"),
                     Map('部門ID -> 132,
                         '部門名 -> "北関東支店",
                         '親部門ID -> Some(13),
                         '親部門名 -> "首都圏統括"),
                     Map('部門ID -> 133,
                         '部門名 -> "南関東支店",
                         '親部門ID -> Some(13),
                         '親部門名 -> "首都圏統括"))

  def main(args: Array[String]) {
    val a = build部門(records)
    val b = buildTree(a)
    showTree(b)
  }

  def build部門(records: Seq[Map[Symbol, Any]]): Map[Int, 部門] = {
    records.foldRight(Map[Int, 部門]())((x, a) => {
      val 部門ID = x('部門ID).asInstanceOf[Int]
      val 部門名 = x('部門名).asInstanceOf[String]
      val 親部門ID = x('親部門ID).asInstanceOf[Option[Int]]
      val 親部門名 = x.get('親部門名).asInstanceOf[Option[String]]
      a + (部門ID -> 部門(部門ID, 部門名, 親部門ID, 親部門名))
    })
  }

  def buildTree(sections: Map[Int, 部門]): Tree[部門] = {
    def build(sec: 部門): Tree[部門] = {
      val children = sections collect {
        case (k, v) if v.親部門ID == sec.部門ID.some => v
      }
      node(sec, children.toStream.sortBy(_.部門ID).map(build))
    }

    build(sections(1))
  }

  def showTree(tree: Tree[部門]) {
    println(tree.drawTree(showA[部門]))
  }
}

case class 部門(部門ID: Int, 部門名: String,
                親部門ID: Option[Int], 親部門名: Option[String])
ドメイン・モデル

検索結果をアプリケーションのドメイン・モデルに写像するわけですが、ドメイン・オブジェクトとして「部門」を定義しました。

業務アプリケーションの場合、ドメイン・モデルとのインピーダンス・ミスマッチを極力排除するためにドメイン・モデルが日本語の場合はプログラムの識別子にも同じ日本語を使うべき、と考えているので業務アプリケーションらしくこのサンプルもそうしています。DDDにおけるユビキタス言語の実践の一つですね。

build部門メソッドはrecordsの値(SQLでの検索結果)から部門一覧のMapを作成します。このメソッドが、SQLの結果とドメイン・モデルを写像する前半分の処理になります。

build部門メソッドでは、関数型プログラミングでおなじみのfoldRightメソッドを使って、問い合わせ結果を部門のMapに畳み込んでいます。Mapに対する畳込みがこの際のイディオムですね。

木構造の実現

buildTreeメソッドは部門一覧の表から部門の木構造を生成します。このメソッドが、SQLの結果とドメイン・モデルを写像する後半分の処理になります。

部門ツリーはScalazの代表的な永続データ構造であるTreeを用いて木構造を構築しています。

木構造の表示

部門の木構造の表示はshowTreeメソッドで行なっています。ScalazのTreeの機能を用いるので簡単に表示を行うことができます。

実行

プログラムを実行すると、以下のように組織ツリーが表示されました。

部門(1,営業統括,None,Some(None))
|
+- 部門(11,東日本統括,Some(1),Some(営業統括))
|  |
|  +- 部門(111,北海道支店,Some(11),Some(東日本統括))
|  |
|  +- 部門(112,東北支店,Some(11),Some(東日本統括))
|  |
|  +- 部門(113,北陸支店,Some(11),Some(東日本統括))
|  |
|  `- 部門(114,中部支店,Some(11),Some(東日本統括))
|
+- 部門(12,西日本統括,Some(1),Some(営業統括))
|  |
|  +- 部門(121,近畿支店,Some(12),Some(西日本統括))
|  |
|  +- 部門(122,中国支店,Some(12),Some(西日本統括))
|  |
|  +- 部門(123,四国支店,Some(12),Some(西日本統括))
|  |
|  +- 部門(124,九州支店,Some(12),Some(西日本統括))
|  |
|  `- 部門(125,沖縄支店,Some(12),Some(西日本統括))
|
`- 部門(13,首都圏統括,Some(1),Some(営業統括))
   |
   +- 部門(131,東京支店,Some(13),Some(首都圏統括))
   |
   +- 部門(132,北関東支店,Some(13),Some(首都圏統括))
   |
   `- 部門(133,南関東支店,Some(13),Some(首都圏統括))

0 件のコメント:

コメントを投稿