Staring at the entirety of Rails docs becomes daunting when you need something specific.
— Anonymous PollEvian
Rails is a huge and powerful framework. It handles everything from database interactions and automatic file loading on the backend to rendering views that are immune to CSRF on the frontend, in ways that are geared toward ergonomic developer experiences. The sheer scope can be daunting to learn, and it will inevitably take time to become familiar with all its nooks and crannies. The Rails guides provide excellent documentation for many of the core features, and experienced Rails devs will sometimes tout the Principle of Least Astonishment as a way to navigate Rails’ immensity, but even this can be overwhelming to someone unfamiliar with Rails. This article details a collection of tips and tricks for working in Rails that you may have missed in your first or second time through the guides.
In this article, I’ll offer tips for interacting with an existing app through the Rails console, debugging code, and eliminating a common Rails performance issue.
The Rails console
The Rails console is an interactive Ruby shell that has access to the full Rails environment. Accessible by running
rails console or
rails c, the console is a great way to explore the interface of an object or the queries generated by a particular method.
To provide access to functionality that is only available in certain contexts, like named route helpers and view helper methods, the Rails console provides the
helper instances. The
app instance has access to all named route helpers, as well as helpers for simulating HTTP requests coming in to the application.
Be aware that
app.get(app.article_path(Article.first)) will render the entire view, so if the view includes assets handled by Webpack it will run through the entire slow Webpack boot process. Additionally, it will not necessarily have the correct credentials, or allow you to view the response. The
app helper is most useful if you want to see what path will be generated by a helper, or if you want to see what controller a given request will hit.
helper instance provides access to Rails’s built-in view helpers and any methods defined in a module in
By default, the Rails console runs with full write access to the database. If you want to test out some code without having to worry about the side effects, you can run the console with the
--sandbox flag, which will wrap your session in a database transaction and roll back all changes when you’re done. Note that ActiveRecord
after_commit hooks will not be triggered in this mode. You can use the safer_rails_console gem to apply sandbox mode by default.
Ruby has a generally wonderful property called “implicit return”: every statement has a return value. In particular, assignment statements return the value that was assigned, which allows for the very useful method pattern
Unfortunately, this means that any assignment you make in the console will print the value being assigned. For large ActiveRecord models, this can take up a lot of screen space and make it hard to keep track of what you were doing.
With the Pry console (see below), you can suppress this output by adding a semicolon to the end of the statement. With IRB, add
nil after the semicolon. You can also replace
nil with some check on the object you assigned.
Showing database queries
ActiveRecord query methods conveniently log the queries they execute at the
debug level, so these queries are visible in development and tests, but hidden in staging and production. This is usually a good thing, since we don’t want a record of every query to fill up our logs. However, if you’re diagnosing a problem in a Rails console in a production-like environment, it can sometimes be helpful to see the queries being made. To re-enable this behavior, reassign ActiveRecord::Base.logger to something that will output in debug mode.
On the other hand, if you just want to see the SQL that would be executed, but don’t need or want to actually run the query, you can call
to_sql on any ActiveRecord relation.
Note that some methods, like
update, force a query to be made, and so you will not be able to use this technique to preview the SQL they will execute. If you do need to know what SQL an expensive or destructive method will execute, you can run the console in sandbox mode with logging for ActiveRecord queries enabled as described above.
In development, the Rails console will use the Pry console, an alternative to IRB with tons of powerful features like syntax coloring and show-source. The two features that I use most often are
cd. Like their Unix counterparts, these commands list what’s available in a given scope and change the current scope, respectively.
One further thing to note are the locals that Pry makes available. Of these, the two most helpful are
__, which reference the returned values from the most recent and second most recent lines, respectively. These are useful if you ran an expensive computation and forgot to put the result in a variable, or if you want to refine the last result.
Pry has many other superpowers, including documentation browsing, method editing, and history navigation. Learn more about them from Pry’s homepage or by running
help in a Pry console.
Of course, Pry and the Rails console are great for exploring the system, but sometimes you also need to debug existing code. Fortunately, they have your back there as well.
prying open your code
To hook into a debugger in Ruby, add
binding.irb if you don’t have pry) to the code anywhere you want to pause and explore execution. At that point, code execution will pause and present a Pry console, which has all the great features mentioned above. If the process is running in a test environment, you will also have access to stack navigation commands like
step (into next method call),
next (line in current stack frame), and
finish (current stack frame). For a full listing of commands, see Pry Byebug.Note that
binding.pry stops the current process and waits for user input. If the code is running in a process that can’t accept input, as with a Rails server run with
foreman start, it will block the entire process, and you won’t be able to get in. In these cases, use
binding.pry_remote instead of
binding.pry. When the process reaches your breakpoint, you will be able to connect to it by running
pry-remote from a terminal. In this context, the stack navigation commands from Byebug are not available, but core Pry functionality like
Pry in method chains
One issue you may encounter is a method chain or pipeline that isn’t returning what you want. In this case, it would be inconvenient to refactor your code into many local variables just for debugging, and then have to undo all the work once you’ve found and fixed the problem. In cases such as these, you can insert
binding.pry into the method chain using Ruby’s
Object#tap, which takes a block, yields the current object to it, and then returns the current object for subsequent chaining.
The “bad” version requires manual work to turn back into the method chain you wanted. The “good” version provides all the same functionality, and can be cleaned up just by deleting lines. In vim, it’s as easy as
Where is this method called from?
Sometimes, you know some code is being executed, but you just can’t figure out how or why. Is it being called from the controller? a service object? a private model method? an ActiveRecord callback? If you need to know how and why some code is being executed, put in a
binding.pry and then use Ruby’s
Kernel#caller to see an array of file locations as strings. The backtrace is often a lot to take in, but since it’s just an array of strings, you can call
grep to whittle it down.
Using global variables
Using global variables is usually frowned upon in application development, and for good reason. However, in testing, and especially in controller testing, global variables are a valuable tool. Ruby’s global variables are any normal variable name preceded by a
$.One simple but common use case is to avoid debugger calls from test setup code, allowing you to focus on the code when it’s actually being tested. In the following example, we’re only interested in the
callback’s behavior within the controller action, but it will be invoked for each of the 100
Foos we create in the setup. With a simple
binding.pry we’d have to
continue through all 100 of these before reaching the one we want, but with the conditional, we will only hit the breakpoint when we’re saving the
Foo from the controller.
A more advanced use case for global variables is when you want to stash data from the test context to compare it against data when the code is actually running. For example, if a test is failing because you
expect(model).to receive(:message) but it never does even though you know the code is being hit, you might wonder if the
model in the code is the same object as the
model in the test. In this case, you can stash the
model.object_id in a global variable, and compare it against the
model.object_id at runtime (
object_id is unique to each separate object in Ruby’s runtime).
One common issue in Rails apps is the N+1 issue: after loading a collection of objects from the database, you loop through them and accidentally make an extra database hit for each object in the collection. This is one of Rails’s sharpest knives, because automatic loading of associations from the database is one of the things that makes ActiveRecord so convenient to get started with, but too many extra database hits can kill performance. It’s even been mentioned in Rails’s 2020 May of WTFs. N+1s can be eliminated by using ActiveRecord’s
includes method (or, if you need finer control,
eager_load), but it’s not always obvious what associations need to be preloaded. For this, we use the Bullet gem in test environments. While Bullet can be configured to detect N+1s across the entire test suite, Poll Everywhere’s Rails App simply has too many tests with real and false positives to be feasible. Instead, we define a helper method
with_n_plus_one_logging to pinpoint N+1s in code that we already suspect to be causing them during development and testing.
I recommend running
rm log/test.log before running this test, or you may find it hard to find what you’re looking for.
The Rails framework and its ecosystem provide a vast array of tools for developing a web application quickly and comfortably, but that same immensity can also make it daunting to approach. I hope that this article will have provided you with some useful tricks that will make it easier for you to dig into Rails.