[rails] funzionamento di update e update_attribute

Allora, forse mi sto facendo noie per nulla, comunque chiedo. Dunque,
sto usando will_paginate, e l’helper check_box per il campo ‘visible’.

La view di admin/list:

<% form_tag :action => ‘update_entries’ do%>
<% @entries.each do |e| -%>
<% @e=e -%>
<%= e.date -%>

<%= e.title -%>

<%= e.content -%>

<%= check_box( ‘e[]’,‘visible’,{},true,false) %>



<% end -%>
<%= submit_tag %>
<% end %>

Quando spedisco il form, viene eseguito il seg. metodo:

def update_entries
Entry.update(params[:e].keys, params[:e].values);
redirect_to :action => ‘list’
end

Il quale effettivamente funziona e scrive nel db le variazioni. Quindi
tutto ok.

Ma quello che mi chiedo è:

  1. questo update, mi riscrive solo la colonna ‘visible’ o anche tutte le
    altre?
  2. E come faccio a fargli aggiornare solo le colonne delle righe che
    sono state cambiate?

Leggendo nella console di Mongrel, sembrerebbe che sovrascrive tutte le
colonne di tutte le righe visibili in pagina, a prescindere. Infatti per
ogni riga trovo scritto:

Entry Load (0.001326) SELECT * FROM entries WHERE (entries.id = 9)
Entry Update (0.001142) UPDATE entries SET “visible” = ‘f’, “content”
= ‘contenuto 7’, “title” = ‘titolo 7’, “date” = ‘16-09-2007’ WHERE id =
9

Ho provato tra l’altro anche a cambiare update_entries in questo modo:

def update_entries
params[:e].each do |p|
row = Entry.find(p[0])
row.update_attribute(p[1].keys,p[1].keys)
end
redirect_to :action => ‘list’
end

Dalla quale mi aspettavo per lo meno che mi sovrascrivesse solo la
colonna ‘visible’, dato che p = [id, visible => true/false]. Ma Mongrel
scrive sempre le stesse, cose come sopra.

Qualcuno sa darmi dei suggerimenti? :slight_smile:

Le risposte me le sono trovate da me… Pubblico perché può servire a
qualche altro nuovo arrivato come me.

  1. Il fatto di salvare anche i checkbox che non cambiano è insito nella
    costruzione stessa di check_box. Siccome se == false non posta nulla,
    viene creato un campo hidden che si occupa di ‘tappare’ il problema, e
    questo implica che lo stato venga salvato comunque. Si potrebbe usare
    check_box_tag, ma allora non siamo più legati al modello. Alla fine
    comunque mi sta bene così.

  2. La cosa grave non è tanto che vengono salvati anche gli attributi che
    non cambiano in una singola colonna, bensì che vengo salvate anche le
    colonne non interessate!
    Ora, alla fine ho capito che Rails, di suo, riscrive sempre e comunque
    l’intera riga, anche se usi update_attribute, o _attributes!!! Questo è
    un comportamento quantomeno discutibile, e a sto punto non si capisce
    neanche tanto bene perché esistano detti metodi. Comunque saperlo è giÃ
    un passo avanti…

  3. Soluzione almeno parziale al problema 2:

def update_entries
params[:e].each do |p|
#l’equivalente sql di ciò che mi serve è questo:
#UPDATE entries SET “visible” = ‘f’ WHERE “id” = 2

 #Quindi faccio una select delle righe con id + attributi che devo 

aggiornare ( SQL: SELECT id, visible FROM entries WHERE (entries.“id” =
6) )

 att = Entry.find(p[0], :select => "id, visible")

 #e poi semplicemente salvo..
 att.update_attribute(:visible,p[1].values[0])

end

redirect_to :action => ‘list’
end

Ecco. Così salvo SOLO le colonne che modifico.
Però, bastano solo 2 righe: strano che nessuno fosse al corrente di
questo.

  1. La cosa grave non è tanto che vengono salvati anche gli attributi che
    non cambiano in una singola colonna, bensì che vengo salvate anche le
    colonne non interessate!
    Ora, alla fine ho capito che Rails, di suo, riscrive sempre e comunque
    l’intera riga, anche se usi update_attribute, o _attributes!!! Questo è
    un comportamento quantomeno discutibile, e a sto punto non si capisce
    neanche tanto bene perché esistano detti metodi. Comunque saperlo è già
    un passo avanti…

Se ho capito bene, l’obiezione è che tu ti aspetti che se fai, per
esempio,

user.update_attribute(:name, “gino”)

venga eseguito

update users set name = ‘gino’ where id = 123

mentre invece quello che fa Rails è

user.name = “gino”
user.save

E la “save” salva tutti gli attributi. Il ragionamento per cui
update_attribute fa così è che in questo modo sei garantito che
l’oggetto
salvato sul database passi le validazioni. Se tu invece ti limiti ad
aggiornare un solo attributo, non sei più garantito che l’oggetto salvato
sia valido. Considera:

class User
def validate
if role != “admin” and credit = 0
errors.add_to_base “se non sei amministratore devi acquistare
credito”
end
end
end

A questo punto se due azioni concorrenti modificano la stessa riga di
user,
per esempio:

user = User.find(123)
user.update_attribute :role, “peone”

e

user = User.find(123)
user.update_attribute :credit, 0

potrebbe succedere, se non salvi tutti gli attributi ma solo uno, che la
riga sul database contenga role = ‘peone’ e credit = 0. Entrambi gli
oggetti nei due thread erano validi quando sono stati salvati, ma
salvando
un attributo solo alla volta rischio di ottenere una riga che una volta
ricaricata mi da un oggetto che non è più valido.

Se però non è questo il tuo caso e vuoi una semplice update secca di un solo
attributo, puoi sempre fare

user.connection.execute(“update users set x = y where id = z”)

oppure

User.update_all(“x = y”, “id = z”)

che fa poi esattamente la stessa cosa, con il vantaggio che puoi
sfruttare
la sostituzione dei parametri per evitare il pericolo di sql injection:

User.update_all([“x = ?”, z], [“id = ?”, z])

M

Peeeero’! Questa si che è una spiegazione coi fiocchi!!! :wink: Non si
finisce mai di imparare… Che detto da me che sono appena arrivato
suona quasi comico comunque… :slight_smile:
Grazie mille, molto esauriente, ora modifico il mio codice in base alle
cose che mi hai spiegato.

Buona giornata,

Luca

Matteo V. wrote:

  1. La cosa grave non � tanto che vengono salvati anche gli attributi che
    non cambiano in una singola colonna, bens� che vengo salvate anche le
    colonne non interessate!
    Ora, alla fine ho capito che Rails, di suo, riscrive sempre e comunque
    l’intera riga, anche se usi update_attribute, o _attributes!!! Questo �
    un comportamento quantomeno discutibile, e a sto punto non si capisce
    neanche tanto bene perch� esistano detti metodi. Comunque saperlo � gi�
    un passo avanti…

Se ho capito bene, l’obiezione � che tu ti aspetti che se fai, per
esempio,

user.update_attribute(:name, “gino”)

venga eseguito

update users set name = ‘gino’ where id = 123

mentre invece quello che fa Rails �

user.name = “gino”
user.save

E la “save” salva tutti gli attributi. Il ragionamento per cui
update_attribute fa cos� � che in questo modo sei garantito che
l’oggetto
salvato sul database passi le validazioni. Se tu invece ti limiti ad
aggiornare un solo attributo, non sei pi� garantito che l’oggetto salvato
sia valido. Considera:

class User
def validate
if role != “admin” and credit = 0
errors.add_to_base “se non sei amministratore devi acquistare
credito”
end
end
end

A questo punto se due azioni concorrenti modificano la stessa riga di
user,
per esempio:

user = User.find(123)
user.update_attribute :role, “peone”

e

user = User.find(123)
user.update_attribute :credit, 0

potrebbe succedere, se non salvi tutti gli attributi ma solo uno, che la
riga sul database contenga role = ‘peone’ e credit = 0. Entrambi gli
oggetti nei due thread erano validi quando sono stati salvati, ma
salvando
un attributo solo alla volta rischio di ottenere una riga che una volta
ricaricata mi da un oggetto che non � pi� valido.

Se per� non � questo il tuo caso e vuoi una semplice update secca di un solo
attributo, puoi sempre fare

user.connection.execute(“update users set x = y where id = z”)

oppure

User.update_all(“x = y”, “id = z”)

che fa poi esattamente la stessa cosa, con il vantaggio che puoi
sfruttare
la sostituzione dei parametri per evitare il pericolo di sql injection:

User.update_all([“x = ?”, z], [“id = ?”, z])

M

E’ strano. Usando una delle tue soluzioni, e quindi:

Entry.connection.execute(“update entries set ‘visible’ =
'”+p[1].values[0]+"’ where ‘id’ = "+p[0])

oppure

Entry.update_all("‘visible’ = ‘"+p[1].values[0]+"’ where ‘id’="+p[0])

Mongrel scrive questo, ma in realtà NON SALVA i dati:

SQL (0.030970) update entries set ‘visible’ = ‘false’ where ‘id’ = 6
SQL (0.002068) update entries set ‘visible’ = ‘true’ where ‘id’ = 1
SQL (0.001952) update entries set ‘visible’ = ‘false’ where ‘id’ = 2
SQL (0.002420) update entries set ‘visible’ = ‘false’ where ‘id’ = 3
SQL (0.002110) update entries set ‘visible’ = ‘true’ where ‘id’ = 4
SQL (0.001870) update entries set ‘visible’ = ‘true’ where ‘id’ = 5

Se faccio a modo mio:

att = Entry.find(p[0], :select => “id, visible”)
att.update_attribute(:visible,p[1].values[0])

Mongrel scrive questo ed effettivamente SALVA:

Entry Load (0.001283) SELECT id, visible FROM entries WHERE
(entries.“id” = 6)
Entry Update (0.036973) UPDATE entries SET “visible” = ‘f’ WHERE “id”
= 6
Entry Load (0.001142) SELECT id, visible FROM entries WHERE
(entries.“id” = 1)
Entry Update (0.001824) UPDATE entries SET “visible” = ‘t’ WHERE
“id” = 1

Eccetera…

Come mai uno salva e l’altro no? Tieni presente 2 cose però:

  • sto usando sqlite3
  • il codice di cui sopra è dentro un ciclo di questo tipo:

params[:e].each do |p|
[…]
end

  • nella view sto usando will_paginate:

<% @entries.each do |e| -%>
<% @e=e %>

 <tr class="<%= cycle("pari", "dispari") -%>">
    <td><%= e.entry_date -%></td>
    <td><%= e.title -%></td>
    <td><%= e.content -%></td>
    <td><%= check_box( 'e[]','visible',{},true,false) -%></td>
    [ecc...]
 </tr>

<% end -%>

Bah… :stuck_out_tongue:

Manca una “commit” forse?

Ho anche pensato che dipendesse da fatto che un modo setta ‘false’ e
‘true’ mentre l’altro, quello che salva, in realtà setta ‘f’ e ‘t’,
dunque ho ‘corretto’ la cosa:

v = (p[1].values[0] == ‘false’) ? ‘f’ : ‘t’
Entry.update_all("‘visible’ = ‘"+v+"’ where ‘id’="+p[0])

E dunque Mongrel mostra:

Entry Update (0.012924) UPDATE entries SET ‘visible’ = ‘t’ where
‘id’=6

Ma ugualmente non salva nulla…

Boh…

On 11/12/07, Luca R. [email protected] wrote:

Matteo V. wrote:

Manca una “commit” forse?

No. Penso tu intenda questo, che copio incollo dalla sorgente:

<input name="commit" type="submit" value="Save changes" />

No, intendo un commit della transazione.

Prova ad aggiungere Entry.connection.execute(“commit”) e vedi se cambia
qualche cosa.

M

Mi restituisce addirittura un errore:

SQLite3::SQLException: SQL logic error or missing database: commit

Ma comunque update_all non dovrebbe aver di per se bisogno di commit, o
sbaglio? Se la domanda è sciocca chiedo scusa.

Luca

Matteo V. wrote:

On 11/12/07, Luca R. [email protected] wrote:

Matteo V. wrote:

Manca una “commit” forse?

No. Penso tu intenda questo, che copio incollo dalla sorgente:

<input name="commit" type="submit" value="Save changes" />

No, intendo un commit della transazione.

Prova ad aggiungere Entry.connection.execute(“commit”) e vedi se cambia
qualche cosa.

M

On 11/12/07, Luca R. [email protected] wrote:

Mi restituisce addirittura un errore:

SQLite3::SQLException: SQL logic error or missing database: commit

Ma comunque update_all non dovrebbe aver di per se bisogno di commit, o
sbaglio? Se la domanda è sciocca chiedo scusa.

Anche a me pare che dovrebbe funzionare. Non so come mai non funzioni.

M

Luca

Matteo V. wrote:

Manca una “commit” forse?

No. Penso tu intenda questo, che copio incollo dalla sorgente:

<input name="commit" type="submit" value="Save changes" />

Comunque non credo, perché con il mio metodo salva correttamente.

Hola!