【Rails入門説明書】includesについて解説
はじめに
Railsアプリケーションは、データベースとブラウザの間を取り持つのが、主な役割です。
つまり、Railsアプリケーションの主な働きは、2つにまとめられます。
・データベースからデータを取得して、そのデータに適した表示画面を作る
・入力された値を適切に処理して、データベースへ記録する
そのため、通信速度とデータベースへのアクセス速度が、アプリケーションのパフォーマンスに大きな影響を与えます。
特に、データベースへのアクセス速度は、安易に作ってしまうと、データ量に応じて急激に応答が遅くなることがあります。つまり、アプリケーションの「つくり」によって大きな差が出てくるわけです。
そんなパフォーマンスを考慮したプログラムを考えるなら、ぜひincludesを利用しましょう。
includesを使うことで、データベースへのアクセス回数を減らし、アプリケーションの応答速度を上げることができるのです。
今回は、そんなincludesについて、詳しく解説していきます。
また、同様に複数のテーブルからデータを取得する以下のメソッドも、あわせて紹介します。
・joins
・preload
・eager_load
Ruby on Railsとは?4つの作れるものや人気の理由・学習方法を解説
Railsのincludesとは
Railsのincludesは、関連している複数のテーブルからデータを取得してくるときのアクセス回数を大きく減らすことができるメソッドです。
また、事前に検索やフィルタリング、ソートなどをしたデータを取得することもできるため、アプリケーション側でそれらの処理を行う必要がなくなります。そのため、シンプルで効率的なプログラムになるのです。
どの程度改善できるのか、具体的な環境を作って、確認していきます。
テスト環境の作成
ここでは、関連した2つのテーブルを持つRailsアプリケーションの雛形を、scaffoldを使って作成します。
コマンドプロンプトにて、以下のコマンドを実行してください。(scaffoldの詳細は「【Rails入門説明書】scaffoldについて解説」を参照してください)
[bash]> rails new inc_test
> cd inc_test
> rails generate scaffold Saler name:string
> rails generate scaffold Item saler:references title:string price:integer
> rake db:migrate
[/bash]
以上の操作で、販売者のテーブルと、商品のテーブルが生成されました。次に、テーブル間の関連付けを行うため、モデルクラスにhas_many(参照される側)とbelongs_to(参照する側)の追加を行います。
(has_manyについては、「【Rails入門説明書】has_manyについて解説」を参照してください)
(app/models/saler.rb)
class Saler < ApplicationRecord
has_many :items
end
※referencesを使用したため、参照するテーブルには、belongs_toが自動で追加されています。
テスト用に以下のデータを登録しておきましょう。(Webアプリケーションを実行してブラウザから登録する、rails consoleで登録するなどの方法がありますが、登録方法は問いません)
テーブル | データ |
---|---|
Saler | name: “Tanaka” name: “Ohashi” name: “Kimura” name: “Takahashi” name: “Ogawa” name: “Kobayashi” |
Item | saler_id: 3,title: “book”, price:400 saler_id: 2,title: “cap”, price:8000 saler_id: 4,title: “shoes”, price:1500 saler_id: 2,title: “ring”, price:12000 saler_id: 1,title: “ring”, price:50000 saler_id: 5,title: “computer”, price:150000 saler_id: 2,title: “shirts”, price:1000 saler_id: 5,title: “mouse”, price:100 |
もし、rails consoleで登録する場合は、以下のコマンドを順に実行していくことになります。
[bash]Saler.create(name:”Tanaka”)
Saler.create(name:”Ohashi”)
Saler.create(name:”Kimura”)
Saler.create(name:”Takahashi”)
Saler.create(name:”Ogawa”)
Saler.create(name:”Kobayashi”)
Item.create(saler_id:3,title:”book”, price:400)
Item.create(saler_id:2,title:”cap”, price:8000)
Item.create(saler_id:4,title:”shoes”, price:1500)
Item.create(saler_id:2,title:”ring”, price:12000)
Item.create(saler_id:1,title:”ring”, price:50000)
Item.create(saler_id:5,title:”computer”, price:150000)
Item.create(saler_id:2,title:”shirts”, price:1000)
Item.create(saler_id:5,title:”mouse”, price:100)
[/bash]
念のため、正しく登録されていることを確認しておきましょう。
サーバーを起動して、ブラウザから「localhost:3000/salers」と「localhost:3000/items」にアクセスします。
[bash]> rails s
[/bash]
itemsのリストのSalerが値になっていないのは、referencesで関連付けを行ったためです。このままでは、この後の処理が確認しづらくなりますので、itemsのリストに、IDとSalerテーブルに登録されている名前が表示されるようにします。
変更するのはビューファイルです。
(app/views/items/index.html.erb)
<%= notice %>
<h1>Items</h1>
<table>
<thead>
<tr>
<th>ID</th>
<!– (1) –>
<th>Saler</th>
<th>Title</th>
<th>Price</th>
<th colspan=”3″></th>
</tr>
</thead>
<tbody>
<% @items.each do |item| %>
<tr>
<td><%= item.id %></td>
<!– (2) –>
<td><%= item.saler.name %></td>
<!– (3) –>
<td><%= item.title %></td>
<td><%= item.price %></td>
<td><%= link_to ‘Show’, item %></td>
<td><%= link_to ‘Edit’, edit_item_path(item) %></td>
<td><%= link_to ‘Destroy’, item, method: :delete, data: { confirm: ‘Are you sure?’ } %></td>
</tr>
<% end %>
</tbody>
</table>
<%= link_to ‘New Item’, new_item_path %>
コメントを追加している3行が追加、修正する部分です。
(1)と(2)はItemのIDを表示するための追加行、(3)は、salerテーブルから名前を取得するために、「item.saler」を「item.saler.name」へ変更しています。
保存後、ブラウザに戻って「localhost:3000/items」を読み込み直して、IDとSalerの名前が表示されていれば、準備完了です。
N+1問題とincludes
前述の環境構築で、ItemsのリストにSalerテーブルに登録してある名前が出力されていますので、基本的にはこれで完了だと考えがちです。
しかし、それがパフォーマンスの問題を起こしてしまう元凶なのです。
ブラウザにリストを表示した状態で、コマンドプロンプトへ戻ってみましょう。
そこには、次のようなログが出力されています。
[bash]Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
Item Load (1.0ms) SELECT “items”.* FROM “items”
└app/views/items/index.html.erb:17
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 3], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Saler Load (1.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 2], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Saler Load (1.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 4], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
CACHE Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 2], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 1], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 5], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
CACHE Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 2], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
CACHE Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 5], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Rendered items/index.html.erb within layouts/application (62.8ms)
Completed 200 OK in 326ms (Views: 301.6ms | ActiveRecord: 6.0ms)
[/bash]
これは、リストを表示するためにデータベースへアクセスした内容(SQL)が出力されているものです。
今回のリスト表示では、合計で9回データベースへアクセスしているということを表しています。この9回というのは、リストに出力された行数+1回です。
つまり、今のままでは、データベースに登録されたデータの数+1回アクセスしなければ、リストの表示ができません。それは、このままでは、もしデータの数が100件、10,00件と増えたら、リストの表示に10倍、100倍の時間が必要になるということです。
これは「N+1問題」として知られているもので、Webアプリケーションのパフォーマンスを考慮する上で重要な改善ポイントなのです。
データベースへのアクセスは、ログに出力されるように、実際はSQLコマンドによって行われます。そのため、この改善は「最適なSQL文を作成する」ことで行われるのが一般的です。
しかし、Railsには、そのSQL文を自動で作ってくれるメソッドが、すでに準備されています。それが、「includes」をはじめとしたメソッドなのです。
“未経験”でもたった1ヶ月で営業からエンジニアとして転職!『WebCamp』受講者インタビュー
includesで効率の良い処理を
では、実際にincludesを使って改善していきましょう。
今回の環境で修正するのは、scaffoldが自動生成した、Itemコントロールのindexメソッドの処理です。
(app/controllers/items_controller.rb)
:
def index
@items = Item.all
end
:
f
この、「Item.all」の部分がN+1問題の元凶です。
allメソッドは、ただ指定のテーブルのデータを取得してくるだけのメソッドです。そのため、Itemの各行を表示するときに、Salerテーブルから名前を取得するためのアクセスが発生していました。
この部分を変更することで、パフォーマンスの向上を図ります。
なお、上記のallメソッドを含めて、データベースへアクセスするためのメソッドは以下の5種類存在しています。
メソッド | 説明 | 関連テーブルの事前読み込み (associationのeager loading) |
生成したテーブルの保持 (JOIN) |
---|---|---|---|
all | テーブルのすべてのレコードを取得する | しない | 生成しない |
joins | テーブルの指定した条件のレコードを取得する | しない | 保持する |
eager_load | 関連テーブルの条件が一致するデータと指定テーブルのデータを取得する | する | 保持する |
preload | 関連テーブルの条件が一致するデータと指定テーブルのデータを取得する | する | 保持しない |
includes | 関連テーブルの条件が一致するデータと指定テーブルのデータを取得する | する | 必要に応じて保持する |
N+1問題を解消するために重視するのは、関連テーブルの事前読み込み(associationのeager loading)です。この処理を行っていれば、すでにメモリ上に関連テーブルがありますので、いちいちデータベースへアクセスする必要がないわけです。
構文は、それぞれ以下のようになっています。
# all
モデル.all
# joins
モデル.joins(テーブル) # 指定されたテーブルを結合
モデル.joins(条件) # 条件に合致するカラムを結合
# eager_load
モデル.eager_load(テーブル)
# preload
モデル.preload(テーブル)
# includes
モデル.includes(テーブル)
実際に動かして確認してみましょう。
joins
以下のように修正して、ブラウザで「localhost:3000/items」を再読み込みしてください。
(app/controllers/items_controller.rb)
:
def index
@items = Item.joins(:saler)
end
:
コマンドプロンプトのログを確認すると、allメソッドと同じく、データベースへのアクセスが9回発生しています。
[bash]Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
Item Load (0.0ms) SELECT “items”.* FROM “items” INNER JOIN “salers” ON “salers”.”id” = “items”.”saler_id”
└app/views/items/index.html.erb:17
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 3], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 2], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Saler Load (1.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 4], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
CACHE Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 2], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 1], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 5], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
CACHE Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 2], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
CACHE Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” = ? LIMIT ? [[“id”, 5], [“LIMIT”, 1]]
└app/views/items/index.html.erb:20
Rendered items/index.html.erb within layouts/application (74.8ms)
Completed 200 OK in 318ms (Views: 296.7ms | ActiveRecord: 6.0ms)
[/bash]
joinsは、関連テーブルの事前読み込みを行いませんので、N+1問題が発生しています。
(「【Rails入門説明書】joinについて解説」でjoinsについて詳しく解説していますので、参考にしてください)
eager_load
次に、eager_loadメソッドを試してみましょう。indexメソッドを次のように修正して、ページを再読み込みしましょう。
(app/controllers/items_controller.rb)
:
def index
@items = Item.eager_load(:saler)
end
:
Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
SQL (0.0ms) SELECT “items”.”id” AS t0_r0, “items”.”saler_id” AS t0_r1, “items”.”title” AS t0_r2, “items”.”price” AS t0_r3, “items”.”created_at” AS t0_r4, “items”.”updated_at” AS t0_r5, “salers”.”id” AS t1_r0, “salers”.”name” AS t1_r1, “salers”.”created_at” AS t1_r2, “salers”.”updated_at” AS t1_r3 FROM “items” LEFT OUTER JOIN “salers” ON “salers”.”id” = “items”.”saler_id”
└app/views/items/index.html.erb:17
Rendered items/index.html.erb within layouts/application (33.9ms)
Completed 200 OK in 909ms (Views: 891.8ms | ActiveRecord: 1.0ms)
[/bash]
eager_loadメソッドの場合、データベースへのアクセスが1回だけになっています。データの数が少ないので、体感速度に大きな違いはない(むしろ遅く感じる場合もあります)かもしれませんが、この処理回数の違いは、データの数が大きくなると大きな違いになってくるのです。
また、関連テーブルも結合して生成したテーブルを保持していますので、絞り込みなども高速に行うことができます。しかし、その分メモリを多く消費しますので、データ量やアクセスするユーザーの数が増えると、システムへの負担が大きくなるでしょう。
preload
preloadメソッドも、eager_loadとほぼ同じように、データベースへのアクセス回数を減らすことができます。
(app/controllers/items_controller.rb)
:
def index
@items = Item.preload(:saler)
end
:
Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
Item Load (1.0ms) SELECT “items”.* FROM “items”
└app/views/items/index.html.erb:17
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” IN (?, ?, ?, ?, ?) [[“id”, 3], [“id”, 2], [“id”, 4], [“id”, 1], [“id”, 5]]
└app/views/items/index.html.erb:17
Rendered items/index.html.erb within layouts/application (46.9ms)
Completed 200 OK in 349ms (Views: 322.6ms | ActiveRecord: 4.0ms)
[/bash]
preloadメソッドはSQLを分けてアクセスするため、アクセス数が2回になっています。しかし、データ量が増えても2回のままですので、allメソッドやjoinsメソッドのようにN+1問題とは無縁です。
1回のアクセスで済むeager_loadメソッドよりは速度の面で劣りますが、生成したテーブルを保持しませんので、メモリ消費は抑えられます。そのため、巨大なテーブルを扱うときに適しているでしょう。ただし、テーブルを保持していないため、絞り込みや並べ替えなどができません。
includes
今回の主役のincludesも、N+1問題には無縁のメソッドです。確認してみましょう。
(app/controllers/items_controller.rb)
:
def index
@items = Item.includes(:saler)
end
:
Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
Item Load (1.0ms) SELECT “items”.* FROM “items”
└app/views/items/index.html.erb:17
Saler Load (0.0ms) SELECT “salers”.* FROM “salers” WHERE “salers”.”id” IN (?, ?, ?, ?, ?) [[“id”, 3], [“id”, 2], [“id”, 4], [“id”, 1], [“id”, 5]]
└app/views/items/index.html.erb:17
Rendered items/index.html.erb within layouts/application (23.9ms)
Completed 200 OK in 236ms (Views: 218.4ms | ActiveRecord: 4.0ms)
[/bash]
preloadメソッドと同じく、データベースへのアクセスは2回で、高速な処理が期待できます。
また、includesメソッドの場合は、必要に応じて生成したテーブルを保持しますので、eager_loadメソッドとpreloadメソッドの良いとこどりをしたようなメソッドと言えます。
https://web-camp.io/magazine/archives/12654
絞り込みと並べ替え
ここまでの説明で、includesメソッドについての説明は終わりですが、最後に絞り込みと並べ替えについて、説明しておきます。
絞り込みは「where」
データベースからテーブルを取得するときに、whereメソッドで取得するデータの条件を設定することで、絞り込みを行うことができます。
whereメソッドは、ActiveRecordオブジェクトのメソッドですので、オブジェクトがあれば単体でも利用できますが、includesメソッドなどと組み合わせることで、データ取得と絞り込みを一気に行って、パフォーマンスを向上させることができるのです。
例えば、以下のようにすることで、金額(price)が5,000円未満のもののItemリストを表示できます。
(app/controllers/items_controller.rb)
:
def index
@items = Item.includes(:saler).where(“price <= ?”, 5000)
end
:
また、Ohashiさんからの出品商品だけを確認したい場合は、関連付けしているSalerテーブルから名前を取得するため、以下のような記載になります。
(app/controllers/items_controller.rb)
:
def index
@items = Item.includes(:saler).where(salers: {name: “Ohashi”})
end
:
並べ替えは「order」
includesなどで取得したテーブルを、任意のカラムで並べ替えるには、orderメソッドを利用します。
orderメソッドは、以下のような構文で利用します。
モデル.order(ソート式)
いくつか具体的な例をあげますので、プログラムコードを確認してください。
特定のカラムで昇順
金額の昇順で並べ替えてみましょう。
並べ替えのデフォルト設定は昇順になっていますので、昇順の並べ替えの場合は、キーとなるカラムを指定するだけになります。
(app/controllers/items_controller.rb)
:
def index
@items = Item.includes(:saler).order(“price”)
end
:
なお、カラムの指定は、文字列だけではなく、以下のような指定の方法でも構いません。
@items = Item.includes(:saler).order(“price”) # 文字列
@items = Item.includes(:saler).order(:price) # シンボル
@items = Item.includes(:saler).order(“price ASC”) # 文字列
@items = Item.includes(:saler).order(:price => :asc) # シンボル
@items = Item.includes(:saler).order(price: “asc”) # 昇順指定を文字列
@items = Item.includes(:saler).order(price: :asc) # 昇順指定をシンボル
昇順であることを表すASCを付加した方が、プログラムコードの可読性は上がるかもしれません。
特定のカラムで降順
降順にする場合は、降順指定を行います。
(app/controllers/items_controller.rb)
:
def index
@items = Item.includes(:saler).order(“price DESC”)
end
:
記載方法は昇順と同じく、様々な方法が使用できます。
複数のカラムで並べ替え
複数のカラムを並べ替え条件にすることもできますが、デフォルトでは必ず降順指定が必要です。
商品名の降順、金額の昇順で並べ替えましょう。
(app/controllers/items_controller.rb)
:
def index
@items = Item.includes(:saler).order(“title DESC”, “price ASC”)
end
:
関連付けされたテーブルのデータで並べ替える
関連付けされたテーブルのカラムを指定することで、関連付けされたデータで並べ替えることもできます。
出品者名で並べ替える場合は、次のようになります。
(app/controllers/items_controller.rb)
:
def index
@items = Item.includes(:saler).order(“salers.name”)
end
:
https://web-camp.io/magazine/archives/12481
まとめ
Railsのデータベースアクセス用メソッド、includesの説明を行いました。
どんなに素晴らしいサービスを作っても、応答速度が遅くては使い勝手が悪く、誰も使ってくれません。そのため、パフォーマンス問題は、Railsアプリケーションを作る上では、避けては通れないところでしょう。
もちろん、RailsをはじめとしたWebアプリケーションは、ネットワークを介して動作しますので、ある程度の応答時間がかかることもあります。しかし、そのこととアプリケーションのパフォーマンスは別問題です。
includesを使ってN+1問題を避け、「アプリケーションが遅い」ということがないようにしていきましょう。
・includesメソッドは、関連している複数のテーブルからデータを取得してくるときのアクセス回数を大きく減らすことができる
・N+1問題というのは、データベースからデータ取得するときに、データの数+1回のデータベースアクセスが発生すること
・allメソッドやjoinsメソッドは、N+1問題を起こすため、データが多くなるとパフォーマンスに影響が出る
・eager_loadメソッドは、1度のアクセスで関連付けられたテーブルと生成したテーブルを取得するので、もっとも早いがメモリの消費量が多い
・preloadメソッドは、eager_loadメソッドと同じだが生成したテーブルを保持しないのでメモリにやさしい。しかし絞り込みなどが行えない
・includesメソッドは、場合によってeager_loadメソッドとpreloadメソッドのどちらかの動きをする柔軟性のあるメソッド
・whereメソッドを付記することで、絞り込みができる
・orderメソッドで並べ替えができる
【インタビュー】1ヶ月でRubyをゼロから学び、Webエンジニアとして転職!
ブラジルから帰国し技術をつけようとRubyエンジニアを目指してWebCampでRubyを学び、見事Webエンジニアとして転職を果たした田中さんにお話を伺いました。
「Rubyの学習がしたい。基礎をしっかりと理解したい」
「転職のサポートがほしい」
と考えている方はぜひお読み下さい。