通过多态关联查询连接ActiveRecord :: Relations

时间:2012-11-29 10:58:43

标签: ruby-on-rails ruby-on-rails-3 activerecord ruby-on-rails-3.2

我的应用程序有一个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

除此之外:该方法有点像黑客,因为它返回一个数组而不是ActiveRecord::Relation。有趣的是,如果我尝试将作业连接到employee_jobs,它就会爆炸。它可能会或可能不会为我的目的。

我想向名为Client的{​​{1}}添加范围。这应该返回系统中有工作或有员工的所有客户。

with_commissioned_jobs

如何实施此方法?

我正在使用Rails 3.2.9。

更新

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

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

现在我需要做的就是将这两个方法调用合并为一个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 的实例,我无法在其上链接更多范围。

更新2

使用Relation似乎也不起作用。这是AR查询和生成的SQL。

merge

顺便说一句,这是我试图通过的测试。第一次测试失败,因为当我使用merge时没有结果。

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'

5 个答案:

答案 0 :(得分:1)

试试这个:

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

它应该加入客户的工作以及客户员工&#39;工作。有关更全面的信息,请参阅the guides

修改

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

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

答案 1 :(得分: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

答案 2 :(得分:1)

你有一个重要的理由坚持多态吗?

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

我真的很喜欢rails中的多态性,但是当你尝试加入它们时它会变得棘手,就像在这种情况下一样。即使你有单独的Client和ClientEmployee id,这在db中也会更有效(两个int对比int和string)。

答案 3 :(得分: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

然后扩展你的多态Job:

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

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

答案 4 :(得分: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