At some point in a Rails project, you’ll have to go through a period of understanding the models and the relationships inside them. This will be more likely before you start contributing to a rails open source project or at your new job. This part of onboarding usually involves a dauting process of going back and forth between the model files and the schema.rb file with the sole purpose of mapping relationships between database tables as well as establishing what the models them selves are capable of. Luckily for us, there is a better way to achieve the same, and with less frustration by using the rails-lens gem.

Rails Lens will annotate model files in your app and highlight relationships between database tables in your application, all by running a single command. For purposes of this tutorial, I’ve set up a sample Rails app.

This rails app is the usual rails blog application. It has a couple models namely Post, User, Comment and Category. These models have various associations between each other in the sense that, a post belongs to a user, has many comments and belongs to a category. Attached below are the model files.

To set up rails lens in the application, add the line below to your gem file. I’ve chosen to install version 0.2.6

Run bundle install

If the installation is successful, you’ll see the output below in your terminal when you run bundle info rails_lens

  * rails_lens (0.2.6)
	Summary: Comprehensive Rails application visualization and annotation
	Homepage: https://github.com/seuros/rails_lens
	Source Code: https://github.com/seuros/rails_lens
	Changelog: https://github.com/seuros/rails_lens/blob/main/CHANGELOG.md
	Path: /usr/share/rvm/gems/ruby-3.3.4/gems/rails_lens-0.2.6

Note: At the time of writing this article, you have to specify the version of rails-lens you want to be installed. By just adding gem 'rails-lens' to your gem file and running bundle install, the version that ends up being installed is a placeholder version named 0.0.0. With this version, you’ll run into the error below when you run the command responsible for annotation.

bundler: command not found: rails_lens
Install missing gem executables with `bundle install`

After installing the rails-lens gem, now onto the fun part, annotation.

To annotate all the model files in the application, run the command below:

bundle exec rails_lens annotate

The above command adds annotations the 4 model files in our rails app.

Let’s take a quick look at the post.rb model file.

# <rails-lens:schema:begin>
# table = "posts"
# database_dialect = "SQLite"
#
# columns = [
#   { name = "id", type = "integer", primary_key = true, nullable = false },
#   { name = "title", type = "text", nullable = true },
#   { name = "body", type = "text", nullable = true },
#   { name = "category_id", type = "integer", nullable = false },
#   { name = "user_id", type = "integer", nullable = false },
#   { name = "created_at", type = "datetime", nullable = false },
#   { name = "updated_at", type = "datetime", nullable = false }
# ]
#
# indexes = [
#   { name = "index_posts_on_user_id", columns = ["user_id"] },
#   { name = "index_posts_on_category_id", columns = ["category_id"] }
# ]
#
# foreign_keys = [
#   { column = "user_id", references_table = "users", references_column = "id" },
#   { column = "category_id", references_table = "categories", references_column = "id" }
# ]
#
# == Notes
# - Association 'comments' should specify inverse_of
# - Association 'comments' has N+1 query risk. Consider using includes/preload
# - Column 'title' should probably have NOT NULL constraint
# - Column 'body' should probably have NOT NULL constraint
# - Large text column 'title' is frequently queried - consider separate storage
# - Large text column 'body' is frequently queried - consider separate storage
# <rails-lens:schema:end>


class Post < ApplicationRecord
  has_many :comments, dependent: :destroy
  belongs_to :category
  belongs_to :user
end

With the lines below, we can identify that this is a schema annotation for the Posts table in a rails application using SQlite for the database.

# <rails-lens:schema:begin>
# table = "posts"
# database_dialect = "SQLite"
# rest of annotation ommitted for brevity
# <rails-lens:schema:end>

The annotation also lets us know about the columns and indexes of the Posts table under the columns and indexes sections. Each of the columns of the Post table in the annotation is displayed with either nullable = false or nullable = true highlighting whether for the value of a given column can be null or not at the time of saving a post in the database.

# columns = [
#   { name = "id", type = "integer", primary_key = true, nullable = false },
#   { name = "title", type = "text", nullable = true },
#   { name = "body", type = "text", nullable = true },
#   { name = "category_id", type = "integer", nullable = false },
#   { name = "user_id", type = "integer", nullable = false },
#   { name = "created_at", type = "datetime", nullable = false },
#   { name = "updated_at", type = "datetime", nullable = false }
# ]
#
# indexes = [
#   { name = "index_posts_on_user_id", columns = ["user_id"] },
#   { name = "index_posts_on_category_id", columns = ["category_id"] }
# ]

The foreign_keys section of the annotation highlights the associations between the Post model and other models in the application namely the User and Category.

We get extra notes as part of the annotation as well and this is where rails-lens shines compared to other annotation gems.

As part of the output you can see that rails-lens notified us of some ways in which the schema and therefore our application could use some improvements. This includes the N+1 query risk associated with the Comment model.

# - Association 'comments' has N+1 query risk. Consider using includes/preload

Also you might remember that aas part of the columns section of the annotation, the title and body columns had nullable set to true which means that its possible to create a post without a title or a body. This is not ideal as we would want every post within the application to have a title and a body. Rails lens lets us know about this as well.

# - Column 'title' should probably have NOT NULL constraint
# - Column 'body' should probably have NOT NULL constraint

And now that we know about it, we can proceed to address it whether by adding a validation or any other approach.

Lastly Rails lens lets us know of the potential impacts on performance we many run into if we have so many records in the application and it doesn’t stop at that, it tells us what could potentially cause performance problems and what we can do to solve this.

# - Large text column 'title' is frequently queried - consider separate storage
# - Large text column 'body' is frequently queried - consider separate storage

And there you have it, we were able to get information about the models in the application, all without constantly going back and forth between the model files and the schema.rb file.

The annotated Comment, User and Category models are attached below.

# <rails-lens:schema:begin>
# table = "users"
# database_dialect = "SQLite"
#
# columns = [
#   { name = "id", type = "integer", primary_key = true, nullable = false },
#   { name = "name", type = "string", nullable = true },
#   { name = "email", type = "string", nullable = true },
#   { name = "created_at", type = "datetime", nullable = false },
#   { name = "updated_at", type = "datetime", nullable = false }
# ]
#
# == Notes
# - Column 'name' should probably have NOT NULL constraint
# - Column 'email' should probably have NOT NULL constraint
# - String column 'name' has no length limit - consider adding one
# - String column 'email' has no length limit - consider adding one
# - Column 'email' is commonly used in queries - consider adding an index
# <rails-lens:schema:end>


class User < ApplicationRecord
end
# <rails-lens:schema:begin>
# table = "comments"
# database_dialect = "SQLite"
#
# columns = [
#   { name = "id", type = "integer", primary_key = true, nullable = false },
#   { name = "content", type = "text", nullable = true },
#   { name = "post_id", type = "integer", nullable = false },
#   { name = "created_at", type = "datetime", nullable = false },
#   { name = "updated_at", type = "datetime", nullable = false }
# ]
#
# indexes = [
#   { name = "index_comments_on_post_id", columns = ["post_id"] }
# ]
#
# foreign_keys = [
#   { column = "post_id", references_table = "posts", references_column = "id" }
# ]
#
# == Notes
# - Association 'post' should specify inverse_of
# - Column 'content' should probably have NOT NULL constraint
# - Large text column 'content' is frequently queried - consider separate storage
# <rails-lens:schema:end>


class Comment < ApplicationRecord
  belongs_to :post
end
# <rails-lens:schema:begin>
# table = "categories"
# database_dialect = "SQLite"
#
# columns = [
#   { name = "id", type = "integer", primary_key = true, nullable = false },
#   { name = "name", type = "string", nullable = true },
#   { name = "created_at", type = "datetime", nullable = false },
#   { name = "updated_at", type = "datetime", nullable = false }
# ]
#
# == Notes
# - Column 'name' should probably have NOT NULL constraint
# - String column 'name' has no length limit - consider adding one
# <rails-lens:schema:end>


class Category < ApplicationRecord
end

Also, its possible to annotate specific model files using the command below;

bundle exec rails_lens annotate --model Post Comment

To remove the annotations, run the command below:

bundle exec rails_lens remove