ORM嫌いについて

はじめに

Object-Relational Mappingツール・ライブラリ(以下、ORM)と言えば必ずといって良いほど誰かの不満の種デス。自分も使ってはいるけど不満はあります。ところで、あなたのご不満のORMとはなんのことを指してますか?そもそも、ORMという言葉は何の厳密な機能範囲も定義していないのですが、みんな過去にハマったライブラリの設定や自分のプロジェクトで使いにくかったことをもって「だからORMはダメだ」と主張している節もあると思います。下記のこの記事も(元ネタはBlikiですが)、ついてるコメントはみんなバラバラのことを指して不満点や解決案を書いています。
Martin Fowler on ORM Hate

自分にとってのORM

ORM vs SQLという対立軸で語られることもありますが、自分はSQL大好きですよ。宣言的でとても効率的に関係の操作を表現できる言語だと思います。

自分が現存するORMで期待しているところは、オブジェクトと行の詰め替え処理です。新しいオブジェクトのプロパティ値をINSERT文のVALUESに詰め替えたり、結果行の各カラム値をオブジェクトのプロパティに詰め替えたりするのは、誰がやっても同じになるコードをクエリごとに書かないといけないわけで、そこはライブラリの力を借りても良いところでしょう。

C#でLINQ to SQL(.NET4からはEntity Frameworkになったんでしたっけ?)を書いた時には、形安全クエリが書けることがうれしかったですね。静的型付け言語の経験が長いせいか、データベースのクエリでも型に対する誤りはコンパイラがチェックしてくれるということの安心感や、IDEがプロパティ名を補完してくれたり候補を表示してくれたりするのが思いのほか心地良かった。クエリを書く時にDDLファイルとか(あるいはテーブル定義書.xlsとか)を開いてテーブル名やカラム名をコピペしなくて良いのです。また、テーブル名やカラム名の変更も、IDEのリファクタリング機能で行えるのです。Javaも、8でラムダ式がサポートされたらこうなっていくでしょう。てか、scalaでできるよ、それ!

多分、自分はリレーショナルデータベース(関係代数演算)の操作がプログラミング言語にうまく統合されて欲しいのかもしれないですね。今はその手段として一番近い(まだ遠いけど)のがORMだというわけです。そりゃ、ORMの現状には不満がいっぱいあります。

ORMだと性能に問題アリですか?

性能の問題が起きた時にSQLクエリを直接さわれないと困るってよく言われることだと思います。
しかし、SQLを直接書いたからといって、性能の問題が起きないSQLクエリなんてあるんでしょうか。
SQLは宣言的な言語であり、クエリは「このような結果(What)をください」と記述します。DBMSはそのクエリをどのように(How)実行するか制御します。クエリ実行にあたっては、一番レコードを絞り込めるWHERE条件や件数の少ないテーブルの結合から実行すると速いとか、件数少ないテーブルはINDEX領域を取得してからより実レコード領域を直接取得するほうがIOを節約できるとか考えなくてはいけませんが、そういったことは DBMSにおまかせできます。その反面、DBMSが判断を誤るとクエリ実行時間が爆発することもあります。
このようなこともあり、Oracleではヒント句というのが用意されてますが、Oracleがクエリ実行計画を立てる時の統計情報を正しく保つように運用することが推奨されてます。

宣言的な言語は、機能的に動作させるまでが早い反面、いま言ったように性能的に効率良く動作させるためにはその実行系をチューニングして、速くなるよう仕向けるような間接的な作業をしなくてはいけません。でもそれってもう普通ですよね。みんな例えばメモリ領域をどう確保していつ解放するなんて直接書かずに ガベージコレクタ(GC)のパラメータでチューニングする言語・実行系を使ってるんじゃないですか?
もちろんGCのような機能はあらゆる処理で最速に動作することのは不可能なので、大抵の処理でそれなりに速いというところを狙ってて、実行環境によって違うところはパラメータでチューニング可能にしていると思います多分。それでもダメな場合はNativeコードを併用する必要性も出てきます。しかし、その場合でも、システムのあらゆるコードを1から10まですべてNativeコードでメモリ管理を明示的に書くべきとは思いません。性能問題が起きないようにすべてのSQLにヒント句をいれることを強制するプロジェクトとか、ORMをやめてSQLを書くべきだとかいうのは、それと同じものを感じます。
そして大抵のORMライブラリでは、DBMSのNativeなSQLを発行する機能があります。ORMかSQLかの2択などではなく、必要な時に必要なものを使い分ければ良いのです。

抽象化とバカ量産説

かつて(いまでも?)GCが「メモリ管理ができない低能プログラマを量産する」と言われたように、ORMとSQLにも同じ指摘があります。ある意味では、自分もコンパイル言語のせいで生み出された、マシン語が読めない低能プログラマということになります。
自分の場合は再帰処理のコールスタックくらいは意識しますが、書いたプログラムの1から10まで常に意識しなくてすむのはありがたいことです。仮に2割くらいはそういったローレベルを意識しているとして、でも残りの8割は脳みそ空っぽで過ごしているかというとそんなことはありません。その時間のほとんどは、自分のコードの問題領域(ドメイン)をどのように解決すべきかを考えるのに費やしているはずです。
より高度化するドメインの複雑さを解決することに集中できる道具なのであれば、自分は積極的に受け入れたい派です。コンピュータリソースが高性能化かつ廉価になり、実行系やツールの進化もあれば、高度化するニーズや複雑化する問題領域の解決に応えられなければならないでしょう。

最後に

ORM嫌いについてというタイトルですが、嫌い・不満の理由はひとによって様々あり、それらを網羅的にひとつの記事で書くつもりはありません。今回は、自分の観測範囲でよくありそうなアンチORMの主張を書いてみました。
ローレベルであれば「出来ないこと」は少なくなるということで、それを万能であると言うのは、それは「万能」包丁みたいな意味であり、目的に特化したらもっと良い方法あるよってことです。
そしてコンピュータリソースの高性能化や、実行系やツールの進化は、よりハイレベルの万能機能を産んでいくでしょう。いまはそうでなくても、長期的にはそういう流れはあると思います。そうしてプログラマは楽になるかというとそうではなくて、システムやサービスが価値を産むための仕事にとりかかれるようになるということです。

広告

[ORM][Java] JavaのORMをいくつか比較してみる

昼間のお仕事の関係でORMをいくつか触ったので、比較してみることにした。今更なーという感じはするけど、現時点で選ぶなら何なのだろうかと考える人の一助になれば。

※ 2012/09/18 改訂。Spring DATA JPAと QueryDslについては詳細をGistにあげたので、リンクを貼りました。

  • Java Persistence API (JPA)
    • JSR-317. 一応JavaEE6の標準になっているが、使いやすいわけではなく補助的に組み合わせて使うライブラリがいくつか存在するのでそれらについて後述。
    • 大まかに、クエリを書く方法は2通り
      • JPQL(SQLに似非の独自クエリ言語)を文字列で書く
        • 例: select o from Order o where o.date < '2012-09-01'
      • Criteria API(メタクラスを生成すればある程度は型安全だが、APIが使いにくい)で書く
        • クエリ言語として流れるように書けないため、表現力が低い
        • これを解決するのが後述するQueryDsl。Blog記事が参考になる。
  • Spring DATA JPA
    • Spring Frameworkの拡張ライブラリ(springframework-jdbcシリーズのようだが、安定したら本流に組み込まれるのかもしれない)
    • Gistに詳しく書いてみました。こちら
  • QueryDsl(JPA)
    • Open Sourceのライブラリ。クエリを型安全で流れるようなインタフェースなDSLで記述することができるライブラリで、JPAのCriteria APIに変換するDSLと、直接SQLに変換するDSLとがある。後者はQueryDsl(SQL)として後述。
    • 前者のJPAのCriteria APIに変換するDSLについては、QueryDslの Blog記事を参照のこと。コードを見てもらえば、これ以上特筆することはない。
    • ちなみに更新系のinsert, update, delete処理は、JPAのEntityManagerのAPIをそのまま使用する
  • QueryDsl(SQL)
    • QueryDSL(SQL)については Gistに詳しく書いてみました。こちら
  • Spring DATA JDBC Extension
    • Spring JDBCの拡張ライブラリ。上述の Spring DATA JPAともまた別モノ。
    • Spring JDBC本体の JdbcTemplate から QueryDslの型安全クエリを生成できるが、流れるように書けないという残念なAPI。
    • 上記QueryDsl(SQL)の説明Gistに一緒に書きました。

 

ここに挙げたライブラリの動作確認コードは、 githubに上げてます。

総合評価的なマトリクスは昼間のお仕事でそのような流れになったら作成する予定。いま選ぶとしたら個人的には QueryDsl(JPA)かなぁ。

それから、実行時性能を比較するためにベンチマークのテストを仕込んでいるところ。これについては後日書く予定。

[gradle] Java6 Annotation Processing

  • QueryDsl(JPA)を例に GradleでAnnotation Processingを実行させてみる
  • antの例があるので、ほぼこのままだったりするけど。。

annotation processingとは

dependenciesの設定

  • Annotation Processorを含むjarに対する依存関係を設定する。QueryDslは本体機能へのAPI(querydsl-jpa.jar など)とは別に、Annotation処理のjarが切り出されている(querydsl-apt.jar)。querydsl-apt.jarは、実行時には不要であるため、独自にdependency condfigurationを設定した。(下記processor)

 configurations {
      processor {
          description: "classpath for annotation processors"
      }
  }
  • dependenciesクロージャで、processorquerydsl-apt.jar を指定する
 dependencies {
      compile "org.eclipse.persistence:javax.persistence:2.0.0"
      compile "org.eclipse.persistence:eclipselink:2.4.0"
      compile "com.mysema.querydsl:querydsl-jpa:2.7.2"
      compile "org.slf4j:slf4j-log4j12:1.6.6"

      processor "com.mysema.querydsl:querydsl-apt:2.7.2"
  }

antのjavac呼出タスク定義

  • Annotation Processingの出力先は、build/processorGen/srcとした。今回は拡張プロパティext.destdirに設定しておく。

task execProcessor {
      ext.destdir = new File(buildDir, "processorGen/src")
      // ...
  }
  • buildディレクトリ配下としたのは、cleanタスクで消してくれるため。例えばモデルクラスの名前を変更すると旧名のメタクラスが残ったままになってしまうなど、cleanできたほうが都合がよい。

  • もちろん毎回消して再生成もよいのだけど、今回はUP-TO-DATEを仕掛けてビルドを効率化したかった。
  • UP-TO-DATEは下記のように inputsとoutputsにそれぞれファイルまたはディレクトリを設定することで、変更の有無(つまりAnnotation Processingタスクの実行が必要かどうか)をGradleが判定してくれるようになる。

 task execProcessor {
      ext.destdir = new File(buildDir, "processorGen/src")
      inputs.source(sourceSets.main.java)
      outputs.dir(ext.destdir)
      doLast{ /* ここにjavacを呼び出す処理 */ }
  }
  • doLastクロージャで、このタスクの実行処理を記述する。この例ではantjavacを呼び出してみる。

doLast {
      ext.destdir.mkdirs() // 出力先が事前に存在しないとエラーになるのを回避

      def primaryOptions = [
          includeAntRuntime: false,
          destdir: ext.destdir,
          classpath: configurations.compile.plus(configurations.processor).asPath,
      ]

      ant.javac(primaryOptions) {
         sourceSets.main.java.addToAntBuilder((Object)ant, 'src', FileCollection.AntType.MatchingTask)
         compilerarg(value: "-proc:only") 
         compilerarg(value: "-implicit:none")
         compilerarg(value: "-processor")
         compilerarg(value: "com.mysema.query.apt.jpa.JPAAnnotationProcessor")
      }
  }
  • 上記のant.javacはおおよそ下記のようなant記述と同じ意味

    <javac includeAntRuntime=false
           destdir="${ext.destdir}"
           classpathref="${compiler.plus(processor).asPath}">
      <src ...ここはaddToAntBuilder()がゴニョゴニョする  />
      <compilerarg value="-Xlint"/>
      <compilerarg value="-proc:only"/>
      <compilerarg value="-implicit:none/>
      <compilerarg value="-processor/>
      <compilerarg value="com.mysema.query.apt.jpa.JPAAnnotationProcessor/>
    </javac>

compileJavaの設定

  • これで execProcessorを実行すればQueryDslによってメタクラスがbuild/processorGen/srcに生成される。だがこのソースが compileの対象になっていないのでこのままではコンパイルされない。
  • ここで src/main/javaディレクトリにコピーするという手もあるが、UP-TO-DATE判定を考えると好ましくない。また、Checkstyleといった静的解析ツールが処理する対象ディレクトリから自動生成されたコードを分離したいということもあるかもしれない
  • ということで、compileJavaの対象ソースディレクトリとして execProcessorの出力先ディレクトリを追加する。
  • また、compileJavaの依存タスクとして execProcessorを設定する。

compileJava {
      dependsOn execProcessor
      source execProcessor.ext.destdir
 }

これでcompileJavaを実行するとexecProcessorが先に実行される。

$ gradle compileJava
:execProcessor
[ant:javac] 注意:Running JPAAnnotationProcessor
[ant:javac] 注意:Serializing Entity types
[ant:javac] 注意:Generating .... for [...]
[ant:javac] 注意:Running JPAAnnotationProcessor
[ant:javac] 注意:Running JPAAnnotationProcessor
:compileJava

BUILD SUCCESSFUL

また、変更がなければ UP-TO-DATEと判定され、実行されない。

$ gradle compileJava
:execProcessor UP-TO-DATE
:compileJava UP-TO-DATE

BUILD SUCCESSFUL