みなさんは、2つの関連付けられたテーブルを扱っているときに、どのように値を取り出していますか?
パフォーマンスを気にせず、allやjoinを使って、N+1問題を発生させていませんか?
それともパフォーマンスを気にして、includesやpreload、eager_loadを使っていますか?
この記事では、パフォーマンスを上げるための1つの解法であるincludesを取り上げ、以下の内容を説明しています。
・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」にアクセスします。
(4)ブラウザで「http://localhost:3000/posts」にアクセスします。
うまく登録されていますね!
次に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 gemとhirb-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)
includesとwhereを同時に使用すると、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を説明しましたが、同様のメソッド(joins、preload、eager_load)もありますので、しっかり理解して、適材適所でデータを取り出しましょう。
これを機にそれぞれの違いを勉強してみてはいかがでしょうか。
さらに、データを並べ替えた状態で取得するorder/reorderの使い方、order_as_specified gemの使い方も説明しました。
もしincludesやorderの使い方を忘れてしまったら、この記事でもう一度確認してくださいね!