I recently ran into the situation where I had multiple models that each had a
identifier field, and each object of each model had to have a unique value for this field.
Now, in my specific situation, I could have used UUIDs, because I was using Postgres and the
identifier didn't have any other requirements apart from being unique, but there is a nice generic way to implement uniqueness checks across models in Rails, which is what I chose to use.
Let's look at the code first:
# app/models/concerns/validate_identifier_uniqueness_across_models.rb module ValidateIdentifierUniquenessAcrossModels extend ActiveSupport::Concern @@included_classes =  included do @@included_classes << self validate :identifier_unique_across_all_models end private def identifier_unique_across_all_models return if self.identifier.blank? @@included_classes.each do |klass| scope = klass.where(identifier: self.identifier) if self.persisted? && klass == self.class scope = scope.where.not(id: self.id) end if scope.any? self.errors.add :identifier, 'is already taken' break end end end end # app/models/product.rb class Product < ActiveRecord::Base include ValidateIdentifierUniquenessAcrossModels end # app/models/category.rb class Category < ActiveRecord::Base include ValidateIdentifierUniquenessAcrossModels end
So what's happening here? We defined a Concern with the (quite verbose) name of
ValidateIdentifierUniquenessAcrossModels and included it in our
Category models. Once a model includes this concern, two things happen:
- the model is added to the Concern's class variable
- a validation is added
Let's validate together
Let's look at the validation method
identifier_unique_across_all_models more closely. What we want to do in this method is to look at all models that have included
ValidateIdentifierUniquenessAcrossModels and check each object of each model for a matching
identifier. If we find only one object with the identifier of the object we're validating, we add an error message to the
identifier field and stop checking (using
One thing to make sure is that we're excluding the object that is being validated, in case it is already persisted, since otherwise the object under validation will be found in the database and mistaken for a duplicate and thus the validation will always fail.
Reusable and testable
The beauty of packaging up this logic in a
Concern is that it can be added to more models later on effortlessly (imagine a
User model, which also needs to have unique identifiers, is added to your app later on. Simply
include ValidateIdentifierUniquenessAcrossModels in that model and you're good to go) but also that it can be quite simply tested out of the context of a product or category object (in your tests, simply create a new model,
include ValidateIdentifierUniquenessAcrossModels, and test the behavior of that model).
Caveat: race condition
As Jan de Poorter points out in the comments, this approach has a race condition: between checking for existing
identifiers and creating/updating the record, there is a small window where a duplicate can be created. So if you're creating lots of records simultaneously, you might end up with duplicates after all. Keep that in mind when using this method!