13 min read
Rails is built on the principle of convention over configuration. Simply put, this means that, by default, Rails assumes that its expert developers will follow “standard” best practice conventions (for things like naming, code structure, and so on) and, if you do, things will work for you “auto-magically” without your needing to specify these details. While this paradigm has its advantages, it is also not without its pitfalls. Most notably, the “magic” that happens behind the scenes in the framework can sometimes lead to headfakes, confusion, and “what the heck is going on?” types of problems. It can also have undesirable ramifications with regard to security and performance.
Accordingly, while Rails is easy to use, it is also not hard to misuse. This tutorial looks at 10 common Rails problems, including how to avoid them and the issues that they cause.
Common Mistake #1: Putting too much logic in the controller
Rails is based on an MVC architecture. In the Rails community, we’ve been talking about fat model, skinny controller for a while now, yet several recent Rails applications I’ve inherited violated this principle. It’s all too easy to move view logic (which is better housed in a helper), or domain/model logic, into the controller.
The problem is that the controller object will start to violate the single responsibility principle making future changes to the code base difficult and error-prone. Generally, the only types of logic you should have in your controller are:
- Session and cookie handling. This might also include authentication/authorization or any additional cookie processing you need to do.
- Model selection. Logic for finding the right model object given the parameters passed in from the request. Ideally this should be a call to a single find method setting an instance variable to be used later to render the response.
- Request parameter management. Gathering request parameters and calling an appropriate model method to persist them.
- Rendering/redirecting. Rendering the result (html, xml, json, etc.) or redirecting, as appropriate.
While this still pushes the limits of the single responsibility principle, it’s sort of the bare minimum that the Rails framework requires us to have in the controller.
Common Mistake #2: Putting too much logic in the view
The out-of-the-box Rails templating engine, ERB, is a great way to build pages with variable content. However, if you’re not careful, you can soon end up with a large file that is a mix of HTML and Ruby code that can be difficult to manage and maintain. This is also an area that can lead to lots of repetition, leading to violations of DRY (don’t repeat yourself) principles.
This can manifest itself in a number of ways. One is overuse of conditional logic in views. As a simple example, consider a case where we have a
current_user method available that returns the currently logged in user. Often, there will end up being conditional logic structures like this in view files:
<h3> Welcome, <% if current_user %> <%= current_user.name %> <% else %> Guest <% end %> </h3>
A better way to handle something like this is to make sure the object returned by
current_user is always set, whether someone is logged in or not, and that it answers the methods used in the view in a reasonable way (sometimes referred to as a null object). For instance, you might define the
current_user helper in
app/controllers/application_controller like this:
require 'ostruct' helper_method :current_user def current_user @current_user ||= User.find session[:user_id] if session[:user_id] if @current_user @current_user else OpenStruct.new(name: 'Guest') end end
This would then enable you to replace the previous view code example with this one simple line of code:
<h3>Welcome, <%= current_user.name -%></h3>
A couple of additional recommended Rails best practices:
- Use view layouts and partials appropriately to encapsulate things that are repeated on your pages.
- Use presenters/decorators like the Draper gem to encapsulate view-building logic in a Ruby object. You can then add methods into this object to perform logical operations that you might otherwise have put into your view code.
Common Mistake #3: Putting too much logic in the model
Given the guidance to minimize the logic in views and controllers, the only place left in an MVC architecture to put all that logic would be in the models, right?
Well, not quite.
Many Rails developers actually make this mistake and end up sticking everything in their
ActiveRecord model classes leading to mongo files that not only violate the single responsibility principle but are also a maintenance nightmare.
Functionality such as generating email notifications, interfacing to external services, converting to other data formats and the like don’t have much to do with the core responsibility of an
ActiveRecord model which should be doing little more than finding and persisting data in a database.
So if the logic shouldn’t go in the views, and it shouldn’t go in the controllers, and it shouldn’t go in the models, well then, where should it go?
Enter plain old Ruby objects (POROs). With a comprehensive framework like Rails, newer developers are often reluctant to create their own classes outside of the framework. However, moving logic out of the model into POROs is often just what the doctor ordered to avoid overly complex models. With POROs, you can encapsulate things like email notifications or API interactions into their own classes rather than sticking them into an
So with that in mind, generally speaking, the only logic that should remain in your model is:
ActiveRecordconfiguration (i.e., relations and validations)
- Simple mutation methods to encapsulate updating a handful of attributes and saving them in the database
- Access wrappers to hide internal model information (e.g., a
full_namemethod that combines
last_namefields in the database)
- Sophisticated queries (i.e., that are more complex than a simple
find); generally speaking, you should never use the
wheremethod, or any other query-building methods like it, outside of the model class itself
Common Mistake #4: Using generic helper classes as a dumping ground
This mistake is really sort of a corollary to mistake #3 above. As discussed, the Rails framework places an emphasis on the named components (i.e., model, view, and controller) of an MVC framework. There are fairly good definitions of the kinds of things that belong in the classes of each of these components, but sometimes we might need methods that don’t seem to fit into any of the three.
Rails generators conveniently build a helper directory and a new helper class to go with each new resource we create. It becomes all too tempting, though, to start stuffing any functionality that doesn’t formally fit into the model, view, or controller into these helper classes.
While Rails is certainly MVC-centric, nothing prevents you from creating your own types of classes and adding appropriate directories to hold the code for those classes. When you have additional functionality, think about which methods group together and find good names for the classes that hold those methods. Using a comprehensive framework like Rails is not an excuse to let good object oriented design best practices go by the wayside.
Common Mistake #5: Using too many gems
Ruby and Rails are supported by a rich ecosystem of gems that collectively provide just about any capability a developer can think of. This is great for building up a complex application quickly, but I’ve also seen many bloated applications where the number of gems in the application’s
Gemfile is disproportionately large when compared with the functionality provided.
This causes several Rails problems. Excessive use of gems makes the size of a Rails process larger than it needs to be. This can slow down performance in production. In addition to user frustration, this can also result in the need for larger server memory configurations and increased operating costs. It also takes longer to start larger Rails applications, which makes development slower and makes automated tests take longer (and as a rule, slow tests simply don’t get run as often).
Bear in mind that each gem you bring into your application may in turn have dependencies on other gems, and those may in turn have dependencies on other gems, and so on. Adding other gems can thus have a compounding effect. For instance, adding the
rails_admin gem will bring in 11 more gems in total, over a 10% increase from the base Rails installation.
As of this writing, a fresh Rails 4.1.0 install includes 43 gems in the
Gemfile.lock file. This is obviously more than is included in
Gemfile and represents all the gems that the handful of standard Rails gems bring in as dependencies.
Carefully consider whether the extra overhead is worthwhile as you add each gem. As an example, developers will often casually add the
rails_admin gem because it essentially provides a nice web front-end to the model structure, but it really isn’t much more than a fancy database browsing tool. Even if your application requires admin users with additional privileges, you probably don’t want to give them raw database access and you would be better served by developing your own more streamlined administration function than by adding this gem.
Common Mistake #6: Ignoring your log files
While most Rails developers are aware of the default log files available during development and in production, they often don’t pay enough attention to the information in those files. While many applications rely on log monitoring tools like Honeybadger or New Relic in production, it is also important to keep an eye on your log files throughout the process of developing and testing your application.
As mentioned previously in this tutorial, the Rails framework does a lot of “magic” for you, especially in the models. Defining associations in your models makes it very easy to pull in relations and have everything available to your views. All the SQL needed to fill up your model objects is generated for you. That’s great. But how do you know that the SQL being generated is efficient?
One example you will often run in to is called the N+1 query problem. While the problem is well understood, the only real way to observe it happening is to review the SQL queries in your log files.
Say for instance you have the following query in a typical blog application where you will be displaying all of the comments for a select set of posts:
def comments_for_top_three_posts posts = Post.limit(3) posts.flat_map do |post| post.comments.to_a end end
When we look at the log file of a request that calls this method we’ll see something like the following, where a single query is made to get the three post objects then three more queries are made to get each of those objects’ comments:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:13 -0700 Processing by PostsController#some_comments as HTML Post Load (0.4ms) SELECT "posts".* FROM "posts" LIMIT 3 Comment Load (5.6ms) ELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]] Comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]] Comment Load (1.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 3]] Rendered posts/some_comments.html.erb within layouts/application (12.5ms) Completed 200 OK in 581ms (Views: 225.8ms | ActiveRecord: 10.0ms)
ActiveRecord’s eager loading capability in Rails makes it possible to significantly reduce the number of queries by letting you specify in advance all the associations that are going to be loaded. This is done by calling the
preload) method on the Arel (
ActiveRecord::Relation) object being built. With
ActiveRecord ensures that all of the specified associations are loaded using the minimum possible number of queries; e.g.:
def comments_for_top_three_posts posts = Post.includes(:comments).limit(3) posts.flat_map do |post| post.comments.to_a end end
When the above revised code is executed, we see in the log file that all of the comments were collected in a single query instead of three:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:18 -0700 Processing by PostsController#some_comments as HTML Post Load (0.5ms) SELECT "posts".* FROM "posts" LIMIT 3 Comment Load (4.4ms) SELECT "comments".* FROM "comments" WHERE"comments "."post_id" IN (1, 2, 3) Rendered posts/some_comments.html.erb within layouts/application (12.2ms) Completed 200 OK in 560ms (Views: 219.3ms | ActiveRecord: 5.0ms)
Much more efficient.
This solution to the N+1 problem is really only meant as an example of the kind of inefficiencies that can exist “under-the-hood” in your application if you’re not paying adequate attention. The takeaway here is that you should be checking your development and test log files during development to check for (and address!) inefficiencies in the code that builds your responses.
Reviewing log files is a great way to be tipped off to inefficiencies in your code and to correct them before your application goes into production. Otherwise, you may not be aware of a resulting Rails performance issue until your system goes live, since the dataset you work with in development and test is likely to be much smaller than in production. If you’re working on a new app, even your production dataset may start out small and your app will look like it’s running fine. However, as your production dataset grows, Rails problems like this will cause your application to run slower and slower.
If you find that your log files are clogged up with a bunch of information you don’t need here are some things you can do to clean them up (the techniques there work for development as well as production logs).
Common Mistake #7: Lack of automated tests
Ruby and Rails provide powerful automated test capabilities by default. Many Rails developers write very sophisticated tests using TDD and BDD styles and make use of even more powerful test frameworks with gems like rspec and cucumber.
Despite how easy it is to add automated testing to your Rails application, though, I have been very unpleasantly surprised by how many projects I’ve inherited or joined where there were literally no tests written (or at best, very few) by the prior development team. While there is plenty of debate about how comprehensive your testing should be, it is pretty clear that at least some automated testing should exist for every application.
As a general rule of thumb, there should be at least one high-level integration test written for each action in your controllers. At some point in the future, other Rails developers will most likely want to extend or modify the code, or upgrade a Ruby or Rails version, and this testing framework will provide them with a clear way of verifying that the basic functionality of the application is working. An added benefit of this approach is that it provides future developers with a clear delineation of the full collection of functionality provided by the application.
Common Mistake #8: Blocking on calls to external services
3rd party providers of Rails services usually make it very easy to integrate their services into your application via gems that wrap their APIs. But what happens if your external service has an outage or starts running very slowly?
To avoid blocking on these calls, rather than calling these services directly in your Rails application during the normal processing of a request, you should move them to some sort of background job queuing service where feasible. Some popular gems used in Rails applications for this purpose include:
In cases where it is impractical or infeasible to delegate processing to a background job queue, then you will need to make sure that your application has sufficient error handling and fail-over provisions for those inevitable situations when the external service goes down or is experiencing problems. You should also test your application without the external service (perhaps by removing the server your application is on from the network) to verify that it doesn’t result in any unanticipated consequences.
Common Mistake #9: Getting married to existing database migrations
Rails’ database migration mechanism allows you to create instructions to automatically add and remove database tables and rows. Since the files that contain these migrations are named in a sequential fashion, you can play them back from the beginning of time to bring an empty database to the same schema as production. This is therefore a great way to manage granular changes to your application’s database schema and avoid Rails problems.
While this certainly works well at the beginning of your project, as time goes on, the database creation process can take quite a while and sometimes migrations get misplaced, inserted out of order, or introduced from other Rails applications using the same database server.
Rails creates a representation of your current schema in a file called
db/schema.rb (by default) which is usually updated when database migrations are run. The
schema.rb file can even be generated when no migrations are present by running the
rake db:schema:dump task. A common Rails mistake is to check a new migration into your source repo but not the correspondingly updated
When migrations have gotten out of hand and take too long to run, or no longer create the database properly, developers should not be afraid to clear out the old migrations directory, dump a new schema, and continue on from there. Setting up a new development environment would then require a
rake db:schema:load rather than the
rake db:migrate that most developers rely on.
Some of these issues are discussed in the Rails Guide as well.
Common Mistake #10: Checking sensitive information into source code repositories
The Rails framework makes it easy to create secure applications impervious to many types of attacks. Some of this is accomplished by using a secret token to secure a session with a browser. Even though this token is now stored in
config/secrets.yml, and that file reads the token from an environment variable for production servers, past versions of Rails included the token in
config/initializers/secret_token.rb. This file often mistakenly gets checked into the source code repository with the rest of your application and, when this happens, anyone with access to the repository can now easily compromise all users of your application.
You should therefore make sure that your repository configuration file (e.g.,
.gitignore for git users) excludes the file with your token. Your production servers can then pick up their token from an environment variable or from a mechanism like the one that the dotenv gem provides.
Rails is a powerful framework that hides a lot of the ugly details necessary to build a robust web application. While this makes Rails web application development much faster, developers should pay attention to the potential design and coding errors, to make sure that their applications are easily extensible and maintainable as they grow.
Developers also need to be aware of issues that can make their applications slower, less reliable, and less secure. It’s important to study the framework and make sure that you fully understand the architectural, design, and coding tradeoffs you’re making throughout the development process, to help ensure a high quality and high performance application.