2009-04-07 11 views
6

ActiveRecord validates_uniqueness_of es vulnerable to race conditions. Para garantizar realmente la singularidad, se requieren garantías adicionales. Una sugerencia de la ActiveRecord RDocs es crear un índice único en la base de datos, por ejemplo, al incluir en sus migraciones:¿Cómo puedo determinar si mi objeto ActiveRecord infringe una clave/índice de base de datos única?

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

Esto asegurará a nivel de base de datos que el nombre es único. Pero una desventaja de este enfoque es que la excepción ActiveRecord::StatementInvalid devuelta al intentar guardar un duplicado no es muy útil. Uno no puede estar seguro al detectar esta excepción que el error fue generado por un registro duplicado y no solo por un SQL quebrado.

Una solución, como sugieren los RDOCs, es analizar el mensaje que viene con la excepción e intentar detectar palabras como "duplicado" o "único", pero esto es kludgy y el mensaje es específico del back-end de la base de datos. Para SqlLite3, entiendo que el mensaje es totalmente genérico y no se puede analizar de esta manera.

Dado que este es un problema fundamental para los usuarios de ActiveRecord, sería bueno saber si existe algún enfoque estándar para manejar estas excepciones. Voy a ofrecer mi sugerencia a continuación; por favor comente o brinde alternativas; ¡Gracias!

Respuesta

7

Analizando el mensaje de error no es tan malo, pero se siente kludgy. Una sugerencia que encontré (no recuerdo dónde) que parece atractiva es que en el bloque de rescate puede verificar la base de datos para ver si de hecho hay un registro duplicado. Si lo hay, es probable que el StatementInvalid se deba a un duplicado y usted pueda manejarlo como corresponda. Si no existe, entonces StatementInvalid debe ser de otra cosa, y debe manejarlo de manera diferente.

Así que la idea básica, asumiendo un índice único en recipe.name que el anterior:

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 

he tratado de automatizar este comprobando con lo siguiente:

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 

Así que ahora usted debe ser capaz de utilizar generic_record.violates_unique_index? en su bloque de rescate para decidir cómo manejar StatementInvalid.

Espero que sea útil! Otros enfoques?

+0

Implementé esto y parece hacer el trabajo. Tentado de sacar validates_uniqueness ya que esto esencialmente hace lo mismo más eficientemente. Publicará cualquier actualización relevante. – Chinasaur

+0

Un pequeño detalle: recibes el error incorrecto si el duplicado se elimina entre obtener la excepción y consultar el duplicado. – mpartel

2

¿Es realmente un gran problema?

Si se utiliza un índice único, junto con una restricción validates_uniqueness_of, entonces

  • integridad de los datos se mantendrá
  • Se hará en el peor sólo recibe un error cuando dos separados solicitudes intenta insertar un no -único fila al mismo tiempo

Así que a menos que tenga una aplicación que tenga muchos posibles insertos duplicados (en cuyo caso me gustaría ver el redesig eso) Veo que esto rara vez es un problema en la práctica.

+0

Correcto, solo recibirá la excepción raramente. Pero aún así pensé que sería bueno manejarlo :). Una pregunta que surge de su respuesta: si puedo evitarla, ¿quiero utilizar 'validates_uniqueness_of'? Si mi respuesta funciona, me sentiría tentado de al menos arrojar una: si está en ella ... – Chinasaur

Cuestiones relacionadas