ruby on rails - references - Lenguaje de rieles para evitar duplicados en has_many: a través de
rails model references (7)
Creo que la regla de validación adecuada se encuentra en su modelo de unión users_roles:
validates_uniqueness_of :user_id, :scope => [:role_id]
Tengo una relación estándar de muchos a muchos usuarios y roles en mi aplicación Rails:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, :through => :user_roles
end
Quiero asegurarme de que a un usuario solo se le pueda asignar cualquier función 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}.
Me doy cuenta de que puedo hacerlo así:
user.roles << role unless user.roles.include?(role)
o creando un método de envoltura (por ejemplo, add_to_roles(role)
), pero esperaba una forma idiomática de hacerlo automático a través de la asociación, para poder 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 predeterminado:
>> 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">]
Creo que quieres 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
Me encontré con esto hoy y terminé usando #replace , que "realizará un diff y eliminará / agregará solo los registros que hayan cambiado".
Por lo tanto, debe aprobar la unión de los roles existentes (para que no se eliminen) y su (s) nuevo (s) rol (es):
new_roles = [role]
user.roles.replace(user.roles | new_roles)
Es importante tener en cuenta que tanto esta respuesta como la aceptada están cargando los objetos de roles
asociados en la memoria para realizar 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 opciones que comprueben la existencia a través de consultas primero, o utilice una consulta de tipo INSERT ON DUPLICATE KEY UPDATE
(mysql) para insertar.
Puede usar una combinación de validates_uniqueness_of y overriding << en el modelo principal, aunque esto también detectará cualquier otro error de validación en el modelo de combinació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
Siempre que la función adjunta sea un objeto ActiveRecord, lo que está haciendo:
user.roles << role
Debe desduplicar automáticamente para :has_many
asociaciones.
Para has_many :through
, intenta:
class User
has_many :roles, :through => :user_roles do
def <<(new_item)
super( Array(new_item) - proxy_association.owner.roles )
end
end
end
si super no funciona, es posible que deba configurar un alias_method_chain.
Tal vez es posible crear la regla de validación
validates_uniqueness_of :user_roles
luego capte la excepción de validación y continúe con gracia. Sin embargo, esto se siente muy raro y es muy poco elegante, incluso posible.
Use el |=
Método de combinación de Array.
Puede usar el método |=
join de Array para agregar un elemento a la matriz, a menos que ya esté presente. Solo asegúrate de envolver el elemento en una matriz.
role #=> #<Role id: 1, name: "1">
user.roles #=> []
user.roles |= [role] #=> [#<Role id: 1, name: "1">]
user.roles |= [role] #=> [#<Role id: 1, name: "1">]
También se puede usar para agregar elementos múltiples que pueden o no estar presentes:
role1 #=> #<Role id: 1, name: "1">
role2 #=> #<Role id: 2, name: "2">
user.roles #=> [#<Role id: 1, name: "1">]
user.roles |= [role1, role2] #=> [#<Role id: 1, name: "1">, #<Role id: 2, name: "2">]
user.roles |= [role1, role2] #=> [#<Role id: 1, name: "1">, #<Role id: 2, name: "2">]
Encontré esta técnica en esta respuesta de .