【问题标题】:Rails left_joins eliminate all if associations if condition met如果条件满足,Rails left_joins 会消除所有 if 关联
【发布时间】:2019-08-26 15:15:34
【问题描述】:

我有一个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

我有一个索引页面,我想在其中加载所有文本消息,但已关闭的消息除外。这就是来自TextMessagelatest_messages 的用武之地。

.where("customer_service_actions.name != 'close' OR customer_service_actions.name IS NULL")

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

... OR customer_service_actions.name IS NULL 将加载还没有任何 customer_service_actions 的短信,并被认为是“未读”给用户。

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

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

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

这可能只用 SQL 吗?

这是我的 SQL 输出。

SQL (1.0ms)  
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" 
LEFT OUTER JOIN "customer_service_histories" 
  ON "customer_service_histories"."item_id" = "text_messages"."id" 
  AND "customer_service_histories"."item_type" = $1 
LEFT OUTER JOIN "customer_service_actions" 
  ON "customer_service_actions"."id" = "customer_service_histories"."customer_service_action_id" 
LEFT OUTER JOIN "phones" 
  ON "phones"."id" = "text_messages"."phone_id" 
LEFT OUTER JOIN "customers" 
  ON "customers"."id" = "phones"."customer_id" 
  AND "customers"."company_id" = $2 
WHERE (
  customer_service_actions.name != 'close' 
  OR customer_service_actions.name IS NULL
) 
AND (
  text_messages.created_at = (
    SELECT MAX(text_messages.created_at) 
    FROM text_messages 
    WHERE text_messages.phone_id = phones.id
  )
) 
ORDER BY 
  customer_service_histories.customer_service_action_id DESC, 
  text_messages.created_at 
DESC LIMIT $3 OFFSET $4  
[["item_type", "TextMessage"], ["company_id", 1], ["LIMIT", 10], ["OFFSET", 0]]

【问题讨论】:

    标签: sql ruby-on-rails ruby activerecord


    【解决方案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 中减去查询。

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

    可能有效

    【讨论】:

    • 虽然这可能可行,但我找不到在 Rails 中实现此功能以生成该查询的方法
    • 显然 Rails ActiveRecord 不支持 EXCEPT 查询。您可以在 Rails 中减去查询。 q1 = TextMessage.all q2 = TextMessage.includes(:histories).where(customer_service_actions:{name: 'close'}) result = q1 - q2 可能有效
    【解决方案2】:

    我有一些工作,但不令人满意。

    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]]
    

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

    字符串优势

    无论如何,我通过运行查询 1,000 次进行了基准测试,发现字符串查询比 ruby​​ 查询快 25%。此外,它们不会添加任何不必要的连接或条件,这对数据库服务器来说工作量较少。我想我会坚持使用琴弦。

    【讨论】:

      猜你喜欢
      • 2015-03-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-12-21
      相关资源
      最近更新 更多