Rails 4: ¿Cómo crear un formulario y utilizar accepts_nested_attributes_for con Has-Many-Through Relations? algunos consejos para entenderlo desde cero


rubyonrailsLa idea de escribir este post, surge de la idea de comenzar a hacer piezas cortas de código para practicar más y por otro lado ver que una y otra vez, se hacen formularios de todo tipo de relaciones entre modelos, unos son básicos en cuanto a que no existe complejidad alguna, por ejemplo una has_may/belongs_to y algunos otros se van complicando y viendo ejemplos existentes en Internet e intentar recopilar una buena información sobre cómo montar un formulario con form_for cuando tengo relaciones has_many through y necesito validar los campos que pertenecen a otro modelo utilizando accepts_nested_attributes_for demás de los propios del modelo principal, me he encontrado que no disponía de uno completo desde cero, así que me he decidido a desarrollar con un ejemplo práctico paso a paso y viendo todos los detalles que necesitas para entenderlo. Por lo que he montando un proyecto con un scaffolding y básico en este sentido, para que te lo puedas descargar de mi repositorio y probarlo por ti mismo. Hazlo y cuando lo tengas en marcha en local, sigue leyendo para terminar de entenderlo.

Antes de continuar una mención personal: me gustaría agradecer en este caso a Francis (twitter ciscou) por echarme un cable en primero, confirmar que lo estaba haciendo bien (viniendo de él es algo muy fiable) y segundo en darme cuenta finalmente que lo que estaba haciendo mal era la declaración de mi strong parameters (pensando en una declaración primitiva de los atributos de entities_attributes), haciendo que el comportamiento de la aplicación sea muy diferente y confuso a lo normalmente esperado, haciéndome creer la aplicación, que la interpretación del problema por mi parte lo tenía en en los accepts_nested_attributes_for y en las relaciones (no te preocupes que ahora te lo voy a explicar paso por paso y lo vas a entender cuando termines de leer el post o puedes ir directamente al paso cuarto, si ya sabes de qué estoy hablando). En definitiva, muchas gracias Francis!!

Vamos al caso concreto, como decía, voy a aclarar cómo montar un formulario con una relación Has-Many-Through y que además necesito validar campos de dos modelos de ejemplo, Notice y Entity, utilizando accepts_nested_attributes_for y validate y que en mi opinión personal para entenderlo, considero esenciales 5 pasos para entender todo lo que hay alrededor, para que luego puedas por ti mismo reproducirlo sin problemas. Las versiones utilizadas en este ejemplo son Rails 4.2.0 y Ruby 2.1.1. La base de datos en es caso me daba un poco igual, así que he utilizado la que Rails ofrece por defecto, sqlite. Me he centrado en las acciones de new y create principalmente.

Primer paso: Las migraciones

Vamos a preparar las migraciones, un punto de arranque fundamental a la hora de preparar las validaciones en nuestros modelos, si no le decimos que considere, en este caso “title”, que no debe quedar vacío este campo cuando se rellene en el formulario (no debe contener null), estaríamos metiendo un error mudo que luego cuando ejecutemos el código no nos vamos a dar cuenta hasta un buen rato después y perderemos tiempo en algo que es básico y el tiempo es muy valioso. Por tanto, nos creamos las tres migraciones, una por cada una de nuestras tablas a crear:

class CreateNotices < ActiveRecord::Migratione
   def change
      create_table :notices do |t|
         t.string :title, null: false …………. es el primer paso a considerar para nuestra validación en el modelo Notices

         t.timestamps
      end
   end
end

class CreateEntityRoles < ActiveRecord::Migration
      def change
         create_table :entity_roles do |t|
              t.belongs_to :entity, index: true
              t.belongs_to :notice, index: true

              NOTA: también lo podemos poner en vez de con un belongs_to con un references, “a gusto del consumidor”

             t.references :entity, index: true
             t.references :notice, index: true

             t.timestamps
       end
   end
end

class CreateEntities < ActiveRecord::Migration

      def change
            create_table :entities do |t|
                 t.string :name, null: false …………. es el primer paso a considerar para nuestra validación en el modelo Entity
                 t.string :address, null: false …………. es el primer paso a considerar para nuestra validación en el modelo Entity

                 t.timestamps
            end
       end
end

Segundo paso: las relaciones con nuestros modelos, los accepts_nested_attributes y los validates

Lo que tenemos que hacer ahora en nuestros modelos, es algo más sencillo de lo que en un primer momento parece, vamos a crear las relaciones entre los tres modelos creando las relaciones has_many y has_many through entre Notice y Entity y los belongs_to en el modelo nexo EntityRole.  Lo siguiente que haremos es creamos los validates en los modelos Notice y Entity para controlar los campos que vamos a incorporar en el formulario para que sean obligatorios rellenarlos y finalmente pondremos en marcha el  accepts_nested_attributes_for para que tenga conocimiento de los atributos del modelo Entity en el modelo Notice que finalmente pondremos en el formulario para crear la noticia. ¿Qué necesitamos entonces poner en nuestros modelos?:

class Notice < ActiveRecord::Base
      has_many :entity_roles, inverse_of: :notice
      has_many :entities, through: :entity_roles
      validates :title, presence: true Segundo paso para nuestra validación que está relacionado con la “migrate” que hicimos en el paso 1 t.string :title, null: false )

      accepts_nested_attributes_for :entities 

end

class EntityRole < ActiveRecord::Base
      belongs_to :entity, inverse_of: :entity_roles
      belongs_to :notice, inverse_of: :entity_roles

end

class Entity < ActiveRecord::Base
      has_many :entity_roles, inverse_of: :entity
      has_many :notices, through: :entity_roles

      validates :name, presence: true Segundo paso para nuestra validación que está relacionado con la “migrate” que hicimos en el paso 1 t.string :name, null: false )
      validates :address, presence: true Segundo paso para nuestra validación que está relacionado con la “migrate” que hicimos en el paso 1  t.string :address, null: false )

end

He utilizado inverse_of en todas las relaciones, pero para lo que estamos haciendo en este ejemplo no nos afecta, pero es importante responder a la pregunta ¿tengo claro qué es inverse_of y en qué me ayuda? si la respuesta sincera es un no o no lo tengo del todo claro, no te preocupes que te lo explico en un post que estoy preparando.

Tercer paso: En el controlador dos métodos cuando tratamos con un formulario de creación, el new y el create

En este tercer paso lo que haremos será preparar el controlador para las acciones concretas de new, cuando creemos una nueva noticia y llegue al formulario, este sepa que es lo que tiene que hacer, es decir, cuando actúe el link de crear una nueva noticia, lo iniciamos con @notice = Notice.new y preparamos la relación con Entity con @notice.entities.build ya que en ambos casos el new y build necesitan al final un save para crearse en la base de datos y lo haremos a la vuelta en el método create si todo ha ido bien:

# GET /notices/new
def new

   @notice = Notice.new
   @notice.entities.build

end

# POST /notices
# POST /notices.json
def create
   @notice = Notice.new(notice_params)

   respond_to do |format|
      if @notice.save
         format.html { redirect_to @notice, notice: ‘Notice was successfully created.’ }
         format.json { render :show, status: :created, location: @notice }
      else
         format.html { render :new }
         format.json { render json: @notice.errors, status: :unprocessable_entity }
      end
   end
end

Fíjate en que cuando hacemos el create únicamente le decimos @notice = Notice.new(notice_params) y en el método notice_params tenemos la lista de parámetros permitidos, pero en lo que quiero que te fijes, es que con Notice.new(notice_params),  que Rails sabe cómo tiene que actuar ( aunque se lo hemos especificado en el modelo claro ) cuando le pasamos primero los parámetros (le llegan en formato “notice”=>{“title”=>””, “entities_attributes”=>{“0″=>{“name”=>””, “address”=>””}}} ), más las asociaciones que tenemos en el modelo Notice de has_many :entity_roleshas_many :entities, through: :entity_roles y finalmente el accepts_nested_attributes_for :entities que nos aseguramos que no queden en blanco ninguno de los campos especificados, se encarga de hacer  Rails tal como decía anteriormente, las llamadas por debajo pertinentes para cuadrar los datos en ambos modelos Notice y Entity insertando los datos en notices y entities, al igual que en la relación Has-Many-Through de ambos entity_roles. Lo hacemos en consola y verificamos los inserts que hace:

2.1.1 :017 > @notice = Notice.new(title: “example”, entities_attributes: [{ name: “submitter”, address: “my address” }])
=> #<Notice id: nil, title: “example”, created_at: nil, updated_at: nil>
2.1.1 :018 > @notice.save!
(0.1ms) begin transaction
SQL (26.1ms) INSERT INTO “notices” (“title”, “created_at”, “updated_at”) VALUES (?, ?, ?) [[“title”, “example”], [“created_at”, “2015-01-02 22:31:17.510688”], [“updated_at”, “2015-01-02 22:31:17.510688”]]
SQL (0.2ms) INSERT INTO “entities” (“name”, “address”, “created_at”, “updated_at”) VALUES (?, ?, ?, ?) [[“name”, “submitter”], [“address”, “my address”], [“created_at”, “2015-01-02 22:31:17.557882”], [“updated_at”, “2015-01-02 22:31:17.557882”]]
SQL (0.1ms) INSERT INTO “entity_roles” (“notice_id”, “entity_id”, “created_at”, “updated_at”) VALUES (?, ?, ?, ?) [[“notice_id”, 4], [“entity_id”, 4], [“created_at”, “2015-01-02 22:31:17.559286”], [“updated_at”, “2015-01-02 22:31:17.559286”]]
(491.5ms) commit transaction
=> true

Cuarto paso: strong parameters

Es necesario tener muy claro qué es strong_parameters en esta parte y cómo nos llegan los parámetros para que no tengamos problemas de comportamiento que nos despisten.

def create
   @notice = Notice.new(notice_params)

…………….

def notice_params

   params.require(:notice).permit!

NOTA: el hacer .permit! me dio la pista del error que tenía cuando hice la declaración mal así: params.require(:notice).permit(:title, :entities_attributes)

         lo habitual es encontrarse una lista de parámetros definida y no un .permit! que es un todo permitido, entonces declaramos params correctamente como:

   params.require(:notice).permit(:title, entities_attributes: [:name, :address])
end

Entonces cuando ejecutamos la creación de la nueva noticia, nos llegan los parámetros de la siguiente forma:

Started POST “/notices” for 127.0.0.1 at 2015-01-02 19:43:15 +0100
Processing by NoticesController#create as HTML
Parameters: {“utf8″=>”✓”, “authenticity_token”=>”xxxxuzA==”, “notice”=>{“title”=>””, “entities_attributes”=>{“0″=>{“name”=>””, “address”=>””}}}, “commit”=>”Create Notice”}
(0.1ms) begin transaction
(0.1ms) rollback transaction
Rendered notices/_form.html.erb (4.5ms)
Rendered notices/new.html.erb within layouts/application (6.0ms)
Completed 200 OK in 81ms (Views: 74.2ms | ActiveRecord: 0.2ms)

Fíjate el cómo nos llegan los parámetros desde el formulario ya que “Parameters” es la clave para casar la lista blanca que debemos formar para que todo funcione bien, notice“=>{“title”=>””, “entities_attributes“=>{“0″=>{“name”=>””, “address”=>””}}} con params.require(:notice).permit(:title, entities_attributes: [:name, :address]). Ahora cuidado!!!, si se te ocurre poner lo siguiente, tendrás un comportamiento de la aplicación no deseado:

  • params.require(:notice).permit( :title, :entities_attributes )
  • params.require(:notice).permit( :title, entities_attributes: [] )
  • params.require(:notice).permit( :title, entities_attributes: )

el comportamiento como digo en estos casos, es de lo más diverso y puede confundirte en el resultado de qué es lo que te está pasando, prueba y verás!!

Quinto paso: la creación del form

En este último paso vamos a ver cómo tendríamos que tener construido nuestro formulario para que todo encaje bien:

<%= form_for(@notice) do |f| %>
   <% if @notice.errors.any? %>
      <div id=”error_explanation”>
        <h2><%= pluralize(@notice.errors.count, “error”) %> prohibited this notice from being saved:</h2>

        <ul>
            <% @notice.errors.full_messages.each do |message| %>
            <li><%= message %></li>
       </ul>
      </div>

   <% end %>
    </div>
<% end %>

<div class=”field”>
  <%= f.label :title %><br />
  <%= f.text_field :title %>
</div>

<%= f.fields_for(:entities) do |entity_form| %>

   <%= entity_form.text_field :name, label: ” Name” %>

   <%= entity_form.text_field :address, label: ” Address” %>
<% end %>

<div class=”actions”>

   <%= f.submit %>
</div>
<% end %>

Entonces veamos todos los pasos cuando creemos una nueva noticia. Desde el controlador notices_controller.rb en el método new, nos debe hacer llegar al formulario en form_for la variable de instancia @notice, con la creación de un nuevo objeto Notice @notice = Notice.new y su relación @notice.entities.build para que sepa el formulario que es lo que necesita entender cuando le digamos form_for(@notice) do |f|  y cuando llegue a f.fields_for(:entities) do |entity_form| , tengamos en el formulario los campos del título de la noticia, el nombre y dirección de la entidad para validar los campos del formulario en la declaraciones de nuestros modelos Notice y Entity. Valida en en modelo Notice los datos pasados del formulario, si pasa las validaciones puestas nos crea un nueva noticia y la relaciones que le hemos especificado y en caso contrario volvemos al formulario de creación con los errores de validación.

Pues estos con los 5 pasos esenciales que considero fundamentales y espero que sirva de ayuda!! y recuerda que puedes descargarte el código completo para que pruebes desde aquí y comentar lo que quieras al respecto.

2 comentarios en “Rails 4: ¿Cómo crear un formulario y utilizar accepts_nested_attributes_for con Has-Many-Through Relations? algunos consejos para entenderlo desde cero

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s