2013年9月20日金曜日

補足と訂正・クエリーがどのようにしてモジュール性を阻害し得るか

前回はRDBMSの使用が引き起こすモジュール性の低下について議論したが、その根拠を具体的に示さなかった。その補足ということで今回は小さな例を1つ示したい。

また、関数プログラミングが副作用の多い状況に対して弱みを持つ、と書いたのは乱暴であったのでお詫びして訂正したい。
正しくは、副作用を含むロジックに対しては、注意深く抽象化を行う必要があり、副作用・外部入出力の種類が違えば、そのたびごとに抽象化のモデル・実装について熟慮して抽象化を行う必要があるということである。
これはうまい抽象化が不可能だということではないので、それを弱みと呼んだのは間違った一般化であった。

しかし、ありがちな抽象化の方法では、クエリーの発行がモジュール性の低下を引き起こしかねないということを小さな例を使って示したいと思う。この例は手続き的な擬似コードによるものだが、RDBMSを抽象化するfunctionalなDSLであっても同じような問題が起こる「可能性」があることには同意してもらえると思う。一方で同じ問題を起こさないDSLが存在する可能性も十分あるが、それについては後に考察する。

おそらくありそうな例

以下は、Python風の手続き的な擬似コードだ。

def procA(x):
    rows = queryA(x)
    return calcA(rows)

def procB(x):
    rows = queryB(x)
    return calcB(rows)

queryAとqueryBはSQLクエリーを発行して結果を返す手続きだ。
そしてcalcAとcalcBは純粋に汎用言語で書かれた副作用の無い手続きだ。
procAは、queryAを呼び出し、更にcalcAによる計算をして結果を返す。
procBは、queryBを呼び出し、更にcalcBによる計算をして結果を返す。

ここでprocAとprocBの手続きを続けて行うprocAandBという手続きを作りたいとする。
(関数の直列合成より現実によくありそうな形を選んだが深い意味は無い。)
その定義は以下のように書けるはずだ。

def procAandB(x):
    y = procA(x)
    z = procB(x)
    return calcC(y,z)

ここでパフォーマンスの問題が起こったとする。
queryAとqueryBを両方共呼び出すのであれば、queryAandBという形にひとつのクエリーにまとめることができ効率が良いと判明する。
そこで次のように合成されたクエリーqueryAandBを使って手続きを書き換えることになる。

def procAandB2(x):
    rows = queryAandB(x)
    y = calcA(rows)
    z = calcB(rows)
    return calcC(y,z)

プログラムは大して汚くなったわけではないが、なにか余計な面倒をしょいこんだぞ、とあなたのゴーストが囁くはずだ。

何が起こったのか

クエリーを含む手続きを複数合成するときに、パフォーマンスの観点から手続きの中で使用しているクエリーを合成し書き直す必要がでる。このようなパターンはありふれたものだと思われる。

そしてこういった書き換えが頻繁に発生すれば、モジュール性の低下、コードの冗長化を引き起こすということは明らかだろう。
そしてこれはパフォーマンスを無視出来る状況であれば問題になりえないのだが、無視できない状況が十分たくさんあるのが現状だ。

パフォーマンスのための最適化は仕方ないか?

パフォーマンスのためのチューニングでコードが汚くなるのは珍しいことではなく、仕方の無いものかもしれない。しかし、リレーショナルモデルは、本来インデックスの使い方や管理といったパフォーマンス指向の関心を、ビジネスロジックから分離するというのが売りであったはずだ。これでは本末転倒だ。ここは開発現場ではないので、もうすこし理想を追いかけてみたい。

本当のところどうあって欲しかったか

procAとprocBには必要なクエリーと必要な計算がすべて記述してあるので、procAandBを両者の合成として定義するだけで、あとは実際にどんなクエリーを発行するかはコンパイラが最適化してくれればよい。
つまりprocAandBのような記述をして、実際の実行はprocAandB2のように行って欲しいのだ。
そうすれば余計な荷物を背負い込む必要はなかったはずだ。

ところが、SQLを知らない汎用言語のコンパイラにそれを期待することは出来ないので、ライブラリとしてのDSLのレベルでSQLの合成とコンパイルを行ってくれれば良い。
ある計算をアプリケーション側とRDBMS側のどちらで実行するかも抽象化できれば理想的なので、クエリーとそれ以外のロジックをまとめてDSLで記述して適切にSQLと汎用言語にコンパイルもしくは逐次実行してくれると良いかもしれない。

こういったことは、手続き的な言語上のO/Rマッパではおそらく相当に難しいはずだ。
functionalなDSLでは、当然どんなモデルに基づくDSLなのかによる。

どんなモデルに基づくDSLであれば良いか

此処から先は多分に推測を含む。
そのようなDSLは、SQLと、汎用言語の側で計算してほしい部分のロジックの療法を理解する必要がある。
そして汎用言語側で行う処理は、リレーショナルモデルの範囲を超えたものであることが多い。
そのようなロジックとSQLの混合物を上手く記述するためには、DSLのモデルは関係モデルそのものではなく、関係モデルを包含するものか、全く別のモデルである必要があるだろうと思われる。
それは一体どんなモデルであろうか。
そういったモデルやDSLがすでに存在している可能性は高いが今のところ見つけられていない。

しかしそのDSLライブラリが既存であろうがなかろうが重要なのはそのモデルがどんなものかということだ。
そのモデルは少なくともリレーショナルモデルを包含するか、もしくはその良いところを同じように持ち合わせている必要がある。
さらにリレーショナルモデルの欠点(もしあるならば)を克服していればさらに良いだろう。

次回

そのモデルなり具体的なDSLを見つけることができれば、それを調べて紹介したいと思う。
見つからなければ、リレーショナルモデルについての考察に移りたいと思う。

2013年9月18日水曜日

関数プログラミングのボトルネックとしてのRDBMS

プログラム開発は、多くの人々が目的達成のため、もがき苦闘するタールの沼である

– Frederic P. Brroks, Jr., 人月の神話

モジュール性はプログラミング成功の鍵である

– John Hughes, 関数プログラミングはなぜ重要か


タールの沼の底から

 タールの沼と聞いて連想するのは大規模なSIである。業務アプリケーションやWebアプリケーションは規模が大きくなればなるほど、複雑さが増し収拾がつかなくっていく。そしてそのようなアプリケーションを大きな単位で上手くモジュール化し、さらには再利用することは不可能に近い。
その技術的な原因の一端、そして問題を解く鍵は、そのようなアプリケーションが常に携えているRDBMSの周辺にあり、さらに言えばおそらくRDBMSとアプリケーションロジックの組み合わせにあると考えている。
ここでは、アプリケーションロジックの実装に関数プログラミングを適用してもこの問題が容易には解決しないことを示し、この問題の根の深さと重要性を主張したい。

関数プログラミングの強み

関数プログラミングは、非常に強力である。その所以は関数が提供する高いモジュール性にある。
関数の純粋性を損ない、モジュール性を阻害する副作用はモナドを使って記述し、安全に分離しておくことができる。
これが関数プログラミングの強みである。

関数プログラミングの弱み

では関数プログラミングの弱点はなにか。
それは副作用を含む処理が大半を占めるようなシステムのプログラミングに対しては、その強みが発揮しづらいことである。そのような副作用が支配的なプログラミングとして、UI、ネットワーク、データベースプログラミングなどがある。どれも重要だが今回のターゲットはデータベースプログラミングである。

UIプログラミング

UIは、そもそもインタラクティブな入出力を扱うものである。ウィジェットやフォームといった部品がそれぞれ状態を持ち、ユーザからの入力という副作用を元にそれらの状態・表示を更新していかなければいけない。このため多くのロジックは必然的に副作用を含むものとなる。
このUIプログラミングはリアクティブプログラミングの主な応用領域のひとつである。UIとリアクティブプログラミングの組み合わせ、UIとRDBMSの組み合わせに関しての詳しい議論は別の機会としたい。ここではリアクティブプログラミングにより一応の解決策が与えられているとしよう。ネットワークプログラミングについても、リアクティブプログラミングの応用例があり、UIプログラミングと同様にここでは議論しない。

データベースプログラミング

最後がデータベースプログラミングでありこれが今回のターゲットである。
RDBMSとのやりとりもまた副作用であり、UIがシステムの末端に位置するのに対しRDBMSは中心に位置することが多い。
このRDBMSに対する副作用が、いかにモジュール性を阻害するかについて考えてみよう。

ボトルネックとしてのRDBMS


RDBMSを中心とするシステム、特にWebアプリケーションや業務アプリケーションなど、
外部からの入力をもとにRDBMSを操作するシステムにおいては、
関数プログラミングの利点は大きく阻害されることになる。
その理由について、リッチクライアントに対してAPIを提供するWebアプリケーションサーバの実装を例に考えてみたい。

API

APIはクライアントからのリクエストを入力に取り、なんらかのレスポンスを出力する。
ここでレスポンスが、リクエストの内容だけから一意に決まるのであれば、レスポンスを生成するロジックは純粋な関数として記述でき全く問題ない。
しかし、そのようなAPIは明らかに非常に稀であり、多くのAPIがリクエストの内容に基づきRDBMSに対するクエリーを発行し、その結果をもってしてレスポンスを生成することとなる。
このようなAPIは副作用を含む手続きであり、純粋な関数としてみなすことはできない。
APIの内部のドメインロジックを分解していっても、重要なロジックほど内部にデータベースとのやりとりを含む。

データベースのモジュール化の難しさ

そこで状態の側、データベース自体を分割しモジュール化すればよいように思える。各テーブルやそれをまとめるスキーマといったものを利用できないだろうか。テーブルをグループ化することはもちろん出来る。しかしアプリケーションロジックをそのグループに合わせてモジュール化しようとしても、なかなか上手く行かない。なぜなら、個々ののAPIやビジネスロジックが、苦労して作ったテーブルのグループ分けをまたぐようにして状態にアクセスすることが頻繁に発生するからだ。
個々のロジックが複数のテーブルを結びつけていき、綺麗に分けることのできない塊にしてしまうのだ。

巨大な単一オブジェクト

その結果として、この種のプログラミングは、
RDBMSという複雑な状態を内部に抱え、メソッドとして多くのAPIを持つ、
巨大な単一オブジェクトの実装という様相を呈することになる。
俯瞰してみた場合のプログラムの大まかな構造は関数プログラミングの技法を使おうが使うまいが、変わらないということになってしまう。

オブジェクト指向とMVC

この問題は、オブジェクト指向やMVCといった技法に基づきデータベースを抽象化しても全くかわらない。
またオブジェクト指向に基づきオブジェクトに分解することはもちろん可能である。しかしカプセル化や疎結合を実現しようと努力しても状態を通じた暗黙的なオブジェクト同士の依存関係を断ち切ることはできない難しい。可変状態を抱えるオブジェクトを苦労して分解したところで、それをつなぐ手続きの記述が煩雑になるばかりで全体の複雑さはなかなか下がらない。それはSOAの苦戦が示すところである。
オブジェクト指向の効能はもちろんある。しかしここで重要なのは、俯瞰的に見れば 手続き+RDBMS という構図は変わらないということだ。そしてそもそも手続きではなぜ駄目なのか、という点についてきちんと議論するのは本当に難しい。それを示すためには、手続き以外のもの、関数プログラミングやリレーショナルモデルが何を提供しているかについて詳しく見ていく必要があるだろう。オブジェクト指向に関しては今回の主要なターゲットではないので、別の機会に詳しく議論したい。

ではリレーショナル・データベースが「悪い」のか

そもそもRDBMSではなく、ほかのデータモデルに基づくDBMSを使ったらどうだろうか。しかし、ドキュメント指向データベースやグラフデータベース、オブジェクトデータベースなどを用いても問題が解決することはないだろう。
もしくは、そもそもDBMSを使わないという選択肢はないだろうか。
DBMSを使うその理由は永続化やトランザクションのためだけだろうか。
もしメモリ空間が無限大で、プロセスが永遠に落ちないのであれば、
データベースなど必要ないのだろうか。

おそらくRDBMSを使わないという選択は、問題を悪化させる。それは、RDBMSの提供するトランザクションとリレーショナルモデルがすでにかなりの程度問題の度合いを軽減してくれているからだ。
この点については他の機会に詳しく議論したい。

気がつけばそこはタールの沼の底だった


各々のAPIは、DBMSを通じて暗黙的に相互に依存し合い、それにより個々のテーブルも結び付けられていき、さらに各「画面」もまた複数のAPIにまたがることになる。斯くしてAPIや画面といった単位より大きなモジュール性は完全に阻害されることになる。

ではどうすればよいのか

結局のところ問題の核心はどこなのだろうか。
RDBMSだけ、もしくはアプリケーションロジックだけであればそもそも問題は起こらなかったはずだ。その2つを組み合わせたときに初めて問題が起こるのである。
そしてこれは古の問題、パラダイムやモデルの異種接続が引き起こすインピーダンスミスマッチングよりも、おそらく根が深い。
RDBMS内の状態とアプリケーションロジックの結びつきがモジュール性を阻害することが問題の原因であるならば、それらの境界だけを見るのではなく、システム全体に視野を広げ、データベースモデルとプログラミングパラダイムの両面から抽象化のやり方について考え直す必要があるのではないだろうか。
さらにDBMSと汎用プログラミング言語という分業体制を懐疑的に眺めつつ、もし全体の抽象化についてやり直すとしたらどうすればよいか、ということもおそらく考えてみるべきだろう。

次回の予定

リアクティブプログラミングをもってしてもこの問題を解決するのは容易ではない。
それは第一にUIプログラミングと違い、状態がメモリ内部ではなく、DBMSという外部にあるからであり、第二に、データベースを宣言的な形で隠蔽・抽象化するということは、別のデータベースモデルを作ることに限りなく近いからだ。UIとリレーショナルモデルとリアクティブプログラミングについて考える前の準備として、次回はリレーショナルモデルの恩恵と問題点について考えてみたい。

ご意見募集

実務家、理論家の双方の方々からのご意見ツッコミを切に望みます。
コメント欄もしくはtwitterの@pokarimへどうぞ。

ログ

 2013/9/19:「オブジェクト指向とMVC」に修正・追記した。
 2013/9/19:「ではどうすればよいか」に追記した。

引っ越しました

ブログを引っ越し、統合しました。
今まで、具体的な言語やライブラリ関係の小ネタを本ブログ"Clojureで行こう"で、
プログラミングパラダイムとかデータベースモデルに関するも比較的抽象的な話を
"Conceptual Contexture"というブログの方でやっていました。

どちらもほとんど更新していませんでしたが、
今回2つのブログをひとつにまとめて再開することにしました。
Clojureを最近は触っていないこともあり、タイトルはConceptual~の方にしました。

ソフトウェア開発やプログラミングに関する小ネタと、
プログラミングやデータベースに関するモデルやパラダイムについての両方について
書いていきたいと思っています。

2011年12月7日水曜日

Play Framework 2.0 と JSON ライブラリ Jerksonについて調べてみました。

この記事はPlay! framework Advent Calendar 2011 jp #play_ja
の7日目です。
勢いで参加してみたものの、ネタが見つからず困っていたところ、@mumoshu さんのつぶやきが目にとまりました。

  1. むもしゅ
    mumoshu この2日間のPlay 2.0は、Scala向けのJSON APIが変更されたり、Actionの合成方法がドキュメントにそった形になった。JSONの方は、使うライブラリがsjsonからJerksonへ変更。Jerksonをベースに、sjsonのようなAPIを実装している。
-- this quote was brought to you by quoteurl

Jerksonというライブラリは全く知りませんでした。
調度良いのでこのJerksonについて調べたことを書こうと思います。
(Play20のplay.api.jsonは、現在絶賛開発中といった感じなので、これからしばらく不安定な様子です。
おそらくここに書くこともすぐに古くなると思いますのでご了承ください。)

Jerksonって?


Jerksonは、Java JSON-processorであるところのJacksonの、Scala wrapperです。

これまで候補であったsjsonについては@eed3si9n さんの翻訳記事、
sjson: Scala の型クラスによる JSON シリアライゼーション に詳しく書かれています。sjsonは、implicit parameterによる型クラスエミュレートを使った便利なライブラリです。sjsonでは、型クラスを使うことでシリアライズ、デシリアライズプロトコルのカスタマイズ手段を提供しています。
sjsonからJackson/Jerksonに移行した理由はわかりませんが、ScalaとJavaを両方サポートするPlay!ではJavaベースのJacksonとそのScala wrapperというのが都合が良かったのかもしれません。
play.api.jsonでは、このJerksonに、sjsonライクなシリアライズ/デシリアライズプロトコルをかぶせた形になっています。
apiとしては、まずJsValue型とStringの相互変換をする2つの関数が用意されています。

def parseJson(input: String): JsValue = JerksonJson.parse[JsValue](input)
def stringify(json: JsValue): String = JerksonJson.generate(json)


parseJsonは、StringからJsValueへ、stringfyはその逆で、JsValueをStringにします。
これらの中身はJerksonにそのままお任せで、特にカスタマイズも想定されていますん。
JsValue という型が中間表現になっていて、任意のScalaオブジェクトをJsValueにしてからStringにしたり、
JSON文字列をパースしてまずはJsValueにしてから、任意のScalaオブジェクトに変換することになります。
ちなみにJsValueという型は次のような構成になっています。

sealed trait JsValue {...}
case object JsNull extends JsValue {...}
case class JsUndefined(error: String) extends JsValue {...}
case class JsBoolean(override val value: Boolean) extends JsValue
case class JsNumber(override val value: BigDecimal) extends JsValue
case class JsString(override val value: String) extends JsValue
case class JsArray(override val value: List[JsValue]) extends JsValue {...}
case class JsObject(override val value: Map[String, JsValue]) extends JsValue {...}


ベタにJSONのデータ構造そのままですね。
それぞれcase classなので、このままでも使うことができますが、ちょっと不便です。
そこで、任意の型との相互変換を行う関数として、次の2つが用意されています。


def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = tjs.writes(o)
def fromJson[T](json: JsValue)(implicit fjs: Reads[T]): T = fjs.reads(o)


Writes[T]がT型からJsValueへの変換を、
Reads[T]がJsValueからT型への変換を提供するオブジェクトの型になっています。
それぞれimplicitとなっているので、

import play.api.json.Reads._
import play.api.json.Writes._

などとしておけば、


assert (toJson(Map("a"->2)).toString =="""{"a":2.0}""")
assert (fromJson[Int](parseJson("1")) == 1)
assert (fromJson[Map[String,SortedSet[Int]]](parseJson("""{"a":[1,5,3]}""")) == Map("a" -> SortedSet(1,3,5)))
assert (fromJson[Map[String,List[Int]]](parseJson("""{"a":[1,5,3]}""")) == Map("a" -> List(1,5,3)))


といった形で、よくある感じの型にはデフォルトで変換プロトコルが提供されていて、
簡単に使うことができます。
ちなみにWrites[T] とReads[T]はこんな感じです。

trait Writes[T] {
def writes(o: T): JsValue
}
trait Reads[T] {
def reads(json: JsValue): T
}


(Writes Readsの他に次のようなFormatという型も用意されています。おそらく今後これを実装したobjectが提供されるんだと思います。

trait Format[T] extends Writes[T] with Reads[T]



カスタム変換プロトコル


次に自分の好きな変換を提供する方法について考えてみます。
まずは、Reads[T]の実装をみてみます。
シンプルな例としてReads[Boolean]は次のようになっています。

implicit object BooleanReads extends Reads[Boolean] {
def reads(json: JsValue) = json match {
case JsBoolean(b) => b
case _ => throw new RuntimeException("Boolean expected")
}
}

def reads(json: JsValue):Boolean
な型のメソッドを持つReads[Boolean]なオブジェクトをimplicitで提供するだけですね。
変換したい型が、List[T]などのパラメータ化された型の場合はもう少し複雑になります。
List[Int]、List[Boolean]などについて個別にReads[List[Int]]などを用意していけば、
Reads[Boolean]と同じ形に書けますが、これだとList[List[List[Int]]]などのネストを考えると
事前にすべて用意する訳にはいかなそうです。
そこで、Tがなにかはわからないけれど、T(例えばIntだったりする何か)について変換する方法を
教えてくれれば、List[T]の変換方法を教えましょう、という形の関数を書くことになります。
Tの変換方法とはつまりReads[T]型のオブジェクトで、
List[T]の変換方法とはReads[List[T]]型のオブジェクトです。
なので型でいうと、Reads[T]型のオブジェクトを受け取って、Reads[List[T]]型のオブジェクトを返す関数を
書くことになります。実際のソースの該当部分は次のようになります。


implicit def listReads[T](implicit fmt: Reads[T]): Reads[List[T]] = new Reads[List[T]] {
def reads(json: JsValue) = json match {
case JsArray(ts) => ts.map(t => fromJson(t)(fmt))
case _ => throw new RuntimeException("List expected")
}
}

ここで、implicit fmt: Reads[T]と、Reads[T]型の引数についてもimplicitが指定してあるので、
例えば先ほどのimplicit object BooleanReads extends Reads[Boolean]
などが提供されていれば、listReadsにBooleanReadsが暗黙に自動的に供給されて、
Reads[List[Boolean]] も自動的に供給されることになります。
この仕組みを使って、ここでは試しにReads[Either[A, B]]な変換方法の提供を考えてみたいと思います。
JSONは基本動的型付けなので、[1, 2, null]のように、
ひとつの配列の中に様々な型の値が一緒に入っていることがあります。
連想配列であれば、違うキーに対しては、むしろ別の型の値が入っていることの方が普通でしょう。
例えば、{"name":"milk", "price":150}のように。
こういったものでもEither型への変換ができればすこしだけ便利かもしれません。
Either[A,B]は型パラメータを二つとるので、変換方法もReads[A]とReads[B]の2種類が必要です。
fromJsonを使って型の変換に失敗したときは例外が投げられるので、それをキャッチして
分岐してもいいのですが、成功したときはSome[A]、変換に失敗したときなNoneを返してくれる
便利なasOptメソッドがあるのでここではそれを使ってみました。

implicit def eitherReads[A,B](implicit fmtA: Reads[A], fmtB: Reads[B]):Reads[Either[A, B]] =
new Reads[Either[A, B]] {
def reads(json: JsValue) =
json.asOpt[A](fmtA) match {
case Some(a) => Left(a)
case None => Right(fromJson[B](json)(fmtB))
}
}




assert (fromJson[Either[String,Int]](json.parseJson("""1""")) == Right(1))
assert (fromJson[Either[String,Int]](json.parseJson(""""a"""")) == Left("a"))
assert (fromJson[List[Either[Either[Boolean,String],Int]]](json.parseJson("""["a",true, 5]"""))
== List(Left(Right("a")), Left(Left(true)), Right(5.0)))

これでちょっとは便利になりましたが、Either型をたくさん使うと
どこかでパターンマッチをたくさん書くことになって、JsValueに対して
パターンマッチを書いていくのとあまり変わらないことになってしまいます。
なにより型の混ざった配列ならともかく、
{"name":"milk", "price":150}のような連想配列は
Map[String, Either[String,Int]]
に変換するのではなく、
case class Hoge(name:String, price:Int)
のような何らかのクラスに変換したいのが普通でしょう。
もちろん特定のcaseクラスへの変換を記述することはそれほど大変なことではありませんが、
case クラスがたくさんある場合は、ちょっと面倒そうです。
そんな時のために、sjsonでは、任意のcase クラスへの変換プロトコルを簡単に提供するための
便利な関数が用意されています。Scalaでは可変長の型引数はサポートされていないので、
case クラスのメンバの数に応じて、asProduct1、asProduct2、asProduct3...
といったメソッドが用意されています。
sjsonのソースでは、テンプレートを使ってこれらの定義が自動生成していますが、
ここではplay.api.json用に、asProduct3だけをポーティングしてみたいと思います。
まず、asProduct3のシグネチャは次のようになります。

def asProduct3[S, T1, T2, T3]
(f1: String, f2: String, f3: String)
(apply : (T1, T2, T3) => S)
(unapply : S => Product3[T1, T2, T3])
(implicit bin1: Format[T1], bin2: Format[T2], bin3: Format[T3])
: Format[S]

まず、型パラメータの[S, T1, T2, T3] です。
ひとつめのSは、変換対象のcase クラスです。
T1,T2,T3はそれぞれ、caseクラスのメンバ変数の型になります。
最初の引数、
(f1: String, f2: String, f3: String)
は、メンバ変数に体操するJSONでのキーになります。
メンバ変数の名前と同じものになることも多いでしょう。
次の二つの引数 applyとunapplyは、
(apply : (T1, T2, T3) => S)
(unapply : S => Product3[T1, T2, T3])
caseクラスのコンパニオンオブジェクトのapply unapplyメソッド
を受け取ります。
最後に、各メンバ変数の型ごとにReads[T1]やWrites[T1]を供給するために
Format[T1]をimplicitに受け取ります。

たとえば、case class Hoge(name:String, price:Int, flag:Boolean)
を、{"name":"...", "price":1234, "flag":true}に変換するのであれば、

asProduct3[Hoge, String, Int, Boolean]
("name", "price", "flag")
(Hoge.apply)
(Hoge.unapply)
といった引数を渡すだけで、
Format[Hoge]の実装を得ることができます。
asProduct3の実装は次のような感じになります。

def asProduct3[S, T1, T2, T3](f1: String, f2: String, f3: String)
(apply : (T1, T2, T3) => S)
(unapply : S => Product3[T1, T2, T3])
(implicit bin1: Format[T1], bin2: Format[T2], bin3: Format[T3])
= new Format[S]{
def writes(s: S) = {
val product = unapply(s)
JsObject(
Map(
(f1, toJson(product._1)),
(f2, toJson(product._2)),
(f3, toJson(product._3))
))
}
def reads(js: JsValue) = js match {
case JsObject(m) =>
apply(
fromJson[T1](m(f1)),
fromJson[T2](m(f2)),
fromJson[T3](m(f3))
)
case _ => throw new RuntimeException("object expected")
}
}


これを使えば、例えば次のようなcase クラスに対して、


case class Shop(store: String, item: String, price: Int)


次のように変換プロトコルを簡単に用意することができます。

object ShopProtocol {
import DefaultFormat._
implicit val ShopFormat: Format[Shop] =
asProduct3("store", "item", "price")(Shop)(Shop.unapply(_).get)
}

詳しくは、sjsonのgithubの該当部分をご覧ください。
https://github.com/debasishg/sjson/blob/master/src/main/scala/sjson/json/Generic.scala

以上です。
これを書いている間に日が変わってしまいました!ゴメンナサイ!!
次回12月8日は@hagikuratakeshiさんです。

よろしくお願いします!!

2010年12月11日土曜日

SQLAlchemyのモデルクラス用のFlaskコンバータを作ってみる

最近、PythonのWebフレームワークの中では、軽量級のFlaskをよく使っています。
Flaskは、wsgiツールキット的なWerkzeugを基盤に構築されたフレームワークです。
Flaskではroutingの仕組みは、Werkzeugのものをほぼそのまま使い、
関数を簡単にコントローラとして公開できるようなデコレータが用意されています。
これはたとえば以下のように使います。


@app.route("/post/<int:post_id>")
def show_post(post_id):
return "hello"


<int:post_id>というところに注目してください。
この定義により、"/post/10"や"/post/5"のようなURLにマッチし、
show_post関数の引数post_idに、マッチした部分をintに変換し
10や5といった値がわたってくる事になります。

この<int:hoge>の部分は、Converterと呼ばれる仕組みで自作することができるようになっています。
今回は、SQLAlchemyとからめて、idを受け取って、特定のモデルクラスのインスタンスを
抽出するようなConverterを書いてみたいと思います。
モチベーションとしては、以下のようにIDをintとして受け取って、
そのidでインスタンスを検索し、なかったら404にするような操作を、

@app.route("/post/<int:post_id>")
def show_post(post_id):
post = Post.query.get(post_id)
if not post:
raise werkzeug.exceptions.NotFound()
return post.hoge()

次のように、モデルと同名のコンバータを指定する事で、
自動でIDの文字列からインスタンスをfetchして引数として渡し、
もし該当IDがなければ、自動で404としてくれれば、便利かなという感じです。

@app.route("/post") # idが該当しなければ404
def show_post(post):
return post.hoge()


まず準備として、FlaskアプリとSQLAlchemyの初期化を行います。

from flask import Flask, request, url_for, redirect
from flaskext.sqlalchemy import SQLAlchemy
from sqlalchemy import Table, Column, Integer, Unicode
from sqlalchemy.orm import mapper
from werkzeug.routing import IntegerConverter, ValidationError

app = Flask(__name__)

app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///test.db"
db = SQLAlchemy(app)


アプリやデータベースの準備ができたら、
早速コンバータのベースクラスを定義してみましょう。


class BaseModelConverter(IntegerConverter):
def __init__(self, map):
super(BaseModelConverter, self).__init__(map)

def to_python(self, value):
id_ = super(BaseModelConverter, self).to_python(value)
obj = self.model.query.get(id_)
if not obj:
raise ValidationError()
return obj

def to_url(self, value):
return str(value.id)

IntegerConverterを継承するので、正規表現的に\d+にマッチするかのチェックが
まず行われます。
to_pythonメソッドでは、valueつまりマッチした部分の文字列を受け取ります。
IntegerConverter.to_pythonに渡し、int値を得ます。(この時点で失敗すればその中でValidationErrorが投げられます)
このint値、つまりIDをもとにself.model.query.get(id_)により、インスタンスを取得し、
Noneが得られた場合はValidationErrorとします。
to_urlメソッドの方は、その逆に、インスタンスを受けて、そのidをstrにしたものを返すだけです。

そして、モデルを定義するたびにコンバータを追加していると手間なので、
特定のベースモデルクラスを継承したモデルを定義すると自動で同名のコンバータを
追加するようなベースクラスを用意します。
ベースクラスとそのメタクラスの定義は次のようになります。



class BaseModelType(type):
def __init__(cls, name, bases, attrs):
super(BaseModelType, cls).__init__(name, bases, attrs)
if name == 'BaseModel':
return
conv = type(name + "Converter", (BaseModelConverter,), dict(model=cls))
app.url_map.converters[name] = conv

class BaseModel(object):
__metaclass__ = BaseModelType

BaseModelType.__init__の中では、
BaseModelのコンバータは不要なので、とりあえず名前で
BaseModelかどうかのチェックを行います。
そうでなければ、そのモデルの名前 + "Converter"
たとえば UserConverterのような名前の、BaseModelConverterを継承したクラスを生成します。
これは、

class UserConverter(BaseModelCOnverter):
model = User

と同等です。
そして得られたコンバータクラスを、app.url_map.convertersに、モデルと同じ名前で登録します。
BaseModelは、BaseModelTypeをメタクラスとして持ち後は空のクラスです。

ではこれをつかって試しにUserクラスを定義します。


user_table = Table(
"user_table", db.metadata,
Column('id', Integer, primary_key=True),
Column('name', Unicode(100)),
)

class User(BaseModel):
query = db.session.query_property()
def __init__(self, name):
self.name = name

mapper(User, user_table)


とりあえず、上記のように、idとnameだけを持つモデルを定義してみました。
あとはこれをつかったコントローラを書きます。
今回は紙面の都合上テンプレートの使用は省略して
直接文字列を返しておきます。


@app.route("/detail/<User:user>", methods=('GET',))
def detail(user):
return """
<html><body>
<h1>%s</h1>
</body></html>
""" % user.name


もう、説明することがないくらいシンプルなコントローラです。
あとは、Userインスタンスを登録し、そのリストを一覧でだせるようにしましょう。


@app.route("/", methods=('GET',))
def users():
userlist = "".join(
'<a href="http://www.blogger.com/%s">%s</a>' % (
url_for('detail', user=user), user.name)
for user in User.query.all())
return """
<html><body>
%s
<hr />
<form method="post" action="/">
<input type="text" name="name">
<input type="submit">
</form>
</body></html>
""" % userlist

@app.route("/", methods=('POST',))
def add_user():
user = User(request.form['name'])
db.session.add(user)
db.session.commit()
return redirect(url_for('users'))

url_for('detail', user=user)の部分で、UserConverter.to_urlが内部で呼び出され、
適切なURL、たとえば/detail/1 といったURLが生成されることになります。

これで準備はととのいました。
あとは、実行するだけです。



if __name__ == "__main__":
db.metadata.create_all(db.engine)
app.debug=True
app.run(host="127.0.0.1",port=8000)


と最後にかいて、ファイル全体をmain.pyなどの適当な名前で保存し、
python main.py
とすればサーバが立ち上がります。
適当にユーザを追加して動作を確認しましょう!

今回はモデルのIDをマッチする単純なものでしたが、
id以外のカラムにマッチしたり、難読化したidにマッチしたり、
制限時間付きのhmacトークンを埋め込むURLを生成したりといったことが
コンバータの追加で行う事ができます。ちょっとしたことではありますが、
コンバータで抽出、整合性のチェックを行い、失敗すれば404にしてくれるのは
地味に便利だったりします。
皆さんもFlaskやWerkzeugを使って便利なコンバータを使って、
すてきな Python Web FWライフを!
ということでつぎは、@r_rudi にバトンを渡したいと思います。よろしく!>@r_rudi



2009年12月16日水曜日

zope.sqlalchemyの紹介

中西さんよりZope/Ploneアドベントカレンダーのバトンを頂戴したので、Zope/Ploneネタを書いて16日目を担当したいと思います。

普段の仕事ではZope/PloneよりもSQLAlchemyなどを使いつつRDBMSを使用するようなフレームワークを使うことの方が多いです。
どんなネタにしようかとすこし悩みましたが、Zope/PloneからSQLAlchemy越しにRDBMSにアクセスする方法について調べてみました。

Zopeは自前のオブジェクトデータベースZODBを持っているのでZope/PloneからRDBMSを使用する機会はあまりありませんが、既存のデータベースとやりとりが必要になることは、たまにあります。
ZopeからRDBMSを操作する方法としては、
を使用するが昔からあって、
select * from products where id=<dtml-sqlvar type="string">
このような感じで使用するようですが、ちょっと古い感じがしないでもないです。

現在PythonでSQLとなれば、
The Python SQL Toolkit and Object Relational Mapperであるところの
SQLAlchemyを使うのが一般的です。
ということで、zope.sqlalchemyを使う方法を調べてみました。
zope.sqlalchemyは、"Minimal Zope/SQLAlchemy transaction integration"ということで
ZopeのトランザクションとSQLAlchemyのトランザクションを統合してくれる
ツールのようで、その際、きちんと二相コミットもしてくれるようです。
zope.sqlalchemyの守備範囲は、トランザクションに関してだけで、データベースエンジンやデータモデルの記述については特別なサポートはありません。
つまり、SQLAlchemyの作法に則って、DBエンジンやモデルについての記述を行うことはできるけれども、ZMIからデータベースエンジンの設定を行ったり、zcmlで定義したり、Zopeのオブジェクトモデルとマッピングしたり、といった機能はなく、あくまでトランザクションの統合機能だけが提供されます。
>>> from sqlalchemy import create_engine
>>> engine = create_engine("mysql://hoge", convert_unicode=True)
>>> from sqlalchemy.orm import scoped_session, sessionmaker]
>>> from zope.sqlalchemy import ZopeTransactionExtension
>>> Session = scoped_session(sessionmaker(bind=engine,
... twophase=True, extension=ZopeTransactionExtension()))
>>> session = Session()
>>> session.add(Hoge(name='fuga'))
>>> session.query(Hoge).all()
[]
>>> import transaction
>>> transaction.commit()

とこんな感じの使い方になるようです。
(詳しくはzope.sqlalchemyを参照してください。)
ポイントは、
sessionmaker(bind=engine,
twophase=True, extension=ZopeTransactionExtension())

の部分と
>>> import transaction
>>> transaction.commit()

のところです。


コードの説明に入るまえに、そもそもなぜトランザクションの統合が必要になるか、ということについて簡単に説明します。

Wikipediaのトランザクションの項によると、
"トランザクション (transaction) とは、分ける事の出来ない一連の情報処理の単位である"とあります。
つまりひとつのトランザクションに含まれる処理であれば、全部が成功もしくは全部が失敗のどちらかになるべきで、一部が失敗して一部が成功した、といった状態になったら困るわけです。これを実現するために、ひとつの
トランザクション処理の途中で、なんらかの不都合が発生した場合、そこまでの処理をすべてなかったことにするロールバックや、すべての処理が成功した段階で、変更結果を永続化させるコミットの機能がZODBやRDBMSにより提供されます。

ZopeのZODBはトランザクションをサポートしており、作法に則ったやり方でZODBにアクセスすれば、処理の途中で例外が発生した場合ロールバックされ、一貫性が保たれます。
しかし、そこでロールバックされるのは当然ZODBに対する書き込み操作のみで、もし直接外部のRDBMSに対して書き込みを行っていた場合、その処理まで自動でロールバックされるわけではありません。すると、Zope/Ploneとしては処理が途中で失敗したのでロールバックを行い、ZODBの中身的には変更が無かったことになっているのに、外部のRDBMS には処理が成功したかのように変更が書き込まれてしまう、といった結果になる可能性があります。多くの場合において、これは望んだ結果ではないでしょう。
これを防ぐには、ZODBとRDBMSに対するふたつのトランザクションを統合し、ZODBに対する処理とRDBMSに対する処理が、両方成功するか、両方失敗するか、どちらかであるようにする必要があります。

これを実現する中核となるのが、zope.sqlalchemy.ZopeTransactionExtensionです。これはSQLAlchemyの
extending-sessionという機能を利用したもので、
sqlalchemy.orm.interfaces.SessionExtensionを継承(or実装。ここらへんはPythonなので微妙なところですね。)したクラスです。このクラスを使用することにより、zope.sqlalchemyはセッションの開始や、セッションの中でオブジェクトの追加や変更、削除などをフックすることができます。
でもって、セッションの開始でTwoPhaseSessionDataManagerインスタンスを作成し、zope_transaction.get().join(TwoPhaseSessionDataManager(session, state))
のような感じでzopeのトランザクションに結びつけます。
そして、SQLAlchemyのセッションでオブジェクトの変更などをフックして、状態を初期状態のSTATUS_ACTIVEからSTATUS_CHANGEDに変更します。
そんでもって
>>> import transaction
>>> transaction.commit()

によりzopeのトランザクションがコミットされた場合は、TwoPhaseSessionDataManagerのメソッドが呼び出され、
もし状態がSTATUS_CHANGEDになっていれば、二相コミットを行います。



STATUS_ACTIVE/STATUS_CHANGEDと、2相コミットに関して補足します。

2相コミットとは、複数のデータベースが存在する時に、各トランザクションを統合して、全体として成功/失敗のどちらかにするための技術です。ちょっと説明が適当なので、
詳しくは
Wikipedia:"2相コミット"を参照してください。
例えば、データベースAとBがあった場合、よくありそうな問題としては、全部の処理がうまくいったので、
まずAにコミットして成功し、つぎにBに対してコミットを行おうとしたら、コミットに失敗してしまった、という状況です。
これを防ぐために2相コミットでは、トランザクションのコミットしようとするとき、
参加しているデータベースに対して、コミットが可能であるか、まず全員に質問します。ここで、全員から同意が得られれば、全体をあらためてコミットします。逆に、どれかひとつでも同意が得られなかった場合は、全体をロールバックさせます。この2相コミットを使用すれば、DBMSが落ちる、DBMSとのネットワークが切れるなどの事態が肝心なタイミングで発生しなければ、全体の一貫性を保つことができます。
この2相コミットは、それなりに面倒な処理です。RDBMSの方に書き込みを行わなかった場合、そもそも2相コミットを行う必要がありません。このためにzope.sqlalchemyでは、SQLAlchemyのセッションに変更操作を行ったかどうかを、STATUS_ACTIVE/STATUS_CHANGEDという状態により管理し、zopeのtransaction.commit()時にSTATUS_ACTIVEのままであれば2相コミットを省略するような動きになっているようです。
STATUS_ACTIVE/STATUS_CHANGEDの状態は、sqlalchemy.orm.interfaces.SessionExtensionのafter_bulk_updateメソッドなどが呼び出されるタイミングで、STATUS_CHANGEDに変更されます。
このフックメソッドは、次のようにORMを通さないでSQLが発行された場合は呼び出されません。
(よびだしてくれればいいような気がしますが。)

>>> conn = session.connection()
>>> conn.execute(update_expression)

この問題を防ぐため、このような場合には、つぎのように明示的にSTATUS_CHANGEDをセットします。
>>> from zope.sqlalchemy import mark_changed
>>> mark_changed(session)

<追記 2009-12-18>
sessionmakerの作成の際に、twophase=Falseにしておけば、2相コミットをせずに
動いてくれます。当然不整合が起こる可能性は高くなりますが。
あと、twophase=Trueの場合は、当然ですがRDBMS側でも2相コミットが可能な設定になっている必要があります。

ということで、ということで、次のバトンは昔からブログでお世話になっているPythonの大先輩のnakagamiさんにお願いします。

2009年12月6日日曜日

InfoQ ClojarsとLeiningenを使ったClojure向け自動ライブラリ依存関係管理

PythonのPYPI、easy_install/pipにあたるもののようだ。