How to get scope with polymorphic association - ruby-on-rails

I am building a Rails 5 app and in this app I got two models.
First one is called Timeoff and second one is called Approval.
I want to get all Timeoff objects that got no approvals.
The time off model
class Timeoff < ApplicationRecord
scope :not_approved, -> { self.approvals.size > 0 }
has_many :approvals, as: :approvable, dependent: :destroy
end
The Approval model
class Approval < ApplicationRecord
belongs_to :approvable, polymorphic: true
end
I am calling it like this
Timeoff.not_approved
I get the error
NoMethodError: undefined method `approvals' for #<Class:0x007f9698587830>

You're trying to call approvals in the class context, but it actually belongs to an instance of Timeoff. For example:
Timeoff.approvals # doesn't work
Timeoff.first.approvals # works
That's why you get the undefined method error.
But I think you want a database query here. You could go two ways - that I know of:
Make two queries: find the timeoffs that have approvals and then query for the other ones using NOT IN
timeoff_ids = Approval.where(approvable_type: 'Timeoff').pluck(:approvable_id)
Timeoff.where.not(id: timeoff_ids)
This may get really slow if your tables are big.
Or you could do a join on the approvals table and filter to where the id is null:
Timeoff.joins("LEFT JOIN approvals ON timeoffs.id = approvals.approvable_id AND approvals.approvable_type = 'Timeoff'").where("approvals.id IS NULL")
This should also work, and may be faster - but you should measure with your own data to be sure.
Also, take a look at this question: How to select rows with no matching entry in another table? there is a complete explanation of the second query and some other ways to solve it.

Related

Rails: How to get directly related records, but in context of a relationship :through

I've got kind of a strange case, in which I have a DayReport that has many Reports, which belong to an Account. I set-up my DayReport module using a has_many through for the accounts:
class DayReport < ActiveRecord::Base
has_many :reports
has_many :accounts, -> { order(:last_name) }, through: :reports
end
What I would like to do now, is get all the accounts and display all the reports for that account, but only if they are related to the DayReport. I can't use account.reports, because that also contains other reports.
One approach I took is to create an instance method that uses a where clause to fetch the appropriate reports:
def reports_for_account account
reports.where(account: account)
end
Problem is that this will trigger a query for each of the accounts, which I think is unnecessary. I'm only having trouble figuring out the correct approach.
I hope i understand it correctly.
You can decrease the numbers of quires by using eager loadet association with: .includes, because includes will load (1. query) all records of the parent and (2. query) all of the records referenced in the includes method (where).
In .where you can look if the day_report id exists or is a certain id (i'm not quite sure for what you're asking here)
For example (with id):
Account.includes(:reports).where(reports: {day_report_id: specified_id})
Or if they have a relationship at all:
Account.includes(:reports).where.not(reports: {day_report_id: nil})
That would decrease the number of queries to two.
Hope it helps!
Maybe joins helps you:
Account.includes(:reports).joins(:day_reports)
It fetches all accounts which have records in DayReport and preloaded reports.

Undefined method error for scope on STI subclass

The Setup
I have an STI setup like so:
class Transaction < ActiveRecord::Base
belongs_to :account
scope :deposits, -> { where type: Deposit }
end
class Deposit < Transaction
scope :pending, -> { where state: :pending }
end
class Account < ActiveRecord::Base
has_many :transactions
end
If I call:
> a = Account.first
> a.transactions.deposits
...then I get what I expect, a collection of Deposit instances, however if I look at the class of what's returned:
> a.transactions.deposits.class
...then it's actually not a Deposit collection, it's still a Transaction collection, ie. it's a Transaction::ActiveRecord_AssociationRelation
The Problem
So, to the problem, if I then want to call one of the Deposit scopes on that collection it fails:
> a.transactions.deposits.pending
NoMethodError: undefined method `pending' for #<Transaction::ActiveRecord_Associations_CollectionProxy:0x007f8ac1252d00>
Things I've Checked
I've tried changing the scope to Deposit.where... which had no effect, and also to Deposit.unscoped.where... which actually returns the right collection object, but it strips all the scope, so I lose the account_id=123 part of the query so it fails on that side.
I've checked this and the problem exists for both Rails 4.1 and 4.2. Thanks for any pointers on how to make this work.
I know there's a workaround, but...
I know I could work around the issue by adding a has_many :deposits into Account, but I'm trying to avoid that (in reality I have many associated tables and many different transaction subclasses, and I'm trying to avoid adding the dozens of extra associations that would require).
Question
How can I get what's returned by the deposits scope to actually be a Deposit::ActiveRecord_Association... so that I can chain my scopes from Deposit class?
I created an isolated test for your issue here:https://gist.github.com/aalvarado/4ce836699d0ffb8b3782#file-sti_scope-rb and it has the error you mentioned.
I came across this post from pivotal http://pivotallabs.com/merging-scopes-with-sti-models/ about using were_values in a scope to get all the conditions. I then used them on unscope to force the expected class, basically this:
def self.deposits
conditions = where(nil).where_values.reduce(&:and)
Deposit.unscoped.where(conditions)
end
This test asserts that it returns a Deposit::ActiveRecord_Relation https://gist.github.com/aalvarado/4ce836699d0ffb8b3782#file-sti_scope2-rb
Update
You can also write this as a scope if you prefer:
scope :deposits, -> { Deposit.unscoped.where where(nil).where_values.reduce &:and }
As a quick workaround you can do > a.transactions.deposits.merge(Deposit.pending), but can't think of a different way of solving it. I'll think and try more options later and come back if I find anything.
You might want to say that an Account has_many :deposits
class Account < ActiveRecord::Base
has_many :transactions
has_many :deposits
end
Then you should be able to query
a.deposits.pending

where constraint on a related record

I'm not getting a concept (nothing new there) on how to scope a Active Record query. I want to only receive the records where there is a certain condition in a related record. The example I have happens to be polymorphic just in case that is a factor. I'm sure there is somewhere where this is explained but I have not found it for whatever reason.
My Models:
class User < ActiveRecord::Base
belongs_to :owner, polymorphic: true
end
class Member < ActiveRecord::Base
has_one :user, as: :owner
end
I want to basically run a where on the Member class for related records that have a certain owner_id/owner_type.
Lets say we have 5 Members with ids 1-5 and we have one user with the owner_id set to 3 and the owner_type set to 'Member'. I want to only receive back the one Member object with id 3. I'm trying to run this in Pundit and thus why I'm not just going at it form the User side.
Thanks for any help as always!!!
Based on your comment that you said was close I'd say you should be able to do:
Member.joins(:user).where('users.id = ?', current_user.id)
However based on how I'm reading your question I would say you want to do:
Member.joins(:user).where('users.owner_id = ?', current_user.id)
Assuming current_user.id is 3.
There may be a cleaner way to do this, but that's the syntax I usually use. If these aren't right, try being a little more clear in your question and we can go from there! :)

Query that joins child model results item erroneously shown multiple times

I have the following models, each a related child of the previous one (I excluded other model methods and declarations for brevity):
class Course < ActiveRecord::Base
has_many :questions
scope :most_answered, joins(:questions).order('questions.answers_count DESC') #this is the query causing issues
end
class Question < ActiveRecord::Base
belongs_to :course, :counter_cache => true
has_many: :answers
end
class Answer < ActiveRecord::Base
belongs_to :question, :counter_cache => true
end
Right now I only have one Course populated (so when I run in console Course.all.count, I get 1). The first Course currently has three questions populated, but when I run Course.most_answered.count (most_answered is my scope method written in Course as seen above), I get 3 as the result in console, which is incorrect. I have tried various iterations of the query, as well as consulting the Rails guide on queries, but can't seem to figure out what Im doing wrong. Thanks in advance.
From what I can gather, your most_answered scope is attempting to order by the sum of questions.answer_count.
As it is there is no sum, and since there are three answers for the first course, your join on to that table will produce three results.
What you will need to do is something like the following:
scope :most_answered, joins(:questions).order('questions.answers_count DESC')
.select("courses.id, courses.name, ..., SUM(questions.answers_count) as answers_count")
.group("courses.id, courses.name, ...")
.order("answers_count DESC")
You'll need to explicitely specify the courses fields you want to select so that you can use them in the group by clause.
Edit:
Both places where I mention courses.id, courses.name, ... (in the select and the group), you'll need to replace this with the actual columns you want to select. Since this is a scope it would be best to select all fields in the courses table, but you will need to specify them individually.

Querying a polymorphic association

I have a polymorphic association like this -
class Image < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Page < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Site < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Approval < ActiveRecord::Base
belongs_to :approvable, :polymorphic => true
end
I need to find approvals where approval.apporvable.deleted = false
I have tried something like this -
#approvals = Approval.find(:all,
:include => [:approvable],
:conditions => [":approvable.deleted = ?", false ])
This gives "Can not eagerly load the polymorphic association :approvable" error
How can the condition be given correctly so that I get a result set with approvals who's approvable item is not deleted ?
Thanks for any help in advance
This is not possible, since all "approvables" reside in different tables. Instead you will have to fetch all approvals, and then use the normal array methods.
#approvals = Approval.all.select { |approval| !approval.approvable.deleted? }
What your asking, in terms of SQL, is projecting data from different tables for different rows in the resultset. It is not possible to my knowledge.
So you'll have to be content with:
#approvals = Approval.all.reject{|a| a.approvable.deleted? }
# I assume you have a deleted? method in all the approvables
I would recommend either of the answers already presented here (they are the same thing) but I would also recommend putting that deleted flag into the Approval model if you really care to do it all in a single query.
With a polymorphic relationship rails can use eager fetching on the polys, but you can't join to them because yet again, the relationships are not known so the query is actually multiple queried intersected.
So in the end if you REALLY need to, drop into sql and intersect all the possible joins you can do to all the types of approvables in a single query, but you will have to do lots of joining manually. (manually meaning not using rails' built-in mechanisms...)
thanks for your answers
I was pretty sure that this couldn't be done. I wanted some more confirmation
besides that I was hoping for some other soln than looping thru the result set
to avoid performance related issues later
Although for the time being both reject/select are fine but in the long run I
will have to do those sql joins manually.
Thanks again for your help!!
M

Resources