My application has a Job model. Every job in the system has a contact. This is like a person you would call up if you need to ask a question about the job. A contact can either be a client or an employee of a client (ClientEmployee).
class Job < ActiveRecord::Base
belongs_to :contact, polymorphic: true
end
class Client < ActiveRecord::Base
has_many :jobs, as: :contact
has_many :employees, class_name: 'ClientEmployee'
end
class ClientEmployee < ActiveRecord::Base
belongs_to :client
has_many :jobs, as: :contact
end
Clients have the idea of commissioned_jobs. The clients commissioned jobs are those jobs for which the client is the contact OR one of the client’s employees is the contact.
class Client < ActiveRecord::Base
has_many :jobs, as: :contact
has_many :employee_jobs, through: :employees, source: :jobs
def commissioned_jobs
jobs << employee_jobs
end
end
Aside: That method is a bit of a hack because it returns an array rather than an ActiveRecord::Relation. It’s also interesting that it blows up if I try to concat jobs into employee_jobs. It may or may not do for my purposes.
I would like to add a scope to Client called with_commissioned_jobs. This should return all the clients in the system who have jobs or who have employees who have jobs.
class Client < ActiveRecord::Base
def self.with_commissioned_jobs
# I can get clients with jobs using: joins(:jobs). How do
# I also include clients with employees who have jobs?
end
end
How do I implement this method?
I’m using Rails 3.2.9.
Update:
I’ve made some progress and I now have two methods, each of which does half of what I need.
class Client < ActiveRecord::Base
# Return all clients who have an employee with at least one job.
def self.with_employee_jobs
joins(employees: :jobs)
# SQL: SELECT "clients".* FROM "clients" INNER JOIN "client_employees" ON "client_employees"."employer_id" = "clients"."id" INNER JOIN "jobs" ON "jobs"."contact_id" = "client_employees"."id" AND "jobs"."contact_type" = 'ClientEmployee'
end
# Return all clients who have at least one job.
def self.with_jobs
joins(:jobs)
# SQL: SELECT "clients".* FROM "clients" INNER JOIN "jobs" ON "jobs"."contact_id" = "clients"."id" AND "jobs"."contact_type" = 'Client'
end
end
Now all I need to do is combine these two method calls into one ActiveRecord::Relation. I can obviously do this:
def self.with_commissioned_jobs
with_jobs + with_employee_jobs
end
The problem is that that returns an array rather than an instance of Relation and I can’t chain more scopes on it.
Update 2:
Using merge doesn’t appear to work either. Here is the AR query and the resulting SQL.
joins(:jobs).merge(joins(employees: :jobs))
SELECT "clients".* FROM "clients" INNER JOIN "jobs"
ON "jobs"."contact_id" = "clients"."id"
AND "jobs"."contact_type" = 'Client'
INNER JOIN "client_employees"
ON "client_employees"."employer_id" = "clients"."id"
INNER JOIN "jobs" "jobs_client_employees"
ON "jobs_client_employees"."contact_id" = "client_employees"."id"
AND "jobs_client_employees"."contact_type" = 'ClientEmployee'
By the way, here are the tests I’m trying to pass. The first test fails because there are zero results when I use merge.
describe "with_commissioned_jobs" do
# A client with a job.
let!(:client_with) { create :client }
let!(:job) { create :job, contact: client_with }
# A client who does not himself have a job, but who has an employee
# with a job.
let!(:client_with_emp) { create :client }
let!(:employee) { create :client_employee, employer: client_with_emp }
let!(:emp_job) { create :job, contact: employee }
# A client with nothing. Should not show up.
let!(:client_without) { create :client }
it "should return clients with jobs and clients with employee jobs" do
Client.with_commissioned_jobs.should == [client_with, client_with_emp]
end
it "should return a relation" do
Client.with_commissioned_jobs.should be_instance_of(ActiveRecord::Relation)
end
end
Have you considered the gem meta_where? The main thing seems to be that you want to return an
ActiveRecord:Relationobject for further chaining.UPDATE 2: got it working with
LEFT OUTER JOINjobs twice with aliasingSeeing if it works: