Durante muito tempo a funcionalidade mais requisitada ao core team do Rails era a simplificação do gerenciamento de múltiplos modelos em apenas um formulário. Eu mesmo cheguei a comentar sobre uma nova opção chamada :accessible que facilitaria atribuições em massa em objetos ActiveRecord (aqui e aqui).
Infelizmente este recurso foi incluído ao Rails cedo demais, já que ele só dava suporte a nested models (é como chamamos os modelos que estão “acoplados” a um outro modelo, como quando usamos belongs_to ou has_many) durante a criação dos objetos e por isto ele foi removido afim de ser aprimorado.
No Rails 2.3 esta funcionalidade volta a existir, mas de uma maneira diferente. A primeira coisa que devemos fazer é informar ao modelo que ele se beneficiará deste recurso incluindo uma chamada ao método accept_nested_attributes_for, como no exemplo:
class Project < ActiveRecord::Base
has_many :tasks
accept_nested_attributes_for :tasks, :allow_destroy => true
end
Como visto acima estou “ligando” a atribuição em massa para o modelo Task via o modelo Project. Isto também vale para qualquer tipo de relacionamento, como belongs_to, has_one, has_many e has_and_belongs_to_many.
Uma vez feito isto, agora eu posso criar, editar e apagar tarefas (tasks) através do objeto Project:
# Adicionando uma nova tarefa ao projeto
@project.task_attributes = { 'new_1' => { :name => 'Task 1' } }
@project.task #=> [ <#Task: name: 'Task 1'> ]
@project.task.clear
# Adicionando duas tarefas ao projeto
@project.task_attributes =
{ 'new_1' => { :name => 'Task 1' }, 'new_2' => { :name => 'Task 2' } }
@project.save
@project.task #=> [ <#Task: name: 'Task 1'>, <#Task: name: 'Task 2'> ]
# Alterando a primeira tarefa (assumindo o id == 1)
@project.task_attributes = { '1' => { :name => 'My Task' } }
@project.save
# Alterando a segunda tarefa (id == 2) e incluindo uma nova
@project.task_attributes = {
'2' => { :name => 'My Second Task' },
'new_1' => { :name => 'Task 3' } }
@project.save
# Apaga o último registro (id == 3)
@project.task_attributes = { '3' => { '_delete' => '1' } }
@project.save
Talvez neste momento você esteja se questionando sobre estes formatos estranhos, como ao apagar um registro. Sim, estes hashs são meio confusos mesmo, mas eles não foram criados para serem usados desta maneira. O uso prático deste novo recurso está na criação de formulários:
<% form_for @project do |project_form| %>
<div>
<%= project_form.label :name, 'Project name:' %>
<%= project_form.text_field :name %>
</div>
<!-- PRESTE ATENÇÃO AQUI -->
<% project_form.fields_for :tasks do |task_form| %>
<p>
<div>
<%= task_form.label :name, 'Task:' %>
<%= task_form.text_field :name %>
</div>
<% unless task_form.object.new_record? %>
<div>
<%= task_form.label :_delete, 'Remove:' %>
<%= task_form.check_box :_delete %>
</div>
<% end %>
</p>
<% end %>
<% end %>
<%= project_form.submit %>
<% end %>
Ao definir project_form.fields_for :tasks estamos dizendo que aquele trecho do formulário deve usar o recurso de atribuição em massa para criar, editar ou apagar uma tarefa (task) já existente.
Caso uma das validações da classe Task não passe (imagine que esta tenha um validates_presence_of :name e que eu deixei o campo name em branco), a mensagem correspondente a esta validação será copiada para a classe pai, no caso para a classe Project e estará acessível através do método error_messages_for dela.
Este novo sistema também conta com o recurso de transações. Isto significa que ao realizar uma série de operações de uma só vez, se uma der errado, nenhuma delas será efetivada. Lembre-se apenas que como toda transação no Rails, embora no banco de dados nada aconteça, na instancia do seu objeto ele ainda continuará com as alterações marcadas.
O mais importante em tudo isto é que o código em seus controllers continuarão exatamente da mesma forma como já é hoje. Nenhuma alteração é necessária. Seguindo os exemplos acima, veja como ficaria meu controller:
class ProjectController < ApplicationController
def create
@project = Project.new(params[:project])
if @project.save
redirect_to(project_path(@project))
else
render(:action => :new)
end
end
def update
@project = Project.find(params[:id])
@project.update_attributes(params[:project]) ?
redirect_to(project_path(@project)) : render(:action => :edit)
end
end
Nada mudou correto?
Eloy Duran, o programador responsável por este novo recurso, tem um projeto no GitHub mostrando mais detalhes sobre o seu funcionamento. Recomendo que você dê uma olhada neste formulário em especial, onde ele mostra como incluir múltiplas tarefas no mesmo projeto.
esse é um grande feature!
Caramba, vai ser muito bom isso! Já to até imaginando aonde usar. ;D
Carlos,
Não seria ‘accepts_nested_attributes_for’? Encontrei das formas por aí, mas só no plural que funciona no Rails 2.3.2.
E também o atributo está no plural:
p = Project.create(:name => 'teste', :task_attributes => { 'new_1' => {:name => 'testar' } })
# ActiveRecord::UnknownAttributeError: unknown attribute: task_attributes
# (..)
>> p = Project.create(:name => 'teste', :tasks_attributes => { 'new_1' => {:name => 'testar' } })
=> #
Creio que descobri mais uma coisa: ‘task_attributes’ é para quando é has_one e ‘tasks_attributes’ para quando é has_many.
Realmente, o método foi alterado para accepts_nested_attributes_for.
Estou lutando com isso a um tempo, queria saber porq quando coloco esse trecho de codigo do form so aparece o form do nome do proejeto e nao o de tasks?
Eh normal? to perdido rs
Obrigado
Carlos,
Como você escreveria um teste de controller com nested_attributes ?
Ex.
A criação de um Project com tarefas no momento da criação.
Quais são as expectativas? não estou conseguindo imaginar nenhuma alem dessas:
no rspec:
it “deve ser possivel criar um projeto com tarefas” do
@project = mock_model(Project)
Project.stub!(:new).and_return(@project)
Project.should_receive(:new).with(“name” => ‘teste’, “task_attributes” => [{"name" => 'testar' }]).and_return(@project)
#
# talvez tenha mais alguma espectativa aqui..
#
post :create, :name => ‘teste’, :task_attributes => [ { :name => 'testar' }]
end