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/ruby-2.0.0-p0@whistlr/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>}

person nullnullnull    schedule 22.05.2013    source источник


Ответы (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's only necessary because we
# made new Hashes when we modified the "pseudo-array"
result.permit!

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

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

person charleyc    schedule 29.05.2013
comment
Кажется, это шаг в правильном направлении. Однако, применяя deep_merge к параметрам, я получаю эту ошибку undefined method with_in Different_access' для #‹User:0x00000006a18398›`. - person nullnullnull; 31.05.2013
comment
Спасибо, что изучили это. Я добавил обратную трассировку выше. - person nullnullnull; 31.05.2013
comment
Это обратная трассировка неопределенного метода with_in Different_access? Похоже, это проблема с назначением атрибутов. Как выглядит результат вашего deep_merge в тот момент, когда вы получаете эту ошибку? - person charleyc; 31.05.2013
comment
Да, это обратная трассировка ошибки индифферентного_доступа. Я разместил параметры выше. Они содержат все необходимые атрибуты, хотя объединенные параметры не заключаются в кавычки, в отличие от их соседей. - person nullnullnull; 31.05.2013
comment
Я смог заставить его работать, сделав это: deep_merge(user: current_user, comments_attributes: { "0" => {user: current_user}}). Обратите внимание на "0" =>. К сожалению, это работает только в том случае, если отправлен один комментарий. Есть ли способ увеличить этот 0 из инструкции param? - person nullnullnull; 31.05.2013
comment
Понимаю. Теперь я понимаю проблему. Мой 2. выше неверен, потому что ваши comments_attributes представляют собой массив атрибутов для каждого комментария, чего я не понял. Вы сталкиваетесь с той же проблемой, что и в этом сообщении. - person charleyc; 31.05.2013
comment
Я полностью переписал свой ответ на основе ваших отзывов. Я неправильно понял структуру ваших начальных параметров. Надеюсь, эта версия работает для вас! - person charleyc; 31.05.2013
comment
давайте продолжим это обсуждение в чате - person charleyc; 31.05.2013
comment
Это было намного больше, чем я мог надеяться. Спасибо, Чарли! - person nullnullnull; 31.05.2013