The Most Common Mistakes in Ruby on Rails Web Application Development

Ruby / Ruby on Rails
Oleksandr Vykhor
41
02-07-2020 16:24:00


Ruby on Rails (or simply “Rails”) is a popular open-source framework based on the Ruby programming language that aims to simplify and streamline the web development process.

Rails is built on the principle of convention over configuration. Put simply, this means that by default Rails assumes that expert developers will follow “standard” best-practice conventions (for things like naming, code structure and so on), and if they do, everything will work “auto-magically” without them needing to specify these details. While this paradigm has its advantages, it is not without its pitfalls. In particular, the “magic” that happens behind the scenes in the framework can sometimes lead to headaches, confusion, and “what the heck is going on?” kinds of problems. It can also have undesirable consequences for 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.

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 seen 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.

Mistake 2. Putting too much logic in the view

ERB, the out-of-the-box Rails templating engine, offers great opportunities for building pages with variable content. However, if you’re not careful, you can end up with a large file that is a mix of HTML and Ruby code that is hard to manage and maintain. It can also lead to a lot of repetition, resulting in violations of DRY (don’t repeat yourself) principles.

This can manifest itself in a number of ways. One is excessive use 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, conditional logic structures like this will end up 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 let you replace the previous view code with a single line:

<h3>Welcome, <%= current_user.name -%></h3>

A couple of additional Rails recommendations:

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 to this object to perform logical operations that might otherwise have ended up in your view code.

Mistake 3. Putting too much logic in the model

Given the guidance to minimize logic in views and controllers, the only place left in an MVC architecture to put all that logic would be the model, right?

Well, not quite.

Many Rails developers actually make this mistake and end up stuffing everything into 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 with external services, converting data to other formats and the like doesn’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, the controllers, or the models — then where should it go?

Let’s talk about “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 ActiveRecord model.

So with that in mind, generally speaking, the only logic that should remain in your model is:

ActiveRecord configuration (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_name method that combines the first_name and last_name fields in the database).

Sophisticated queries (i.e., more complex than a simple find). Generally speaking, you should never use this method, or any other query-building method, outside of the model class itself.

Mistake 4. Using generic helper classes as a dumping ground

This mistake is really sort of a corollary of mistake 3. 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 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. It becomes all too tempting to start stuffing in functionality that doesn’t formally fit into the model, view, or controller.

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 Rails is no excuse to let object-oriented design best practices go by the wayside.

Mistake 5. Using too many gems

Ruby on Rails is 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 applications where the number of gems was disproportionately large compared to 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 more server memory, which increases operating costs. It also takes longer to start such an application, 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 depend on other gems, and those may depend on others, and so on. Adding gems can therefore have a compounding effect. For example, adding the rails_admin gem will bring in more than 11 additional gems, a more than 10% increase over 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 what is included in the 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 each time you add a new gem. For example, developers often casually add the rails_admin gem because it 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.

Mistake 6. Ignoring your log files

While most Rails developers are aware of the default log files available during development, they don’t pay much 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 earlier in this tutorial, the Rails framework does a lot of “magic” for you, especially in models. Defining associations makes it very easy to pull in relations between tables and display everything in your views. All the SQL needed is generated automatically. But how do you know the generated SQL is efficient?

One example you will run into often 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 example, you have the following query in a typical blog application where you display 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 for the request that called this method, we see something like the following: one query fetches the three posts, and then three more queries fetch the comments for each of them:

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) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Comment Load (1.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Rendered posts/some_comments.html.erb within layouts/application (12.5ms)
Completed 200 OK in 581ms (Views: 225.8ms | ActiveRecord: 10.0ms)

Active Record 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 includes (or preload) method on the Arel (ActiveRecord::Relation) object being built. With includes, ActiveRecord ensures that all of the specified associations are loaded using the minimum possible number of queries, for example:

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 code above 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.

The N+1 problem is really just one example of the many kinds of inefficiencies that can lurk “under the hood” of your application if you aren’t paying adequate attention. The takeaway from this item is that you should check your development and test log files during development to spot (and fix!) inefficiencies in the code that builds your queries.

Reviewing log files is a great way to find inefficient code and fix it before your application goes into production. Otherwise, you can’t be sure of your system’s performance until it goes “live” — and the database you worked with during development and testing is likely to be much smaller than the one in production. If you’re building a new application, even if your production database starts out small and everything seems to run fine at first, it will grow, and problems like the one described in this example will make the system slower and slower.

If you find that your log files are cluttered with information you don’t need, you can clean them up.

Mistake 7. Lack of automated tests

Ruby on Rails provides powerful automated testing capabilities by default. Many Rails developers write very sophisticated tests using TDD and BDD styles and make use of even more powerful test frameworks provided by gems (rspec and cucumber).

Despite how easy it is to add automated tests to your application, I have been very surprised by how many projects I’ve inherited or joined where there were literally no tests written (or, at best, very few). 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 developers will most likely want to extend or modify the code, or upgrade the version of Ruby on Rails, and a testing framework will give them a clear way of verifying that the basic functionality of the application still works. An additional benefit is that it gives future developers a clear picture of the full range of functionality the application provides.

Mistake 8. Blocking on calls to external services

Third-party services for Rails applications are usually very easy to integrate into your application via gems that wrap their APIs. But what happens if your external service starts failing or becomes very slow?

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 a background job whenever possible. Some popular gems for this are:

Delayed job

Resque

Sidekiq

In cases where it is impractical or infeasible to delegate processing to a background job, you need to make sure your application has sufficient error handling and fail-over provisions for when the external service goes down or runs into problems. You should also test your application without the external service (perhaps by removing the server your application runs on from the network) to verify that it doesn’t lead to any unanticipated consequences.

Mistake 9. Getting married to existing database migrations

The database migration mechanism in Rails lets you create instructions to automatically add and remove tables and rows. Since the files containing these migrations are named sequentially, you can replay them from the beginning of time to bring an empty database up to the same schema as production. This is a great way to manage granular changes to your application’s database schema and avoid Rails problems.

While this works well at the beginning of a 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 mistake is to check a new migration into your source repository but not the correspondingly updated schema.rb file.

When migrations have gotten out of hand and take too long to run, developers shouldn’t be afraid to clear out the old migrations directory, dump a new schema, and continue from there. Setting up a new development environment would then require running rake db:schema:load rather than rake db:migrate, which most developers rely on.

Some of these issues are also discussed in the Rails Guide.

Mistake 10. Checking sensitive information into source code repositories

The Rails framework makes it easy to create secure applications that are protected against many kinds of attacks. Some of this is accomplished using a secret token to secure sessions with the browser. Even though this token is now stored in config/secrets.yml, and that file reads the token from an environment variable on production servers, past versions of Rails included the token in config/initializers/secret_token.rb. This file is often mistakenly checked into the source code repository along with the rest of your application. If that happens, anyone with access to the repository can easily compromise all users of your application.

Therefore, you should make sure your repository configuration file (e.g., .gitignore for git users) excludes the file containing your token. Your production servers can then pick up their token from an environment variable or from a mechanism like the one provided by the dotenv gem.

Summary

Rails is a powerful framework that hides a lot of the ugly details needed to build a robust web application. And while Rails makes web application development much faster, developers should pay attention to potential design and coding mistakes to make sure 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, or less secure. It is important to study the framework and make sure you fully understand the architecture, design, and coding tradeoffs you’re making throughout the development process. This will help you build a high-quality, high-performance application.

Based on materials from: 

mkechinov.ru

toptal.com


Back