【问题标题】:Concatenating ActiveRecord::Relations queried through polymorphic association连接 ActiveRecord::Relations 通过多态关联查询
【发布时间】:2012-11-29 10:58:43
【问题描述】:

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

【问题讨论】:

  • 你调查过arel吗? github.com/rails/arel 它有一个或条件用于查询,并且可以很好地处理复杂的连接。
  • 我当然也会接受基于 Arel 的解决方案。我花了好几个小时试图想出一个,但我似乎无法管理它。

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


【解决方案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

【讨论】:

  • 恐怕不支持 Rails 3.2。我试过Squeel,它类似于MetaWhere,但我似乎无法让它合并AR::Relation's。
  • 您的更新只需稍作修改即可工作,因为我的客户员工外键是employer_id,而不是client_id。干得好,谢谢!
【解决方案2】:

试试这个:

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

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

编辑

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

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

【讨论】:

  • 我正在阅读指南(感谢您提供的链接),据我所知,您提供的代码应该返回“有工作的客户和有工作的员工”。真的,我正在寻找 OR 条件,但我看不到这样做的方法。
  • 恐怕合并似乎也不起作用。我在问题中附加了更多信息以及我正在使用的规范。
【解决方案3】:

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

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

我真的很喜欢 Rails 中的多态性,但是当您尝试像这种情况一样跨它们连接时,它会变得很棘手。即使您有单独的 Client 和 ClientEmployee id,这在 db 中也会更有效(两个 int 与 int 和 string)。

【讨论】:

  • 好建议。当前设置的要点是,虽然只有客户可以为工作付费,但在日常管理中,客户的员工可能是工作的最佳人选,而不是客户自己。我要研究的是让 client_employees 只是客户表中的记录,然后在该表中使用自引用来模拟就业。
  • 在这种情况下,def Job.contact; client_employee || client; end 提供了这将使您的架构根据您的需要进行优化。
【解决方案4】:

您是否尝试过自定义加入?

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 

【讨论】:

  • 这似乎没有提取我在我的问题中包含的测试中直接作为工作联系人的客户。它只返回client_with_emp。
【解决方案5】:
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

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

【讨论】:

  • 在我害怕的问题中包含的测试上运行时,这将返回一个空关系。
  • 我只是讨厌 arel 风格的语法。 )
猜你喜欢
  • 2010-10-15
  • 1970-01-01
  • 2017-01-27
  • 2012-07-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-03
相关资源
最近更新 更多