ORM

紐づいたモデルのデータをまとめて取得する

app/controllers/test_controller.rb
class TestController < ApplicationController
  def index
    @companys = Company.includes(:employees).all
  end
end
app/views/test/index.html.erb
<table>
  <thead>
    <tr>
      <th>id</th>
      <th>名前</th>
      <th>従業員id</th>
      <th>従業員氏名</th>
    </tr>
  </thead>

  <tbody>
    <% @companys.each do |company| %>
      <% company.employees.each_with_index do |employee, index| %> 
        <tr>
          <% if index == 0 %>
            <td rowspan="<%= company.employees.count %>"><%= company.id %></td>
            <td rowspan="<%= company.employees.count %>"><%= company.name %></td>
          <% end %>
          <td><%= employee.id %></td>
          <td><%= employee.name %></td>
        </tr>
      <% end %>
    <% end %>
  </tbody>
</table>

説明

\n

1includesメソッドとN+1問題

\n

includesを使用することで、紐づいたモデルのデータをまとめて取得し、パフォーマンスを改善することができます。

\n \n
ポイント
\n

N+1問題とは? includesは記入しなくても動作しますが、紐づいたモデルのデータを取得するたびにクエリを発行してしまう(N+1)問題が発生します。includesを使用すると、紐づいたデータをまとめて取得することができるため、N+1問題を解決することができます。

\n
\n\n\n\n

2基本的な使い方

\n

includesは以下のような形式で記述します:

\n \n
\n
モデル.includes(:取得するモデルのフィールド).その他のメソッド
\n
\n \n

例えば、CompanyモデルとEmployeeモデルが関連している場合:

\n \n
\n
# N+1問題が発生する例\ncompanies = Company.all\ncompanies.each do |company|\n  puts company.employees.count  # 会社ごとに別のクエリが発行される\n\n# includesを使用して最適化した例\ncompanies = Company.includes(:employees).all\ncompanies.each do |company|\n  puts company.employees.count  # 追加のクエリは発行されない
\n
\n \n

上の例ではCompanyを1側、Employeeを多側としてincludesを使用しています。

\n\n\n\n

3複数の関連モデルを取得

\n

複数のモデルのフィールドを取得したいときはincludesの引数を,区切りで増やすことができます:

\n \n
\n
Employee.includes(:company, :department).all
\n
\n \n

この例では、各Employeeに関連するCompanyとDepartmentの情報が一度に取得されます。

\n\n\n\n

4ネストした関連を取得

\n

紐づいたモデルのさらに先の紐づいたデータを集計するときは、以下のように記述します:

\n \n
\n
モデル.includes(取得するモデルのフィールド: :もう一つ先のフィールド)
\n
\n \n

例えば、Employeeが1側でそれに多側のFamilyが紐づいていた場合は:

\n \n
\n
Company.includes(employees: :families).all
\n
\n \n

さらにFamilyが1側でそれに多側のFavoriteが紐づいていた場合は:

\n \n
\n
Company.includes(employees: {families: :favorites})
\n
\n \n

このように{}で入れ子にしていくことで、複数階層の関連を一度に取得できます。

\n\n\n\n

5includesと他のメソッドの組み合わせ

\n

includesは他のActiveRecordメソッドと組み合わせて使用できます:

\n \n
\n
# whereと組み合わせる\nCompany.includes(:employees).where(employees: { department: "営業部" })\n\n# orderと組み合わせる\nCompany.includes(:employees).order("companies.name ASC")\n\n# limitと組み合わせる\nCompany.includes(:employees).limit(10)\n\n# selectと組み合わせる (必要なカラムのみ取得)\nCompany.includes(:employees).select("companies.id, companies.name")
\n
\n\n\n\n

6実践的な使用例

\n

コントローラーでの実際の使用例:

\n \n
\n
class CompaniesController < ApplicationController\n  def index\n    # 基本的な使用例\n    @companies = Company.includes(:employees).all\n  end\n  \n  def show\n    @company = Company.includes(employees: [:department, :projects]).find(params[:id])\n    # これにより、ビューで@company.employeesやその関連データにアクセスしても追加クエリが発行されない\n  end\n  \n  def dashboard\n    # 複雑な関連を一度に取得\n    @companies = Company.includes(\n      employees: [\n        :department,\n        { projects: :tasks },\n        { families: :favorites }\n      ]\n    ).all\n  end\nend
\n
\n \n

ビューでの例(app/views/companies/show.html.erb):

\n \n
\n
<h1><%= @company.name %></h1>\n\n<h2>従業員一覧</h2>\n<ul>\n  <% @company.employees.each do |employee| %>\n    <li>\n      <%= employee.name %> - <%= employee.department.name %>\n      \n      <h3>プロジェクト</h3>\n      <ul>\n        <% employee.projects.each do |project| %>\n          <li><%= project.name %></li>\n        <% end %>\n      </ul>\n      \n      <h3>家族</h3>\n      <ul>\n        <% employee.families.each do |family| %>\n          <li>\n            <%= family.name %>\n            <p>好きなもの: <%= family.favorites.map(&:name).join(', ') %></p>\n          </li>\n        <% end %>\n      </ul>\n    </li>\n  <% end %>\n</ul>
\n
\n\n\n
ポイント
\n

パフォーマンス最適化のヒント:

\n
    \n
  • includesは内部的にLEFT OUTER JOINまたはPRELOADを使用してデータを取得します。
  • \n
  • 大量のデータを扱う場合、必要な関連のみをincludesに指定するようにしましょう。
  • \n
  • 関連が非常に深い場合や複雑な場合は、パフォーマンスに影響が出る可能性があります。適切な範囲で使用しましょう。
  • \n
  • 開発中に発行されるSQLクエリを確認するには、RailsコンソールでActiveRecord::Base.logger = Logger.new(STDOUT)を実行すると便利です。
  • \n
  • より複雑な条件で関連データを読み込む場合は、joinsメソッドの使用も検討してください。
  • \n
\n