Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Ruby on Rails, how can I create a scope for a has_many relationship?

Here is an example:

Let says I have a Student object which has a has_many relationship with ReportCard objects. ReportCard objects have a boolean field called "graded" that flags they have been graded. So it looks like:

class Student < ActiveRecord
  has_many :report_cards
end

class ReportCard < ActiveRecord
  # graded :boolean (comes from DB)
  belongs_to :student
end

Now, let's say you want to create a default scope so that if a student has no graded ReportCards, you want to see all of them, but if they have at least one graded ReportCard, you only want to see the graded ones. Finally, let's say you order them by "semester_number".

Using this scope on ReportCard works correctly:

scope :only_graded_if_possible, ->(student) { where(graded: true, student: student).order(:semester_number).presence || order(:semester_number) }

But I want it to be the default scope for Student so I tried:

class Student < ActiveRecord
  has_many :report_cards, ->{ where(graded: true).order(:semester_number).presence || order(:semester_number) }
end

but this does not work. It won't return any report_cards if there is a single graded report_card in the whole db. Looking at the queries that are run, first it runs something like:

SELECT report_cards.* FROM report_cards WHERE reports_cards.graded = t ORDER BY semester_number ASC

I think this must be the present? check part of the presence query and notice it does not filter on Student at all! So if there is a single report_card that is graded, the check passes and then it runs the following query to get what to return:

SELECT report_cards.* FROM reports_card WHERE report_card.student_id = 'student id here' AND report_card.graded = t ORDER BY semester_number

This query actually would be correct if the student had a graded report card but it is always empty if he does not.

I assume that possibly the filtering on Student is added afterwards. So I tried to somehow to get it to filter student right off the bat:

has_many :report_cards, ->{ where(graded: true, student: self).order(:semester_number).presence || order(:semester_number) }

This does not work either because it appears that "self" in this scope is not the Student object like I'd assume, but a list of all the report_card ids. Here is the query this one runs:

SELECT report_cards.* FROM report_cards WHERE report_cards.graded = t AND report_cards.student_id IN (SELECT report_cards.id FROM report_cards) ORDER BY semester_number ASC

That isn't even close to correct. How can I get this to work?

I think what it really comes down to is someone being able to pass "self" (meaning the current Student object) as a parameter into the scope being applied in the "has_many". Maybe that isn't possible.

like image 871
Mike Avatar asked Jan 10 '16 01:01

Mike


2 Answers

You can pass object to has_many scope as a parameter to lambda

has_many :report_cards, -> (student) { ... }
like image 127
Oleg Avatar answered Nov 19 '22 17:11

Oleg


Try this:

class Student < ActiveRecord::Base
  has_many :report_cards, ->{ where(graded: true).order(:semester_number).presence || unscoped.order(:semester_number) }
end
like image 41
Marat Amerov Avatar answered Nov 19 '22 19:11

Marat Amerov