Search code examples
mysqlruby-on-railsrubyruby-on-rails-4activerecord

as_json ignoring second part of .where() clause


I'm trying to produce a json structure from an existing database that contains 5 tables: :users :resources :quiz_questions :quiz_answers :quiz_responses

A resource has many quiz_questions, which has many quiz_answers, and in turn have many quiz_responses. users also has_many quiz_responses. (The idea is that a user takes a multiple choice quiz and they select existing answers which in turn creates a new row in quiz_responses.

So I have two lines of code:

questions = QuizQuestion.includes(:quiz_answers, :quiz_responses).where(resource_id: 623, quiz_responses: {user_id: 18276})

which produces this query:

SELECT 
  `quiz_questions`.`id` AS t0_r0,
  `quiz_questions`.`question` AS t0_r1
  `quiz_questions`.`resource_id` AS t0_r2, 
  `quiz_questions`.`created_at` AS t0_r3, 
  `quiz_questions`.`updated_at` AS t0_r4, 
  `quiz_questions`.`question_type` AS t0_r5, 
  `quiz_questions`.`url` AS t0_r6, 
  `quiz_questions`.`auto_next` AS t0_r7, 
  `quiz_questions`.`show_correct` AS t0_r8, 
  `quiz_questions`.`answer_type` AS t0_r9, 
  `quiz_answers`.`id` AS t1_r0, 
  `quiz_answers`.`answer` AS t1_r1, 
  `quiz_answers`.`quiz_question_id` AS t1_r2, 
  `quiz_answers`.`correct` AS t1_r3, 
  `quiz_answers`.`created_at` AS t1_r4, 
  `quiz_answers`.`updated_at` AS t1_r5, 
  `quiz_answers`.`answer_immediately` AS t1_r6, 
  `quiz_answers`.`time_limit` AS t1_r7, 
  `quiz_responses`.`id` AS t2_r0, 
  `quiz_responses`.`user_id` AS t2_r1, 
  `quiz_responses`.`quiz_answer_id` AS t2_r2, 
  `quiz_responses`.`created_at` AS t2_r3, 
  `quiz_responses`.`updated_at` AS t2_r4, 
  `quiz_responses`.`attempt_id` AS t2_r5, 
  `quiz_responses`.`video_url` AS t2_r6, 
  `quiz_responses`.`correct` AS t2_r7, 
  `quiz_responses`.`group_id` AS t2_r8 
FROM `quiz_questions` 
  LEFT OUTER JOIN `quiz_answers` ON `quiz_answers`.`quiz_question_id` = `quiz_questions`.`id` 
  LEFT OUTER JOIN `quiz_answers` `quiz_answers_quiz_questions_join` ON `quiz_answers_quiz_questions_join`.`quiz_question_id` = `quiz_questions`.`id` 
  LEFT OUTER JOIN `quiz_responses` ON `quiz_responses`.`quiz_answer_id` = `quiz_answers_quiz_questions_join`.`id` 
WHERE 
  `quiz_questions`.`resource_id` = 623 
  AND `quiz_responses`.`user_id` = 18276

The second line of code:

questions.as_json(include: { quiz_answers: { include: [:quiz_responses]}})

invokes these additional queries:

  QuizResponse Load (0.8ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 755

  QuizResponse Load (0.8ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 756

  QuizResponse Load (1.5ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 757

  QuizResponse Load (0.7ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 758

  QuizResponse Load (0.6ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 759

  QuizResponse Load (0.7ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 760

  QuizResponse Load (0.6ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 761

  QuizResponse Load (0.6ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 764

  QuizResponse Load (0.8ms)  SELECT `quiz_responses`.* FROM `quiz_responses`  WHERE `quiz_responses`.`quiz_answer_id` = 765

The intended goal of this code is to get all questions related to resource 867 and then return the possible answers as well as the responses related to a particular user (in this case 18276)

The problem is that while only questions related to resource 867 are returned as_json, all users responses are returned in spite of the where clause in the first line, not just user 18276's responses. Why is this? is there some way I can tell as_json to just use what it receives and return the quiz_responses selected in the initial query? What is the 'rails way' to go about this?


Solution

  • Update: New solution

    Rails does not allow to preload parameterised associations. But it's possible to load all necessary data by two queries and parse it to json:

    # load data
    questions = QuizQuestion.preload(:quiz_answers)
    answers_ids = questions.collect{ |question| question.quiz_answers.collect(&:id) }.flatten
    responses = QuizResponse.where(quiz_answer_id: answers_ids).group_by(&:quiz_answer_id)
    
    # making json
    json_string = Jbuilder.encode do |json|
      json.array! questions do |question|
        json.merge! question.attributes
    
        json.quiz_answers question.quiz_answers do |answer|
          json.merge! answer.attributes
    
          json.quiz_responses responses[answer.id] do |response|
            json.merge! response.attributes
          end
        end
      end
    end
    

    This code will not omit quiz_answer from result, if it has not quiz_response for the user in a question.

    First version:

    The data structure has two quiz_responses associations:

    QuizQuestion has_many :quiz_responses, through: :quiz_answers QuizAnswer has_many :quiz_responses The query includes quiz_responses as a relation of QuizQuestion. But as_json uses quiz_responses as a relation of QuizAnswer. And Rails are not smart enough to understand that necessary quiz_responses already loaded.

    All you need to do is to rewrite includes in a nested way, same as you want to use it:

    questions = QuizQuestion.includes(quiz_answers: :quiz_responses)