Rails: объединение вложенного атрибута с помощью strong_params

В Rails 4 можно объединить дополнительные параметры с созданными пользователем типами:

 params.require(:post).permit([:title, :body]).merge(user: current_user)

Также возможно включить вложенные атрибуты так:

 params.require(:post).permit([:title, :body, sections_attributes: [:title, :section_type]])

Теперь, если бы я хотел объединить дополнительные параметры во вложенную модель. Я пробовал это:

params.require(:post).permit([:title, :body, sections_attributes: [:title, :section_type]]).merge(user: current_user, sections_attributes: [user: current_user])

Но когда я проверю параметры с отладчиком впоследствии, я обнаружил, что user перезаписал другой section_attributes, а не слияние с ними. Есть ли лучший способ подойти к этой проблеме?

Full backtrace
--------------

 - activemodel (4.0.0.rc1) lib/active_model/attribute_methods.rb:436:in `method_missing'
 - activerecord (4.0.0.rc1) lib/active_record/attribute_methods.rb:131:in `method_missing'
 - activerecord (4.0.0.rc1) lib/active_record/nested_attributes.rb:432:in `block in assign_nested_attributes_for_collection_association'
 - activerecord (4.0.0.rc1) lib/active_record/nested_attributes.rb:431:in `assign_nested_attributes_for_collection_association'
 - activerecord (4.0.0.rc1) lib/active_record/nested_attributes.rb:322:in `comments_attributes='
 - activerecord (4.0.0.rc1) lib/active_record/attribute_assignment.rb:42:in `_assign_attribute'
 - activerecord (4.0.0.rc1) lib/active_record/attribute_assignment.rb:53:in `block in assign_nested_parameter_attributes'
 - activerecord (4.0.0.rc1) lib/active_record/attribute_assignment.rb:53:in `assign_nested_parameter_attributes'
 - activerecord (4.0.0.rc1) lib/active_record/attribute_assignment.rb:33:in `assign_attributes'
 - activerecord (4.0.0.rc1) lib/active_record/core.rb:192:in `initialize'
 - activerecord (4.0.0.rc1) lib/active_record/inheritance.rb:27:in `new'
 - activerecord (4.0.0.rc1) lib/active_record/reflection.rb:189:in `build_association'
 - activerecord (4.0.0.rc1) lib/active_record/associations/association.rb:235:in `build_record'
 - activerecord (4.0.0.rc1) lib/active_record/associations/has_many_through_association.rb:102:in `build_record'
 - activerecord (4.0.0.rc1) lib/active_record/associations/collection_association.rb:114:in `build'
 - activerecord (4.0.0.rc1) lib/active_record/associations/collection_proxy.rb:229:in `build'
 - app/controllers/forum/topics_controller.rb:16:in `create'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/implicit_render.rb:4:in `send_action'
 - actionpack (4.0.0.rc1) lib/abstract_controller/base.rb:189:in `process_action'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/rendering.rb:10:in `process_action'
 - actionpack (4.0.0.rc1) lib/abstract_controller/callbacks.rb:18:in `block in process_action'
 - activesupport (4.0.0.rc1) lib/active_support/callbacks.rb:422:in `_run__642753351245287313__process_action__callbacks'
 - activesupport (4.0.0.rc1) lib/active_support/callbacks.rb:80:in `run_callbacks'
 - actionpack (4.0.0.rc1) lib/abstract_controller/callbacks.rb:17:in `process_action'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/rescue.rb:29:in `process_action'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/instrumentation.rb:31:in `block in process_action'
 - activesupport (4.0.0.rc1) lib/active_support/notifications.rb:159:in `block in instrument'
 - activesupport (4.0.0.rc1) lib/active_support/notifications/instrumenter.rb:20:in `instrument'
 - activesupport (4.0.0.rc1) lib/active_support/notifications.rb:159:in `instrument'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/instrumentation.rb:30:in `process_action'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/params_wrapper.rb:245:in `process_action'
 - activerecord (4.0.0.rc1) lib/active_record/railties/controller_runtime.rb:18:in `process_action'
 - actionpack (4.0.0.rc1) lib/abstract_controller/base.rb:136:in `process'
 - actionpack (4.0.0.rc1) lib/abstract_controller/rendering.rb:44:in `process'
 - actionpack (4.0.0.rc1) lib/action_controller/metal.rb:195:in `dispatch'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/rack_delegation.rb:13:in `dispatch'
 - actionpack (4.0.0.rc1) lib/action_controller/metal.rb:231:in `block in action'
 - actionpack (4.0.0.rc1) lib/action_dispatch/routing/route_set.rb:80:in `dispatch'
 - actionpack (4.0.0.rc1) lib/action_dispatch/routing/route_set.rb:48:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/journey/router.rb:71:in `block in call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/journey/router.rb:59:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/routing/route_set.rb:654:in `call'
 - request_store (1.0.5) lib/request_store/middleware.rb:9:in `call'
 - warden (1.2.1) lib/warden/manager.rb:35:in `block in call'
 - warden (1.2.1) lib/warden/manager.rb:34:in `call'
 - rack (1.5.2) lib/rack/etag.rb:23:in `call'
 - rack (1.5.2) lib/rack/conditionalget.rb:35:in `call'
 - rack (1.5.2) lib/rack/head.rb:11:in `call'
 -  () home/timothythehuman/.rvm/gems/[email protected]/bundler/gems/remotipart-2d6e0949acc2/lib/remotipart/middleware.rb:30:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/params_parser.rb:27:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/flash.rb:241:in `call'
 - rack (1.5.2) lib/rack/session/abstract/id.rb:225:in `context'
 - rack (1.5.2) lib/rack/session/abstract/id.rb:220:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/cookies.rb:486:in `call'
 - activerecord (4.0.0.rc1) lib/active_record/query_cache.rb:36:in `call'
 - activerecord (4.0.0.rc1) lib/active_record/connection_adapters/abstract/connection_pool.rb:626:in `call'
 - activerecord (4.0.0.rc1) lib/active_record/migration.rb:366:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/callbacks.rb:29:in `block in call'
 - activesupport (4.0.0.rc1) lib/active_support/callbacks.rb:392:in `_run__4051735323972233883__call__callbacks'
 - activesupport (4.0.0.rc1) lib/active_support/callbacks.rb:80:in `run_callbacks'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/reloader.rb:64:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/remote_ip.rb:76:in `call'
 - better_errors (0.9.0) lib/better_errors/middleware.rb:84:in `protected_app_call'
 - better_errors (0.9.0) lib/better_errors/middleware.rb:79:in `better_errors_call'
 - better_errors (0.9.0) lib/better_errors/middleware.rb:56:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/debug_exceptions.rb:17:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/show_exceptions.rb:30:in `call'
 - railties (4.0.0.rc1) lib/rails/rack/logger.rb:38:in `call_app'
 - railties (4.0.0.rc1) lib/rails/rack/logger.rb:21:in `block in call'
 - activesupport (4.0.0.rc1) lib/active_support/tagged_logging.rb:67:in `block in tagged'
 - activesupport (4.0.0.rc1) lib/active_support/tagged_logging.rb:25:in `tagged'
 - activesupport (4.0.0.rc1) lib/active_support/tagged_logging.rb:67:in `tagged'
 - railties (4.0.0.rc1) lib/rails/rack/logger.rb:21:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/request_id.rb:21:in `call'
 - rack (1.5.2) lib/rack/methodoverride.rb:21:in `call'
 - rack (1.5.2) lib/rack/runtime.rb:17:in `call'
 - activesupport (4.0.0.rc1) lib/active_support/cache/strategy/local_cache.rb:83:in `call'
 - rack (1.5.2) lib/rack/lock.rb:17:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/static.rb:64:in `call'
 - railties (4.0.0.rc1) lib/rails/engine.rb:511:in `call'
 - railties (4.0.0.rc1) lib/rails/application.rb:96:in `call'
 - rack (1.5.2) lib/rack/content_length.rb:14:in `call'
 - thin (1.5.1) lib/thin/connection.rb:81:in `block in pre_process'
 - thin (1.5.1) lib/thin/connection.rb:79:in `pre_process'
 - thin (1.5.1) lib/thin/connection.rb:54:in `process'
 - thin (1.5.1) lib/thin/connection.rb:39:in `receive_data'
 - eventmachine (1.0.3) lib/eventmachine.rb:187:in `run'
 - thin (1.5.1) lib/thin/backends/base.rb:63:in `start'
 - thin (1.5.1) lib/thin/server.rb:159:in `start'
 - rack (1.5.2) lib/rack/handler/thin.rb:16:in `run'
 - rack (1.5.2) lib/rack/server.rb:264:in `start'
 - railties (4.0.0.rc1) lib/rails/commands/server.rb:84:in `start'
 - railties (4.0.0.rc1) lib/rails/commands.rb:80:in `block in <top (required)>'
 - railties (4.0.0.rc1) lib/rails/commands.rb:75:in `<top (required)>'
 - bin/rails:4:in `<main>'

Параметры:

{"name"=>"stuff", "description"=>"", "topical_id"=>"1", "topical_type"=>"User", "comments_attributes"=>{"0"=>{"body"=>"1111111111111111111"}, "user"=>#<User id: 1, active: true, bio: nil, birthday: nil, image: nil, location: nil, real_name: nil, twitter_name: nil, username: "tbaron", website: nil, whuffie: #<BigDecimal:6365488,'0.0',9(18)>, slug: nil, created_at: "2013-05-31 00:42:28", updated_at: "2013-05-31 00:42:35", email: "[email protected]", encrypted_password: "$2a$10$jSrDsC9Ai.yFU5sttCxIiuRBthDUYiy9wWyZnie70qbp...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 1, current_sign_in_at: "2013-05-31 00:42:35", last_sign_in_at: "2013-05-31 00:42:35", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", confirmation_token: nil, confirmed_at: nil, confirmation_sent_at: nil, unconfirmed_email: nil, failed_attempts: 0, unlock_token: nil, locked_at: nil>}, "user"=>#<User id: 1, active: true, bio: nil, birthday: nil, image: nil, location: nil, real_name: nil, twitter_name: nil, username: "tbaron", website: nil, whuffie: #<BigDecimal:6365488,'0.0',9(18)>, slug: nil, created_at: "2013-05-31 00:42:28", updated_at: "2013-05-31 00:42:35", email: "[email protected]", encrypted_password: "$2a$10$jSrDsC9Ai.yFU5sttCxIiuRBthDUYiy9wWyZnie70qbp...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 1, current_sign_in_at: "2013-05-31 00:42:35", last_sign_in_at: "2013-05-31 00:42:35", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", confirmation_token: nil, confirmed_at: nil, confirmation_sent_at: nil, unconfirmed_email: nil, failed_attempts: 0, unlock_token: nil, locked_at: nil>}

Ответ 1

Метод merge, который вы вызываете, является обычным методом Ruby Hash#merge. Для каждого ключа в аргументе merge значение перезаписывает значение, которое в настоящее время присутствует для этого ключа, если таковое имеется. В этом случае вы переписываете значение sections_attributes.

Так как sections_attributes является "псевдо-массивом" формы {"0" => first_hash, "1" => second_hash} и т.д., вам нужен способ дублировать ваш user один раз за запись. Вы можете сделать это, используя способность слияния взять блок, который изменяет объединенное значение. Здесь один подход:

filtered_params = params.require(:post)
                        .permit([:title, 
                                 :body, 
                                 sections_attributes: [:title, :section_type]])
additional_params = {user: current_user, sections_attributes: [user: current_user]}
result = filtered_params.merge(additional_params) do |key, oldval, newval|
  if newval.is_a? Array
    # Arrays are expected to be one-element arrays containing a Hash that is
    #   supposed to be merged into each element of the currently-existing
    #   "pseudo-array"
    oldval ||= {}
    Hash[oldval.map {|k, v| [k, v.merge(newval.first)]}]
  elsif newval.is_a? Hash
    # Hashes are merged into existing hashes
    oldval ||= {}
    oldval.merge newval
  else
    # Other types are passed as-is (and replace any existing value)
    newval
  end
end

# This marks the newly added parameters as permitted.  It only necessary because we
# made new Hashes when we modified the "pseudo-array"
result.permit!

Если вы обнаружите, что это работает для вас, вы можете инкапсулировать это в методе, либо взяв внутреннюю часть блока, либо сделав его отдельным методом, либо установив все это как именованный метод на Parameters класс.

Обратите также внимание на то, что вышеописанное обрабатывает только один уровень рекурсии. Пойти глубже, чем это немного сложно. Если вам это нужно, я с удовольствием напишу объяснение.