4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

(Rails, Ransack)リレーション先のテーブルのカラムをソートする

Last updated at Posted at 2021-05-24

やりたいこと

Railsのgem、ransackで検索やソートができますが、リレーション先のレコードをソートしたいです。

例えば、postsテーブルとusersテーブルがあるとします。
postsのビュー index.html.erbで、postを投稿したユーザーも表示しソートできるようにしたいです。

各モデルは以下のような感じ(必要なとこだけ抜粋しています)。

post.rb
# == Schema Information
#
# Table name: posts
#
#  id         :bigint           not null, primary key
#  user_id    :bigint           not null
#  body       :text             not null

belong_to :user
user.rb
# == Schema Information
#
# Table name: users
#
#  id               :bigint           not null, primary key
#  name_kanji       :text             not null
#  name_kana        :text             not null

has_many :posts

ユーザーのカラムには、漢字氏名の他、読み仮名も持たせています。

環境

ruby 2.7.2
Rails 6.1.3.1

実装!

View

posts/index.hrml.erb
<%= sort_link(@q, :user_name_kana, 'ユーザー') %>
  • sort_linkの第二引数に:nameと書きたくなってしまいますが、ユーザーのnameというカラムを持っているのはpost.rbではなく、user.rbなので思った通りの挙動にならないはずです。ここではuser_name_kanaというscopeを使います。これは次にModelで定義します。

Model

postモデルでscopeを作ります。
今回の本題ではないのですが、ransackさんは日本語のソートがあまり得意ではないようので、下のように生のクエリを書くような実装となります。

post.rb
  scope :sort_by_user_name_kana_asc, lambda {
    eager_load(:user)
      .order(Arel.sql('users.name_kana COLLATE "C" ASC'))
  }

  scope :sort_by_user_name_kana_desc, lambda {
    eager_load(:user)
      .order(Arel.sql('users.name_kana COLLATE "C" DESC'))
  }

いくつかポイントを列挙します。

  • scopeメソッドは特定のSQL文に名前を付けてメソッド化できるメソッドです。第一引数に付けたいメソッド名、第二引数にlambdaで処理を記述します。

  • ransackはこのscopeで:sort_by_任意のカラム名_asc :sort_by_任意のカラム名_descと書いてあげることで複雑なソートも対応してくれます。ここでは、この任意のカラム名に先ほどビューで書いたuser_name_kanaを使います。(参考:Ransack GitHub Ransack's sort_link helper creates table headers that are sortable links

  • name_kanaの前にusers.と付加することで、ORDER BY posts.name_kanaではなくORDER BY users.name_kanaというクエリが生成され、1対多で関連付いている別のモデルのカラムをSQLで操作することができます。

  • eager_loadとはRailsのORMであるActive Recordのメソッドで、生のSQLを書くことなく、クエリを実行できるものです。

  • このeager_loadしてあげないとSQLがびっくりしてしまいます。「うちPostやけん、usersテーブルとか知らんし」と。なのでeager_loadして根回ししておきます。「お宅の関連会社のuserさんおられますわな?そこのデータ見てきてもろてよろしいでしょうか?」的な。

  • (追記)↑ずいぶんと頭の悪そうな説明を書いてしまいましたが、要はLEFT OUTER JOINのSQLを発行するメソッド見たいですね。。https://api.rubyonrails.org/v6.1.4/classes/ActiveRecord/QueryMethods.html#method-i-eager_load

実際にSQLを見てみる

eager_loadを入れないのと入れるのとでクエリがどう変わるのか、rails cしてどんなクエリが実際走るのか見てみます。

eager_loadを入れないと

pry(main)> Post.sort_by_user_name_kana_asc.to_a

こんなSQLが走ります。(見やすいように改行で整形しています)

Post Load (1.6ms)
SELECT "posts".* FROM "posts"
ORDER BY users.name_kana COLLATE "C" ASC

こんなエラー文が出ました。

ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "users"
LINE 1: ...ELECT "posts".* FROM "posts" ORDER BY users.f...

根回ししていないので、usersテーブルとか知らんしと怒っています。

eager_loadを入れると

pry(main)> Post.eager_load(:user).sort_by_user_name_kana_asc.to_a

こんなSQLが走ります。(見やすいように改行で整形しています。それと、AS以降は参照しないでください。)

SQL (3.5ms)
SELECT "posts"."id" AS t0_r0,
"posts"."user_id" AS t0_r1,
"users"."id" AS t1_r0,
"users"."name" AS t1_r2,
"users"."name_kana" AS t1_r3, 
LEFT OUTER JOIN "users" ON "users"."id" = "posts"."user_id"
ORDER BY users.name_kana COLLATE "C" ASC

ポイントはLEFT OUTER JOINです。これによりusersテーブルをJOINし、スキャンすることに成功しました。

最後に

まとめ

  • リレーション先の他のモデルのカラムをソートしたければ、scopeで定義してあげる。
  • eager_loadでリレーション先のDBをJOINすることを忘れないこと。

何かご指摘などあればバシバシよろしくお願いします。

@yuuu先輩に感謝しつつ。

4
0
0

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
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?