可以急切加载与nested_attributes的关联吗?

发布于 2024-09-25 00:25:51 字数 2070 浏览 6 评论 0原文

简而言之,我遇到了可怕的 2(n) 查询问题。 如果 n = 数据库中的技能数量,那么我的characters#edit 表单将需要 2(n) 次查询来加载页面。每个技能都会选择一次 PlayerSkill(连接表),并且每个技能都会查找一次技能。

这是一些我认为与情况相关的代码。本质上,这个过程中涉及的模型、视图和控制器,更少的模型验证和更少的我不关心的操作。

控制器:

  # GET /characters/1/edit
  def edit
    @character = Character.find(params[:id], :include => {:player_skills => :skill})
    stub_player_skills
  end

  private
    def stub_player_skills
      @skills = Skill.find(:all)
      @skills.each do |skill|
        if (skill.player_skills.empty?)
          ps = @character.player_skills.build(:skill_id => skill.id, :name => skill.name)
        end
      end
    end

模型:

class Character < ActiveRecord::Base
  belongs_to :user
  belongs_to :campaign
  has_many :sheets, :dependent => :destroy
  has_many :tokens, :dependent => :destroy

  has_many :player_skills, :dependent => :destroy
  has_many :skills, :through => :player_skills
  accepts_nested_attributes_for :player_skills, :allow_destroy => true
end

有问题的视图(HAML):

%h1
  Editing Character

- form_for @character do |f|
  = f.error_messages
  %p
    = f.label :name
    %br
    = f.text_field :name
  %p
    = f.label :race
    %br
    = f.text_field :race
  %p
    = f.label :char_class
    %br
    = f.text_field :char_class
  %p
    -f.fields_for :player_skills do |ps|
      =ps.object.skill.name
      =ps.text_field :level
      =ps.hidden_field :skill_id
      -unless ps.object.new_record?
        =ps.check_box '_destroy'
        =ps.label '_destroy', 'Remove'
      %br
  %p
    = f.submit

我对这种情况的理解是,急切加载的存在是为了在(大致)单个额外查询中获取关联。

我需要在两个区域正确应用急切加载,但我只是不知道如何做到这一点。

在stub_player_skills方法中,它需要创建一个PlayerSkill对象假设角色还没有。 它可以从这里的预先加载中受益,因为它循环遍历数据库中的每项技能。这就是第一个“n 查询”的来源。

然后在视图上, fields_for 循环遍历我们已经积累的所有 PlayerSkills,因为这里无法立即加载,当我调用 =ps.object.skill.name 打印出技能名称时,它会进行技能查找,它引入了第二组“n 查询”。

我主要关心的是视图层,我找不到任何文档(Rails API 或其他)来说明如果您使用 fields_for 生成嵌套表单,如何立即加载关联。

感谢您的所有回复:) 〜罗比

Just briefly, I have run into a dreaded 2(n) queries problem.
If n = the number of skills in the database, then my characters#edit form will take 2(n) queries to load the page. It will SELECT a PlayerSkill (the join table) once every skill, and it will look up the Skill once per skill.

Here is some code which I believe is pertinent to the situation. In essence, the models, views, and controllers involved in this process, less the model validations and less the actions I'm not concerned about.

The controller:

  # GET /characters/1/edit
  def edit
    @character = Character.find(params[:id], :include => {:player_skills => :skill})
    stub_player_skills
  end

  private
    def stub_player_skills
      @skills = Skill.find(:all)
      @skills.each do |skill|
        if (skill.player_skills.empty?)
          ps = @character.player_skills.build(:skill_id => skill.id, :name => skill.name)
        end
      end
    end

The model(s):

class Character < ActiveRecord::Base
  belongs_to :user
  belongs_to :campaign
  has_many :sheets, :dependent => :destroy
  has_many :tokens, :dependent => :destroy

  has_many :player_skills, :dependent => :destroy
  has_many :skills, :through => :player_skills
  accepts_nested_attributes_for :player_skills, :allow_destroy => true
end

The offending view (HAML):

%h1
  Editing Character

- form_for @character do |f|
  = f.error_messages
  %p
    = f.label :name
    %br
    = f.text_field :name
  %p
    = f.label :race
    %br
    = f.text_field :race
  %p
    = f.label :char_class
    %br
    = f.text_field :char_class
  %p
    -f.fields_for :player_skills do |ps|
      =ps.object.skill.name
      =ps.text_field :level
      =ps.hidden_field :skill_id
      -unless ps.object.new_record?
        =ps.check_box '_destroy'
        =ps.label '_destroy', 'Remove'
      %br
  %p
    = f.submit

My understanding of the situation is that eager loading exists to grab the association in (roughly) a single extra query.

I need to properly apply eager loading in two areas, and I am just at a loss regarding how to do it.

In the stub_player_skills method, it needs to create a PlayerSkill object assuming the character does not already have one.
It could benefit from eager loading here because it loops through each skill in the database. This is where the first "n-queries" are coming from.

Then on the view, fields_for loops through all the PlayerSkills we've racked up, because there is no way to eager load here, when I call =ps.object.skill.name to print out the skill's name, it does a Skill lookup, which brings in the second set of "n-queries."

My primary concern lies in the view layer, I cannot find any documentation (Rails API or otherwise) that states how you could eager load the associations if you're using fields_for to generate a nested form.

Thanks for any and all responses :)
~Robbie

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

陈独秀 2024-10-02 00:25:51

你能试试这个并看看它是否有效吗?

您可以保持模型不变。

你的控制器看起来像这样

def edit
  # Get all the skill objects once only
  skills = Skill.find(:all)

  # Used to extract Skill#name
  skills_hash = {}
  skills.map { |s| skills_hash[s.id] = s.name }

  # Create an array of the skill-ids
  skill_ids = skills.map { |s| s.id }

  @character = Character.find(params[:id])

  # Determine the character's missing skills
  skill_ids -= @character.player_skill_ids

  # Build all of the missing skills
  skill_ids.each do |id|
    @character.player_skills.build(:skill_id => id, :name => skills_hash[id])
  end
end

Can you try this and see if it will work?

You can keep your model the way it is.

Your controller can then look like this

def edit
  # Get all the skill objects once only
  skills = Skill.find(:all)

  # Used to extract Skill#name
  skills_hash = {}
  skills.map { |s| skills_hash[s.id] = s.name }

  # Create an array of the skill-ids
  skill_ids = skills.map { |s| s.id }

  @character = Character.find(params[:id])

  # Determine the character's missing skills
  skill_ids -= @character.player_skill_ids

  # Build all of the missing skills
  skill_ids.each do |id|
    @character.player_skills.build(:skill_id => id, :name => skills_hash[id])
  end
end
命硬 2024-10-02 00:25:51

如果有人对这个问题的“最终”解决方案感兴趣:

我已经诉诸于存储技能名称数组,并通过计数器在视图中引用它,如下所示:

  %p
    - index = 0
    -f.fields_for :player_skills do |ps|
      =@skill_arr[index]
      =ps.text_field :level
      =ps.hidden_field :skill_id
      -unless ps.object.new_record?
        =ps.check_box '_destroy'
        =ps.label '_destroy', 'Remove'
      - index += 1
      %br

在控制器中,我几乎移动了Stub_player_skills 方法所属的所有逻辑,并从 Coderama 的书中取出一页,我想出了这个:

  private
    def stub_player_skills
      @skills = Skill.find(:all)
      @skills.each do |skill|
        skill_exists = @character.player_skills.select do |i|
          i.skill_id == skill.id
        end
        if skill_exists.empty?
          ps = @character.player_skills.build(:skill_id => skill.id, :name => skill.name)
        end
      end

      @skill_arr = @character.player_skills.map do |el|
        el.name.nil? ? el.skill.name : el.name
      end
    end

在模型层中,我只需 :include =>; has_many :through 关系上的 :skill 可以消除更多查询。

In case anyones interested in my "final" solution to this problem:

I've resorted to storing an array of the skill names, and referencing it in the view via a counter, as seen here:

  %p
    - index = 0
    -f.fields_for :player_skills do |ps|
      =@skill_arr[index]
      =ps.text_field :level
      =ps.hidden_field :skill_id
      -unless ps.object.new_record?
        =ps.check_box '_destroy'
        =ps.label '_destroy', 'Remove'
      - index += 1
      %br

In the controller, I've moved almost all the logic to the stub_player_skills method where it belongs, and taking a page out of Coderama's book, I've come up with this:

  private
    def stub_player_skills
      @skills = Skill.find(:all)
      @skills.each do |skill|
        skill_exists = @character.player_skills.select do |i|
          i.skill_id == skill.id
        end
        if skill_exists.empty?
          ps = @character.player_skills.build(:skill_id => skill.id, :name => skill.name)
        end
      end

      @skill_arr = @character.player_skills.map do |el|
        el.name.nil? ? el.skill.name : el.name
      end
    end

In the model layer, I just had to :include => :skill on the has_many :through relationship to get rid of a few more queries.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文