How to eager load a fields_for with validation

ruby-on-rails, ruby-on-rails-4

Solution

Here's a way to do it.

Imagine these 4 models, related to a question-answers site like StackOverflow, so everyone knows what we are talking about :)

So, a question has N parts, and also N answers. While an answer belongs to a question, and has N parts. Finally, an answer part belongs to a question part.

class Question
  has_many :parts, class_name: 'QuestionPart', dependent: :destroy
  accepts_nested_attributes_for :parts, allow_destroy: true
  has_many :answers, dependent: :destroy

  # ...
end

class QuestionPart
  belongs_to :question

  # ...
end

class Answer
  belongs_to :question
  belongs_to :user
  has_many :parts, class_name: 'AnswerPart', dependent: :destroy
  accepts_nested_attributes_for :parts, allow_destroy: true

  # ...
end

class AnswerPart
  belongs_to :answer
  belongs_to :question_part

  validates :body, presence: true

  # ...
end

Let's take a look at the answer controller:

class AnswersController < ApplicationController
  def new
    @question = Question.find(params[:question_id)
    @answer = @question.answers.new
    @question.parts.each { |p| @answer.parts.build(question_part: p) }
  end

  def edit
    @answer = Answer.find(params[:id])
    @answer_parts = @answer.parts.includes(:question_part)
  end

  def create
    @question = Question.find(params[:question_id])
    @answer = current_user.answers.build(answer_params.merge(question: @question))

    if @answer.save
      redirect_to @question, notice: 'The answer was successfully created.'
    else
      render :new
    end
  end

  def update
    #@answer = Answer.find(params[:id])
    @answer = Answer.where(id: params[:id]).includes(parts: [:question_part]).first

    if @answer.update(answer_params)
      redirect_to @answer.question, notice: 'The answer was successfully updated.'
    else
      render :edit
    end
  end

  private

  def answer_params
    params.require(:answer).permit(:body, parts_attributes: [:id, :question_part_id, :body])
  end
end

Please notice how I retrieve the answer in the update method. I have commented out the standard way, while I'm retrieving it with a custom query (which should be moved into the Answer model). Here is where I had trouble. If anyone knows a better way, just comment or add a new answer.

Now for the templates:

# new.html.erb
...
<%= form_for [@question, @answer] do |f|
  <%= render 'form', f: f, parts: @answer.parts
<% end %>

# edit.html.erb
...
<%= form_for [@answer] do |f|
  <%= render 'form', f: f, parts: @answer_parts || @answer.parts
<% end %>

# _form.html.erb
...
<%= f.fields_for :parts, parts do |builder| %>
  <%= builder.text_field :body, label: builder.object.question_part.description, required: false
  <%= builder.hidden_field :question_part_id %>  
<% end %>

Problem

I have this fields_for in my form: ``` = f.fields_for :parts do |builder| = builder.input :body, label: builder.object.another_relation.description, required: false ``` In the main model I have this validation: ``` validates_associated :parts ``` In the parts model I have this validation: ``` validates :body, presence: true ``` This works well. When I leave some part empty and try to create the model, the form is re-displayed again with proper error messages. But in the logs I see that the label is querying the database for each part, so I need to eager load this. How? This was my try: ``` = f.fields_for :parts, f.object.parts.includes(:question_part) do |builder| = builder.input :body, label: builder.object.another_relation.description, required: false ``` This eager loads the relationship successfully, but when I leave some field empty and try to create the model, the form is re-displayed with all parts empty and no errors. I think this is normal, as I'm forcing the fields_for to load fields from that collection I'm passing. Anyone knows how to solve this?

Original source