LoginSignup
19
8

More than 1 year has passed since last update.

ActiveRecordのorderを複数つなげた場合の動きを調べてみた

Last updated at Posted at 2019-05-21

ActiveRecordの order メソッドを使ったソート機能で、予想した動きをしなかったので調べてみました。

追記(6/15)
正しい結論が判明したので修正しました

環境

Ruby: 2.6.3
Rails: 5.2.3

前準備

簡単なpostsテーブルを想定してみます。

postsテーブル

id title
1 最初の投稿
2 2番目の投稿
3 3番目の投稿

コントローラのindexアクションで、idの降順(値が大きい順番)にします。

app/controllers/posts_controller.rb
  def index
    @posts= Post.order(id: :desc)
  end

これにより、postsテーブルの中身がidの降順で表示されます。
スクリーンショット 2019-05-21 17.14.57.png

SQLも以下の通り。
スクリーンショット 2019-05-21 14.05.26.png

疑問1

ここで、以下の要件が付与されました。

  • 通常はidの降順で表示させるが、titleの昇順でソートさせるボタンもつけること

そこで以下のようにしました。

app/views/posts/index.html.erb
<%= link_to 'Titleでソート', posts_path(title_sort: :true) %>
app/controllers/posts_controller.rb
  def index
    @posts = Post.order(id: :desc)
    if params[:title_sort]
      @posts = @posts.order(title: :asc)
    end
  end

アプリを起動してTitleでソートをクリックしましたが、タイトルの昇順でソートされない!なんで???
スクリーンショット 2019-05-21 17.16.28.png

検証&&仮説

そこでTitleでソートをクリックしたときのSQLをログで確認しました。
スクリーンショット 2019-05-21 14.47.15.png

SQLを見ると
①まずidの降順(DESC)でソート
②続いてtitleの昇順(ASC)でソート
をしています。
一見、問題なさそうなのですが、ORDER BY句で複数の条件を指定した場合、①を実行し、同じものがあった場合に②でさらにソートします。しかし①はidであり同じものが存在しませんので、①でソートした結果をくつがえして②の結果を反映させることはできないのです。

解決方法

ですので、これを解決するには以下のようにします。

app/controllers/posts_controller.rb
  def index
    if params[:title_sort]
      @posts = Post.order(title: :asc)
    else
      @posts = Post.order(id: :desc)
    end
  end

title_sortのパラメータが存在する場合のみタイトルでソートし、それ以外はidでのソートにするのです。
これでtitleの昇順にソートされました!
スクリーンショット 2019-05-21 17.17.36.png

疑問2

ただ、ここで疑問が。
①のソートをしているのは、コントローラの

@posts = Post.order(id: :desc)

の場所であり、②のソートをしているのは、コントローラの

@posts = @posts.order(title: :asc)

です。それぞれ独立したコードなのに、なぜ①と②をまとめたSQLで実行しているのでしょう?
(てか、まとめて実行しなきゃ、タイトルでもソートできるんじゃない?)
 

検証&&仮説

SQLの実行タイミングに原因があるのではないか、ということで、以下のようなことを試しました。

app/controllers/posts_controller.rb
  def index
    @posts = Post.order(id: :desc)
    if params[:title_sort]
      @posts = Post.order(title: :asc)
    end
    binding.pry #これを追記
  end

これで実際にタイトルでのソートを実行したところ、コンソールが起動した画面は以下のとおりでした。
スクリーンショット 2019-06-15 14.18.09.png
あれ?
orderメソッドがある7行目と9行目が終わっても、SQLが発行されてないじゃん!

では思い切って、viewsファイルにこんなコードを追加しました。

views/posts/index.html.erb
// 省略
  <tbody>
    <% puts "今からeachメソッドを実行します" %> # これを追記
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.id %></td>
        <td><%= post.title %></td>
        <td><%= link_to 'Show', post %></td>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Titleでソート', posts_path(title_sort: :true) %></td>
        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
// 省略

つまり、eachメソッドが実行されるタイミングがわかるようにしたのです。
これでタイトルでのソートを実行した時のログがこれ。
スクリーンショット 2019-06-15 14.23.01.png
なんと!
eachメソッドが実行されるタイミングで、ようやくSQLが実行されておりました!

結論

orderwhere などは複数つなげてまとめて実行できる性質をもちます。まとめて実行ではなく、コードごとに毎回実行してしまうとDBとのやりとりが増えてしまいます。
そこでRailsでは、SQLをコントローラでは あえて 実行せずに、実際に必要になったタイミング(今回で言えばeachメソッドのタイミング)で まとめて 実行している、というのが今回の理屈でした(Railsかちこい)。

とても勉強になりました!

19
8
2

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
19
8