ElixirのDBアクセス用ライブラリEctoでjoinしたりpreloadしたりしたテーブルのカラムをselectで絞り込もうとしてハマったので、ここに書き残しておきます。
なお、この投稿はStackOverflowで貰った回答をベースに書いています。
回答者であり、Elixirの作者でもあるJosé Valim氏には深く感謝しています。
やりたいこと
例えば、ユーザが投稿して、投稿には複数のタグが付けられるようなシステム(StackOverflowやQiitaもそうですね)のデータを考えます。
簡易的に表すとこんな感じになります。
User 1---* Post 1---* PostTag *---1 Tag
各テーブルのモデルモジュールは特筆すべきことなく普通に実装します。
敢えて言えば、Postにはhas_manyでPostTagを通じてTagに関連付けをしておきます。
scheme "posts" do
...
has_many :post_tags, PostTag
has_many :tags, [:post_tags, :tag]
end
さて、ここでPostを取得するクエリを書くのですが、このとき、
- Postの所有Userのidとname
- Postに付加されたTagのidとname
も同時に取得したいとします。
方法
結論を書くと、これは次のコードで実現できます。
query = from post in Post,
join: user in User, on post.user_id == user.id,
preload: [:tags, user: user],
select: [:id, :title, user: [:id, :name], tags: [:id, :name]]
これでRepo.all
すれば、UserやTagがロードされたPostの構造体リストが取得できます。
何にハマっていたか?
元々、Joinしたテーブルのカラムを取得するのに、次のような式を書いていました。
query = from post in Post,
join: user in User, on post.user_id == user.id,
select: %{
id: post.id,
title: post.title,
user_id: post.user_id,
user_name: user.name, # <= column at joined table
}
このようにselectに記述することで、joinしたテーブルのカラムも指定のキー(この場合user_name
)に割り当てて取得できるわけです。
この場合、Repo.all
などして取得できるのはMapのリストになります。
問題は、ここにテーブルのpreloadを加えた場合です。
query = from post in Post,
join: user in User, on post.user_id == user.id,
select: %{
id: post.id,
title: post.title,
user_name: user.name, # <= column at joined table
},
preload: [:tags]
このコードは実行エラーとなります。
Mapで取得しているので、preloadする先になるはずのPost構造体がないからです。
考えてみれば当然ですね。