[Scala, Play framework]anormを使ってみて困ったこと、試したこと

きっかけ

プロジェクトの都合で、play frameworkのデータベースアクセスにanormを使うことになりました。

今まではslick,skalikejdbc、そして先輩が作成してくれた自作ORMを
利用して、開発を行ってきた経験がありましたが、後述するような
理由があり、敬遠してきました。

一度モデルを色々書いてみて、じぶんなりに『とりあえずこれはあったほうがいい』
とか『これは便利だから使っておこう』というものをまとめようと思います。
(できればデータベースからそれをリバースエンジニアリングして、ひな形を出力してくれるような
プラグインとかまで書けるといいと思ってるけど、それは次回以降です)

とりあえず基本的な使い方おさらい

SQLの書き方、とかは割愛します。最低限知っておいたら
使える関数とかを挙げます。

update,insert,delete

  • executeUpdate()で更新行を返す
  • executeInsert()でautoincrementされたIDを返す
    (auto incrementが設定されていなければ、0が返ってくる)

UPDATE,DELETEではexecuteUpdate()を、IDにauto incrementを定義していればINSERT
はexecuteInsert()で行うのが通例

IDが自動で採番されない定義の場合はexecuteUpdate()で更新できます。
(executeInsert()でも挿入した行のIDは取れません)

select

めっちゃくちゃ単純なサンプル。

Userテーブルからnameカラムを取得して、取得できなければNoneを返します。

テーブルデータ -> オブジェクトのAPI色々

上記ではSqlParserを利用した最もシンプルな例を出しましたが、anormにはコレ以外にも
SQLの結果をオブジェクトにマッピングするAPIが用意されています。

これが豊富な故に、若干チュートリアルがわかりづらい、というのもあるかも・・・。
(PlayFramework2.3のドキュメントではまだ一部日本語も翻訳されていないのでなおさら)

StreamAPI

SQL()で作成されたインスタンスのapply()を実行することでStream[Row]を取得することができる。
これをmapすることによって、結果を1行ずつ処理することが可能です。各行はRow型のインスタンスになっており
ディクショナリとしてアクセスすることができます。

Rowのインスタンスとパターンマッチ

SQL(“SQL STATEMENT”).apply().collect()でパターンマッチにマッチしたデータだけを抽出することもできます。

これでUser.nameのうち空でないものだけをListにして取得することができます。

ParserAPIによる単一カラムの取得

色々な方法がありますが、結局ParserAPIを利用してマッピングするのが
一番汎用的かなー、と思います。

一番最初のサンプルで利用していたのがこれです。

nameという結果をOption[String]にして返す。値がない場合はNoneが返ってきます。

for-comprehension

ParserAPIをfor文で記述することができます。
公式ページのサンプルそのまんまですが。

これで(String,Int)のタプルを結果として返すことができます。

ParserAPIによる独自クラスインスタンスの取得

for-comprehensionに似ていますが

for-comprehensionと似ています。この例ではparserApiを利用することでList[Long~String~Option[String]]型が取得できるので
それを更にUserクラスのインスタンスにマッピングしています。


anormの使いづらいと感じたところ

おさらいが長くなってしまいましたが、ざっくり使うならSQLの構文と
上記を覚えておけばとりあえず使えると思います。

anormはテーブルと1:1になるクラスがあってもなくてもよくて、
結局規模がある程度になると、その辺柔軟にやりたいし、DSLだときついよねってことだと思う。

ただ、必要最低限な情報をタプルにしたり、必要な情報セットの種類だけ
case classとかを定義するのも後々管理が面倒になることがわかりきっているので、
ある程度は汎用的な型にはめて、それを利用するようにしたいです。

そして、少しanormを使ってみて、僕がつらくなったのは以下

  • 動的にWHEREを構築したいとき
  • 同じようなSQL文を複数回記述必要が出てくる(テーブルやカラム名変更あるとつらい)
  • ParserAPIが大きいテーブルになるとマッパー書くのもつらい

動的にWHEREを構築したい

あるテーブルに対して、ほとんど必ず必要になるであろう基本的な処理として

  • INSERT(挿入 Create)
  • SELECT(参照 Read)
  • UPDATE(更新 Update)
  • DELETE(削除 Delete)

があると思います。
SQLにおいて、既存のデータに対する操作であるSELECT,UPDATE,DELETEは
WHERE句で条件を指定することによって、対象のデータを絞り込みすることができます。

これをscalaとanormで制御することを考えます。

Userテーブルのデータを上記のcase classで定義しました。
そして、Userテーブルに対してID検索を行いたいとき

こんな感じです(前項で挙げたサンプルそのまま)
これはまぁいいんですが、名前や自由記入欄(profile)で検索を行いたいとき

もうちょっとやりようはある気もしますが、わかりやすくつらい例を。
条件は2つだけなのに、4つもパターンを書かないといけなくなりました。
条件が3つなら9個、caseを書く、つらすぎますね。

動的にSQLを組み立てるクラスやトレイトを用意する

こんな感じでWhere句だけを表現するクラスを定義してみました。
使い方はDynamicWhereSupportをミックスインして、条件を引数に受け取る

利用者側は

複数指定したい場合は、複数Where()を引数に渡せばOKです。


同じようなSQL文を複数回記述必要が出てくる(テーブルやカラム名変更あるとつらい)

こんな感じのモデルがあったとして、既にUserというテーブル名、
name,profileというカラム名が文字列として何度も登場しています。

この状態で開発が進んで、Userというテーブル名がCustomerという
名前に変更されると、まぁ規模が小さいうちはいいんだけど。

いろんなところからjoinとかしてると、結構つらくなってきます。
Stringだから全文検索すれば一覧出せるけど、時間かかるし。
変数の参照でリスト出したいし、IDEのリファクタリング機能で一撃でリプレイスしたいところです。

これに関してはテーブル名、カラム名を別途定義して
それをs式で評価させるようにしています。具体的には

こんな感じ。where句がちょっと汚いかも・・・。
要するにS式で変数展開させたあと、その文字列をSQL””” “””内で更に評価して、それに対してonしてます。

ただ、テーブル名、カラム名が設計から滅多に変わらない!
ということもよくあることだと思いますので、この辺は適宜・・・。

ParserAPIが大きいテーブルになるとマッパー書くのもつらい

これ、人がやる作業じゃねぇだろ、って結構いらつきながら今回は頑張りました。
ただ、以降は自動化したいと思ってます。

今までサンプルにあげたUserテーブルとかは
カラム数少ないし、特殊なカラムもないので

この部分
ParserAPIも比較的シンプルに書けました。
ですが

  • カラム数が30個とか大きい
  • 整数を列挙型に変換してインスタンスに持たせたい
  • Timestamp,DateTime,Enum,Blobとかは?
  • NullableとNotNullとか

とかの需要が当然でてきます。
4つのうち、最初以外はドキュメント読めばちゃんと解決できます。
(ただ、初めてのscala,初めてのplay frameworkという人が
なし崩し的にデータアクセスするモジュールにanormを選択した場合は悩むと思う。)

とりあえずは簡単な下3つから

整数を列挙型に変換してインスタンスに持たせたい

Scalaで列挙型(enum)を作る

anormというよりはscalaの話。(mysqlのENUM型ではないです)
整数受け取って列挙型のメンバを返すファクトリメソッドみたいなの用意しました。

僕はscalaをはじめてすぐに上記の参考リンクみたいな話を読んでいたので
直接Enumerationを使ったこともないし、Enumerationの不便さを体感してないですが
解説には納得。

ということでリンクのように列挙型として扱えるようなインスタンスを定義しておく

関数定義できるのが個人的にはEnumrationとくらべてcase object使う一番の理由になってるかも

allは全UserTypeの集合、getはcodeからUserTypeを取得する関数で、
不正なcodeが渡されたらUserType.Invalidを返します。

これなら

こういうインスタンスにしたいときは

こんな感じにすればUserType型を保持したインスタンスに
できます。

特殊なカラムはどうするの?(Timestamp, Date, Blob, Enumとか)

基本的に特殊なカラムはjava.sqlパッケージ内に含まれているので
それを利用すれば利用できます。java.sql._を直接使いたくなければ
自分でコンバータなりラッパーを用意する必要があります。

Dateはjava.util.Dateで取得できます。javaで日付を扱う際によく利用されるjoda time
にしたい場合はnew DateTime(date)でOKです。

EnumはString型として入ったと思います。
参照時はそれでいいんだけど、挿入時に変な値入れないように
上記で紹介した列挙型みたいなのを通したほうがよさそうです。

NullableとNotNullとか

これは既に何回か登場しちゃってますが、非常に簡単です。

anorm.SqlParserパッケージ以下に、標準で利用できるパーサーが
いくか提供されているので、基本はそっちを利用(str(“name”)とか)します。

nullableのカラムはOption[T]型で対応します。任意の型のパーサーは
get[T]で宣言します。(上記の例ではnullableなStringなのでget[Option[String]])
これでnullならNoneを返してくれるパーサが定義できました。

カラム数が非常に大きいテーブルのパーサとon()

以上挙げた3つは、最初ちょっと戸惑うけどドキュメント読めば
書いてある範囲。anormを利用する分にはひと通りの機能が把握できたと
思います。

ようするに解決方法があるのに、その方法を知らないだけという問題に
関しては、一度知ってしまえば問題なくなるんですが。こちらは
ライブラリのアーキテクチャ的に、どうしてもぶつかる壁です。

で、解決方法ですが、特に無いです
頑張って書くしかありません。

パーサを書くだけでなく、onの中も書かないといけません・・・。
ただ、つらいですが、これはやるしかない。
めんどくさくても書くしか無い。
楽をするとしても、コードジェネレータとかを自分で書く必要がでてきます。 
SQL Interpolationを利用すればon書かなくてもいいんだけど、そうすると
前項にあげた、文字列内のテーブル名、カラム名に冗長な記述を強いられる・・・。
うーん、なんとかできないものか。(いまんところ僕は解決策見つけられていません・・・)

まとめ

僕がAnormを使って、とりあえず困った事はこんな感じです。
多分もっと複雑なSQL書くことになると、更に色々ありそうです。

適当に作ったサンプルはこちらです。

一応、テーブルが非常に大きい場合を除いて、それぞれ対応策は挙げられたので
(これでいいのかは不安が残りますが)少しでも参考にしていただけたら幸いです。

多分もっといい方法もあるような気がしているのですが、何故かAnormのベストプラクティスや
充実したチュートリアルは少ない気がしています。

こういうふうに使うと便利だよ!というものがあれば
教えてください><

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です