In my app a Position has_many Questions. In my position edit view, I create a set of checkboxes by looping over an array of Questions and checking those that the current position already includes.
Looks like this:
- @questions.each do |question|
= check_box_tag :question_ids, question.id, @position.questions.include?(question), name: 'position[question_ids][]', id: "question_check_#{question.id}"
= label_tag "question_check_#{question.id}", question.text
I noticed loading this page took an unusually long time and my logs had these (I’ve snipped it for space):
LOG: duration: 827.370 ms statement: SELECT 1 FROM "questions" INNER JOIN "required_questions" ON "questions"."id" = "required_questions"."question_id" WHERE "required_questions"."position_id" IS NULL AND "questions"."id" = 1 ORDER BY account_id desc, "questions".id asc LIMIT 1
LOG: duration: 821.666 ms statement: SELECT 1 FROM "questions" INNER JOIN "required_questions" ON "questions"."id" = "required_questions"."question_id" WHERE "required_questions"."position_id" IS NULL AND "questions"."id" = 2 ORDER BY account_id desc, "questions".id asc LIMIT 1
LOG: duration: 713.379 ms statement: SELECT 1 FROM "questions" INNER JOIN "required_questions" ON "questions"."id" = "required_questions"."question_id" WHERE "required_questions"."position_id" IS NULL AND "questions"."id" = 3 ORDER BY account_id desc, "questions".id asc LIMIT 1
I tracked the problem to this code:
@position.questions.include?(question)
I thought this was a simple Array.include? check so I was surprised by my findings. I was also surprised when I fixed it by adding an explicit .to_a call:
@position.questions.to_a.include?(question)
I was surprised a third time when I tested the original code in the Rails console and no SQL query was generated.
At this point I’m just curious – why did my original code generate a SQL query for each iteration when the same code in Rails console (albeit without the iteration and inside Rails console) did not? Why didn’t it just do an Array.include? check?
This happens because
@position.questionsis not an array. It’s actually anActiveRecord::Relation. The problem is that the console actually behaves differently from your server application.For example, in your console
position.questionsreturns an array. That is because the console is actually evaluating this expression asposition.questions.allwhich is equivalent toposition.questions.to_a. In your server application theto_aorallis not called until you actually use the query. This is a good thing because it means you can continue to build up a query and it will only actually get executed when it’s called.For instance:
Will actually generate two queries that return two records in your server application because the
queryvariable will be assigned anActiveRecord::Relationinstead of anArray. In your console this will generate a single query, but the query will load all of the questions into anArrayand assign that toqueryand then select the first and last elements.The
all,first,last,countand eveninclude?keywords are all triggers that actually execute the query, so in your application when you call@position.questions.include?you are executing a single query on the@position.questionsrelation. Adding theto_acauses this query to be executed immediately.