模型范围内的唯一联接表参考

时间:2018-09-08 07:59:21

标签: ruby-on-rails rails-activerecord

我有2个具有has_many关联的模型:

class Log < ActiveRecord::Base
  has_many :log_details
end

class LogDetail < ActiveRecord::Base
  belongs_to :log
end

Log表具有一个action_type字符串列。 LogDetail表具有2列:keyvalue,均为字符串,并带有Log的对log_id表的引用。

我想在Log模型上编写3个作用域,以查询将Log表与LogModel两次连接的一些细节。这是一个示例:

has_many :payment_gateways, -> {where(key: 'payment_gateway')}, class_name: 'LogDetail', foreign_key: :log_id
has_many :coupon_codes, -> {where(key: 'coupon_code')}, class_name: 'LogDetail', foreign_key: :log_id

scope :initiate_payment, -> {where(action_type: 'INITIATE PAYMENT')}
scope :payment_gateway, -> (pg) {joins(:payment_gateways).where(log_details: {value: pg}) unless pg.blank?}
scope :coupon_code, -> (cc) {joins(:coupon_codes).where(log_details: {value: cc}) unless cc.blank?}

如果我尝试查询,请使用以上范围

Log.initiate_payment.payment_gateway('sample_pg').coupon_code('sample_cc')

我得到了SQL查询:

SELECT 
    `logs`.*
FROM
    `logs`
        INNER JOIN
    `log_details` ON `log_details`.`log_id` = `logs`.`id`
        AND `log_details`.`key` = 'payment_gateway'
        INNER JOIN
    `log_details` `coupon_codes_logs` ON `coupon_codes_logs`.`log_id` = `logs`.`id`
        AND `coupon_codes_logs`.`key` = 'coupon_code'
WHERE
    `logs`.`action_type` = 'INITIATE PAYMENT'
        AND `log_details`.`value` = 'sample_pg'
        AND `log_details`.`value` = 'sample_cc'

代替:(请注意最后一个AND条件的差异)

SELECT 
    `logs`.*
FROM
    `logs`
        INNER JOIN
    `log_details` ON `log_details`.`log_id` = `logs`.`id`
        AND `log_details`.`key` = 'payment_gateway'
        INNER JOIN
    `log_details` `coupon_codes_logs` ON `coupon_codes_logs`.`log_id` = `logs`.`id`
        AND `coupon_codes_logs`.`key` = 'coupon_code'
WHERE
    `logs`.`action_type` = 'INITIATE PAYMENT'
        AND `log_details`.`value` = 'sample_pg'
        AND `coupon_codes_logs`.`value` = 'sample_cc'

第一个查询由于无法正确解析联接表引用而给我零结果。

我该如何修改我的范围/模型以生成正确的查询?我认为我需要在作用域的where子句中引用连接表别名,但是我不确定如何获取该引用。

1 个答案:

答案 0 :(得分:0)

遗憾的是,ActiveRecord没有内置方法来指定加入关联时使用的别名。由于条件被覆盖,使用merge尝试合并两个范围也失败。

3个解决方案:

  1. 使用Areljoins加上别名,但这有点难以理解,您仍然需要为payment_gatewayscoupon_codes
  2. 直接加入SQL

    scope :payment_gateway, -> (pg) { joins(<<-SQL INNER JOIN log_details payment_gateways ON payment_gateways.log_id = logs.id AND payment_gateways.key = 'payment_gateway' AND payment_gateways.value = #{connection.quote(pg)} SQL ) if pg.present? }

    但是您需要手动添加关联中已经定义的条件

  3. 最后,我最喜欢的是一个坚持于ActiveRecord的解决方案

    scope :payment_gateway, -> (pg) do where(id: unscoped.joins(:payment_gateways).where(log_details: {value: pg})) if pg.present? end

    scope :coupon_code, -> (cc) do where(id: unscoped.joins(:coupon_codes).where(log_details: {value: cc})) if cc.present? end

Gotcha#1::如果您使用Rails <5.2,则可能需要使用类方法而不是范围。

Gotcha#2:解决方案#3的性能可能不如#2,请确保EXPLAIN ANALYZE来查看区别。