Um formulário, multiplos modelos: a praticidade dos MultiModel Forms
Autor: fabiolnm | Publicado: 2012-08-01T12:15:00-03:00

MultiModel forms são um poderoso recurso oferecido pelo Rails desde a versão 2.3 - porém desconhecido de muitos programadores. Com eles, é possível editar complexas hierarquias de objetos numa única view. É uma situação que nos deparamos com bastante freqüência:
Editar Carrinho de Compras e Itens do Carrinho
Dados de uma Pessoa e Informações de Contato
Dados de um Usuário e Permissões.
Post num blog e suas Tags.
e por aí vai. Este post é o primeiro de uma série de quatro artigos, para ilustrar a utilização destes recursos:
Parte 1: Nested Models - One-to-One
Parte 2: Nested Models - One-to-Many
Parte 3: Nested Forms
Parte 4: Nested Forms dinâmicos usando Knockout.js
O código desta aplicação de exemplo está disponível neste repositório Github.
Nested Models - One-to-OneSegue o modelo utilizado neste tutorial:
Person has one UserAccount
UserAccount has many Permissions (parte 2)
Em nome da simplicidade, Person será criado via scaffolding:

rails g scaffold Person name:string

Neste ponto, pode-se rodar as migrations e iniciar o servidor de desenvolvimento:

rake db:migrate
rails s

E já estará disponível a tela - bastante rudimentar - para a edição de Person:
http://localhost:3000/people
Como queremos editar UserAccount a partir do model Person, dispensaremos scaffold, sendo necessário criar o respectivo modelo:

rails g model UserAccount person_id:integer username:string password:string

rake db:migrate


Por fim, resta configurar as validações e relacionamentos nos respectivos models.

Nested One-to-OneA relação entre os modelos Person e UserAccount é de um-para-um, sendo que UserAccount possui a chave estrangeira person_id (lado belongs_to):

class Person &lt; ActiveRecord::Base
&nbsp; attr_accessible :name

&nbsp; validates_presence_of :name
&nbsp; has_one :user_account
end


class UserAccount &lt; ActiveRecord::Base
&nbsp; attr_accessible :password, :username

&nbsp; validates_presence_of :username, :password
&nbsp; belongs_to :person
end

Podemos usar o rails console para um teste inicial:

$ rails c
&gt; p = Person.create name: "José da Silva"

(0.1ms)  begin transaction
SQL (16.4ms)  INSERT INTO "people" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 31 Jul 2012 05:57:41 UTC +00:00], ["name", "José da Silva"], ["updated_at", Tue, 31 Jul 2012 05:57:41 UTC +00:00]]
(59.6ms)  commit transaction


No exemplo acima, foi criado um objeto Person, passando para o método create um hash contendo apenas o atributo name.

O uso de nested models possibilita que a chamada create receba também os atributos username e password, para criar uma UserAccount simultaneamente ao objeto Person.

class Person &lt; ActiveRecord::Base
&nbsp; attr_accessible :name, :user_account_attributes
&nbsp; validates_presence_of :name

&nbsp; has_one :user_account
&nbsp; accepts_nested_attributes_for :user_account
end 


Note que é necessário tornar esses atributos acessíveis via mass-assignment, na lista attr_accessible.

&gt; p = Person.create name: "José da Silva", user_account_attributes: { username: "jsilva", password: "abc123" }

(0.1ms)  begin transaction
(0.1ms)  commit transaction
(0.0ms)  begin transaction
SQL (6.1ms)  INSERT INTO "people" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 31 Jul 2012 06:23:26 UTC +00:00], ["name", "José da Silva"], ["updated_at", Tue, 31 Jul 2012 06:23:26 UTC +00:00]]
SQL (0.4ms)  INSERT INTO "user_accounts" ("created_at", "password", "person_id", "updated_at", "username") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 31 Jul 2012 06:23:26 UTC +00:00], ["password", "abc123"], ["person_id", 5], ["updated_at", Tue, 31 Jul 2012 06:23:26 UTC +00:00], ["username", "jsilva"]]
(56.6ms)  commit transaction


ValidaçãoO leitor mais atento pode ter notado que UserAccount não está validando a presença de Person. Isto deixa uma brecha no código, por permitir a criação de uma UserAccount desassociada de uma pessoa:

&gt; UserAccount.create username: "joao", password: "abc123"

(0.2ms)  begin transaction
SQL (1.4ms)  INSERT INTO "user_accounts" ("created_at", "password", "person_id", "updated_at", "username") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 31 Jul 2012 06:25:34 UTC +00:00], ["password", "abc123"], ["person_id", nil], ["updated_at", Tue, 31 Jul 2012 06:25:34 UTC +00:00], ["username", "joao"]]
(33.3ms)  commit transaction


Porém isto não faz sentido no nosso modelo. Façamos a correção:

class UserAccount &lt; ActiveRecord::Base
&nbsp; attr_accessible :password, :username

&nbsp; validates_presence_of :person, :username, :password
&nbsp; belongs_to :person
end


Agora UserAccount passa se comportar conforme esperado:

&gt; u = UserAccount.create username: "a", password: "b"

(0.2ms)  begin transaction
(0.1ms)  rollback transaction

&gt; u.errors.messages
=&gt; {:person=&gt;["can't be blank"]} 


Porém, isto tem um efeito colateral: ela afeta a criação da user_account via nested attributes de Person:

&gt; p = Person.create name: "José da Silva", user_account_attributes: { username: "jsilva", password: "abc123" }
(0.1ms)  begin transaction
(0.1ms)  commit transaction
(0.1ms)  begin transaction
(0.1ms)  rollback transaction
&gt; p.errors.messages 
=&gt; {:"user_account.person"=&gt;["can't be blank"]} 


Para evitar que ocorra a validação quando são usados nested attributes, deve-se informar para Person não validar o atributo person da associação user_account:

class Person &lt; ActiveRecord::Base
&nbsp; attr_accessible :name, :user_account_attributes
&nbsp; validates_presence_of :name

&nbsp; has_one :user_account, inverse_of: :person
&nbsp; accepts_nested_attributes_for :user_account
end

UpdatePara uma operação de atualização, é necessário passar em user_account_attributes o id da user_account previamente criada:

&gt; p.update_attributes user_account_attributes: { id:6, username: "jsilva11", password: "123456" }
(0.1ms)  begin transaction
(0.6ms)  UPDATE "user_accounts" SET "username" = 'jsilva11', "updated_at" = '2012-07-31 07:43:20.649645' WHERE "user_accounts"."id" = 6
(49.9ms)  commit transaction

CUIDADO: caso o id fosse omitido, seria feito um novo insert na tabela user_accounts, e o objeto Person seria reassociado à nova user_account.

DestroyPara completar este tutorial, suponhamos que faça sentido remover uma user_account, sem remover os respectivos dados de person. Isto pode ser obtido marcando o nested_model com allow_destroy:

class Person &lt; ActiveRecord::Base
&nbsp; attr_accessible :name, :user_account_attributes

&nbsp; validates_presence_of :name
&nbsp; has_one :user_account, inverse_of: :person

&nbsp; accepts_nested_attributes_for :user_account, allow_destroy: true
end


Agora, pode-se remover user_account passando _destroy: true no hash user_account_attributes:

&gt; p.update_attributes user_account_attributes: { id:6, _destroy: true }
(0.2ms) begin transaction
UserAccount Load (0.1ms) SELECT "user_accounts".* FROM "user_accounts" WHERE "user_accounts"."person_id" = 7 LIMIT 1
SQL (4.9ms) DELETE FROM "user_accounts" WHERE "user_accounts"."id" = ? [["id", 6]]
(47.8ms) commit transaction

&gt; p.reload.user_account
Person Load (0.3ms) SELECT "people".* FROM "people" WHERE "people"."id" = ? LIMIT 1 [["id", 7]]
UserAccount Load (1.9ms) SELECT "user_accounts".* FROM "user_accounts" WHERE "user_accounts"."person_id" = 7 LIMIT 1
 =&gt; nil 

Referênciashttp://guides.rubyonrails.org/getting_started.html#building-a-multi-model-form
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
http://erikonrails.snowedin.net/?p=267
http://archives.ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes
http://weblog.rubyonrails.org/2009/1/26/nested-model-forms/