8

我的应用程序有一个Job模型。系统中的每项工作都有一个contact。这就像一个人,如果你需要问一个关于这个工作的问题,你会打电话给你。联系人可以是client或客户的雇员(ClientEmployee)。连接ActiveRecord ::通过多态关联查询

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 

客户有想法commissioned_jobs。客户委托的工作是那些客户是联系人的工作,或者客户的其中一名雇员是联系人。

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 

除了:该方法是一个黑客位的,因为它返回一个数组,而不是一个ActiveRecord::Relation。如果我尝试将职位连接到employee_jobs,这也很有趣。它可能会或可能不会为我的目的。

我想添加一个范围Clientwith_commissioned_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 

如何实现此方法?

我正在使用Rails 3.2.9。

更新:

我已经取得了一些进展,我现在有两种方法,每个做什么,我需要的一半。

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 

现在我需要做的就是将这两个方法调用合并为一个ActiveRecord::Relation。我可以明显地做到这一点:

def self.with_commissioned_jobs 
    with_jobs + with_employee_jobs 
    end 

的问题是,它返回一个数组而不是Relation一个实例,我不能链子更范围。

更新2

使用merge似乎并没有擦出火花。这是AR查询和生成的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' 

顺便说一下,这里是我试图通过的测试。第一次测试失败,因为使用合并时结果为零。

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 
+0

你看过arel吗? https://github.com/rails/arel它具有查询条件或条件,并可以很好地处理复杂的联接。 – John

+0

我当然也会接受一个基于Arel的解决方案。我花了好几个小时试图想出一个,我似乎无法管理它。 –

回答

1

你有没有考虑创业板meta_where?主要的东西似乎是,你想返回一个ActiveRecord:Relation对象进一步链接。

更新2:得到它与LEFT OUTER JOIN就业工作两次走样

# scope for ::Client 
    def self.with_commissioned_jobs 
    self.joins("LEFT OUTER JOIN client_employees ON clients.id =client_employees.client_id"). 
     joins("LEFT OUTER JOIN jobs AS cjobs ON clients.id = cjobs.contact_id AND cjobs.contact_type = 'Client'"). 
     joins("LEFT OUTER JOIN jobs AS ejobs ON client_employees.id = ejobs.contact_id AND ejobs.contact_type = 'ClientEmployee'"). 
     where("cjobs.id IS NOT NULL OR ejobs.id IS NOT NULL") 
    end 

看到,如果它的工作原理:

#c1 has no job 
    c1 = Client.create 

    #c2 has a job 
    c2 = Client.create 
    c2.jobs.create 

    #c3 has no job, but has an employee with a job 
    c3 = Client.create 
    c3.employees.create 
    c3.employees.first.jobs.create 

    puts Client.all.inspect    #=> [#<Client id: 1>, #<Client id: 2>, #<Client id: 3>] 
    puts Client.with_commissioned_jobs #=> [#<Client id: 2>, #<Client id: 3>] 

    puts [c2,c3] == Client.with_commissioned_jobs.all #=> true 
+0

不支持Rails 3.2恐怕。我试过[Squeel](https://github.com/ernie/squeel),它与MetaWhere类似,但我似乎无法得到它来合并“AR :: Relation”。 –

+0

由于我的客户雇员外键是'employer_id'而不是'client_id',因此您的更新需要进行一些细微修改。很棒的工作,谢谢! –

1

试试这个:

joins(:jobs, {employees: :jobs}) 

应该加入客户端的作业,以及客户员工的工作。有关更全面的信息,请参阅the guides

编辑

在你的情况,你可以使用Relation.merge

joins(:jobs).merge(joins(employees: :jobs)) 
+0

我正在阅读指南(感谢您的链接btw),并且据我所知,您提供的代码应该返回“有工作且有雇员有工作的客户”。真的,我正在寻找一个OR条件,我没有看到办法做到这一点。 –

+0

合并似乎不工作,或者我害怕。我随同我正在使用的规格一起附加了更多信息。 –

0
class Client < ActiveRecord::Base 
    has_many :jobs, as: :contact 
    has_many :employees, class_name: 'ClientEmployee' 

    scope :with_commissioned_jobs, lambda do 
    includes(:jobs, {:employees => :jobs}).where("jobs.contact_type IS NOT NULL AND jobs.contact_id IS NOT NULL") 
    end 
end 

好吧,我从实际工作的应用程序的另一个决定。老学校帮助你。 :)

该方法只是为AR创建阵列条件:多态东西的关系。

module ActiveRecordHelper 

    def self.polymorphic_sql(*args) 
    conditions = [] 
    table = args.first.table_name 
    stack = args.extract_options! 
    sql_queries = stack.collect do |as_resource, hash| 
     resource_queries = hash.collect do |name, find_options| 
     resource_class = name.to_s.classify.constantize 
     resource_table = resource_class.table_name 
     conditions << resource_class.name 
     if find_options[:conditions].present? 
      conditions += find_options[:conditions][1..-1] 
     end 
     joins_clause = 
     Array.wrap(find_options[:join]).collect do |association| 
      reflection = resource_class.reflections[association]    
      if reflection.macro == :belongs_to && reflection.options[:polymorphic] != true 
      "INNER JOIN #{reflection.klass.table_name} ON #{reflection.active_record.table_name}.#{reflection.foreign_key} = #{reflection.klass.table_name}.id" 
      elsif reflection.macro.in?([:has_many, :has_one]) && reflection.options[:as].nil? 
      "INNER JOIN #{reflection.klass.table_name} ON #{reflection.klass.table_name}.#{reflection.foreign_key} = #{reflection.active_record.table_name}.id" 
      end 
     end.compact.join(" ").strip 
     "(#{table}.#{as_resource}_type = ? AND EXISTS(#{["SELECT 1 FROM #{resource_table}#{joins_clause.left_indent(1) if joins_clause.present?} WHERE #{resource_table}.id = #{table}.#{as_resource}_id", find_options[:conditions].first].compact.join(" AND ")}))" 
     end 
     "CASE WHEN #{table}.#{as_resource}_type IS NOT NULL AND #{table}.#{as_resource}_id IS NOT NULL THEN #{resource_queries.join(" OR ")} ELSE TRUE END" 
    end 
    conditions.insert(0, "(#{sql_queries.join(" OR ")})") 
    end 

end 

然后扩展你的多态工作:

def self.comissioned_by(client) 
    conditions = ActiveRecordHelper.polymorphic_sql(self, :contact => {:client => {:conditions => ["clients.id = ?", client.id]}, :client_employee => {:conditions => ["client_employees.client_id = ?", client.id]}} 
    where(conditions) 
end 

现在拨打:

Job.commissioned_by() # pass client instance 

享受。如果需要任何详细信息,请键入我。

+0

这将返回一个空的关系,当我在包含在我担心的问题中的测试上运行时。 –

+0

我只是讨厌arel风格的语法。 ) –

1

你有一个坚持多态性的重要原因吗?

如果一个ClientEmployee总是有一个Client,也许你应该有Job.belongs_to :client。这使你的关系变得简单。我发现添加一些冗余关联也可以实现很好的性能优化,只要它不会使保持记录一致性变得更加困难(即Client/ClientEmployee关系与Job.Client/Job.ClientEmployee分配同步时都存在)。

我真的很喜欢rails中的多态性,但是当你试图在这种情况下加入它们时,它会变得棘手。即使你有独立的Client和ClientEmployee ID,那在db中也会更有效率(两个ints与int和string)。

+0

好的建议。目前的设置的重点是,虽然只有客户可以为工作收费,但在日常管理中,客户的员工可能是工作的人选,而不是客户自己。我会研究的是,client_employees只是客户表中的记录,然后在该表中使用自引用来模拟就业。 –

+0

在这种情况下'def Job.contact; client_employee ||客户; end'提供这将保持您的模式针对您的需求进行优化。 – aceofspades

0

您是否尝试过自定义连接?

def self.with_commissioned_jobs 
    query = <<-QUERY 
    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') 
     OR (jobs.contact_id = clients.id AND jobs.contact_type = 'Client')) 
    QUERY 

    joins(query) 
end 
+0

这似乎没有提取在我的问题中包含的测试中直接与工作联系的客户。它只返回'client_with_emp'。 –