如果满足条件,Rails left_joins会消除所有关联

时间:2019-08-26 15:15:34

标签: sql ruby-on-rails ruby activerecord

我有一个TextMessage模型,它有很多历史

class TextMessage < ApplicationRecord

  has_many :histories, class_name: :CustomerServiceHistory, as: :item

  scope :latest_messages, -> {
      includes(histories: :action, phone: :customer)
      .where("customer_service_actions.name != 'close' OR customer_service_actions.name IS NULL")
      .where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
  }
end

CustomerServiceHistory属于一个项目(可以是文本消息或电子邮件)。用户可以“读取”或“关闭”项目。为此,CustomerServiceHistory属于用户和操作(读取或关闭)。

class CustomerServiceHistory < ApplicationRecord
  belongs_to :action, class_name: :CustomerServiceAction,
                      foreign_key: :customer_service_action_id
  belongs_to :item, polymorphic: true
  belongs_to :user
end

我有一个索引页面,我想在其中加载所有已关闭的文本消息。这是latest_messages的{​​{1}}进入的地方。

TextMessage

.where("customer_service_actions.name != 'close' OR customer_service_actions.name IS NULL") 将加载没有关联的“关闭”操作的短信。

where("customer_service_actions.name != 'close'...将加载尚无任何customer_service_actions且被用户视为“未读”的文本消息。

问题是当用户先“阅读”然后“关闭”短信时,该短信上现在有两个历史记录。

where子句停止工作,因为它能够过滤出此文本消息与其“关闭”操作之间的关系,但不是其与“读”操作之间的关联。

此外,许多用户可以阅读短信。可能有100位用户阅读了该短信。我希望在此短信上只有一个“关闭”操作时不加载该短信,而不管有多少个“读”操作。

是否可以仅使用SQL?

这是我的SQL输出。

... OR customer_service_actions.name IS NULL

2 个答案:

答案 0 :(得分:1)

也许使用EXCEPT吗?

(SELECT * 
FROM "text_messages"
LEFT OUTER JOIN "customer_service_actions" 
ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id")
EXCEPT
(SELECT * 
FROM "text_messages"
LEFT OUTER JOIN "customer_service_actions" 
ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id"
WHERE "customer_service_actions"."name" LIKE 'close')

编辑:显然,Rails ActiveRecord不支持EXCEPT查询。您可以在Rails tho中减去查询。

q1 = TextMessage.all 
q2 = TextMessage.includes(:histories).where(customer_service_actions:{name: 'close'}) 
result = q1 - q2 

可能有效

答案 1 :(得分:0)

我有一些工作要做,但是还不能令人满意。

class TextMessage
  def self.search(query)
    return latest_messages.active unless query.present?

    # more code
  end

  scope :latest_messages, -> {
    where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
  }

  scope :active, -> {
    where(
      <<~SQL.squish
        text_messages.id NOT IN (
          SELECT text_messages.id
          FROM text_messages
          INNER JOIN customer_service_histories
            ON customer_service_histories.item_id = text_messages.id
            AND customer_service_histories.item_type = 'TextMessage'
          INNER JOIN customer_service_actions
            ON customer_service_actions.id = customer_service_histories.customer_service_action_id
          WHERE customer_service_actions.name = 'close'
        )
      SQL
    )
  }

这会产生SQL

SQL (1.9ms)  
SELECT  DISTINCT "text_messages"."id", 
  customer_service_histories.customer_service_action_id AS alias_0, 
  text_messages.created_at AS alias_1 
FROM "text_messages" 
INNER JOIN "phones" 
  ON "phones"."id" = "text_messages"."phone_id" 
INNER JOIN "customers" 
  ON "customers"."id" = "phones"."customer_id" 
  AND "customers"."company_id" = $1 
LEFT OUTER JOIN "customer_service_histories" 
  ON "customer_service_histories"."item_id" = "text_messages"."id" 
  AND "customer_service_histories"."item_type" = $2 
LEFT OUTER JOIN "customer_service_actions" 
  ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id" 
WHERE (
  text_messages.created_at = (
    SELECT MAX(text_messages.created_at) 
    FROM text_messages 
    WHERE text_messages.phone_id = phones.id
  )
) 
AND (
  text_messages.id NOT IN (
    SELECT text_messages.id
    FROM text_messages
    INNER JOIN customer_service_histories
      ON customer_service_histories.item_id = text_messages.id
      AND customer_service_histories.item_type = 'TextMessage'
    INNER JOIN customer_service_actions
      ON customer_service_actions.id = customer_service_histories.customer_service_action_id
    WHERE customer_service_actions.name = 'close'
  )
) 
ORDER BY 
  customer_service_histories.customer_service_action_id DESC, 
  text_messages.created_at DESC 
LIMIT $3 OFFSET $4  
[["company_id", 1], ["item_type", "TextMessage"], ["LIMIT", 10], ["OFFSET", 0]]

这是正确的SQL,但它使用SQL作为字符串。理想情况下,我想要的是

  1. 加载正确的数据
  2. 使用正确的SQL
  3. 在字符串中使用Rails语法而不是SQL
  4. 具有将这些范围链接在一起的能力

类似这样的东西

class TextMessage
  def self.search(query)
    return latest_messages.active unless query.present?

    # more code
  end


  scope :latest_messages, -> {
    where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
  }

  scope :active, -> {
    where.not(id: TextMessage.select(:id)
                             .joins(histories: :action)
                             .where(customer_service_actions: { name: 'close' })
             )
  }

  # more code
end

使用此Rails代码可加载正确的数据,但由于某些原因会导致SQL过多

SQL (1.2ms)  
SELECT  DISTINCT "text_messages"."id", customer_service_histories.customer_service_action_id AS alias_0, text_messages.created_at AS alias_1 
FROM "text_messages" INNER JOIN "phones" ON "phones"."id" = "text_messages"."phone_id" 
INNER JOIN "customers" ON "customers"."id" = "phones"."customer_id" AND "customers"."company_id" = $1 
LEFT OUTER JOIN "customer_service_histories" ON "customer_service_histories"."item_id" = "text_messages"."id" AND "customer_service_histories"."item_type" = $2 
LEFT OUTER JOIN "customer_service_actions" ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id" 
WHERE (
  text_messages.created_at = (                         -- first condition
    SELECT MAX(text_messages.created_at) 
    FROM text_messages 
    WHERE text_messages.phone_id = phones.id
  )
) 
AND (
  text_messages.id NOT IN (
    SELECT "text_messages"."id" 
    FROM "text_messages" 
    INNER JOIN "customer_service_histories" ON "customer_service_histories"."item_id" = "text_messages"."id" AND "customer_service_histories"."item_type" = 'TextMessage' 
    INNER JOIN "customer_service_actions" ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id" 
    WHERE (
      text_messages.created_at = (                     -- repeated first condition
        SELECT MAX(text_messages.created_at) 
        FROM text_messages 
        WHERE text_messages.phone_id = phones.id
      )
    ) 
    AND "customer_service_actions"."name" = 'close'    -- second condition
  )
) 
ORDER BY 
  customer_service_histories.customer_service_action_id DESC, 
  text_messages.created_at DESC 
LIMIT $3 OFFSET $4  
[["company_id", 1], ["item_type", "TextMessage"], ["LIMIT", 10], ["OFFSET", 0]]

重复created_at条件,然后将其与actions.name条件配对。我尝试了多种不同的组合方式以使其更简洁地使用ruby语法,但是我对SQL输出不满意。

我确实找到了一种使用ruby语法并获取所需SQL的方法,但是我必须将两个where()函数都放在同一作用域中。

class TextMessage
  def self.search(query)
    return latest_messages unless query.present?

    # more code
  end

  scope :latest_messages, -> {
    where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
    .where('text_messages.id NOT IN (?)', TextMessage.active_ids)
  }

  scope :active_ids, -> {
    TextMessage.select(:id).joins(histories: :action).where.not(
      customer_service_actions: { name: 'close' }
    )
  }

  # more code
end

我试图将它们放在不同的范围内

  def self.search(query)
    return latest_messages.active unless query.present?

    # more code
  end

  scope :latest_messages, -> {
    where("text_messages.created_at = (SELECT MAX(text_messages.created_at) FROM text_messages WHERE text_messages.phone_id = phones.id)")
  }

  scope :active, -> {
    where('text_messages.id NOT IN (?)', TextMessage.active_ids)
  )

  scope :active_ids, -> {
    TextMessage.select(:id).joins(histories: :action).where.not(
      customer_service_actions: { name: 'close' }
    )
  }

但这导致子查询中出现更多的连接子句

SQL (1.7ms)  
SELECT  DISTINCT "text_messages"."id", 
  customer_service_histories.customer_service_action_id AS alias_0, 
  text_messages.created_at AS alias_1 
FROM "text_messages" 
INNER JOIN "phones" 
  ON "phones"."id" = "text_messages"."phone_id" 
INNER JOIN "customers" 
  ON "customers"."id" = "phones"."customer_id" 
  AND "customers"."company_id" = $1 
LEFT OUTER JOIN "customer_service_histories" 
  ON "customer_service_histories"."item_id" = "text_messages"."id" 
  AND "customer_service_histories"."item_type" = $2 
LEFT OUTER JOIN "customer_service_actions" 
  ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id" 
WHERE (
  text_messages.created_at = (                      -- first condition
    SELECT MAX(text_messages.created_at) 
    FROM text_messages 
    WHERE text_messages.phone_id = phones.id
  )
) 
AND (
  "text_messages"."id" NOT IN (
    SELECT "text_messages"."id" 
    FROM "text_messages" 
    INNER JOIN "phones"                             -- unnecessary joins on phones 
      ON "phones"."id" = "text_messages"."phone_id" 
    INNER JOIN "customers"                          -- unnecessary joins on customers
      ON "customers"."id" = "phones"."customer_id" 
      AND "customers"."company_id" = $3 
    INNER JOIN "customer_service_histories" 
      ON "customer_service_histories"."item_id" = "text_messages"."id" 
      AND "customer_service_histories"."item_type" = $4 
    INNER JOIN "customer_service_actions" 
      ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id" 
    WHERE (
      text_messages.created_at = (                  -- repeated first condition
        SELECT MAX(text_messages.created_at) 
        FROM text_messages 
        WHERE text_messages.phone_id = phones.id
      )
    ) 
    AND "customer_service_actions"."name" = $5      -- second condition
  )
) 
ORDER BY 
  customer_service_histories.customer_service_action_id DESC, 
  text_messages.created_at 
DESC LIMIT $6 OFFSET $7  
[["company_id", 1], ["item_type", "TextMessage"], ["company_id", 1], ["item_type", "TextMessage"], ["name", "close"], ["LIMIT", 10], ["OFFSET", 0]]

也许有一些我没有尝试过的东西,但是我觉得我尝试了很多组合。

字符串优势

无论如何,我通过运行1000次查询来进行基准测试,发现字符串查询比ruby等效查询快25%。此外,它们不会添加任何不必要的联接或条件,这对于数据库服务器来说工作量较小。我想我会坚持下去。