Le “nested Resource” e
le relative “Nested form” sono dei costrutti che permettono di aggiornare
facilmente, con un unico submit da parte del browser, un modello e degli altri
modelli ad esso collegati:
--- NB: L’obiettivo di
questo articolo è spiegare la logica di ciò che c’è dietro le nested re source,
ovvero il meccanismo in base a cui RoR permette di gestire le risorse ---
Le caratteristiche del
framework Rails di "Convention over Configuration" rendono le
applicazioni RoR "magiche" agli occhi di chi non lo conosce.
In realtà non c'è
nulla di magico in tutto questo, ma si tratta di rispettare alcune convenzioni.
Convenzioni che evolvono, spesso alla ricerca di maggiore protezione e
sicurezza (vedi la direttiva attr_accessible) o rispettando il concetto di base
che la maggioranza della community e degli sviluppatori fanno così.
Una delle
"magie" che mi interessa discutere qui è quella delle nested
resource, ma per completezza dobbiamo fare prima qualche passo indietro.
La creazione o
l'aggiornamento di un modello fa largo uso degli hash. Ad esempio posso
scrivere (ipotizzando che esista il team con id 1)
giocatore
= Player.new(:firstname=>"Nome", :surname => "Cognome", :team_id=>"1")
oppure
parametri={:firstname=>"Nome",
:surname => "Cognome", :team_id=>"1"}
giocatore=
Player.new(parametri)
l’ActionController
implementa questo concetto quando estrae dall’Hash params i dati con cui
costruire le istanze dei modelli. Rispettando le convezioni RoR nei controller
generati con lo scaffold ritroviamo
@player
= Player.new(params[:player]) # nel create
@player.update_attributes(params[:player])
# nell'update
Il form HTML infatti è
stato generato utilizzando il “model_name” come prefisso, e i dati saranno
inviati nel formato:
player[firstname]=Nome;player[lastname]=Cognome;player[team_id]=1;commit=Salva.....
generando un hash di
questo tipo
Parameters: {"utf8"=>"✓", "authenticity_token"=>".....",
"player"=>{"lastname"=>"Cognome",
"firstname"=>"Nome", "team_id"=>"1"}, "commit"=>"Update
Player", "id"=>"1"}
Diventa chiaro come params[:player] rappresenti esattamente la variabile
parametri che avevamo instanziato prima.
Arriviamo alle nested
resource. Ipotizziamo 2 modelli, Team e Player in cui abbiamo le seguenti
definizioni:
class Team <
ActiveRecord::Base
attr_accessible
:name
has_many :players
end
class Player < ActiveRecord::Base
attr_accessible :firstname, :lastname,
:team_id
belongs_to :team
end
Immaginando che ci sia
la seguente struttura
Team
id
|
name
|
1
|
team_1
|
2
|
team_2
|
Player
id
|
firstname
|
lastname
|
team_id
|
1
|
cognome1
|
1
|
|
2
|
nome2
|
cognome2
|
2
|
3
|
nome3
|
cognome3
|
2
|
4
|
nome4
|
cognome4
|
1
|
ActiveRecord genera, a
seguito delle definizioni “has_many” e “belongs_to” dei metodi dinamici che
rappresentano l’oggetto Team o le istanze di Player collegate
In particolare abbiamo
a disposizione 2 metodi di instanza: “players” e “players=”. Vediamone l’uso
>> t=Team.first
Team Load (1.0ms) SELECT "teams".*
FROM "teams" LIMIT 1
#<Team id: 1, name: "team1",
created_at: "2013-02-11 11:55:01", updated_at: "2013-02-11
11:55:01">
>> t.players
[#<Player id: 1, lastname:
"cognome1", firstname: "nome1", team_id: 1, created_at:
"2013-02-11 11:55:46", updated_at: "2013-02-11
11:55:46">, #<Player id: 2, lastname: "cognome2",
firstname: "nome2", team_id: 1, created_at: "2013-02-11
11:55:57", updated_at: "2013-02-11 11:55:57">]
Player Load (1.0ms) SELECT "players".* FROM "players" WHERE "players"."team_id" = 1
Player Load (1.0ms) SELECT "players".* FROM "players" WHERE "players"."team_id" = 1
>> t.players=[]
SQL (1.0ms)
UPDATE "players" SET "team_id" = NULL WHERE "players"."team_id"
= 1 AND "players"."id" IN (1, 2)
[]
Il metodo “players”
risulta abbastanza naturale, ovvero restituisce un array con tutte le istanze
di Player collegate al Team che stiamo esaminando. In base alla stessa
convenzione, “players"=” setta l’array, e nel caso in cui gli si passi un
array vuoto automaticamente elimina tutte le istanze precedentemente associate.
Pertanto se avessimo
predisposto un array di player e lo avessimo passato al metodo palyers= avremmo
ottenuto questo:
>> pl_array=[Player.new(:firstname=>"nome1",:lastname=>"cognome1"),
Player.new(:firstname=>"nome2",
:lastname=>"cognome2")]
[#<Player
id: nil, lastname: "cognome1", firstname: "nome1", team_id:
nil, created_at: nil, updated_at: nil>, #<Player id: nil, lastname:
"cognome2", firstname: "nome2", team_id: nil, created_at:
nil, updated_at: nil>]
>>
t.players=pl_array
(0.0ms) begin transaction
SQL (1.0ms)
UPDATE "players" SET "team_id" = NULL WHERE
"players"."team_id" = 1 AND
"players"."id" IN (4, 5)
SQL (0.0ms)
INSERT INTO "players" ("created_at",
"firstname", "lastname", "team_id",
"updated_at") VALUES (?, ?, ?, ?, ?)
[["created_at", Tue, 12 Feb 2013 08:06:48 UTC +00:00],
["firstname", "nome1"], ["lastname",
"cognome1"], ["team_id", 1], ["updated_at", Tue,
12 Feb 2013 08:06:48 UTC +00:00]]
SQL (0.0ms)
INSERT INTO "players" ("created_at",
"firstname", "lastname", "team_id",
"updated_at") VALUES (?, ?, ?, ?, ?)
[["created_at", Tue, 12 Feb 2013 08:06:48 UTC +00:00],
["firstname", "nome2"], ["lastname",
"cognome2"], ["team_id", 1], ["updated_at", Tue,
12 Feb 2013 08:06:48 UTC +00:00]]
(84.0ms)
commit transaction
[#<Player
id: 6, lastname: "cognome1", firstname: "nome1", team_id:
1, created_at: "2013-02-12 08:06:48", updated_at: "2013-02-12
08:06:48">, #<Player id: 7, lastname: "cognome2",
firstname: "nome2", team_id: 1, created_at: "2013-02-12
08:06:48", updated_at: "2013-02-12 08:06:48">]
Notare come sia stata prima annullata la presenza di
eventuali record e poi inseriti in massa tutti i valori. Inoltre andando ad
analizzare il contenuto di pl_array in cui registrato due nuove instanze di
player notiamo che le istanze sono correttaemente associate a record del db:
>> pl_array
[#<Player
id: 8, lastname: "cognome1", firstname: "nome1", team_id:
1, created_at: "2013-02-12 08:46:39", updated_at: "2013-02-12
08:46:39">, #<Player id: 9, lastname: "cognome2",
firstname: "nome2", team_id: 1, created_at: "2013-02-12
08:46:39", updated_at: "2013-02-12 08:46:39">]
Ripetendo l’operazione t.players=pl_array non otteniamo
nessun risultato in quanto le istanze sono già associate con il team.
Pertanto se nel controller implementassimo la creazione di
un array avremmo ottenuto di salvare modello principale (team) e modelli
collegati (players) con un unico form
La direttiva accepts_nested_attributes_for
semplifica questo passaggio, generando un nuovo metodo, MODELNAME_attributes=
, che nasconde tutti i meccanismi di definizione dell’array di risorse:
class Team < ActiveRecord::Base
attr_accessible :name, :players_attributes
has_many :players
accepts_nested_attributes_for :players
accepts_nested_attributes_for :players
end
non abbiamo più la necessità di generare noi le istanze dei
modelli collegati, ma semplicemente di passare un hash o un array di hash
contenenti gli attributi da aggiornare:
>> pl_attributes = [{ :firstname =>
'nome1', :lastname=>'cognome1'}, { :firstname => 'nome2',
:lastname=>'cognome2'},{ :firstname => 'nome3',
:lastname=>'cognome3'},{ :firstname => 'nome4',
:lastname=>'cognome4'}]
[{:firstname=>"nome1",
:lastname=>"cognome1"}, {:firstname=>"nome2",
:lastname=>"cognome2"}, {:firstname=>"nome3",
:lastname=>"cognome3"}, {:firstname=>"nome4",
:lastname=>"cognome4"}]
>>
t.players
[#<Player
id: 8, lastname: "cognome1", firstname:
"nome1", team_id: 1, created_at: "2013-02-12 08:46:39",
updated_at: "2013-02-12 08:46:39">, #<Player id: 9, lastname:
"cognome2", firstname: "nome2", team_id: 1, created_at:
"2013-02-12 08:46:39", updated_at: "2013-02-12
08:46:39">]
>>
t.players_attributes=pl_attributes
[{:firstname=>"nome1",
:lastname=>"cognome1"}, {:firstname=>"nome2",
:lastname=>"cognome2"}, {:firstname=>"nome3",
:lastname=>"cognome3"}, {:firstname=>"nome4",
:lastname=>"cognome4"}]
>>
t.save
(0.0ms)
begin transaction
SQL (3.0ms)
INSERT INTO "players" ("created_at",
"firstname", "lastname", "team_id",
"updated_at") VALUES (?, ?, ?, ?, ?)
[["created_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00],
["firstname", "nome1"], ["lastname",
"cognome1"], ["team_id", 1], ["updated_at", Tue,
12 Feb 2013 09:14:35 UTC +00:00]]
SQL (1.0ms)
INSERT INTO "players" ("created_at",
"firstname", "lastname", "team_id",
"updated_at") VALUES (?, ?, ?, ?, ?)
[["created_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00], ["firstname",
"nome2"], ["lastname", "cognome2"],
["team_id", 1], ["updated_at", Tue, 12 Feb 2013 09:14:35 UTC
+00:00]]
SQL (0.0ms)
INSERT INTO "players" ("created_at",
"firstname", "lastname", "team_id",
"updated_at") VALUES (?, ?, ?, ?, ?)
[["created_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00],
["firstname", "nome3"], ["lastname",
"cognome3"], ["team_id", 1], ["updated_at", Tue,
12 Feb 2013 09:14:35 UTC +00:00]]
SQL (1.0ms)
INSERT INTO "players" ("created_at",
"firstname", "lastname", "team_id",
"updated_at") VALUES (?, ?, ?, ?, ?)
[["created_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00],
["firstname", "nome4"], ["lastname",
"cognome4"], ["team_id", 1], ["updated_at", Tue,
12 Feb 2013 09:14:35 UTC +00:00]]
(94.0ms)
commit transaction
>>
t.players
[#<Player
id: 8, lastname:
"cognome1", firstname: "nome1", team_id: 1, created_at:
"2013-02-12 08:46:39", updated_at: "2013-02-12
08:46:39">, #<Player id: 9, lastname: "cognome2", firstname:
"nome2", team_id: 1, created_at: "2013-02-12 08:46:39",
updated_at: "2013-02-12 08:46:39">, #<Player id: 10, lastname:
"cognome1", firstname: "nome1", team_id: 1, created_at:
"2013-02-12 09:14:35", updated_at: "2013-02-12
09:14:35">, #<Player id: 11, lastname: "cognome2", firstname:
"nome2", team_id: 1, created_at: "2013-02-12 09:14:35",
updated_at: "2013-02-12 09:14:35">, #<Player id: 12, lastname:
"cognome3", firstname: "nome3", team_id: 1, created_at:
"2013-02-12 09:14:35", updated_at: "2013-02-12
09:14:35">, #<Player id: 13, lastname: "cognome4", firstname:
"nome4", team_id: 1, created_at: "2013-02-12 09:14:35",
updated_at: "2013-02-12 09:14:35">]
Possiamo notare le seguenti cose
-
Non è stato necessario creare i modelli degli
oggetti, ma abbiamo dovuto definire solo un hash
-
L’assegnazione non ha scatenato, come prima, il
salvataggio, ma è stato necessario salvare manualmente il modello principale
-
I dati passati al metodo non hanno sovrascritto
quelli esistenti ma sono andati in aggiunta
La direttiva accepts_nested_attributes_for accetta anche delle
ulteriori opzioni, che permettono di definire meglio il suo comportamento:
-
:allow_destroy – se impostato a true permette di
utilizzare un attributo “_destroy”, valorizzato con true, per eliminare i
record
-
:reject_if – permette di definire le condizioni
in base alle quali scartare la generazione di un record
-
:limit – permette di definire un limite al
numero di record processati
Analizzando meglio il modo in cui deve essere definito
l’hash, e ricordando come lavora il metodo params, per ottenere una definizione @team.players_attributes=… .. dobbiamo generare un passaggio di parametri
come il seguente
team[players_attributes][2][firstname]=Nome2;
team[players_attributes][2][lastname]=Cognome2;
team[players_attributes][3][firstname]=Nome3;
team[players_attributes][3][lastname]=Cognome3; commit=Salva.....
nel caso di aggiornamento avremo
team[players_attributes][1][id]=1;team[players_attributes][1][firstname]=Luca;
team[players_attributes][1][lastname]=Arcara; … commit=Salva.....
nel caso di cancellazione avremo
team[players_attributes][1][id]=1;team[players_attributes][1][_destroy]=1;…;commit=Salva.....
il codice Rails che gestisce il rendering dei campi è il
seguente:
<%= form_for(@team) do |f| %>
<% if @team.errors.any? %>
<div
id="error_explanation">
<h2><%= pluralize(@team.errors.count,
"error") %> prohibited this team from being saved:</h2>
<ul>
<% @team.errors.full_messages.each do
|msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%=f.fields_for :players do |p|%>
<p>Giocatore:
<%=p.text_field :firstname%>
<%=p.text_field :lastname%>
<%=p.check_box
"_destroy"%> Elimina
</p>
<%end%>
<p>Nuovo Giocatore:
<%=text_field_tag
"team[players_attributes][#{@team.players.count}][firstname]"%>
<%=text_field_tag
"team[players_attributes][#{@team.players.count}][lastname]"%>
</p>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
<% end %>
NB: La sezione riguardante il nuovo giocatore può essere
gestita anche in altro modo, ad esempio
creando un nuovo oggetto e verificando l’attributo new_record?, oppure
utilizzando un po’ di javascript ed ajax per creare l’oggetto ed i corrispondenti
tag HTML (un ottimo esempio lo trovate qui http://railscasts.com/episodes/196-nested-model-form-part-1?view=asciicast
)
Nessun commento:
Posta un commento