Elixir/Ecto: Named Bindingを使ってクエリを組み立てる
Mediumを見ているとPhoenix/Elixir: Chain Composable Queries with Ecto Named Bindings.という記事が流れてきました。
この記事に感化されて、日本語でも解説しておきたいと思い、EctoのNamed Bindingsの使い方について解説してみます。
お題
posts has many comments という構造で、comments.bodyに含まれる文字列でpostsを検索するとします。
Named Bindingsを使わない場合(Positional Bindings)
Named Bindingsを使わない場合、次のように書けます。
def search_posts_by_comment_word(comment_word) do
Post
|> join(:left, [post], _ in assoc(post, :comments))
|> where([_post, comment], like(comment.body, ^"%#{comment_word}%")) # 元のPostが1番目, commentsテーブルのjoinが2番目
|> preload([_post, comment], comments: comment)
|> Sample.Repo.all()
end
この場合、joinの順序に依存して where
, select
, preload
などを書いていく必要があります。
ドキュメント上では Positional bindings という用語で記載されています。
この書き方だと実装の順序に依存し、何番目にjoinしたかというのを把握した上で後続の処理を記述する必要があります。実装が変更して、joinが増減した場合にツラいです。
Named Bindingsを使う場合
as:
を利用してそれぞれのjoinに名前をつけることができ、後続の処理でbindingsのリストから参照できます。
def search_posts_by_comment_word(comment_word) do
Post
|> join(:left, [post], _ in assoc(post, :comments), as: :comment)
|> where([comment: comment], like(comment.body, ^"%#{comment_word}%")) # as句で指定したatomで参照できる
|> preload([comment: comment], comments: comment)
|> Sample.Repo.all()
end
Named Bindingsであれば、どう命名したかは把握しておく必要がありますが、順序に依存しなくなるため、whereやselect部分だけを関数に切り出しやすくなります。
def filter_by_word(queryable, word) do
queryable
|> where([comment: comment], like(comment.body, ^"%#{word}%"))
end
end
すでにある名前でbindされているかどうか判定するための has_named_binding?/2
という関数も用意されているため、bindされている場合のみ処理を実行するなど、分岐も可能です。
こちらの書き方の方が順序に依存しない分、joinの追加、削除時に楽です。
まとめ
簡単ですが、Named Bindingsを利用してクエリを組み立てる方法について解説しました。順序を意識せずにpipeでつらつらと書ける方が楽ですので、joinが増えた場合はNamed Bindingsを使った書き方の方がベターそうです。
日本語だと
- Named Bindings: 名前付きバインディング
- Positional Bindings: 位置バインディング
みたいな訳になるのでしょうかね。この辺りは英語のまま解釈した方が分かりやすいかもしれません。
Discussion