Rails scope query on model - ruby-on-rails

I have a model with this relationship:
class Plan < ApplicationRecord
has_many :enrollment_plans
has_many :enrollments, through: :enrollment_plans
...
end
EDIT Here is the join table:
class EnrollmentPlan < ApplicationRecord
belongs_to :enrollment, required: true
belongs_to :plan, required: true
end
I tried to throw this scope on the model:
scope :for_enrollment, -> (enrollment) { where('enrollments.enrollment_id = ?', enrollment.id) }
but I get the following error. I am trying to figure out why I can't do this query. What do I need to change it to?
pry(main)> Plan.for_enrollment(Enrollment.last).to_a
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "enrollments"
LINE 1: SELECT "plans".* FROM "plans" WHERE (enrollments.enrollment_...
^

ActiveRecord does not include associations by default, you need to add enrollments to query by hand. Try:
scope :for_enrollment, -> (enrollment) do
joins(:enrollments).
where('enrollments.id = ?', enrollment.id)
end
This scope will make query with joins of three tables: plans, enrollment_plans and enrollments. You may do the same logic with two tables query:
scope :for_enrollment, -> (enrollment) do
joins(:enrollment_plans).
where('enrollment_plans.enrollment_id = ?', enrollment.id)
end

Related

Referencing different names in joins with scopes

I have a polymorphic association and sometimes I want to preload it's associations.
When I left join the model, my WHERE filters get lost because they don't referenced the named association.
SELECT COUNT(*) FROM `companies` LEFT OUTER JOIN `key_values` `latest_information` ON `latest_information`.`attachment_id` = `companies`.`id` AND `latest_information`.`attachment_type` = 'Company' AND `key_values`.`name` = 'latest_information' WHERE `latest_information`.`id` IS NOT NULL
# => ActiveRecord::StatementInvalid: Mysql2::Error: symbol key_values.`name` not found
This is the query that is generated but it's invalid due to the key_values.name not being referenced.
Here's what my model looks like:
class Company < LeadRecord
has_many :key_values, as: :attachment, dependent: :delete_all
has_one :latest_information,
-> { KeyValue.latest('latest_information') },
class_name: KeyValue.to_s,
as: :attachment
end
class KeyValue < LeadRecord
belongs_to :attachment, polymorphic: true
def self.latest(name)
order(created_at: :desc).where(name: name) # This is the source of the error
end
end
I can probably fix this by passing addition parameters to self.latest such as the association name but I want to know if there's a better Rails way to do this.
In the interim I have solved this by making this change on KeyValue.
# key_value.rb
def self.latest(name, association_name = 'key_values')
order(created_at: :desc).where("#{association_name}.name = ?", name)
end
# company.rb
has_one association_name,
-> {
KeyValue.latest(
method_name.to_s,
association_name.to_s,
)
},
class_name: KeyValue.to_s,
as: :attachment

Active Record query through relationship to find Record where none of its relationships have true for a value

Rails 6.0 with Postrgres
I have got a relational model where an Employee has_many Jobs and every job has a boolean value for active.
If every Job that an Employee has is active: false than that employee is no longer employed. So I want to do a query for employees where every job they have has active: false to figure out which employees are no longer employed.
I tried something like
class Employee < ApplicationRecord
scope :terminated, -> {
includes(:jobs).
where.not(jobs: {
active: true,
})
}
but this is finding employees where they have any job where active is not true. I want to find employees where every job has active false. Can AR do this natively?
One way to do this is to join the jobs table twice and use an alias:
SELECT "employees".* FROM "employees"
INNER JOIN "jobs"
ON "jobs"."employee_id" = "employees"."id"
INNER JOIN "jobs" "inactive_jobs"
ON "inactive_jobs"."employee_id" = "employees"."id"
AND "inactive_jobs"."active" = 0
GROUP BY "employees"."id"
HAVING COUNT("jobs"."id") = COUNT("inactive_jobs"."id") LIMIT ?
ActiveRecord doesn't really have a straight forwards way of doing joins with aliases but with some Arel trickery you can make it happen.
class Employee < ApplicationRecord
has_many :jobs
def self.terminated
jobs = Job.arel_table
# Needed to generate a join with an alias
inactive_jobs = Job.arel_table.alias('inactive_jobs')
# joins all the jobs
joins(
:jobs
)
# joins jobs where active: false
.joins(
self.arel_table.join(
inactive_jobs,
Arel::Nodes::InnerJoin
).on(
inactive_jobs[:employee_id].eq(self.arel_table[:id])
.and(inactive_jobs[:active].eq(false))
).join_sources
)
# groups on employee id
.group(:id)
# Set a condition on the group that the jobs must equal the number of inactive jobs
.having(jobs[:id].count.eq(inactive_jobs[:id].count))
end
end
If you add a counter-cache you can cheat and remove the second join:
class Job
belongs_to :employee, counter_cache: true
end
class Employee < ApplicationRecord
has_many :jobs
def self.terminated
joins(:jobs)
.where(jobs: { active: false })
.group(:id)
.having(arel_table[:jobs_count].eq(Job.arel_table[Arel.star].count))
end
end
Here you're comparing the cached value against the number of join rows.
You should not use scope for this type of queries because if there are no matching records, the scope returns all the records for that Model.
You can write a class method for this:
class Employee < ApplicationRecord
has_many :jobs
def self.terminated
preload(:jobs)
.reject { |employee| employee.jobs.where(active: true).present? }
end
end
class Employee < ApplicationRecord
has_one :jobs
scope :terminated, ->{ joins(:jobs).merge(Job.inactive) }
end
class Job < ApplicationRecord
belongs_to :employee
scope :inactive, -> { where(active: false) }
end
Employee.terminated should return all employees who only have jobs for which active is false.

Trouble with simple Rails nested associations where clause for parent

I have the following models:
class Business < ApplicationRecord
has_many :shopping_trips
end
class ShoppingTrip < ApplicationRecord
belongs_to :business
has_many :purchases
end
class Purchase < ApplicationRecord
belongs_to :shopping_trip
end
So a Business can have many shopping trips, and each of these shopping trips can have many purchases.
I am trying to run a simple query on the Purchase table to find purchases that belong to a particular business. So I'm writing this:
purchases = Purchase.joins(:shopping_trip => :business ).where(:shopping_trip => {:business_id => 1})
Unfortunately it's not working. I get the following error:
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "shopping_trip"
LINE 1: ...sses"."id" = "shopping_trips"."business_id" WHERE "shopping_...
^
: SELECT "purchases".* FROM "purchases" INNER JOIN "shopping_trips" ON "shopping_trips"."id" = "purchases"."shopping_trip_id" INNER JOIN "businesses" ON "businesses"."id" = "shopping_trips"."business_id" WHERE "shopping_trip"."business_id" = $1
The join looks about right but the where clause seems to fail.
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing
FROM-clause entry for table "shopping_trip"
You need to specify table name not the association name inside the where. So shopping_trip should be shopping_trips
purchases = Purchase.joins(:shopping_trip => :business ).where(:shopping_trips => {:business_id => 1})
A better solution is to set up indirect associations so that you can query through the join model without manually joining:
class Business < ApplicationRecord
has_many :shopping_trips
has_many :purchases, through: :shopping_trips
end
class ShoppingTrip < ApplicationRecord
belongs_to :business
has_many :purchases
end
class Purchase < ApplicationRecord
belongs_to :shopping_trip
has_one :business, through: :shopping_trip
end
You can now query from either side:
#business = Business.eager_load(:purchases).find(1)
#purchases = #business.purchases
# or
#purchases = Purchase.eager_load(:buisness).where(businesses: { id: 1 })
Check This...
all_purchase = Purchase.all
all_purchase.each do |each_purchase|
each_purchase.shopping_trip.business
end

Using scopes in Rails 3

I am trying to define a scope in my Account model but it is not working. Here is my code:
class Account < ActiveRecord::Base
has_many :organizations
scope :primary, joins(:organizations).where('organizations.primary = ?', true)
accepts_nested_attributes_for :organizations
end
class Organization < ActiveRecord::Base
belongs_to :account
has_many :locations
accepts_nested_attributes_for :locations
end
From the console, I tried the following command:
Account.primary.first
But I get the following error:
ActiveRecord::StatementInvalid: SQLLite3::SQLException: near "primary":
syntax error: SELECT "accounts".* FROM "accounts" INNER JOIN "organizations" ON
"organizations"."account_id" = "accounts"."id" WHERE (organizations.primary = 't')
LIMIT 1
I think the name 'primary' might be causing the problem. When I renamed the scope to "important" and tried that I get:
NoMethodError: undefined method 'important' for #<Class:0x1f4a900>
If anyone can help I would very much appreciate it.
I think your problem is that you have a column named "primary" and that's a reserved word. Try quoting it:
scope :primary, joins(:organizations).where('organizations."primary" = ?', true)
This exception:
SQLLite3::SQLException: near "primary":
is coming from SQLite, not from ActiveRecord or Rails.

How to return an ActiveRecord relation through two other has_many associations

Model:
class User < ActiveRecord::Base
has_many :requests, class_name: 'Story', foreign_key: 'requester_id'
has_many :ownerships, class_name: 'Story', foreign_key: 'owner_id'
def stories
requests | ownerships
end
end
In this case the method stories will return an array of uniq objects as I want. But I'll need to use something like User.first.stories.where("title = 'foo'") that returns an error, because it's an array, not a relation.
So what can I do to get this same results through relation allowing to use with Arel?
PS.: Im on Rails 3.1.rc6
class User < ActiveRecord::Base
def stories
Story.where "(owner_id = :id) OR (requester_id = :id)", :id => id
end
end
or this could be written even nicer if you use squeel
def stories
Story.where { (owner_id == my{id}) | (requester_id == my{id}) }
end
Only a scope returns a relation. Transforming the method as LOJ scope should help I guess.
scope :stories, select('stories.*').joins('LEFT OUTER JOIN stories ON
users.id = stories.requester_id LEFT OUTER JOIN stories ON users.id = stories.owner_id')

Resources