I am a Sr. Software Developer at Oracle Cloud. The opinions expressed here are my own and not necessarily those of my employer.
Rails testing DB indexes
With ActiveRecord to create indexes we need to run migrations. But with Mongoid we simply specify indexes in our models. Here is a hypothetical User model. We want email and name to be required and email to be unique.
# app/models/user.rb
class User
field :name
field :email
validates :email, name, presence: true
validates :email, uniqueness: true
end
Instead of doing email uniqueness validation in application code it’s better to shift it to DB index via index({ email: 1 }, { unique: true })
.
Removing / creating indexes
To update our DB we need to run these commands after deploying to production.
bundle exec rake db:mongoid:remove_undefined_indexes RAILS_ENV=production
bundle exec rake db:mongoid:create_indexes RAILS_ENV=production
But when we run tests it can be useful to bypass certain validations in test data setup. We could use factory girl and put required fields into our factory file. The problem is that sometimes records must belong to other records and then to create child we must first create parent just to make validation happy.
Here we are creating users w/o name or email:
user1 = User.new
user1.save(validate: false)
user2 = User.new
user2.save(validate: false)
The problem is it will fail to create second user because there is already a user with blank email in our DB.
Failure/Error: [0muser2.save([35mvalidate[0m: [1;36mfalse[0m)
Mongo::Error::OperationFailure:
E11000 duplicate key error collection: my_db_test.users
index: _email_1 dup key: { : undefined } (11000)
So we need to manually specify email (but not necessarily name) to make MongoDB happy:
user1 = User.new(email: 'foo@bar.com')
user1.save(validate: false)
user2 = User.new(email: 'bar@foo.com')
user2.save(validate: false)
But how do we keep our test DB indexes in sync with production DB w/o manually running rake db:*
when we update indexes? If we forget it can cause situation where the tests pass but code fails in production.
rake db:*
are simply Rake tasks so what we need to is run rake from Rspec.
# spec/rails_helper.rb
...
require 'rspec/rails'
# add these lines
require 'rake'
Rails.application.load_tasks
...
RSpec.configure do |config|
config.before(:all) do
# add these lines
Rake::Task['db:mongoid:remove_undefined_indexes'].invoke
Rake::Task['db:mongoid:create_indexes'].invoke
...
end
end
Now the indexes are refreshed before every test suite run.
Updating existing indexes
Another important issue to address is when we update current indexes. What if we implement multitenancy where Users belong to Clients and email uniqueness has to be w/in Client? We need to update DB index.
# app/models/client.rb
class Client
has_many :users
end
# app/models/user.rb
class User
belongs_to :client
field :email
index({ client: 1, email: 1 }, { unique: true })
end
When we run rake db:mongoid:create_indexes
it will create new index with named email_1_client_1
vs email_1
before.
But what if the name does not change? We can switch to background indexes with index({ client: 1, email: 1 }, { unique: true, background: true })
. The best way I can think of is to manually edit index in the DB which is a little messy. In test environment we can just drop the index, collection or entire DB. Otherwise you get this error:
Failure/Error: Rake::Task['db:mongoid:create_indexes'].invoke
Mongo::Error::OperationFailure:
Index with name: email_1_client_1 already exists with different options (85)