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.