ruby-on-rails - accepts_nested_attributes_for - rails formularios anidados
Utilice el modelo anidado de rieles para*crear*objeto externo y simultáneamente*editar*objeto anidado existente? (3)
Intente agregar un campo oculto para la identificación del usuario en el formulario anidado:
<%=user_form.hidden_field :id%>
El guardado anidado usará esto para determinar si es una creación o una actualización para el usuario.
Usando Rails 2.3.8
El objetivo es crear un Blogger mientras se actualiza simultáneamente el modelo de Usuario anidado (en caso de que la información haya cambiado, etc.), O crear un nuevo usuario si aún no existe.
Modelo:
class Blogger < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
Controlador de Blogger:
def new
@blogger = Blogger.new
if user = self.get_user_from_session
@blogger.user = user
else
@blogger.build_user
end
# get_user_from_session returns existing user
# saved in session (if there is one)
end
def create
@blogger = Blogger.new(params[:blogger])
# ...
end
Formar:
<% form_for(@blogger) do |blogger_form| %>
<% blogger_form.fields_for :user do |user_form| %>
<%= user_form.label :first_name %>
<%= user_form.text_field :first_name %>
# ... other fields for user
<% end %>
# ... other fields for blogger
<% end %>
Funciona bien cuando estoy creando un nuevo usuario a través del modelo anidado, pero falla si el usuario anidado ya existe y tiene ID (en cuyo caso me gustaría simplemente actualizar ese usuario).
Error:
Couldn''t find User with ID=7 for Blogger with ID=
Esta pregunta SO trata de un problema similar, y solo la respuesta sugiere que Rails simplemente no funcionará de esa manera. La respuesta sugiere simplemente pasar el ID del elemento existente en lugar de mostrar el formulario, lo que funciona bien, excepto que me gustaría permitir modificaciones a los atributos del Usuario si los hay.
¿Rails profundamente anidados forma using belong_to no funciona?
Sugerencias? Esto no parece ser una situación particularmente poco común, y parece que debe haber una solución.
Estoy usando Rails 3.2.8 y me encuentro con el mismo problema.
Parece que lo que está intentando hacer (asignar / actualizar un registro guardado existente a una asociación belongs_to
( user
) de un nuevo modelo padre no guardado ( Blogger
) simplemente no es posible en Rails 3.2.8 (o Rails 2.3.8, para eso importa, aunque espero que haya actualizado a 3.x por ahora) ... no sin algunas soluciones.
Encontré 2 soluciones alternativas que parecen funcionar (en Rails 3.2.8). Para entender por qué funcionan, primero debe entender el código donde se produjo el error.
Comprender por qué ActiveRecord está provocando el error ...
En mi versión de activerecord (3.2.8), el código que maneja la asignación de atributos anidados para una asociación belongs_to
se puede encontrar en lib/active_record/nested_attributes.rb:332
y se ve así:
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
if (options[:update_only] || !attributes[''id''].blank?) && (record = send(association_name)) &&
(options[:update_only] || record.id.to_s == attributes[''id''].to_s)
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes)
elsif attributes[''id''].present? && !assignment_opts[:without_protection]
raise_nested_attributes_record_not_found(association_name, attributes[''id''])
elsif !reject_new_record?(association_name, attributes)
method = "build_#{association_name}"
if respond_to?(method)
send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
else
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
end
end
end
En la sentencia if
, si ve que ha pasado una identificación de usuario ( !attributes[''id''].blank?
), Intenta obtener el registro de user
existente de la asociación de user
del blogger ( record = send(association_name)
donde association_name es :user
).
Pero como se trata de un objeto Blogger
recientemente creado, blogger.user inicialmente será nil
, por lo que no llegará a la llamada assign_to_or_mark_for_destruction
en esa rama que maneja la actualización del record
existente. Esto es lo que debemos evitar (ver la siguiente sección).
Por lo tanto, se mueve a la rama 1st else if
, que nuevamente comprueba si hay una identificación de usuario presente ( attributes[''id''].present?
). Está presente, por lo que comprueba la siguiente condición, que es !assignment_opts[:without_protection]
.
Dado que está inicializando su nuevo objeto de Blogger con Blogger.new(params[:blogger])
(es decir, sin pasar as: :role
o without_protection: true
), usa los without_protection: true
assignment_opts
predeterminados de {}
. !{}[:without_protection]
es verdadero, por lo que procede a raise_nested_attributes_record_not_found
, que es el error que vio.
Finalmente, si no se tomó ninguna de las otras 2 ramas si, se comprueba si debe rechazar el nuevo registro y (si no) procede a construir un nuevo registro. Esta es la ruta que sigue en el caso "crea un nuevo usuario si aún no existe" en el caso que mencionaste.
Solución 1 (no recomendada): without_protection: true
La primera solución que pensé, pero no recomendaría, era asignar los atributos al objeto de Blogger usando without_protection: true
(Rails 3.2.8).
Blogger.new(params[:blogger], without_protection: true)
De esta manera se salta el primer elsif
y va al último elsif
, que crea un nuevo usuario con todos los atributos de los params, incluyendo :id
. En realidad, no sé si eso hará que actualice el registro de usuario existente como quisieras (probablemente, no, no he probado realmente esa opción), pero al menos evita el error ... :)
Solución 2 (recomendada): configure self.user
en user_attributes=
Pero la solución alternativa que recomendaría más que eso es inicializar / establecer la asociación de user
desde el parámetro: id para que se use la primera rama if
y se actualiza el registro existente en la memoria como desee ...
accepts_nested_attributes_for :user
def user_attributes=(attributes)
if attributes[''id''].present?
self.user = User.find(attributes[''id''])
end
super
end
Para poder sobrescribir el atributo de atributos anidados así y llamar a super
, necesitarás usar edge rails o incluir el parche de monos que publiqué en https://github.com/rails/rails/pull/ 2945 . Alternativamente, puede llamar a assign_nested_attributes_for_one_to_one_association(:user, attributes)
directamente desde su user_attributes=
setter en lugar de llamar a super
.
Si desea hacerlo siempre cree un nuevo registro de usuario y no actualice el usuario existente ...
En mi caso, terminé decidiendo que no quería que las personas pudieran actualizar los registros de usuario existentes de este formulario, así que terminé usando una pequeña variación de la solución anterior:
accepts_nested_attributes_for :user
def user_attributes=(attributes)
if user.nil? && attributes[''id''].present?
attributes.delete(''id'')
end
super
end
Este enfoque también evita que ocurra el error, pero lo hace de manera un poco diferente.
Si se pasa una identificación en los parámetros, en lugar de usarla para inicializar la asociación de user
, simplemente elimino la identificación aprobada para que vuelva a formar un new
usuario del resto de los parámetros de usuarios enviados.
Me encontré con el mismo error en los carriles 3.2. El error ocurrió al usar un formulario anidado para crear un nuevo objeto con una relación de pertenencia para un objeto existente. El enfoque de Tyler Rick no funcionó para mí. Lo que encontré para trabajar fue establecer la relación después de la inicialización del objeto y luego establecer los atributos de los objetos. Un ejemplo de esto es el siguiente ...
@report = Report.new()
@report.user = current_user
@report.attributes = params[:report]
suponiendo que params se parece a algo ... {: informe => {: nombre => "nombre",: atributos_usuarios => {: id => 1, {: atributos_componentes => {"1" => {: nombre => " nombre de la cosa "}}}}}}