ふつうのDoma2の使い方
最近Tuzigiriというリポジトリを作ったりしてる。あまり進んでないけど。
このリポジトリは、GitHubのリポジトリシェアのサービスをオープンソースで作りつつ、色んな人にSpringBootのお手本や参考にしてもらえばいいかなというモチベーションで作っている。このへんのお気持ちはまぁおいおい別記事で書こうと思う。
いまはnoko_kと一緒に走り始めた段階だが、とあるプルリクエストでDoma2の使い方について議論になったのでそのへんのことを書こうと思う。
ワイルドカードというアンチパターン
雑にワイルドカードでカラム指定をしているSQLでnokoによってお縄になった。これはSQLアンチパターンのインプリシットカラム(暗黙の列)というものらしい。(最近SQLアンチパターンを買ったのに積んであって申し訳ない…)
積んであったSQLアンチパターンを読んでみると以下のようなデメリットが書かれていた。
- 列の追加削除をしたとき、添字でアクセスしていた場合ずれてしまう Doma2の場合は影響なさそう。生JDBCのResultSet#getString(columnIndex)のようなケースだと踏んでしまうケースですね。
- 余分なカラムをフェッチするのでパフォーマンスに悪影響がある それほど大量のカラム持ってないし気にするほどか?という気持ちもある。ちなみにDoma2では/*%expand*/ と書くことで、マッピングするEntityのカラムを自動的に展開してくれる機能がある。これを使って回避すると良いっぽそう。
さらに、Domaは特別に設定を書かなかった場合デフォルトでマッピングできないカラムがあったときに例外をスローするのでワイルドカードは危ない。
また、SQLアンチパターンになかった指摘としてカラムの型変換をしたときに明示的に書くことでgrepしやすいというものもあった。これはexpandでは満たせない要求なので難しい。
どうするのが良いのか
ワイルドカードを使わずにカラム指定を使ったほうが良いのは確かになったが、expandか明示的にカラムを書くか考える必要がある。 結論としては以下の理由でexpandを使う方針になった。
- 明示的にカラムを書いていた場合、新しいDBにカラムが追加されたときに複数のsqlファイルを変更する必要があり漏れて例外になる可能性がある。expandの場合マッピングする型の修正のみで済む。
- expandのほうが楽なのと、Domaに乗っかっている感じがある。
まだ探り探りやっているのでこれが普通かどうかはまだ自信がない。
オレオレORM「Moco」についてのお気持ち
去年辺りからチマチマとMocoというオレオレORMを作っているので技術的な話ではなくお気持ち的な話を書いてみたくなった。
技術的な話は気が向いたら別の記事にしようかなと思ってるけどまだ気分ではないかな。
Mocoの思想
JavaでDB操作をする時は自分でゴリゴリSQLを書く薄いORMが好んで使われる。DomaとかSpring JDBCとか使ったことないけどMyBatisもそうなのかな?
自分でSQLを書くことには僕は抵抗がないしDomaとかは個人的に結構好きだったりする。ただ、遊びでピャッっとWebアプリ作ろうかなになった時、リレーションをJavaの世界で組み立てるのが結構面倒だったりする。複数のCustomerに各Customerが複数のOrderを持っているケースでJavaのオブジェクト煮詰めていくのが結構面倒くさいな~と思ったりしてた。
その時に出会ったのがActiveRecordのpreloadで、ActiveRecordでは以下のように書くことができる。
customers = Customer.preload(:orders).to_a orders = customers[0].orders
preloadに関連を書くだけで良い!なんだこれは便利すぎる…。あとSQL書かなくてよいの意外と楽だぞ!?けど補完があまり効かないしそこはJavaのほうが気持ちが良いんだよな…Javaならもっとうまくできるんじゃないのか?となったのが開発着手までの流れです。
なので「リレーションをうまく扱いたいね」と「ちゃんと補完バリバリできる方がいいよね」という思想がベースにあります。
Mocoという名前
Mocoという謎の名前ですが、これはセブンイレブンの「しろもこ」シリーズから取っています。なのでモジュール名もシュークリームっぽい感じにしてます。僕がすごい好きなイラストレーターがFANBOXで定期的にもこシリーズの話を書いていたのでそこから取りました。
ちなみに今まで食べた中ではティラミスもこが一番好きでした。
どうやって気持ちよく書くか
Javaの話に戻って、ActiveRecordっぽいものをJavaでやろうとしたライブラリはいくつかあるようですが、Stringで頑張ろうとしている辛いものばかりでした。MocoではPluggable Annotation Processing APIを使ってコンパイル時にクラスを生成することを選択しました。
POJOだけユーザーが定義して、そこからいい感じ操作をするためのクラスを二種類生成します。
1つ目はPOJOの複数形名のクラスで、カラムを表す定数や後述するEntityListを作るstaticメソッドが生えているTableClassと呼んでいるクラス。
2つ目はEntityListと呼んでいるクエリを作って結果を得るためのクラスです。
これらを使うことで以下のように書くことができます。
// このクラスはユーザー定義 @Table(name = "customers") public class Customer { @Column(name = "id", generatedValue = true, unique = true) private int id; @Column(name = "name") private String name; @Column(name = "city") private String city; @Column(name = "company_id") private int companyId; @BelongsTo(key = "company_id", foreignKey = "id") private Company company; @HasMany(key = "id", foreignKey = "customer_id") private List<Order> orders = new ArrayList<>(); // getter... } // CustomersとCustmerListはコンパイル時に生成される // Tokyoに住んでいる顧客 Customers.all().where(Customers.CITY.eq("Tokyo")).toList(); // preloadで関連を一緒に引いてマッピングする // noteを買ったOrder一覧 List<Order> noteOrders = LineItems.all() .where(LineItems.NAME.eq("note")) .preload(LineItems.LINE_ITEM_TO_ORDER) .stream() .map(LineItem::getOrder) .collect(Collectors.toList());
今後やりたいこと
今は一段のpreloadしかできないですが、近いうちにActiveRecordのpreload(hoge: :fuga)のような多段preloadをできるようにしたいと考えています。
どう書けば気持ち良いんだろうね…。
こう書けたほうが気持ち良いはず!などあればIssueやPRを送っていただけるとうれしいです。
ジェネリクスの型宣言付きの型の可変長引数って何がダメなんだっけ?
ジェネリクスの宣言付きの可変長引数を受け取るメソッドを定義したときにlintに怒られて「あれ?これってダメなんだっけ?」ってなったのでメモ。
やりたいこととしては、ORMを作っていてupdate文を作るときにvoid update(UpdateValuePair<E, ?>... pairs)
というメソッドを作りたい。
呼び出し側はUsers.all().update(Users.NAME.set("hoge"), Users.ACTIVE.set(true))
のように書けるのが理想。
呼び出し側はこういうのやりたいお気持ち pic.twitter.com/AjncV2VT1Z
— 俺九番 (@orekyuu) 2018年5月19日
危険な操作をしてみる
これを書いた時「Possible heap pollution from parameterized vararg type」という警告が出てくる。どのように危険な操作ができるのかサンプルを書きました。
public class Main { public static void main(String[] args) { hoge(Arrays.asList("aaa"), Arrays.asList("bbb")); } public static void hoge(List<String>... args) { Object[] objArray = args; // List<Integer>を入れる objArray[0] = Arrays.asList(1); // List<String>として取り出せてしまう List<String> stringList = args[0]; // 実はIntegerが入っているのでClassCastExceptionが出る String str = stringList.get(0); } }
Object[]を経由してargsの最初の要素にList
対策
取れる対策は2つで
- 可変長引数を使わず専用の型を用意する
- SafeVarargsを使って警告を抑制する
理想としては可変長引数をやめるのが一番安全。ただ、今回はUpdate文を作るときに可変長引数で更新する値を受け取りたいので、更新するたびに新しいインスタンスを作って組み立てて…とすると使いづらいのでSafeVarargsを使うことにした。
SafeVarargsで警告を抑制する
方針としては呼び出し側は問題なくて実装側が気をつければ良さそう(気を付けるだけはもにょるけど…)なので警告を抑制する。
警告を抑制するにはfinal or staticメソッドにして@SafeVarargsアノテーションをつければ良い。
最終的にはMocoのコードは以下のようになりました。
@SafeVarargs public final void update(UpdateValuePair<E, ?>... pairs) { createUpdate(pairs).executeQuery(ConnectionManager.getConnection(), ConnectionManager.createSqlVisitor()); }