Doma2環境の中間テーブルを考える

Doma2環境でレコードを削除するときに中間テーブルをどう扱うか悩んだのでそのメモ。

悩んだ状況

f:id:orekyuu:20180929201610p:plain 上のようなテーブルで、repositoriesのレコードを削除するときのことを考える。
repositoriesにはlanguages、frameworksの間に中間テーブルが存在していて、一緒に削除しないと外部キー制約に引っかかってエラーになる。 repositoriesを削除する前にこの中間テーブルを削除しなければならないが、Doma2ではsqlファイルに複数のSQL文を書くことができないので何かしらの方法を考える必要がある。

考えられる選択肢

カスケードを使う

中間テーブルの外部キーにON DELETE CASCADEをつけてrepositoriesのレコードを削除したときに一緒に削除できるようにする。
DB側で頑張るのでアプリケーションコードがすっきりする。ただし、テーブルA -> B -> C -> Dのように伝播が広がりすぎると手がつけられなくなる可能性がある。ちょっとこの辺は経験不足なのでどうなるかわからない。

アプリケーションコードで頑張る

中間テーブルのDaoを作ってアプリケーションコードで削除するようにする。アプリケーションコードがつらくなる。面倒。
メリットとしてはDBのマイグレーションよりはアプリケーションコードのほうが変更が容易であること。

JOINして削除する[ボツ]

これは試してボツになった案。調べてみて知ったが、joinして複数のテーブルをまとめて削除できるらしい。
DocumentMulti-Table Deletes 参照
以下のようなSQL1発で解決できると思ったが外部キー制約に引っかかってダメだったのでボツ。

delete repositories, rf, rl from repositories
  join repository_frameworks rf on repositories.id = rf.repository_id
  join repository_languages rl on repositories.id = rl.repository_id
where repositories.id = /*repo.id*/2

カスケードを使うことにした

アプリケーションコードがシンプルになる・カスケードに関してはテストである程度担保できるという理由で選択。
カスケードの伝播が広がりすぎて手がつけられなくなりそうになったら撤退する予定。

最近の悩み

最近エンジニアとしての生き方にもやっと悩んだりしているので文章にして整理してみようと思った。

今やっている仕事

 今はtoCのサービスをRailsで書いている。Java好きじゃなくなったかというとそういうわけでもなく、toCやりたいなーと思っていたら成り行きでそうなった感じ。学びがなかったかというとそんなことはなくて異文化に触れることでいろいろ学ぶことはあったけど、RubyRailsを学ぶことに対して熱量が持てていないのが現状。実際趣味でRubyRailsを触ってないしね。
 ただ、toCを作ることの面白さみたいなのはあってユーザーが良い反応してくれると嬉しいし新しいものをリリースする楽しさはとてもある。正直学生の時より楽しい。

Javaが好き

 今でも趣味でJavaを書いていて、Java11がリリースされるのもワクワクしていたし楽しみにしてた。趣味でコードを書くときはJavaしか書かないし、仕事でどうでもいい使い捨てのコードを書くときも無駄にJavaで書いたりしている(きっとRubyで書いたほうが早い)。学生のときほどではないけど今でもJavaの情報は追っているつもり。
 仕事で触ってない今JJUG CCCとかJavaの勉強会で登壇するネタが出せない現状がすごくつらい。Rubyの勉強会のネタはあるのにね…やっぱり業務で触ってないと難しいんだと思う。

今の悩み

 多分だけど、いつか転職するときはJavaが書けるところに行くんだろうなと思っているけど実務経験もなくRailsを書いていた人間がJavaの会社に行けるか不安だし、toCかつJavaという選択肢はすごく狭くてどちらか捨てないといけなくなるとも思っている。どっちも好きだし選ぶのって難しいなー…。エンジニアとして成長するなら多分Javaの会社に行くことだと思っていて、得意な言語を伸ばしていきたい。最近Twitterやってると他のエンジニアとどんどん差ができているような気持ちになってるし危機感。
 ちなみに今の仕事に不満があるわけではなくJavaが書けない以外は最高の職場と業務内容です。

ふつうのDoma2の使い方

最近Tuzigiriというリポジトリを作ったりしてる。あまり進んでないけど。
このリポジトリは、GitHubリポジトリシェアのサービスをオープンソースで作りつつ、色んな人にSpringBootのお手本や参考にしてもらえばいいかなというモチベーションで作っている。このへんのお気持ちはまぁおいおい別記事で書こうと思う。
いまはnoko_kと一緒に走り始めた段階だが、とあるプルリクエストでDoma2の使い方について議論になったのでそのへんのことを書こうと思う。

ワイルドカードというアンチパターン

雑にワイルドカードでカラム指定をしているSQLでnokoによってお縄になった。これはSQLアンチパターンのインプリシットカラム(暗黙の列)というものらしい。(最近SQLアンチパターンを買ったのに積んであって申し訳ない…)

積んであったSQLアンチパターンを読んでみると以下のようなデメリットが書かれていた。

  1. 列の追加削除をしたとき、添字でアクセスしていた場合ずれてしまう Doma2の場合は影響なさそう。生JDBCResultSet#getString(columnIndex)のようなケースだと踏んでしまうケースですね。
  2. 余分なカラムをフェッチするのでパフォーマンスに悪影響がある それほど大量のカラム持ってないし気にするほどか?という気持ちもある。ちなみに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を送っていただけるとうれしいです。

github.com

また、試してみたいだけの方はSQLiteで試せるサンプルリポジトリがあるのでこちらで遊んでみてください。

github.com