Quần Cam

Five Rails Gotchas

It’s undeniable that Rails is a great framework to speedily build up your application. However, despite of its handiness, like other frameworks, Rails has its own flaws and is never a silver bullet. This post is going to show you some of the gotchas (or pitfalls you name it) I encountered while working with Rails.

associations writer

The code below is supposed to assign posts to a specific user.

class User < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
end

user = User.find_by_name 'John'
user.posts = [Post, Post, Post]
user.save!

Of course it works (perfectly)! Then you might ask what’s the problem? Good question. When do you think the posts will get saved?

For a common people with common sense, posts will be persisted when you invoke user.save!, then you’re trapped. Right after user.posts was assigned, the persistence will be invoked, so calling User#save! here is redundant.

after_save callback

I always recommend people not to use Rails callbacks but if you DO have to, use after_commit instead.

A lot of people has written about this, see what Justin Weiss said.

auto-(re)loading

In the example below, if you invoke User.foo without preloading lib/user/bar.rb, your code blows up (probably in development environment) with a constant not found exception.

# app/models/user.rb
class User
  def foo
    Bar.xyz
  end
end

# lib/user/bar.rb
class User
  class Bar
    def xyz
      "foot"
    end
  end
end

But why, didn’t I already define User::Bar under lib directory?

The reason is because Rails treats all missing constants as TOP_LEVEL constants. So when User#foo, since Bar is not yet defined, Rails will look up in app/models/bar.rb and lib/bar.rb. That’s why Bar (of course) cannot be found.

To fix this you can explicitly tell Rails its parent module/class.

# app/models/user.rb
class User
  def foo
    ::User::Bar.xyz
  end
end

eager-loading

How to avoid N+1 query in Rails? Typically this is how many people will do.

User.all.includes(:posts)

But do you know how on earth it works?

First ActiveRecord will fetch all users, then map them to ActiveRecord::Base objects and hold them in memory.

SELECT * FROM `users`;

Then it fetches all posts related to those user IDs, and holds them in memory.

SELECT * FROM `posts` WHERE `posts`.`user_id` = [1, 2, 3, 4, 5, 6, 7, 8, ..., N]

Then it automatically does some “posts-to-user” mapping using Ruby Enumerator.

For instance, here’s the pseudo code of how includes works.

users = User.all
posts = Post.where(user_id: users.pluck(:id))

users.each do |user|
  user.posts = posts.select { |post| post.user_id == user.id }
end

This works seamlessly most of the time, but when you reach 100_000 users with 1_000 posts each, this strategy might not be so efficient, as it consumes up a great deal of memory. (Yeah, if you’re gonna debate how cheap hardware is, go convince your boss!)

Sprockets depend_on

Probably you’re using SASS @import instead of traditional //= require to import CSS sub-files. To know why @import is preferable, see @iain’s comment below.

// app/assets/stylesheets/application.css
@import "users/index";
@import "users/show";
@import "foo";
@import "bar";

But you will find that your application.css is not re-precompiled after you made some changes to the sub-files e.g. users/index, foo, etc.

That is because of Rails Sprockets’ underlying caching mechanism, which is supposed to re-precompile your asset if the file has literally changed. On top of that, the caching framework also observes all dependent files to invalidate cache accordingly. However, the mechanism doesn’t work with SASS’s @import, but its own require directive.

To fix the cache invalidation problem above, you need to explicitly declare which files Sprockets should observe, by using Sprockets offered depend_on directive.

// app/assets/stylesheets/application.css
//= depend_on "users/index";
//= depend_on "users/show";
//= depend_on "foo";
//= depend_on "bar";

@import "users/index";
@import "users/show";
@import "foo";
@import "bar";

Lessons

Good understanding on your tool’s pros/cons is one of the essential things to be a good programmer.

Happing Programming!!!

NGUY HIỂM! KHU VỰC NHIỀU GIÓ!
Khuyến cáo giữ chặt bàn phím và lướt thật nhanh khi đi qua khu vực này.
Chức năng này hỗ trợ markdown và các thứ liên quan.

Bài viết cùng chủ đề

Euruko 2017 Notes

Vừa rồi mình đi Euruko 2017 ở Budapest, một số bài nói cũng khá thú vị nên mình sẽ note lại ở đây.

Bundler Gotcha

A few days ago I encountered a strange behavior of Bundler so this post notes down how my experience with it was.

You don't need RVM gemset

We can’t deny the contribution RVM gemset gave up to the Ruby community, but do we really need gemsets to isolate our project dependencies these days?