【Rails入門】パフォーマンスが悪いならincludes/orderを検討しよう

みなさんは、2つの関連付けられたテーブルを扱っているときに、どのように値を取り出していますか?

パフォーマンスを気にせず、alljoinを使って、N+1問題を発生させていませんか?

それともパフォーマンスを気にして、includespreloadeager_loadを使っていますか?

この記事では、パフォーマンスを上げるための1つの解法であるincludesを取り上げ、以下の内容を説明しています。

・N+1問題とは
・2つのテーブルからデータを取得する方法
・ネストされた複数のテーブルからincludesでデータを一括取得する方法

さらに、テーブルからデータを並べ替えた状態で取得するためのorderを取り上げ、以下の内容を説明します。

・データを並べ替える方法
・複数のカラムを使って並べ替える方法
・reorderの使いどころ
・order_as_specified gemを使って順番を1つ1つ指定してデータを取得する方法

データが少ないうちはパフォーマンスに問題を感じなくても、データが多くなったときのことを考えて、今のうちにN+1問題を解決しておきましょう!

また、データを取得してからRailsで並べ替えるよりも、データを並べ替えた状態で取得したほうがパフォーマンス的にも有利です。

この記事で、orderの使い方もあわせてマスターしてしまいましょう。

では、順番に説明していきますね。

目次

includesとは、N+1問題とは

includesは、関連付けられた2つのテーブルのデータを参照するメソッドです。

主にN+1問題を解決するために利用されます。

N+1問題については、具体例を見ると解りやすいと思いますので、早速見ていきましょう。

以下のような2つのテーブルを作成します。

テーブルカラム
Userテーブルname
posts(has_many)
Postテーブルuser_id(belongs_to)
title
month

そして、Userテーブルから、Postテーブルのtitleを参照する方法を考えます。

一番手っ取り早いのは以下のようなコードですね。

User.all.each { |user| user.posts.each{ |post| puts "#{user.name} / #{post.title}"} }

しかし、これでは、データ数が多くなったときにパフォーマンスに問題が発生してしまいます。

実行結果:

  User Load (0.2ms)  SELECT "users".* FROM "users"
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
山田太郎 / 先日の旅行での話
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]
長瀬来 / 最近少し気になったこと
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 3]]
立川裕美 / 昨日の出来事
立川裕美 / 山登りに行きました
立川裕美 / Ruby on Railsの日
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 4]]
前田達郎 / 友人が結婚しました
前田達郎 / ランニングのコツ
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 5]]
細川修二 / 楽しい休日の過ごし方
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 6]]
+----+----------+-------------------------+-------------------------+
| id | name     | created_at              | updated_at              |
+----+----------+-------------------------+-------------------------+
| 1  | 山田太郎 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 2  | 長瀬来   | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 3  | 立川裕美 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 4  | 前田達郎 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 5  | 細川修二 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 6  | 木村拓磨 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
+----+----------+-------------------------+-------------------------+
6 rows in set

実行結果の見かたを説明しておきます。

データベースのUserテーブルを読み込んだタイミングで「User Load (0.1ms)…」と表示されます。

同様にPostテーブルを読み込んだときに「Post Load (0.1ms)…」と表示されるのですが、これが6回表示されていますね。

つまり、6名分のpost.titleを取得するために、データベースに6回アクセスしているワケですが、includesを使うとこれを1回に削減できます。

1回のアクセスで解決できるものを、6回もアクセスしているので問題とされていて、一般的にN+1問題が発生していると言います。

具体的には、以下のコードでPostテーブルにアクセスする回数を1回に減らせます。

User.includes(:posts).each { |user| user.posts.each{ |post| puts "#{user.name} / #{post.title}"} }

実行結果:

  User Load (0.1ms)  SELECT "users".* FROM "users"
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3, 4, 5, 6)
山田太郎 / 先日の旅行での話
長瀬来 / 最近少し気になったこと
立川裕美 / 昨日の出来事
立川裕美 / 山登りに行きました
立川裕美 / Ruby on Railsの日
前田達郎 / 友人が結婚しました
前田達郎 / ランニングのコツ
細川修二 / 楽しい休日の過ごし方
+----+----------+-------------------------+-------------------------+
| id | name     | created_at              | updated_at              |
+----+----------+-------------------------+-------------------------+
| 1  | 山田太郎 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 2  | 長瀬来   | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 3  | 立川裕美 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 4  | 前田達郎 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 5  | 細川修二 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 6  | 木村拓磨 | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
+----+----------+-------------------------+-------------------------+
6 rows in set

あとで試してもらいますので、ここでは「Post Load」がすごく減っているという雰囲気だけをつかんでください。

includesの動作を理解するためのWebアプリを作成する

Railsの動作を理解するために、scaffoldを使ってWebアプリを作っておきましょう。

scaffoldの使い方は、以下の記事で解説していますので、ぜひご覧ください。

この記事では、app/samurai/includes-demoディレクトリを作成し、以下のコマンドでWebアプリを作成した場合を例に、説明を続けます。

bin/rails generate scaffold User name:string
bin/rails generate scaffold Post user_id:integer title:string month:integer
bin/rails db:migrate
bin/rails server

データの準備

Railsコンソールを起動してテーブルにデータを入力します。

scaffoldで作成していますので、http://localhost:3000/users/newや、http://localhost:3000/posts/newで1つずつデータを入力しても構いません。

(1)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

cd app/samurai/includes-demo
bin/rails console

(2)以下のコードを入力します。

User.create(name:"山田太郎")
User.create(name:"長瀬来")
User.create(name:"立川裕美")
User.create(name:"前田達郎")
User.create(name:"細川修二")
User.create(name:"木村拓磨")
Post.create(user_id:5,title:"楽しい休日の過ごし方" ,month:3)
Post.create(user_id:1,title:"先日の旅行での話" ,month:2)
Post.create(user_id:3,title:"昨日の出来事" ,month:12)
Post.create(user_id:3,title:"山登りに行きました" ,month:8)
Post.create(user_id:4,title:"友人が結婚しました" ,month:4)
Post.create(user_id:2,title:"最近少し気になったこと" ,month:1)
Post.create(user_id:4,title:"ランニングのコツ" ,month:9)
Post.create(user_id:3,title:"Ruby on Railsの日" ,month:9)
exit

UserテーブルとPostテーブルにデータが入力され、Railsコンソールが終了します。

ブラウザでデータを確認しておきましょう。

(3)ブラウザで「http://localhost:3000/users」にアクセスします。

rails-include01

(4)ブラウザで「http://localhost:3000/posts」にアクセスします。

rails-include02

うまく登録されていますね!

次にUserテーブルとPostテーブルを関連付けます。

(5)app/models/user.rbを以下のように編集します。

変更前:

class User < ApplicationRecord
end

変更後:

class User < ApplicationRecord
  has_many :posts
end

(6)app/models/post.rbを以下のように編集します。

変更前:

class Post < ApplicationRecord
end

変更後:

class Post < ApplicationRecord
  belongs_to :user
end

これで、準備ができました。

hirb gemとhirb-unicode gemをインストールする

この後、includesの動作を確認する際、Railsコンソールを使用します。

Railsコンソールで出力したデータの見栄えを良くにするために、以下の2つのgemをインストールしましょう。

gem説明
hirb gem出力結果を表形式で出力する
hirb-unicode gemマルチバイト文字の表示を補正する

(1)Gemfileの最終行に以下の内容を追記します。

gem 'hirb'
gem 'hirb-unicode'

(2)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

cd app/samurai/includes-demo
bundle install

これで、hirb gemhirb-unicode gemがインストールされました。

関連付けられた2つのテーブルのデータを参照する方法

関連付けられた2つのテーブル(上で紹介したUserテーブルとPostテーブル)で、すべてのpostの投稿者を取得する方法を考えてみましょう。

ここからは、Railsコンソールでコードを入力して、実行結果を確認してみましょう。

(1)以下のコマンドを1行ずつ順番に入力します。

bin/rails console
Hirb.enable

(2)すべてのpostのデータを確認するために、以下のコードを入力します。

Post.all

実行結果:

  Post Load (2.1ms)  SELECT "posts".* FROM "posts"
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 14... |
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 14... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 14... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 14... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 14... |
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 14... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 14... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 14... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

(3)すべてのuserのデータを確認するために、以下のコードを入力します。

User.all
  User Load (0.2ms)  SELECT "users".* FROM "users"
+----+----------+-------------------------+-------------------------+
| id | name     | created_at              | updated_at              |
+----+----------+-------------------------+-------------------------+
| 1  | 山田太郎 | 2018-06-21 14:17:20 UTC | 2018-06-21 14:17:20 UTC |
| 2  | 長瀬来   | 2018-06-21 14:17:20 UTC | 2018-06-21 14:17:20 UTC |
| 3  | 立川裕美 | 2018-06-21 14:17:20 UTC | 2018-06-21 14:17:20 UTC |
| 4  | 前田達郎 | 2018-06-21 14:17:20 UTC | 2018-06-21 14:17:20 UTC |
| 5  | 細川修二 | 2018-06-21 14:17:20 UTC | 2018-06-21 14:17:20 UTC |
| 6  | 木村拓磨 | 2018-06-21 14:17:20 UTC | 2018-06-21 14:17:20 UTC |
+----+----------+-------------------------+-------------------------+
6 rows in set

手順(2)で表示されたuser_idを使って、手順(3)で表示されたUserテーブルから該当ユーザーを特定して、nameを取得すればokですね。

では、それを実現する3つの方法を紹介しましょう。

Post.all

Post.allですべてのpostを取得できますので、以下のコードを実行すればよさそうです。

Post.all.each { |post| puts "★ post.id=#{post.id} : post.user.name=#{post.user.name}" }

実行結果:

  Post Load (0.1ms)  SELECT "posts".* FROM "posts"
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
★ post.id=1 : post.user.name=細川修二
  User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
★ post.id=2 : post.user.name=山田太郎
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
★ post.id=3 : post.user.name=立川裕美
  User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
★ post.id=4 : post.user.name=立川裕美
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
★ post.id=5 : post.user.name=前田達郎
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
★ post.id=6 : post.user.name=長瀬来
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
★ post.id=7 : post.user.name=前田達郎
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
★ post.id=8 : post.user.name=立川裕美
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 14... |
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 14... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 14... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 14... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 14... |
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 14... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 14... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 14... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

実行結果で★が表示されている行をご覧ください。

1番のpostの投稿者が細川修二であることが取得できています。

ただ、データベースには全部で9回アクセスしていますので、N+1問題が発生しています。

joins

次はjoinsを使う方法です。

試してみると、Post.allとほとんど同じですが、joinsは内部結合(INNER JOIN)を行っています。

Post.joins(:user).each { |post| puts "★ post.id=#{post.id} : post.user.name=#{post.user.name}" }

実行結果:

  Post Load (0.2ms)  SELECT "posts".* FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id"
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
★ post.id=1 : post.user.name=細川修二
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
★ post.id=2 : post.user.name=山田太郎
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
★ post.id=3 : post.user.name=立川裕美
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
★ post.id=4 : post.user.name=立川裕美
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
★ post.id=5 : post.user.name=前田達郎
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
★ post.id=6 : post.user.name=長瀬来
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
★ post.id=7 : post.user.name=前田達郎
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
★ post.id=8 : post.user.name=立川裕美
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 14... |
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 14... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 14... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 14... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 14... |
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 14... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 14... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 14... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

この方法でも、やはりN+1問題が発生していますね。

includes

3つ目は、includesを使う方法です。

includesは、条件によって2つのテーブルをまとめる方法が変わるという特徴があります。

詳細は省きますが、以下の2つのパターンがあります。

  • 2つのテーブルを左外部結合(LEFT OUTER JOIN)して、そのテーブルをメモリに読み込む
  • 2つのテーブルをそのままメモリに読み込む

どちらのパターンで動作したとしても、関連付けられた2つのテーブルのデータを参照できますので、いったん試してみるのが良いでしょう。

コード実行時に発行されたSQLに、「LEFT OUTER JOIN」と表示されていれば左外部結合をしており、表示されていなければ2つのテーブルをそのままメモリに読み込んでいます。

Post.includes(:user).each { |post| puts "★ post.id=#{post.id} : post.user.name=#{post.user.name}" }

実行結果:

  Post Load (0.1ms)  SELECT "posts".* FROM "posts"
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (5, 1, 3, 4, 2)
★ post.id=1 : post.user.name=細川修二
★ post.id=2 : post.user.name=山田太郎
★ post.id=3 : post.user.name=立川裕美
★ post.id=4 : post.user.name=立川裕美
★ post.id=5 : post.user.name=前田達郎
★ post.id=6 : post.user.name=長瀬来
★ post.id=7 : post.user.name=前田達郎
★ post.id=8 : post.user.name=立川裕美
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 14... |
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 14... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 14... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 14... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 14... |
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 14... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 14... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 14... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

前述の2つとはだいぶ違うことに気がついたでしょうか。

1番のpostの投稿者が細川修二さんであることが取得できています。

が、データベースには全部で2回しかアクセスしておらず、見事にN+1問題を回避していますね!

左外部結合(LEFT OUTER JOIN)とは

左外部結合(LEFT OUTER JOIN)は、1つ目のテーブルのカラムを優先して2つ目のテーブルを結合する、その際、2つのテーブルのどちらかにしか存在しないデータも含めるという意味です。

左外部結合(LEFT OUTER JOIN)以外に、以下のような結合方法があります。

  • 右外部結合(RIGHT OUTER JOIN)
  • 完全外部結合(FULL OUTER JOIN)
  • 内部結合(INNER JOIN)

ちなみに、joinsは内部結合(INNER JOIN)を行うようです。

結合方法について詳しく説明すると一つの記事になってしまいますので、以下の記事を参考にしてください。

参考:Quiita SQL素人でも分かるテーブル結合(inner joinとouter join)

その他の方法

関連付けられた2つのテーブルのデータを参照する方法は、includesを含めると以下の4つが有名です。

  • joins
  • includes
  • preload
  • eager_load

このうちjoinsはN+1問題が発生しますが、それ以外はN+1問題が発生しません。

includesは、preloadとeager_loadを状況に応じて自動的に使い分けるメソッドになっています。

トラブルを避けるためには、preloadとeager_loadの動作を理解して、しっかり使い分ける方が良いでしょう。

includesに条件をつける(where)

includeswhereを同時に使用すると、SQLの発行回数を少なく抑えつつ、条件をつけられます。

Postテーブルからデータを取得するときに、month=9のpostだけを対象に、ユーザー名を取得しましょう。

(1)Railsコンソールで次のコードを入力してください。

Post.includes(:user).where(posts: {month: 9}).each { |post| puts "★ post.id=#{post.id} : post.user.name=#{post.user.name}" }

先ほどのincludesを使ったコードと比較すると、途中に「.where(posts: {month: 9})」が追加されています。

実行結果は以下のとおりです。

  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."month" = ?  [["month", 9]]
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (4, 3)
★ post.id=7 : post.user.name=前田達郎
★ post.id=8 : post.user.name=立川裕美
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 14... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 14... |
+----+---------+------------------+-------+-----------------+------------------+
2 rows in set

このコードの場合は、以下のように動作します。

①Postテーブルから(すべてのpostを取得するのではなく)month=9のpostだけを取得し、取得したpostのuser_id(今回は4, 3)を保持(キャッシュ)する

②Userテーブルから、id = 4, 3のデータだけを取得する。

③取得したuserデータを参照して、post.user.nameを表示する。

whereを追加して条件を付けても、SQLを発行するのは①と②の2回だけです。

(2)続けて、次のコードを入力してください。

Post.includes(:user).where(users: {name: "立川裕美"}).each { |post| puts "★ post.id=#{post.id} : post.user.name=#{post.user.name}" }

name="立川裕美"のusersだけを対象に、ユーザー名を取得します。

実行結果は以下のとおりです。

  SQL (0.3ms)  SELECT "posts"."id" AS t0_r0, "posts"."user_id" AS t0_r1, "posts"."title" AS t0_r2, "posts"."month" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "users"."id" AS t1_r0, "users"."name" AS t1_r1, "users"."created_at" AS t1_r2, "users"."updated_at" AS t1_r3 FROM "posts" LEFT OUTER JOIN "users" ON "users"."id" = "posts"."user_id" WHERE "users"."name" = ?  [["name", "立川裕美"]]
★ post.id=3 : post.user.name=立川裕美
★ post.id=4 : post.user.name=立川裕美
★ post.id=8 : post.user.name=立川裕美
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 14... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 14... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 14... |
+----+---------+------------------+-------+-----------------+------------------+
3 rows in set

先ほどのSQL文とはまったく異なりますが、期待するデータが取得できていますね。

このコードの場合は、以下のように動作しています。

①UserテーブルのidとPostテーブルのuser_idを対応させる方法で、PostテーブルとUserテーブルを左外部結合(LEFT OUTER JOIN)する。

②左外部結合したテーブルから、users.name = "立川裕美"のデータだけを取得する。

③最後に、取得したデータを参照して、post.user.nameを表示する。

①と②をまとめて、1回のSQLで済ませていますね。

ネストされた複数のテーブルからincludesでデータを一括取得する方法

最後に、ネストされた複数のテーブルを扱ったときにN+1問題が発生しない方法を紹介しましょう。

(1)混乱を避けるために、すでに起動しているRailsサーバーを終了します。

次に、scaffoldで作成したデータをすべて削除しましょう。

(2)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

cd app/samurai/includes-demo
bin/rails destroy scaffold user
bin/rails destroy scaffold posts
bin/rails db:drop DISABLE_DATABASE_ENVIRONMENT_CHECK=1

(3)以下のコマンドを1行ずつ順番に入力します。

bin/rails generate scaffold Team name
bin/rails generate scaffold User name:string team_id:integer
bin/rails generate scaffold Post user_id:integer title:string month:integer
bin/rails db:migrate
bin/rails server

(4)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

cd app/samurai/includes-demo
bin/rails console

(5)以下のコードを入力します。

Team.create(name: "Aチーム")
Team.create(name: "Bチーム")
User.create(name:"山田太郎", team_id:1)
User.create(name:"長瀬来", team_id:1)
User.create(name:"立川裕美", team_id:2)
User.create(name:"前田達郎", team_id:2)
User.create(name:"細川修二", team_id:1)
User.create(name:"木村拓磨", team_id:1)
Post.create(user_id:5,title:"楽しい休日の過ごし方" ,month:3)
Post.create(user_id:1,title:"先日の旅行での話" ,month:2)
Post.create(user_id:3,title:"昨日の出来事" ,month:12)
Post.create(user_id:3,title:"山登りに行きました" ,month:8)
Post.create(user_id:4,title:"友人が結婚しました" ,month:4)
Post.create(user_id:2,title:"最近少し気になったこと" ,month:1)
Post.create(user_id:4,title:"ランニングのコツ" ,month:9)
Post.create(user_id:3,title:"Ruby on Railsの日" ,month:9)
exit

UserテーブルとPostテーブルにデータが入力され、Railsコンソールが終了します。

次にTeamテーブルとUserテーブル、Postテーブルを関連付けます。

(4)app/models/team.rbを以下のように編集します。

変更前:

class Team < ApplicationRecord
end

変更後:

class Team < ApplicationRecord
  has_many :users
end

(5)app/models/user.rbを以下のように編集します。

変更前:

class User < ApplicationRecord
end

変更後:

class User < ApplicationRecord
  has_many :posts
  belongs_to :team
end

(6)app/models/post.rbを以下のように編集します。

変更前:

class Post < ApplicationRecord
end

変更後:

class Post < ApplicationRecord
  belongs_to :user
end

これで、データの準備ができました。

(7)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

cd app/samurai/includes-demo
bin/rails console
Hirb.enable

(8)以下のコードを入力します。

Post.all.each { |post| puts "★ post.id=#{post.id} : post.user.team.name=#{post.user.team.name}" }

実行結果:

 Post Load (0.1ms)  SELECT "posts".* FROM "posts"
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
  Team Load (0.1ms)  SELECT  "teams".* FROM "teams" WHERE "teams"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
★ post.id=1 : post.user.team.name=Aチーム
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Team Load (0.1ms)  SELECT  "teams".* FROM "teams" WHERE "teams"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
★ post.id=2 : post.user.team.name=Aチーム
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  Team Load (0.1ms)  SELECT  "teams".* FROM "teams" WHERE "teams"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
★ post.id=3 : post.user.team.name=Bチーム
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  Team Load (0.1ms)  SELECT  "teams".* FROM "teams" WHERE "teams"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
★ post.id=4 : post.user.team.name=Bチーム
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
  Team Load (0.1ms)  SELECT  "teams".* FROM "teams" WHERE "teams"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
★ post.id=5 : post.user.team.name=Bチーム
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Team Load (0.1ms)  SELECT  "teams".* FROM "teams" WHERE "teams"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
★ post.id=6 : post.user.team.name=Aチーム
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
  Team Load (0.1ms)  SELECT  "teams".* FROM "teams" WHERE "teams"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
★ post.id=7 : post.user.team.name=Bチーム
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  Team Load (0.1ms)  SELECT  "teams".* FROM "teams" WHERE "teams"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
★ post.id=8 : post.user.team.name=Bチーム
+----+---------+------------------------+-------+-------------------------+-------------------------+
| id | user_id | title                  | month | created_at              | updated_at              |
+----+---------+------------------------+-------+-------------------------+-------------------------+
| 1  | 5       | 楽しい休日の過ごし方   | 3     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 2  | 1       | 先日の旅行での話       | 2     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 3  | 3       | 昨日の出来事           | 12    | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 4  | 3       | 山登りに行きました     | 8     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 5  | 4       | 友人が結婚しました     | 4     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 6  | 2       | 最近少し気になったこと | 1     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 7  | 4       | ランニングのコツ       | 9     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 8  | 3       | Ruby on Railsの日      | 9     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
+----+---------+------------------------+-------+-------------------------+-------------------------+
8 rows in set

見事な多段N+1問題とでも言えるほど、SQLが発行されていますね。

(9)次は以下のコードを入力します。

Post.includes(user: :team).each { |post| puts "★ post.id=#{post.id} : post.user.team.name=#{post.user.team.name}" }

実行結果:

  Post Load (0.1ms)  SELECT "posts".* FROM "posts"
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (5, 1, 3, 4, 2)
  Team Load (0.3ms)  SELECT "teams".* FROM "teams" WHERE "teams"."id" IN (1, 2)
★ post.id=1 : post.user.team.name=Aチーム
★ post.id=2 : post.user.team.name=Aチーム
★ post.id=3 : post.user.team.name=Bチーム
★ post.id=4 : post.user.team.name=Bチーム
★ post.id=5 : post.user.team.name=Bチーム
★ post.id=6 : post.user.team.name=Aチーム
★ post.id=7 : post.user.team.name=Bチーム
★ post.id=8 : post.user.team.name=Bチーム
+----+---------+------------------------+-------+-------------------------+-------------------------+
| id | user_id | title                  | month | created_at              | updated_at              |
+----+---------+------------------------+-------+-------------------------+-------------------------+
| 1  | 5       | 楽しい休日の過ごし方   | 3     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 2  | 1       | 先日の旅行での話       | 2     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 3  | 3       | 昨日の出来事           | 12    | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 4  | 3       | 山登りに行きました     | 8     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 5  | 4       | 友人が結婚しました     | 4     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 6  | 2       | 最近少し気になったこと | 1     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 7  | 4       | ランニングのコツ       | 9     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
| 8  | 3       | Ruby on Railsの日      | 9     | 2018-06-21 15:26:39 UTC | 2018-06-21 15:26:39 UTC |
+----+---------+------------------------+-------+-------------------------+-------------------------+
8 rows in set

このコードなら多段N+1問題を回避できますね!

データを並べ替える(order)

ここまでで、効率良くデータを取り出す方法を説明しました。

この記事の最後では、ここまで説明してきたincludesを使って効率良くデータを取り出し、これから説明するorderを使ってデータを並べ替えるという合わせ技を紹介します。

その準備段階として、orderの使い方を説明しておきましょう。

まずは、基本的な方法から始めます。

(1)Railsコンソールで以下のコードを入力します。

Post.order(:month)

実行結果:

  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."month" ASC
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 16... |
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 16... |
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 16... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 16... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 16... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 16... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

monthが昇順(asc:ascending order)に並んでいますね。

次は降順(desc:descending order)に並べてみましょう。

(2)Railsコンソールで以下のコードを入力します。

Post.order(month: :desc)

実行結果:

  Post Load (0.1ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."month" DESC
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 16... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 16... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 16... |
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 16... |
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 16... |
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 16... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

ちなみに、Post.order(month: :desc)は、以下のどの書きかたでも同じ結果が得られます。

Post.order(month: :desc)
Post.order(month: "desc")
Post.order(:month => :desc)
Post.order("month desc")

複数のカラムを使って並べ替える

次は複数のカラムを条件にする方法です。

先ほどのPostテーブルを、user_id順に、user_idが同じ場合はmonth順に並べ替えてみましょう。

まずは、user_id順に並べ替えるコードからですが、これは上で説明したコードのmonthをuser_idに変えるだけです。

(1)Railsコンソールで以下のコードを入力します。

Post.order(:user_id)

実行結果:

  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."user_id" ASC
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 16... |
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 16... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 16... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 16... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 16... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 16... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

user_idが3のデータのmonthに注目すると、12, 8, 9の順に並んでいますね。

これをmonth順に並べてみましょう。

(2)Railsコンソールで以下のコードを入力します。

Post.order(:user_id, :month)

実行結果:

  Post Load (0.1ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."user_id" ASC, "posts"."month" ASC
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 16... |
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 16... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 16... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 16... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 16... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 16... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

monthを降順にしてみましょう。

(3)Railsコンソールで以下のコードを入力します。

Post.order(:user_id, month: :desc)

実行結果:

  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."user_id" ASC, "posts"."month" DESC
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 2  | 1       | 先日の旅行での話 | 2     | 2018-06-21 1... | 2018-06-21 16... |
| 6  | 2       | 最近少し気に...  | 1     | 2018-06-21 1... | 2018-06-21 16... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 16... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 16... |
| 7  | 4       | ランニングのコツ | 9     | 2018-06-21 1... | 2018-06-21 16... |
| 5  | 4       | 友人が結婚し...  | 4     | 2018-06-21 1... | 2018-06-21 16... |
| 1  | 5       | 楽しい休日の...  | 3     | 2018-06-21 1... | 2018-06-21 16... |
+----+---------+------------------+-------+-----------------+------------------+
8 rows in set

ちなみに、Post.order(:user_id, month: :desc)は、以下のどの書きかたでも同じ結果が得られます。

Post.order(:user_id, month: :desc)
Post.order(:user_id, month: "desc")
Post.order(:user_id, :month => :desc)
Post.order(:user_id, "month desc")
Post.order(:user_id).order(month: :desc)
Post.order(:user_id).order(month: "desc")
Post.order(:user_id).order(:month => :desc)
Post.order(:user_id).order("month desc")

設定済みのorderを上書きする(reorder)

orderの書きかたを探しているとreorderの説明も見つかると思います。

ただ、ほとんどのサンプルコードでorderの直後にreorderしていて、使いどころがあるの!?という気がします。

でも、ちゃんと使いどころがあるんです。

ちなみに、reorderはdeprecated(廃止予定、非推奨)になった時期もありましたが、現在はdeprecatedではありませんので、堂々と使いましょう。

(1)Railsコンソールを終了します。

(2)app/models/user.rbを以下のように編集します。

変更前:

class User < ApplicationRecord
  has_many :posts
end

変更後:

class User < ApplicationRecord
  has_many :posts, -> {order("month asc")}
end

この変更で、User.find(3).postsのようにアクセスしたときに、postsをmonth順に取得できるようになります。

(3)以下のコマンドを1行ずつ順番に入力します。

bin/rails console
Hirb.enable
User.find(3).posts

実行結果:

  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? ORDER BY month asc  [["user_id", 3]]
+----+---------+--------------------+-------+-------------------------+-------------------------+
| id | user_id | title              | month | created_at              | updated_at              |
+----+---------+--------------------+-------+-------------------------+-------------------------+
| 4  | 3       | 山登りに行きました | 8     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 8  | 3       | Ruby on Railsの日  | 9     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 3  | 3       | 昨日の出来事       | 12    | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
+----+---------+--------------------+-------+-------------------------+-------------------------+
3 rows in set

手順(2)で指定したとおり、month順に取得できていますね。

では、monthを逆順に並べ替えてみましょう。

まずは、ここまでと変わらずorderを使ってみます。

(4)Railsコンソールで以下のコードを入力します。

User.find(3).posts.order(month: :desc)

実行結果:

  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? ORDER BY month asc, "posts"."month" DESC  [["user_id", 3]]
+----+---------+--------------------+-------+-------------------------+-------------------------+
| id | user_id | title              | month | created_at              | updated_at              |
+----+---------+--------------------+-------+-------------------------+-------------------------+
| 4  | 3       | 山登りに行きました | 8     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 8  | 3       | Ruby on Railsの日  | 9     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 3  | 3       | 昨日の出来事       | 12    | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
+----+---------+--------------------+-------+-------------------------+-------------------------+
3 rows in set

SQLも変わっていますが、残念ながら逆順になりませんでした。

ここでreorderを使ってみましょう。

(5)Railsコンソールで以下のコードを入力します。

User.find(3).posts.reorder(month: :desc)

実行結果:

  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? ORDER BY "posts"."month" DESC  [["user_id", 3]]
+----+---------+--------------------+-------+-------------------------+-------------------------+
| id | user_id | title              | month | created_at              | updated_at              |
+----+---------+--------------------+-------+-------------------------+-------------------------+
| 3  | 3       | 昨日の出来事       | 12    | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 8  | 3       | Ruby on Railsの日  | 9     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 4  | 3       | 山登りに行きました | 8     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
+----+---------+--------------------+-------+-------------------------+-------------------------+
3 rows in set

SQL文も変わり、意図したとおり逆順になっていますね!

このように、あらかじめ順番が指定されているデータを、別の方法で並べ替えるときはreorderを使う必要があるのです。

順番を1つ1つ指定してデータを取得する(order_as_specified gem)

order_as_specified gemを使って、順番を1つ1つ指定してデータを取得する方法を紹介しましょう。

参考:https://github.com/panorama-ed/order_as_specified

order_as_specified gemをインストールする

順番を1つ1つ指定してデータを取得するために、order_as_specified gemをインストールしましょう。

(1)Gemfileの最終行に以下の内容を追記します。

gem 'order_as_specified'

(2)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

cd app/samurai/includes-demo
bundle install

これで、order_as_specified gemがインストールされました。

順番を1つ1つ指定してデータを取得する

では、順番を指定してデータを取得してみましょう。

ここでは、Postテーブルから、user_idが「1、3、5、2、4」の順番になるようにデータを取得してみます。

(1)Railsコンソールを終了します。

(2)app/models/post.rbを以下のように編集します。

変更前:

class Post < ApplicationRecord
  belongs_to :user
end

変更後:

class Post < ApplicationRecord
  extend OrderAsSpecified
  belongs_to :user
end

(3)以下のコマンドを1行ずつ順番に入力します。

bin/rails console
Hirb.enable
Post.order_as_specified(user_id: [1,3,5,2,4])

実行結果:

  Post Load (0.3ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."user_id"=1 DESC, "posts"."user_id"=3 DESC, "posts"."user_id"=5 DESC, "posts"."user_id"=2 DESC, "posts"."user_id"=4 DESC
+----+---------+------------------------+-------+-------------------------+-------------------------+
| id | user_id | title                  | month | created_at              | updated_at              |
+----+---------+------------------------+-------+-------------------------+-------------------------+
| 2  | 1       | 先日の旅行での話       | 2     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 3  | 3       | 昨日の出来事           | 12    | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 4  | 3       | 山登りに行きました     | 8     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 8  | 3       | Ruby on Railsの日      | 9     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 1  | 5       | 楽しい休日の過ごし方   | 3     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 6  | 2       | 最近少し気になったこと | 1     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 5  | 4       | 友人が結婚しました     | 4     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 7  | 4       | ランニングのコツ       | 9     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
+----+---------+------------------------+-------+-------------------------+-------------------------+
8 rows in set

user_idに注目すると、「1、3、5、2、4」の順番でデータを取得できていることがわかります。

これをさらに、month順に並べてみましょう。

(5)Railsコンソールで以下のコードを入力します。

Post.order_as_specified(user_id: [1,3,5,2,4]).order(:month)

実行結果:

  Post Load (0.3ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."user_id"=1 DESC, "posts"."user_id"=3 DESC, "posts"."user_id"=5 DESC, "posts"."user_id"=2 DESC, "posts"."user_id"=4 DESC, "posts"."month" ASC
+----+---------+------------------------+-------+-------------------------+-------------------------+
| id | user_id | title                  | month | created_at              | updated_at              |
+----+---------+------------------------+-------+-------------------------+-------------------------+
| 2  | 1       | 先日の旅行での話       | 2     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 4  | 3       | 山登りに行きました     | 8     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 8  | 3       | Ruby on Railsの日      | 9     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 3  | 3       | 昨日の出来事           | 12    | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 1  | 5       | 楽しい休日の過ごし方   | 3     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 6  | 2       | 最近少し気になったこと | 1     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 5  | 4       | 友人が結婚しました     | 4     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
| 7  | 4       | ランニングのコツ       | 9     | 2018-06-21 16:20:39 UTC | 2018-06-21 16:20:39 UTC |
+----+---------+------------------------+-------+-------------------------+-------------------------+
8 rows in set

素晴らしいですね!

includesの結果を並べ替える(includes、order)

ここまでで、includesとorderの使い方を別々に確認しましたので、最後に、includesとorderを同時に使うケースを紹介します。

Postテーブルのmonthの値を使って、結果を並び替えてみましょう。

(1)Railsコンソールで以下のコードを入力します。

Post.includes(:user).where(users: {name: "立川裕美"}).order("posts.month asc").each { |post| puts "★ post.id=#{post.id} : post.user.name=#{post.user.name}" }

先ほど試したname="立川裕美"のusersだけを対象にしたコードに「.order(“posts.month asc”)」を追加しました。

orderには、並び替えに使うカラム(今回の例では、posts.month)と、並び替える順番(asc:昇順、desc:降順)を指定します。

  SQL (0.2ms)  SELECT "posts"."id" AS t0_r0, "posts"."user_id" AS t0_r1, "posts"."title" AS t0_r2, "posts"."month" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "users"."id" AS t1_r0, "users"."name" AS t1_r1, "users"."created_at" AS t1_r2, "users"."updated_at" AS t1_r3 FROM "posts" LEFT OUTER JOIN "users" ON "users"."id" = "posts"."user_id" WHERE "users"."name" = ? ORDER BY posts.month asc  [["name", "立川裕美"]]
★ post.id=4 : post.user.name=立川裕美
★ post.id=8 : post.user.name=立川裕美
★ post.id=3 : post.user.name=立川裕美
+----+---------+------------------+-------+-----------------+------------------+
| id | user_id | title            | month | created_at      | updated_at       |
+----+---------+------------------+-------+-----------------+------------------+
| 4  | 3       | 山登りに行き...  | 8     | 2018-06-21 1... | 2018-06-21 14... |
| 8  | 3       | Ruby on Rails... | 9     | 2018-06-21 1... | 2018-06-21 14... |
| 3  | 3       | 昨日の出来事     | 12    | 2018-06-21 1... | 2018-06-21 14... |
+----+---------+------------------+-------+-----------------+------------------+
3 rows in set

ascをdescに変更すれば、逆順になります。

まとめ

この記事では、パフォーマンスに影響が出るN+1問題を、includesを使って解決する方法を紹介しました。

今回はincludesを説明しましたが、同様のメソッド(joinspreloadeager_load)もありますので、しっかり理解して、適材適所でデータを取り出しましょう。

これを機にそれぞれの違いを勉強してみてはいかがでしょうか。

さらに、データを並べ替えた状態で取得するorder/reorderの使い方、order_as_specified gemの使い方も説明しました。

もしincludesやorderの使い方を忘れてしまったら、この記事でもう一度確認してくださいね!

この記事を書いた人

侍エンジニア塾は「人生を変えるプログラミング学習」をコンセンプトに、過去多くのフリーランスエンジニアを輩出したプログラミングスクールです。侍テック編集部では技術系コンテンツを中心に有用な情報を発信していきます。

目次