Earlier this week I was given a pretty straightforward user story of figuring out how many Answers a given Question had received.
Wrote my acceptance tests and then was surprised at how complicated this was to actually implement. After devouring the Active Record documentation in hopes of finding a method that would leverage the two models associations I ended up bootstrapping a method to get it to work:
# in Question Model:
def self.unanswered
unanswered = []
Question.all.each do |q|
if q.answers.count == 0
unanswered << q
end
end
unanswered
end
I knew that I hated this method and it had N + 1 written all over it. After discussing solutions with one of our Senior team members we ended up writing this gnarly SQL statement to find the id
's of all the questions with no answers associated to them.
Finally, we decided it would be best to just implement a Counter Cache on the Question model. Here’s how to do that:
class AddAnswerCountToQuestions < ActiveRecord::Migration
class Question < ActiveRecord::Base
has_many :answers
end
class Answer < ActiveRecord::Base
belongs_to :question, counter_cache: true
end
def up
add_column :questions, :answers_count, :integer, null: false, default: 0
Question.pluck(:id).each do |id|
Question.reset_counters(id, :answers)
end
end
def down
remove_column :questions, :answers_count
end
end
answers_count
column to the Questions model so that everytime an answer gets created for a question that will go up by 1. It also sets a default value of 0 since when a Question gets created it has no answers yet.answers_count
column to 0 so we don’t have any issues with legacy questions.In our migration we adjusted the belongs_to
method to accept the counter_cache: true
but that was just for our dummmy models in the migration. Now we need to change our actual models like this:
class Answer < ActiveRecord::Base
belongs_to :user
belongs_to :question, counter_cache: true
end
class Question < ActiveRecord::Base
has_many :answers
end
The last thing to finish the original user story is to refactor that original method to implement the counter cache. Here is how I implemented it:
def self.unanswered
where(answers_count: 0)
end
Much, much cleaner than the original method!
I love teaching and writing new content but sometimes find it hard to justify.
Getting your email motivates me to spend more time creating awesome content and notifies you when new posts or screencasts come out.
I will never share your email or spam. Expect less than 5 emails a year and feel free to unsubscribe at any time.