LoginSignup
2

More than 5 years have passed since last update.

Ectoでjoin,preloadしたテーブルのカラムを選択する方法

Last updated at Posted at 2018-02-16

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構造体がないからです。
考えてみれば当然ですね。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2