DatabaseValidations helps you to keep the database consistency with better performance. Right now, it supports only ActiveRecord.
The more you use the gem, the more performance increase you have. Try it now!
Add this line to your application's Gemfile:
gem 'database_validations'
And then execute:
bundle
Or install it yourself as:
gem install database_validations
Have a look at example application for details.
Imagine, you have User
model defines as
class User < ActiveRecord::Base
validates :email, :full_name, uniqueness: true
belongs_to :company
belongs_to :country
end
and then replace with
class User < ActiveRecord::Base
validates :email, :full_name, db_uniqueness: true
# OR
# validates_db_uniqueness_of :email, :full_name
db_belongs_to :company
db_belongs_to :country
# OR
# belongs_to :company
# belongs_to :country
# validates :company, :country, db_presence: true
end
you will get the following performance improvement:
db_belongs_to
doesn't work with SQLite due to a poor error message.validate: false
option is passed to save
/save!
. They incorrectly return a validation message instead of raising a proper constraint violation exception. In Rails >= 5 they correctly raise the exceptions they supposed to.Supported databases are PostgreSQL
and MySQL
.
Note: Unfortunately, SQLite
raises a poor error message
by which we can not determine exact foreign key which raised an error.
class User < ActiveRecord::Base
db_belongs_to :company
end
user = User.create(company_id: nil)
# => false
user.errors.messages
# => {:company=>["must exist"]}
ActiveRecord's belongs_to
has optional: false
by default. Unfortunately, this
approach does not ensure existence of the related object. For example, we can skip
validations or remove the related object after we save the object. After that, our
database becomes inconsistent because we assume the object has his relation but it
does not.
db_belongs_to
solves the problem using foreign key constraints in the database
also providing backward compatibility with nice validations errors.
Advantages:
ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK'] = 'true'
if you want to
skip it in some cases. (For example, when you run migrations.) Note: we skip it for the abstract classes.Disadvantages:
Option name | PostgreSQL | MySQL |
---|---|---|
class_name | + | + |
foreign_key | + | + |
foreign_type | - | - |
primary_key | + | + |
dependent | + | + |
counter_cache | + | + |
polymorphic | - | - |
validate | + | + |
autosave | + | + |
touch | + | + |
inverse_of | + | + |
optional | - | - |
required | - | - |
default | + | + |
Supported databases are PostgreSQL
, MySQL
and SQLite
.
class User < ActiveRecord::Base
validates :email, db_uniqueness: true
# The same as following:
# validates :email, uniqueness: {case_sensitive: true, allow_nil: true, allow_blank: false}
end
original = User.create(email: 'email@mail.com')
dupe = User.create(email: 'email@mail.com')
# => false
dupe.errors.messages
# => {:email=>["has already been taken"]}
User.create!(email: 'email@mail.com')
# => ActiveRecord::RecordInvalid Validation failed: email has already been taken
Complete case_sensitive
replacement example (for PostgreSQL
only):
validates :slug, uniqueness: { case_sensitive: false, scope: :field }
Should be replaced by:
validates :slug, db_uniqueness: {index_name: :unique_index, case_sensitive: false, scope: :field}
Keep in mind: because valid?
method uses default validator you should:
scope
optioncase_sensitive
optionwhere
optionUnfortunately, ActiveRecord's validates_uniqueness_of
approach does not ensure
uniqueness. For example, we can skip validations or create two records in parallel
queries. After that, our database becomes inconsistent because we assume some uniqueness
over the table but it has duplicates.
validates_db_uniqueness_of
solves the problem using unique index constraints
in the database also providing backward compatibility with nice validations errors.
ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK'] = 'true'
if you want to skip it in some cases. (For example, when you run migrations.) Note: we skip it for the abstract classes.Option name | PostgreSQL | MySQL | SQLite |
---|---|---|---|
mode | + | + | + |
scope | + | + | + |
message | + | + | + |
if | + | + | + |
unless | + | + | + |
index_name | + | + | - |
where | + | - | - |
case_sensitive | + | - | - |
allow_nil | - | - | - |
allow_blank | - | - | - |
The validation has an option :rescue
with two values:
:default
(default option) that follows default ActiveRecord behavior. It respects validate: false
option for save/save!
(for example, this is being used for nested associations):always
that catches database constraint errors and turns them to ActiveRecord validations filling .errors
properly. You may want to use rescue: :always
in case you save nested associations with accepts_nested_attributes_for
helper and you want the validation to happen automatically when a user
provides duplicated data in the same request.
There are 3 mode
options:
:optimized
- the default one. In this mode it turns DB constraint exceptions into proper validation messages.:enhanced
- a combination of the standard uniqueness validation and the db uniqueness validation. Runs a query first but also rescues from exception. The preferable mode for user-facing validations.:standard
- in this mode works pretty much the same way as validates_uniqueness_of
(except the index existence check).Add require database_validations/rspec/matchers'
to your spec
file.
Example:
class User < ActiveRecord::Base
validates_db_uniqueness_of :field, message: 'duplicate', where: '(some_field IS NULL)', scope: :another_field, index_name: :unique_index
end
describe 'validations' do
subject { User }
it { is_expected.to validate_db_uniqueness_of(:field).with_message('duplicate').with_where('(some_field IS NULL)').scoped_to(:another_field).with_index(:unique_index) }
end
DatabaseValidations provides custom cops for RuboCop to help you consistently apply the improvements.
To use all of them, use rubocop --require database_validations/rubocop/cops
or add to your .rubocop.yml
file:
require:
- database_validations/rubocop/cops
Or you case use some specific cop directly:
require:
- database_validations/rubocop/cop/belongs_to
- database_validations/rubocop/cop/uniqueness_of
You need to have installed and running postgresql
and mysql
.
And for each adapter manually create a database called database_validations_test
accessible by your local user.
Then, run rake spec
to run the tests.
To check the conformance with the style guides, run:
rubocop
To run benchmarks, run:
ruby -I lib benchmarks/composed_benchmarks.rb
To install this gem onto your local machine, run bundle exec rake install
.
To release a new version, update the version number in version.rb
, and then
run bundle exec rake release
, which will create a git tag for the version,
push git commits and tags, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the DatabaseValidations project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.