Active Record Doctor helps to keep the database in a good shape. Currently, it can:
- index unindexed foreign keys
- detect extraneous indexes
- detect missing foreign key constraints
- detect models referencing undefined tables
More features coming soon!
Want to suggest a feature? Just shoot me an email.
The preferred installation method is adding active_record_doctor
to your
Gemfile
:
gem 'active_record_doctor', group: :development
Then run:
bundle install
Foreign keys should be indexed unless it's proven ineffective. However, Rails makes it easy to create an unindexed foreign key. Active Record Doctor can automatically generate database migrations that add the missing indexes. It's a three-step process:
- Generate a list of unindexed foreign keys by running
rake active_record_doctor:unindexed_foreign_keys > unindexed_foreign_keys.txt
-
Remove columns that should not be indexed from
unindexed_foreign_keys.txt
as a column can look like a foreign key (i.e. end with_id
) without being one. -
Generate the migrations
rails generate active_record_doctor:add_indexes unindexed_foreign_keys.txt
- Run the migrations
rake db:migrate
Let me illustrate with an example. Consider a users
table with columns
first_name
and last_name
. If there are two indexes:
- A two-column index on
last_name, first_name
. - A single-column index on
last_name
.
Then the latter index can be dropped as the former can play its role. In
general, a multi-column index on column_1, column_2, ..., column_n
can replace
indexes on:
column_1
column_1, column_2
- ...
column_1, column_2, ..., column_(n - 1)
To discover such indexes automatically just follow these steps:
- List extraneous indexes by running:
rake active_record_doctor:extraneous_indexes
-
Confirm that each of the indexes can be indeed dropped.
-
Create a migration to drop the indexes.
The indexes aren't dropped automatically because there's usually just a few of them and it's a good idea to double-check that you won't drop something necessary.
Also, extra indexes on primary keys are considered extraneous too and will be reported.
Note that a unique index can never be replaced by a non-unique one. For
example, if there's a unique index on users.login
and a non-unique index on
users.login, users.domain
then the tool will not suggest dropping
users.login
as it could violate the uniqueness assumption.
If users.profile_id
references a row in profiles
then this can be expressed
at the database level with a foreign key constraint. It forces
users.profile_id
to point to an existing row in profiles
. The problem is
that in many legacy Rails apps the constraint isn't enforced at the database
level.
active_record_doctor
can automatically detect foreign keys that could benefit
from a foreign key constraint (a future version will generate a migrations that
add the constraint; for now, it's your job). You can obtain the list of foreign
keys with the following command:
rake active_record_doctor:missing_foreign_keys
The output will look like:
users profile_id
comments user_id article_id
Tables are listed one per line. Each line starts with a table name followed by
column names that should have a foreign key constraint. In the example above,
users.profile_id
, comments.user_id
, and comments.article_id
lack a foreign
key constraint.
In order to add a foreign key constraint to users.profile_id
use the following
migration:
class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
def change
add_foreign_key :users, :profiles
end
end
Active Record guesses the table name based on the class name. There are a few cases where the name can be wrong (e.g. you forgot to commit a migration or changed the table name). Active Record Doctor can help you identify these cases before they hit production.
The only think you need to do is run:
rake active_record_doctor:undefined_table_references
If there a model references an undefined table then you'll see a message like this:
The following models reference undefined tables:
Contract (the table contract_records is undefined)
On top of that rake
will exit with status code of 1. This allows you to use
this check as part of your Continuous Integration pipeline.
This gem is developed and maintained by Greg Navis.