Rails’ conventions become so instinctual in engineers as they develop that they oftentimes copy the same code and design patterns not realising that there are a whole bunch of ways to do things that are either more performant, or cleaner and easier to maintain.
If you look a bit harder you can uncover a world of “better” solutions to do many commonly needed functions.

ActiveRecord’s #missing and #associated
Did you know you can easily query models that have either have zero or at least one of the specified association?
# (6.1) Gets all posts that have no comments and no tags.
Post.where.missing(:comments, :tags)
# (7.0) Gets all posts that have at least 1 comment
Post.where.associated(:comments)
ActiveRecord greater and less than using infinity range
As long as you are using Rails 5.0+ and Ruby 2.6, you can use the (infinity) range object for less than and greater than in an ActiveRecord relation?
# (5.0) Returns all users created in the last day.
User.where(created_at: 1.day.ago..)
# (5.0) Returns all users with less than 10 login attempts.
User.where(login_attempts: ..10)
ActionPack Variant to dynamically render different layouts
This one blew my mind!
Sometimes you want to use a different view layout, for example regular users use one layout, and admins use another. Request variants do exactly that!
# (4.1) ActionPack variants
class DashboardController < ApplicationController
def show
request.variant = current_user.admin? ? :admin : :regular
end
end
# If admin, uses: app/view/dashboards/show.html+admin.erb
# If not, uses: app/view/dashboards/show.html+regular.erb
Using #scoped and #none
Sometimes you need to either return an ActiveRecord relation object that represents all the records of a model, or perhaps no records at all. This can be done with the #scoped
and #none
. Historically, ‘none’ has been simulated by returning an empty array, but using an array causes problems, and means that you cannot guarantee that the returned value (for example with the sample below) will respond to the same method signatures that the other pathways will do. This is just better object-oriented design.
def search(query)
case query
when :all
scoped
when :published
where(:published => true)
when :unpublished
where(:published => false)
else
none
end
end
Why is my query slow? Use #to_sql
and #explain
Sometimes ActiveRecord relations do not always act the way you expect them to, or sometimes, you need to verify that the database queries are using the correct indices. Check that your hard-fought struggles with your ActiveRecord relations are generating the SQL (and database behaviour you envision).
# Output the SQL the relation will generate.
Post.joins(:comments).to_sql
# Output the database explain text for the query.
Post.joins(:comments).explain
Filtering ActiveRecord results with merge
I really cannot believe that this isn’t covered (or at least if it is, I haven’t seen it) in any of the default documentation, nor in any book or guide I’ve. It is completely bewildering since its an incredibly common usage pattern and hardly anyone knows about it. It lets you join onto a named scope, filtered by the result of that named scope.
class Post < ApplicationRecord
# ...
# Returns all the posts that have unread comments.
def self.with_unread_comments
joins(:comments).merge(Comment.unread)
end
end
Multiple variable assignment using the splat *
operator
One thing that everyone should know about is using the splat operator on objects other than arrays.
match, text, number = *"Something 981".match(/([A-z]*) ([0-9]*)/)
# match = "Something 981"
# text = "Something"
# number = 981
other examples include:
a, b, c = *('A'..'Z')
Job = Struct.new(:name, :occupation)
tom = Job.new("Tom", "Developer")
name, occupation = *tom
(thanks to slack-overflow community wiki for this one)
Asynchronous Querying
Rails 7.0 introduced #load_async
that loads ActiveRecord relations (queries) in background threads. This can dramatically improve performance which you need to load several unrelated queries in the controller.
def PostsController
def index
@posts = Post.load_async
@categories = Category.load_async
end
end
In Rails 6.0 (or less) the queries above took 200ms each, the controller would take 400ms to execute these serially. With #load_async
in Rails 7 the equivalent code would only take as long as the longest query!
Stream Generated Files from Controller Actions
send_stream
in Rails 7.0 lets you stream data to the client from the controller for data being generated on the fly. Previously this had to be buffered (or stored in a tempfile) and then used send_data
to transmit the data. This will be awesome for SSE and long-polling options in Rails.
send_stream(filename: "subscribers.csv") do |stream|
stream.write "email_address,updated_at\n"
@subscribers.find_each do |subscriber|
stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
end
end
find_each
is an oldie but a goodie that is massively under-used!
Finally, stop using #each
to iterate over large numbers of records. When you use #each
Active Record runs the entire query, instantiates ALL the objects needed for the query and populates their attributes from the result set. If the query returns a LOT of data, this process is slow and more importantly, uses a tonne of memory. Instead when you know there are going to be 100s or 1000s of results, use #find_each
to only load the object in batches of (by default) 1000 records (you can change this on each usage). Here is an example:
Book.where(:published => true).find_each do |book|
puts "Do something with #{book.title} here!"
end