ruby on rails - En una tabla de unión, ¿cuál es la mejor solución para la ausencia de una clave compuesta de Rails?
ruby-on-rails database (5)
create_table :categories_posts, :id => false do |t|
t.column :category_id, :integer, :null => false
t.column :post_id, :integer, :null => false
end
Tengo una tabla de unión (como arriba) con columnas que se refieren a una tabla de categorías correspondiente y una tabla de publicaciones . Quería imponer una restricción única en la clave compuesta category_id, post_id en la tabla de unión categories_posts . Pero Rails no soporta esto (creo).
Para evitar la posibilidad de filas duplicadas en mis datos que tengan la misma combinación de category_id y post_id, ¿cuál es la mejor solución para la ausencia de una clave compuesta en Rails ?
Mis suposiciones aquí son:
- La columna predeterminada de números automáticos (id: integer) no haría nada para proteger mis datos en esta situación.
- ActiveScaffold puede proporcionar una solución, pero no estoy seguro de si es excesivo incluirla en mi proyecto simplemente por esta característica única, especialmente si hay una respuesta más elegante.
Agregue un índice único que incluya ambas columnas. Eso evitará que inserte un registro que contenga un par duplicado category_id / post_id.
add_index :categories_posts, [ :category_id, :post_id ], :unique => true, :name => ''by_category_and_post''
Creo que puede encontrar más fácil validar la unicidad de uno de los campos con el otro como un alcance:
DESDE LA API:
validates_uniqueness_of(*attr_names)
Valida si el valor de los atributos especificados es único en todo el sistema. Útil para asegurarse de que solo un usuario pueda ser nombrado "davidhh".
class Person < ActiveRecord::Base
validates_uniqueness_of :user_name, :scope => :account_id
end
También puede validar si el valor de los atributos especificados es único en función de múltiples parámetros de alcance. Por ejemplo, asegurarse de que un maestro solo pueda estar en el horario una vez por semestre para una clase en particular.
class TeacherSchedule < ActiveRecord::Base
validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
end
Cuando se crea el registro, se realiza una verificación para asegurarse de que no exista ningún registro en la base de datos con el valor dado para el atributo especificado (que se asigna a una columna). Cuando se actualiza el registro, se realiza la misma comprobación pero sin tener en cuenta el propio registro.
Opciones de configuración:
* message - Specifies a custom error message (default is: "has already been taken")
* scope - One or more columns by which to limit the scope of the uniquness constraint.
* case_sensitive - Looks for an exact match. Ignored by non-text columns (true by default).
* allow_nil - If set to true, skips this validation if the attribute is null (default is: false)
* if - Specifies a method, proc or string to call to determine if the validation should occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The method, proc or string should return or evaluate to a true or false value.
Es muy difícil recomendar el enfoque "correcto".
1) El enfoque pragmático.
Utilice el validador y no agregue un índice compuesto único. Esto te da buenos mensajes en la interfaz de usuario y simplemente funciona.
class CategoryPost < ActiveRecord::Base
belongs_to :category
belongs_to :post
validates_uniqueness_of :category_id, :scope => :post_id, :message => "can only have one post assigned"
end
También puede agregar dos índices separados en sus tablas de unión para acelerar las búsquedas:
add_index :categories_posts, :category_id
add_index :categories_posts, :post_id
Tenga en cuenta (según el libro Rails 3 Way ) que la validación no es infalible debido a una posible condición de carrera entre las consultas SELECT y INSERT / UPDATE . Se recomienda utilizar una restricción única si debe estar absolutamente seguro de que no hay registros duplicados.
2) El enfoque a prueba de balas.
En este enfoque queremos poner una restricción en el nivel de la base de datos. Así que significa crear un índice compuesto:
add_index :categories_posts, [ :category_id, :post_id ], :unique => true, :name => ''by_category_and_post''
La gran ventaja es una gran integridad de la base de datos, la desventaja no es un informe de errores muy útil al usuario. Tenga en cuenta que al crear un índice compuesto, el orden de las columnas es importante.
Si coloca columnas menos selectivas como columnas iniciales en el índice y coloca la mayoría de las columnas selectivas al final, otras consultas que tienen una condición en columnas de índices no principales también pueden aprovechar el INDEX SKIP SCAN. Es posible que deba agregar un índice más para obtener ventaja de ellos, pero esto depende en gran medida de la base de datos.
3) Combinación de ambos
Se puede leer acerca de la combinación de ambos, pero tiendo a gustarme solo el número uno.
Implemento ambos de los siguientes cuando tengo este problema en los rieles:
1) Debe tener un índice compuesto único declarado en el nivel de la base de datos para garantizar que los dbms no permitan que se cree un registro duplicado.
2) Para proporcionar mensajes de error más suaves que solo los anteriores, agregue una validación al modelo de Rails:
validates_each :category_id, :on => :create do |record, attr, value|
c = value; p = record.post_id
if c && p && # If no values, then that problem
# will be caught by another validator
CategoryPost.find_by_category_id_and_post_id(c, p)
record.errors.add :base, ''This post already has this category''
end
end
Una solución puede ser agregar tanto el índice como la validación en el modelo.
Así que en la migración tienes: add_index: categories_posts, [: category_id,: post_id],: unique => true
Y en el modelo: validates_uniqueness_of: category_id,: scope => [: category_id,: post_id] validates_uniqueness_of: post_id,: scope => [: category_id,: post_id]