質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.04%

RailsのN+1問題とアソシエーションについて

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 526

score 2949

前提・実現したいこと

N+1問題とアソシエーションに関してきちんと理解ができず、少し混乱気味なので、現在作成中のアプリを実例として教えていただければと思います。

会社に所属する人、会社の銀行口座といった形で下記のようなモデルを作っています。

#model
CompanyーーーーUser
     | └ーUser
     | └ーUser
     |
     └ーーーBank(銀行・支店コード)ー(?)ーBankcode(銀行・支店名)
     └ーーーBank(銀行・支店コード)ー(?)ーBankcode(銀行・支店名)

発生している問題・エラーメッセージ

①この場合、アソシエーションの設定は下記のような形になるとの認識で良いでしょうか?

class Company < ApplicationRecord
  has_many :users
end

class User < ApplicationRecord
  belongs_to :company
end

class Bank < ApplicationRecord
  belongs_to :company
  has_one    :bankcode
end

class Bankcode < ApplicationRecord
  belongs_to :bank
end

②下記のような形で銀行口座名を求めたのですが、コンソールを確認するとN+1問題が発生していました。
RailsGuideのAuthorとBookだと理解できていると思うのですが、いざ自分でやろうとするとわからなくなってしまいました。
解決方法を教えていただければと思います。

#banks_controller.rb > index.html.erb

  <tbody>
    <% @banks.each do |bank| %>
      <tr>

        <td><%= Bankcode.find_by(bank: bank.bank_code).name %></td>
        <td><%= Bankcode.find_by(bank: bank.bank_code, branch: bank.branch_code ).name %></td>

      </tr>
    <% end %>
  </tbody>
#class BanksController < ApplicationController

  def index

    #deviseを利用しているためcurrent_user.idにてログイン中のUserIDを取得
    @user = User.find_by(id: current_user.id)
    @company = Company.find(@user.company_id)
    @banks = Bank.where(company_id:@company.id)

  end


どうぞよろしくお願いいたします。

補足情報(FW/ツールのバージョンなど)

ruby 2.6.2
rails 5.2.2

追記(Bank(銀行・支店コード)ーBankcode(銀行・支店名)について)

Bank(銀行・支店コード)とBankcode(銀行・支店名)のテーブルの構造が下記のようになっています。
そのため通常のアソシエーションではうまくデータを取得できないことがわかりました。

#会社の銀行口座の銀行コード(bank_code)、支店コード(branch_code)が記載されているテーブル

ID|bank_code|branch_code|company_id|......
100017382         |......
200015992         |......
300050562         |......
#全国銀行協会で定められたの銀行コード(bank)、支店コード(branch)が記載されているテーブル

ID|bank|branch|name     |......
10001000   |みずほ銀行   |......
100001738   |たまプラーザ支店|......
200001599   |あざみ野支店   |......
300005000   |三菱UFJ銀行   |......
300005056   |あびこ支店    |......


このようなテーブルの場合に下記のような結果を得るためにはどのようにすればよいでしょうか?

#banks_controller.rb > index.html.erb

  <tbody>
    <% @banks.each do |bank| %>
      <tr>
        <td>銀行名</td>
        <td>支店名</td>
      </tr>
    <% end %>
  </tbody>
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+1

bankcodeに銀行と支店の双方が入っているなら、アソシエーションもそれぞれ作らないといけません

class Bank < ApplicationRecord
  belongs_to :company

  # with_options で同じ条件をまとめるつつ
  # 銀行名は1つなので has_one, 支店は複数なので has_many で関連を作る
  with_options primary_key: :bank_code, 
               foreign_key: :bank_code,
               class_name: 'Bankcode' do
    has_one :bank_bankcode, -> { where(branch_code: '0000') }
    has_many :branch_bankcodes, -> { where.not(branch_code: '0000') }
  end
end

class Bankcode < ApplicationRecord
  with_optoins primary_key: :bank_code, foreign_key: :bank_code do
    belongs_to :bank

    # 自身と同じ bank_code で branch_code が 0000 が自分の銀行
    has_one :main_bankcode, -> { where(branch_code: '0000') }, class_name: 'Bankcode'
  end
end

描画例A

  def index
    # 略

    # それぞれ事前読み込みを行います
    @banks = @company.banks.preload(:bank_bankcode, :branch_bankcodes) 
  end
  <tbody>
    <% @banks.each do |bank| %>
      - # 表示の目的である支店でループしつつ
      <% bank.branch_bankcodes.each do |branch_bankcode| %>
        <tr>
          - # 銀行名は bank 側で事前読み込みしているものを参照する
          <td><%= bank.bank_bankcode.name %></td>

          - # 支店を表示する
          <td><%= branch_bankcode.name %></td>
        </tr>
      <% end %>
    <% end %>
  </tbody>

描画例B

  def index
    # 略

    # 支店側で銀行まで事前読み込みを行います
    @banks = @company.banks.preload(branch_bankcodes: :bank_bankcode) 
  end
  <tbody>
    <% @banks.each do |bank| %>
      - # 表示の目的である支店でループしつつ
      <% bank.branch_bankcodes.each do |branch_bankcode| %>
        <tr>
          - # 銀行名は支店側で事前読み込みしているものを参照する
          <td><%= branch_bankcod.bank_bankcode.name %></td>

          - # 支店を表示する
          <td><%= branch_bankcode.name %></td>
        </tr>
      <% end %>
    <% end %>
  </tbody>

補足

外部データのようなので仕方ないですがRails的な理想は違う構造なら別テーブルであることが理想です
それなら上記のアソシエーションは where とかが無くなりシンプルになると思います
上記アソシエーション設定は、設定上で下記構造になるよう切り分けるために頑張っていると思ってください

#bankcodes
ID|bank|branch|name     |......
10001000   |みずほ銀行   |......
300005000   |三菱UFJ銀行   |......
#branchcodes
ID|bank|branch|name     |......
100001738   |たまプラーザ支店|......
200001599   |あざみ野支店   |......
300005056   |あびこ支店    |......

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/04 12:18

    詳しい説明を頂きありがとうございます。
    アソシエーションに関して、より深く理解することができました。
    また「Rails的な理想」というのも頂いた回答をみて納得しました。
    銀行テーブルに関してはこちらで加工ができるので、そちらも検討してみたいと思います。

    キャンセル

+1

定義

参照しているので company - banks も定義しておくと良いでしょう

class Company < ApplicationRecord
  has_many :users
  has_many :banks
end
  def index
    @user = User.find_by(id: current_user.id)
    @company = @user.company # belongs_to定義してるので呼べる

    # has_many定義すると @company.banks で呼べる
    # preload で事前に一括で読み込む
    @banks = @company.banks.preload(:bankcode) 
  end

find_byは each の中で使うべきではない

find_byはSQLを発行するのでその使い方だと必ずN+1になります

<tbody>
    <% @banks.each do |bank| %>
      <tr>
        - # preloadしたものを呼ぶことで N+1 を解消 
        <td><%= bank.bankcode.name %></td>

        - # branchとは何でしょうか?
        - # アソシエーションとは異なる関連があるならN+1の回避は難しいです
        <td><%= Bankcode.find_by(bank: bank.bank_code, branch: bank.branch_code ).name %></td>
      </tr>
    <% end %>
</tbody>

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/03 02:35

    ご回答いただきありがとうございます。
    アソシエーションに関するコメントで非常にわかりやすくて助かりました。

    こちらをもとに動かそうとして混乱している部分がわかったので、再度、質問させてください。

    Bank⇔Bankcodeのアソシエーションを勘違いしていました。
    質問にテーブルの構成を追記しましたので、このような場合の値の取り出し方を教えていただけないでしょうか?
    どうぞよろしくお願いいたします。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.04%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る