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

3

11

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.

Anglonorman answered 10/1, 2016 at 1:37 Comment(1)
what version of Rails are you using?Boathouse
S
2

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

has_many :report_cards, -> (student) { ... }
Sparoid answered 10/1, 2016 at 3:58 Comment(5)
That would require changing every time the association is used from: student.report_cards to the redundant student.reports_cards(student) which is something I'm trying to avoid.Anglonorman
Please explain. This doesn't answer my question. Every single reference to student.report_cards will throw an exception because the number of parameters no longer matches.Anglonorman
No, it will not. ActiveRecord relation builder will pass object for you. Just try it.Sparoid
Wow, seems to work so far. I'm really confused as to how it gets the parameter magically. I would have never guessed this would have worked. I tried putting the parameter on a scope in the report_cards object and calling that scope in the has_many with no parameter and that didn't work.Anglonorman
I wonder if this is actually fully supported. When running my rspec tests some of them suddenly started failing and I get the very interesting warning: " DEPRECATION WARNING: The association scope 'report_cards' is instance dependent (the scope block takes an argument). Preloading happens before the individual instances are created. This means that there is no instance being passed to the association scope. This will most likely result in broken or incorrect behavior. Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future."Anglonorman
M
1

Try this:

class Student < ActiveRecord::Base
  has_many :report_cards, ->{ where(graded: true).order(:semester_number).presence || unscoped.order(:semester_number) }
end
Mandrill answered 27/1, 2016 at 22:14 Comment(0)
G
0

I am using this in my project where I have a model which is associated with users:

has_many :users, -> { only_deleted }

And in the Users model create a scope of only_deleted, returning your users which are deleted.

Goosegog answered 28/1, 2016 at 19:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.