Rails - как заполнить идентификатор родительского объекта, используя вложенные атрибуты для дочернего объекта и сильные параметры?
У меня есть ситуация, подобная представленной в Railscast 196-197: Вложенная форма модели. Однако я столкнулся с конфликтом между этим подходом и сильными параметрами. Я не могу найти хороший способ заполнить поле id родительской записи на дочернем объекте, так как я не хочу, чтобы это было назначено через форму (чтобы пользователи не связывали дочерние записи с родительскими записями, которые они не имеют). У меня есть решение (см. Код ниже), но это похоже на то, что Rails может иметь умный и простой способ сделать для меня.
Здесь код...
Здесь есть родительский объект (вызовите его Survey), у которого есть дочерние объекты has_many (назовите их "Вопросы" ):
# app/models/survey.rb
class Survey
belongs_to :user
has_many :questions
accepts_nested_attributes_for :questions
end
# app/models/question.rb
class Question
validates :survey_id, :presence => true
belongs_to :survey
end
Существует форма, позволяющая пользователям одновременно создавать опрос и вопросы об этом опросе (для простоты приведенный ниже код рассматривает опросы, как будто они имеют только вопрос):
# app/views/surveys/edit.html.erb
<%= form_for @survey do |f| %>
<%= f.label :name %>
<%= f.text_field :name %><br />
<%= f.fields_for :questions do |builder| %>
<%= builder.label :content, "Question" %>
<%= builder.text_area :content, :rows => 3 %><br />
<% end %>
<%= f.submit "Submit" %>
<% end %>
Проблема заключается в контроллере. Я хочу защитить поле survey_id в записи вопроса с помощью сильных параметров, но при этом вопросы не проходят проверку, так как survey_id является обязательным.
# app/controllers/surveys_controller.rb
class SurveysController
def edit
@survey = Survey.new
Survey.questions.build
end
def create
@survey = current_user.surveys.build(survey_params)
if @survey.save
redirect_to @survey
else
render :new
end
end
private
def survey_params
params.require(:survey).permit(:name, :questions_attributes => [:content])
end
end
Единственный способ решить эту проблему - собрать вопросы отдельно от опроса следующим образом:
def create
@survey = current_user.surveys.build(survey_params)
if @survey.save
if params[:survey][:questions_attributes]
params[:survey][:questions_attributes].each_value do |q|
question_params = ActionController::Parameters.new(q)
@survey.questions.build(question_params.permit(:content))
end
end
redirect_to @survey
else
render :new
end
end
private
def survey_params
params.require(:survey).permit(:name)
end
(Rails 4 beta 1, Ruby 2)
UPDATE
Возможно, лучший способ справиться с этой проблемой состоит в том, чтобы разделить "объект формы", как предлагается в этом сообщении в блоге Code Climate. Я оставляю вопрос открытым, хотя, поскольку мне любопытно в других точках зрения
Ответы
Ответ 1
Итак, проблема, с которой вы сталкиваетесь, заключается в том, что дочерние объекты не проходят проверку, правильно? Когда дочерние объекты создаются одновременно с родителем, дочерние объекты не могут знать идентификатор своего родителя, чтобы пройти проверку, это правда.
Вот как вы можете решить эту проблему. Измените свои модели следующим образом:
# app/models/survey.rb
class Survey
belongs_to :user
has_many :questions, :inverse_of => :survey
accepts_nested_attributes_for :questions
end
# app/models/question.rb
class Question
validates :survey, :presence => true
belongs_to :survey
end
Различия здесь в :inverse_of
, переданных ассоциации has_many
, и что вопрос теперь проверяется только на :survey
вместо :survey_id
.
:inverse_of
делает так, что когда дочерний объект создается или создается с использованием ассоциации, он также получает обратную ссылку на родителя, который его создал. Это похоже на то, что должно произойти автоматически, но, к сожалению, это не произойдет, если вы не укажете эту опцию.
Проверка :survey
вместо :survey_id
- это скорее компромисс. Валидация уже не просто проверяет наличие чего-то непустого в поле survey_id; теперь он фактически проверяет связь для существования родительского объекта. В этом случае это полезно из-за :inverse_of
, но в других случаях ему действительно нужно будет загрузить ассоциацию из базы данных, используя идентификатор, чтобы проверить. Это также означает, что идентификаторы, не соответствующие чему-либо в базе данных, не пройдут проверку.
Надеюсь, что это поможет.