9

我的应用程序有一个 Job 模型。系统中的每个作业都有一个contact. 这就像如果您需要询问有关工作的问题,您会打电话给一个人。联系人可以是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

客户有这样的想法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

旁白:该方法有点 hack,因为它返回一个数组而不是一个ActiveRecord::Relation. 如果我尝试将工作连接到employee_jobs 中,它也会爆炸,这也很有趣。它可能会或可能不会出于我的目的。

我想为Client被调用添加一个范围with_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
4

5 回答 5

1

试试这个 :

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

它应该加入客户的工作以及客户员工的工作。有关更详尽的信息,请参阅指南。

编辑

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

 joins( :jobs ).merge( joins(employees: :jobs) )
于 2012-11-29T12:38:02.463 回答
1

您是否考虑过 gem 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
于 2012-12-10T18:15:42.283 回答
1

你有坚持使用多态性的重要理由吗?

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

我真的很喜欢 Rails 中的多态性,但是当您尝试像本例那样跨它们连接时,它会变得很棘手。即使您有单独的 Client 和 ClientEmployee id,这在数据库中也会更有效(两个整数与整数和字符串)。

于 2012-12-11T18:20:09.453 回答
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:Relation 为多态的东西创建数组条件。

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

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

于 2012-12-10T22:56:16.677 回答
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 
于 2012-12-11T19:17:48.863 回答