2009-04-07 8 views
6

ActiveRecord's validates_uniqueness_of è vulnerable to race conditions. Assicurare davvero l'unicità richiede ulteriori salvaguardie. Un suggerimento dal ActiveRecord rdocs è quello di creare un indice univoco nel database, ad esempio includendo nelle migrazioni:Come posso determinare se il mio oggetto ActiveRecord viola una chiave/indice di database univoco?

add_index :recipes, :name, :unique => true 

Questo assicurerà a livello di database che il nome sia univoco. Ma uno svantaggio di questo approccio è che l'eccezione ActiveRecord::StatementInvalid restituita nel tentativo di salvare un duplicato non è molto utile. Non si può essere sicuri quando si rileva questa eccezione che l'errore è stato generato da un record duplicato e non solo da SQL rotto.

Una soluzione, come suggerito da RDocs, è di analizzare il messaggio che viene fornito con l'eccezione e provare a rilevare parole come "duplicate" o "unique", ma questo è kludgy e il messaggio è specifico del back-end del database. Per SqlLite3, la mia comprensione è che il messaggio è totalmente generico e non può essere analizzato in questo modo.

Dato che questo è un problema fondamentale per gli utenti di ActiveRecord, sarebbe bello sapere se esiste un approccio standard per gestire queste eccezioni. Offrirò il mio suggerimento qui sotto; si prega di commentare o fornire alternative; Grazie!

risposta

7

L'analisi del messaggio di errore non è poi così male, ma si sente kludgy. Un suggerimento che ho trovato (non ricordo dove) sembra interessante nel fatto che nel blocco di salvataggio è possibile controllare il database per vedere se c'è effettivamente un record duplicato. Se c'è, allora è probabile che la dichiarazione non valida sia dovuta al duplicato e che tu possa gestirla di conseguenza. In caso contrario, la dichiarazione non valida deve provenire da qualcos'altro e devi gestirla in modo diverso.

Quindi l'idea di base, ipotizzando un indice univoco recipe.name come sopra:

begin 
    recipe.save! 
rescue ActiveRecord::StatementInvalid 
    if Recipe.count(:conditions => {:name => recipe.name}) > 0 
    # It's a duplicate 
    else 
    # Not a duplicate; something else went wrong 
    end 
end 

ho cercato di automatizzare questo controllo con il seguente:

class ActiveRecord::Base 
    def violates_unique_index?(opts={}) 
    raise unless connection 
    unique_indexes = connection.indexes(self.class.table_name).select{|i|i.unique} 
    unique_indexes.each do |ui| 
     conditions = {} 
     ui.columns.each do |col| 
     conditions[col] = send(col) 
     end 
     next if conditions.values.any?{|c|c.nil?} and !opts[:unique_includes_nil] 
     return true if self.class.count(:conditions => conditions) > 0 
    end 
    return false 
    end 
end 

Così ora si dovrebbe essere in grado di utilizzare generic_record.violates_unique_index? nel blocco di salvataggio per decidere come gestire StatementInvalid.

Spero che sia utile! Altri approcci?

+0

Ho implementato questo e sembra di portare a termine il lavoro. Tentativo di prendere validates_uniqueness poiché questo essenzialmente fa la stessa cosa in modo più efficiente. Pubblicherà qualsiasi aggiornamento pertinente. – Chinasaur

+0

Un nitpick molto piccolo: si ottiene l'errore sbagliato se il duplicato viene eliminato tra ottenere l'eccezione e interrogare per il duplicato. – mpartel

2

È davvero un grosso problema?

Se si utilizza un indice univoco con un vincolo validates_uniqueness_of, quindi

  • L'integrità dei dati sarà mantenuta
  • Sarete nel peggiore dei casi ottenere solo un errore quando due distinti richieste tenta di inserire un non -Unique fila simultaneamente

Quindi, a meno che non si dispone di un app che fa molti potenziali inserti duplicati (nel qual caso vorrei guardare redesig Ciò significa che raramente si tratta di un problema nella pratica.

+0

Esatto, raramente riceverai l'eccezione. Ma pensavo ancora che sarebbe stato carino gestirlo :). Una domanda dalla tua risposta: se posso evitarlo, voglio anche usare 'validates_uniqueness_of'? Se la mia risposta funziona, sarei tentato di lanciare almeno un: se su di esso ... – Chinasaur