Tadas Sasnauskas Tech/Engineering Blog

Avoid ActiveRecord model self-save

I’ve seen this, what I’d call anti-pattern, number of times in pretty much every code base I worked with. When controller action updates 1-2 model attributes it is tempting to move #save call into the model itself.

Say we have an article section in some private knowledge base. It started with a generated article scaffold. Here’s the controller code:

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
  end
end

Later we introduced “last visitor” feature. First iteration looked like this:

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    @article.last_seen_by = current_user
    @article.last_seen_at = Time.zone.now
    @article.save
  end
end

But during code review someone suggested moving tracking code somewhere else. We quickly came up with this:

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    @article.mark_last_seen_by(current_user)
  end
end
class Article < ApplicationRecord
  def mark_last_seen_by(user)
    self.last_seen_by = user
    self.last_seen_at = Time.zone.now
    save
  end
end

So far so good until we are asked to implement article view counter:

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    @article.mark_last_seen_by(current_user)
    @article.bump_view_count
  end
end
class Article < ApplicationRecord
  def mark_last_seen_by(user)
    self.last_seen_by = user
    self.last_seen_at = Time.zone.now
    save
  end

  def bump_view_count
    self.view_count += 1
    save
  end
end

And now we have a small eyesore. Every article view makes 2 database update queries. But what would happen if we avoided calling #save from within the model instance itself? Here’s the code:

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    @article.mark_last_seen_by(current_user)
    @article.bump_view_count
    @article.save
  end
end
class Article < ApplicationRecord
  def mark_last_seen_by(user)
    self.last_seen_by = user
    self.last_seen_at = Time.zone.now
  end

  def bump_view_count
    self.view_count += 1
  end
end

With this code article updates are now grouped into a single update query. And attribute changes making methods are now composable.

RubyTapas.com expands on this topic more in episodes #402, #403, #404, #405.

PS: little note about code examples: I cannot show any real world examples here since nearly all the code I worked with is proprietary. Examples here are inspired by the Rails Getting Started guide. Because of this it may sometimes look somewhat contrived / unnatural.