やりたいこと
Railsのgem、ransackで検索やソートができますが、リレーション先のレコードをソートしたいです。
例えば、posts
テーブルとusers
テーブルがあるとします。
posts
のビュー index.html.erb
で、postを投稿したユーザーも表示しソートできるようにしたいです。
各モデルは以下のような感じ(必要なとこだけ抜粋しています)。
# == Schema Information
#
# Table name: posts
#
# id :bigint not null, primary key
# user_id :bigint not null
# body :text not null
belong_to :user
# == 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
<%= sort_link(@q, :user_name_kana, 'ユーザー') %>
- sort_linkの第二引数に
:name
と書きたくなってしまいますが、ユーザーのnameというカラムを持っているのはpost.rbではなく、user.rbなので思った通りの挙動にならないはずです。ここではuser_name_kana
というscopeを使います。これは次にModelで定義します。
Model
postモデルでscopeを作ります。
今回の本題ではないのですが、ransackさんは日本語のソートがあまり得意ではないようので、下のように生のクエリを書くような実装となります。
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先輩に感謝しつつ。