Rails includes with where - ruby-on-rails

I'm trying to eager load some associations, but I want to do filtering on some of the sub-relations here is what I mean.
class MyModel < ActiveRecord::Base
has_many :my_model_details, dependent: :destroy
end
class MyModelDetails < ActiveRecord::Base
belongs_to :property
belongs_to :my_model
belongs_to :track
belongs_to :person
end
So if I want to get all MyModel objects which have details that belong to certain property with property name I would do this.
MyModel.includes(my_model_details: [:property, :person]).where('property.property_name = ?', 'Property name')
The reason why I want use includes instead of joins, is that I want to have my model details in grouped by the last property and person name. So I don't want to fire the N+1 query in that case.
Ex
If my query returns single MyModel object with two MyModelDetail records.
Where mymodeldetail id 1 has property id 1 and mymodeldetail id2 has property id 2. And they both belong to the same person.
So on my UI I would display this:
MyModel 1
MyModelDetail 2 - Property name: 'some name' - Person: 'Bob'
Now what happens when I use my includes query is this :
MyModel.includes(my_model_details: [:property, :person]).where('property.property_name = ?', 'Property name')
SELECT "MY_MODEL".* FROM "MY_MODEL" WHERE (PROPERTY.PROPERTY_NAME = 'Prop')
If I use includes with where, join sql is not generated at all and so this query fails of course:
"PROPERTY"."PROPERTY_NAME": invalid identifier
Why is that, I'm using rails 4? How can I make it work with both joins and includes

I think it should be properties, because table name is pluralized according to CoC, try using properties.property_name instead
MyModel.includes(my_model_details: [:property, :person]).where('properties.property_name = ?', 'Property name')
As suggested by BroiSatse, add .references(:properties) to your query
Hope that helps!

Related

Rails3: Use joined table's data

I'm trying to access my joined table data but alas..
For example association between authors and books, author have many books:
I'm using join funtion to join the author table to the books table in order to access the author's author_name attribute
class Author < ActiveRecord::Base
has_many :books
attr_accessible :author_name
end
class Book < ActiveRecord::Base
belongs_to :author
end
random_author = Author.first
books = random_author.books.where(id > 5).joins(:author) #this should make all author attributes available to each book, right?
book_1 = books.first
book_1.author_name
=> NoMethodError: undefined method `author_name' for #<Book:0x111111>
Of course using the association would work: book_1.author.author_name but that will require another query, which is what I'm trying to avoid.
I mean the joins operation joins the author's data- there must be a way to access it right?
p.s. I can use includes method. and eager load the author data as well, but since I'm just needing a single attribute- is there a way to accomplished it with only a joins method? Thank you
You need to add
.select('books.*, author.name AS author_name')
So your query becomes
books = random_author.books.where(id > 5).joins(:author).select('books.*, author.name AS author_name')

How to retrieve all related objects from an existing selection set in Rails

In a Rails app I have two models which are related via has_many and belongs_to associations:
class Entity < ActiveRecord::Base
has_many :applicants
end
class Applicant < ActiveRecord::Base
belongs_to :entity
end
I'm using SearchKick to first select some Entities - the details of this don't really matter, but given an existing collection of Entity objects, how can I retrieve all of the related Applicant objects?
# select some entities (in my app I'm using SearchKick to run the search)
entities = Entity.search params[:query]
# now find the applicants related to the selected entities
applicants = entities.???
This works, but is very slow given a large selection:
# retrieve the applicants related to the selected entities
applicants = []
entities.each do |entity|
applicants += entity.applicants
end
Is there a Rails short-hand to retrieve the Applicants related to the selected Entities?
Other things I've tried:
entities.class => Entity::ActiveRecord_Relation
entities.applicants => #error
entities.applicant_ids => #error
Like this?
Applicant.joins(:entity).where("entities.id < ?", 1000)
This answer came from Andrew Kane, the developer of SearchKick:
One way is to use the include option to eager load associations.
Entity.search "*", include: [:applicants]
Another option is to get all ids from the entities, and pass them into
a query for applicants.
Applicant.where(entity_id: entities.map(&:id))
I found that the second option worked well for me.
Entity.where("id < ? ", 1000).includes(:applicants)
To get all applicants
Entity.where("id < ? ", 1000).includes(:applicants).collect(&:applicants).flatten

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 association returns "NoMethodError for ActiveRecord::Relation"

I have 3 models with "1 to n" associations, like this
Client --1 to n--> Category --1 to n--> Item
In one page, I need to display a list of Items along with their Categories. This page is subject to 3 level of filtering:
Client filtering: I know the client id (I'll use 'id=2' in this example)
Category name: dynamic filter set by the user
Item name: dynamic filter set by the user
And I'm getting more and more confused with ActiveRecord Associations stuff
In my ItemsController#index, I tried this:
categories = Client.find(2).categories
.where('name LIKE ?', "%#{params[:filter_categories]}%")
#items = categories.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
The second line raises a NoMethodError undefined method 'items' for ActiveRecord::Relation. I understand that the first line returns a Relation object, but I cannot find a way to continue from here and get the list of Items linked to this list of Categories.
I also started to extract the list of categories ids returned by the first line, in order to use them in the where clause of the second line, but while writing the code I found it inelegant and thought there may be a better way to do it. Any help would be very appreciated. Thanks
models/client.rb
class Client < ActiveRecord::Base
has_many :categories
has_many :items, through: :categories
...
end
models/category.rb
class Category < ActiveRecord::Base
belongs_to :client
has_many :items
...
end
model/item.rb
class Item < ActiveRecord::Base
belongs_to :category
has_one :client, through: :category
...
end
You can only call .items on a category object, not on a collection. This would work:
#items = categories.first.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
To get what you want, you can do the following:
#items = Item
.where('category_id IN (?) AND name LIKE ?', categories, "%#{params[:filter_items]}%")
Assuming that in the end you are only interested in what is in #items, it would be even better to do it in one query instead of two, using joins:
#items = Item.joins(:category)
.where('items.name LIKE ? AND categories.name = ? AND categories.client_id = 2', "%#{params[:filter_items]}%", "%#{params[:filter_categories]}%")
You can try smth like this:
item_ids = Client.find(2).categories.inject([]) { |ids, cat| ids |= cat.item_ids; ids }
items = Item.find(item_ids)
This how you can get a list of nested objects that associated through another table.

Rails "has_many" association with NULL foreign key

Our Rails 3 application has models Person and Message. Messages can be specific to a Person (when the message person_id column is set) or they can be "global" (when the person_id column is NULL).
We would like to have a simple has_many relationship using the :conditions option as such:
class Person < ActiveRecord::Base
has_many :messages,
:conditions => proc { ['(messages.person_id IS NULL) OR ' +
'(messages.person_id = ?)'], self.id }
# ...
end
But it appears that the has_many class method encodes the "conditions" option as a logical "AND" clause after enforcing the foreign key constraint of equality to the Person object's id (e.g. "FROM messages WHERE person_id=123 AND (person_id IS NULL OR person_id=123)"). It appears that there is no way to allow associated objects with null foreign keys to belong to such associations.
Does Rails 3 / ActiveRecord provide a way to do this or must I hack my own association-like methods?
You can't have an OR clause like you want using conditions on the ActiveRecord assocation. You could have the association without conditions and then add a method to include the global messages you want. That way you could still take advantage of the association when building associated records.
# person.rb
has_many :messages
def all_messages
Message.where('messages.person_id=? OR messages.person_id IS NULL', id)
end
Here's my standard plug for the Squeel gem, which is handy for more advanced queries like this if you don't want to have bits of SQL in your code. Check it out.
I think you're correct, you could forgo has_many, and create a scope with an outer join, something like:
class Person < ActiveRecord::Base
scope :messages lambda {
joins("RIGHT OUTER Join messages on persons.id = messages.person_id")
.where('persons.id = ?',self.id)
}

Resources