Use Rails STI model with join conditions - ruby-on-rails

I have the following models defined:
class Group < ActiveRecord::Base
end
class Person < ActiveRecord::Base
end
class Policeman < Person
end
class Firefighter < Person
end
Inside Group, I would like to get all groups that have Policemen, for example:
class Group < ActiveRecord::Base
has_many :policemen
scope :with_policemen, -> { joins(:policemen).uniq }
end
This works as expected. Now if I want to grab all groups that have a Policeman that has status: 3, I would do:
class Group < ActiveRecord::Base
has_many :policemen
scope :with_policemen, -> { joins(:policemen).where(policemen: { status: 3 }).uniq }
end
But unfortunately this doesn't work, since ActiveRecord constructs the query using policemen table, which obviously doesn't exist. A solution would be to use where(people: { status: 3 }) inside the scope, but I was wondering why can't ActiveRecord put the correct table in the WHERE clause, since it has the necessary associations set.

According to the docs, the format expected for the hash syntax is table_name: { column_name: val }.
scope :with_policemen, -> { joins(:policemen).where(people: { status: 3 }).uniq }
I agree with you - it would make more sense if the where and joins syntax were similar. Another inconsistency - the group method doesn't take a hash, only a string or array.

Related

Need correct wording for scope method seeking specific wording in the object

I cannot figure the correct wording for this scope method. The task is to create a scope method that specifies the requests(which have notes) to display only the requests with the word "kids" in the note . I can't seem to get the right wording for this to work
I have tried scope :requests_with_kids_in_note, -> { where('note').includes(text: "kids")}
scope :requests_with_kids_in_note, -> { select('note').where('note').includes(text: "kids")}
error messages aren't helping
You can do this very easily through scopes, but you should format the where clause differently. Also I'd advise to make it more generic:
scope :note_contains, -> (word) { where('note LIKE %?%', word) }
And use it like this:
Model.note_contains('kids')
You can use Arel to compose that query.
Assuming that you have a Request that have a has_many :notes association, you can write the scope like this:
class Request < ApplicationRecord
has_many :notes
scope :with_kids_in_note, -> {
joins(:notes).where(Note.arel_table[:content].matches('%kids%'))
}
end
class Note < ApplicationRecord
# In this model, i'm assuming that "content" is the column
# in which you have to find the word "kids".
belongs_to :request
end

Rails scope on belongs_to association

Imagine a Book and a Chapter model. Each Chapter belongs_to :book, and a Book has_many :chapters. We have scopes on the Chapter, for example :very_long returns chapters with over 300 pages.
Many times we want to get all books with any chapters over 300 pages. The way we usually achieve this is like so:
# book.rb
scope :has_very_long_chapter, -> { where(id: Chapter.very_long.select(:book_id) }
However, as you can imagine, it gets pretty tedious to proxy the scope every time we want to filter Books by chapter scopes. Is there any more idiomatic or cleaner way to achieve this?
To get those books you can use ActiveRecord::SpawnMethods#merge, and you don't have to use another scope:
Book.joins(:chapters).merge(Chapter.very_long)
I think one thing you could do would be to use joins/merge
class Book < ARBase
scope :with_long_chapters, ->{ joins(:chapter).merge(Chapter.very_long) }
end
class Chapter < ARBase
scope :very_long, -> { logic for querying chapters with over 300 pages }
end
EDIT(#2): A more reusable scope
class Book < ARBase
scope :by_chapter_page_count, ->(pages){ joins(:chapter).merge(Chapter.by_page_count pages) }
scope :with_climax, -> { joins(:chapter).merge(Chapter.by_category :climax) }
scope :long_with_climax, -> { by_chapter_page_count(400).with_climax }
end
class Chapter < ARBase
scope :by_page_count, ->(pages) { where("pages > ?", pages }
scope :by_category, ->(category) { where category: category }
end
Book.by_chapter_page_count(100)
You can get fairly creative with how you write your scopes

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.
You can pass object to has_many scope as a parameter to lambda
has_many :report_cards, -> (student) { ... }
Try this:
class Student < ActiveRecord::Base
has_many :report_cards, ->{ where(graded: true).order(:semester_number).presence || unscoped.order(:semester_number) }
end
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.

ActiveRecord Scope on Polymorphic Relationshps

I have three models that have a polymorphic relationship as follows:
class DataSource < ActiveRecord::Base
belongs_to :sourceable, polymorphic: true
end
class Foo < ActiveRecord::Base
has_many :data_sources, as: :sourceable
# For the sake of this example, I have place the scope here.
# But I am going to put it in a Concern since Baz needs this scope as well.
scope :bar_source_id, -> (id) do
joins(:data_sources)
.where(data_sources: { source: 'bar', source_id: id })
.first
end
end
class Baz < ActiveRecord::Base
has_many :data_sources, as: :sourceable
end
I want to be able to find a Foo record based on the source_id of the related DataSource. In otherwords, Foo.bar_source_id(1) should return a Foo record who's data_sources contains a record with source: 'bar', source_id: 1. However the scope on the Foo model does not work as I expected.
Foo.bar_source_id(1) returns the correct Foo record but if there is no DataSource with a source_id of 1 Foo.all is returned.
On the other hand Foo.joins(:data_sources).where(data_sources: { source: 'bar', source_id: 1 }).first will always return either the correct record or nil if no record exists. This is the behaviour I expected.
Why does this query work when I call it off of the model itself, but not when I include it in the model as a scope?
Edit:
I have a partial answer to my question. .first in the scope was causing a second query that loads all Foo records. If I remove .first I will get back an ActiveRecord_Relation.
Why does .first behave differently in these two contexts?
After digging around I found this: ActiveRecord - "first" method in "scope" returns more than one record
I assume the rationale here is that scope isn't meant to return a single record, but a collection of records.
I can get the results I need by adding a method bar_source_id instead of using a scope.
def self.bar_source_id(id)
joins(:data_sources)
.where(data_sources: { source: 'bar', source_id: id })
.first
end

Ruby on rails associations parents

I would like to know, whatever the association is (simple belongs_to, polymorphic ...), when I make an association like :
class Toto < ActiveRecord::Base
belongs_to :test_one
belongs_to :test_two
end
class TestOne < ActiveRecord::Base
has_many :totos
end
class TestTwo < ActiveRecord::Base
has_many :totos
end
and then
test_one = TestOne.create
test_two = TestTwo.create
test1 = test_one.totos.create
test2 = test_two.totos.create
I would like to know into a callback of Toto what object instantiate me. In this case, it's obviously test_one and then test_two. I know I could check ids for example but the problem is when i do :
test3 = test_one.totos.create(test_two: test_two)
I can't know if test3 was created through test_one or test_two.
Thank you.
According to your example, I understand that you want to identify the type of object which is associated to your totos object (has_many :totos).
Since there are multiple different objects that might be associated to your totos object through the has_many and belongs_to associations, you might want to perform some kind of verification first to identify the type of the associated object.
First Answer:
This will only work if you know beforehand all the object types that has_many :totos
if test3.respond_to?(:test_one)
test = test3.test_one
elsif test3.respond_to?(:test_two)
test = test3.test_two
end
Second Answer:
I found this on Stackoverflow, and it somehow answeres your question. So if I rephrase the answer to:
def get_belongs_to(object)
associated = []
object.class.reflect_on_all_associations(:belongs_to).map do |reflection|
associated << object.try(reflection.name)
end
associated.compact
end
This method will return an array of all objects associated to your totos object. This will also work when totos belongs to multiple objects say test_one and test_two at the same time. So the following:
associated_objects = get_belongs_to(test3)
and in your case associated_objects[0] will yield the object you desire.
Hope this helps.
Rails does not persist the data you're looking for, so you'll have to store it yourself if you want it. This means you'll need a migration for the new field:
rails generate migration AddOriginalParentTypeToTotos original_parent_type:string
rake db:migrate
You can then override the assignment methods so that the first parent assigned will assign the original_parent_type attribute (and it will remain the same once assigned):
class Toto < ActiveRecord::Base
def test_one=(val)
self[:original_parent_type] ||= 'test_one'
super
end
def test_one_id=(val)
self[:original_parent_type] ||= 'test_one'
super
end
def test_two=(val)
self[:original_parent_type] ||= 'test_two'
super
end
def test_two_id=(val)
self[:original_parent_type] ||= 'test_two'
super
end
end
You can then use send to add an original_parent method:
class Toto < ActiveRecord::Base
def original_parent
send(original_parent_type) if original_parent_type
end
end

Resources