2009-08-22 14 views
34

Tengo un estándar de muchos-a-muchos relación entre los usuarios y los roles en mi aplicación Rails:rieles idioma para evitar duplicados en has_many: a través de

class User < ActiveRecord::Base 
    has_many :user_roles 
    has_many :roles, :through => :user_roles 
end 

quiero para asegurarse de que un usuario sólo puede asignarse cualquier rol una vez Cualquier intento de insertar un duplicado debe ignorar la solicitud, no arrojar un error o causar una falla de validación. Lo que realmente quiero representar es un "conjunto", donde insertar un elemento que ya existe en el conjunto no tiene ningún efecto. {1,2,3} U {1} = {1,2,3}, no {1,1,2,3}.

que se dan cuenta de que puedo hacerlo de esta manera:

user.roles << role unless user.roles.include?(role) 

o mediante la creación de un método de envoltura (por ejemplo add_to_roles(role)), pero esperaba por alguna forma idiomática para que sea automático a través de la asociación, de manera que Puedo escribir:

user.roles << role # automatically checks roles.include? 

y simplemente hace el trabajo por mí. De esta forma, no tengo que acordarme de buscar dúplex o usar el método personalizado. ¿Hay algo en el marco que me falta? Primero pensé que la opción: uniq a has_many lo haría, pero básicamente es solo "seleccionar distinto".

¿Hay alguna manera de hacerlo declarativamente? Si no, ¿tal vez mediante el uso de una extensión de asociación?

Aquí hay un ejemplo de cómo falla el comportamiento por defecto:

 >> u = User.create 
     User Create (0.6ms) INSERT INTO "users" ("name") VALUES(NULL) 
    => #<User id: 3, name: nil> 
    >> u.roles << Role.first 
     Role Load (0.5ms) SELECT * FROM "roles" LIMIT 1 
     UserRole Create (0.5ms) INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3) 
     Role Load (0.4ms) SELECT "roles".* FROM "roles" INNER JOIN "user_roles" ON "roles".id = "user_roles".role_id WHERE (("user_roles".user_id = 3)) 
    => [#<Role id: 1, name: "1">] 
    >> u.roles << Role.first 
     Role Load (0.4ms) SELECT * FROM "roles" LIMIT 1 
     UserRole Create (0.5ms) INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3) 
    => [#<Role id: 1, name: "1">, #<Role id: 1, name: "1">]

Respuesta

23

Mientras el papel adjunto es un objeto ActiveRecord, lo que está haciendo:

user.roles << role 

debe cancelar el duplicar de forma automática para :has_many asociaciones.

Para has_many :through, intente:

class User 
    has_many :roles, :through => :user_roles do 
    def <<(new_item) 
     super(Array(new_item) - proxy_association.owner.roles) 
    end 
    end 
end 

si súper no funciona, puede que tenga que configurar una alias_method_chain.

+0

No funciona así. Actualizaré la publicación para incluir la prueba. – KingPong

+0

Gracias, probaré la extensión de asociación. – KingPong

+0

Eso funcionó perfectamente. ¡Gracias! La parte que me faltaba cuando probé algo como esto fue el bit proxy_owner. – KingPong

0

Quizás es posible crear la regla de validación

validates_uniqueness_of :user_roles 

luego coger la excepción validación y continuar con gracia. Sin embargo, esto se siente muy raro y es muy poco elegante, incluso posible.

2

Creo que la regla de validación adecuada está en sus users_roles unen modelo:

validates_uniqueness_of :user_id, :scope => [:role_id] 
+0

Gracias. Sin embargo, eso en realidad no hace lo que quiero (que es un comportamiento similar a un set), y he aclarado qué es eso en la publicación original. Lo siento por eso. – KingPong

+0

Creo que esta es la mejor respuesta para su problema. Si tiene cuidado al crear su interfaz, un usuario tendría que hackearla para agregar la función incorrecta de todos modos, en cuyo caso una excepción de validación es una respuesta totalmente adecuada. – austinfromboston

+1

Heh, ¿estás loco? Los usuarios no agregan sus propios roles :-) El caso típico de uso es que un usuario se convierte en miembro de un rol como un efecto secundario de otra cosa. Por ejemplo, comprar un producto en particular. Otros productos también pueden proporcionar la misma función, por lo que existe la posibilidad de duplicación allí. Prefiero hacer la verificación de duplicación en un lugar que en los lugares aleatorios necesarios para garantizar que el usuario tenga un rol. En este sentido, darle a un usuario un rol que ya tiene NO es una condición de error. – KingPong

3

Se puede utilizar una combinación de validates_uniqueness_of y primordial < < en el modelo principal, aunque esto también va a coger cualquier otro error de validación en el modelo de unión

validates_uniqueness_of :user_id, :scope => [:role_id] 

class User 
    has_many :roles, :through => :user_roles do 
    def <<(*items) 
     super(items) rescue ActiveRecord::RecordInvalid 
    end 
    end 
end 
+1

¿No podría cambiar esa excepción a 'ActiveRecord :: RecordNotUnique'? Me gusta esta respuesta Sin embargo, tenga en cuenta [condiciones de carrera] (http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of). – Ashitaka

+0

Buena respuesta. Lo usé sin 'validates_uniqueness_of', habiendo declarado un índice único en la base de datos y funciona con encanto. –

0

creo que quiere hacer algo como:

user.roles.find_or_create_by(role_id: role.id) # saves association to database 
user.roles.find_or_initialize_by(role_id: role.id) # builds association to be saved later 
0

me encontré con esto hoy y terminó usando #replace, que "se realizará una diferenciación y eliminar/añadir sólo los registros que han cambiado" .

Por lo tanto, es necesario pasar a la unión de las funciones existentes (para que no se borran) y su nuevo papel (s):

new_roles = [role] 
user.roles.replace(user.roles | new_roles) 

Es importante tener en cuenta que tanto esta respuesta y la uno aceptado está cargando los objetos roles asociados en la memoria para ejecutar el Array diff (-) y la unión (|). Esto podría generar problemas de rendimiento si tiene que lidiar con una gran cantidad de registros asociados.

Si eso es una preocupación, es posible que desee buscar primero las opciones que comprueban la existencia mediante consultas, o utilizar una consulta de tipo INSERT ON DUPLICATE KEY UPDATE (mysql) para insertar.

Cuestiones relacionadas