【问题标题】:Rails 6.1: Create a scope containing .first or .last through a has_many relationRails 6.1:通过 has_many 关系创建包含 .first 或 .last 的范围
【发布时间】:2021-10-13 21:18:29
【问题描述】:

在 Rails 6.1 中,假设有两个模型:

  • PeriodicJob: has_many :executions
  • Execution 的字段为 state,即 succeededfailed

我想运行查询:

给我所有PeriodicJobs,最后一个(最高ID)执行状态为succeed

一个潜在的解决方案是带有子查询的原始 SQL,正如另一个 Stackoverflow 问题中指出的那样:Ransack searching for instances with specific value in last of has_many associations

但是,对于这样一个简单的英文问题,这似乎过于复杂的代码。鉴于 Rails 的强大功能,我本来希望看到类似的内容:

PeriodicJob.joins(:executions).where(cool_trick_im_yet_unaware_of_to_get_last_ordered_by_id_execution: { state: 'succeeded' }

Rails 中是否存在这样的东西,如何将它应用到这个示例中?

【问题讨论】:

    标签: ruby-on-rails join rails-activerecord


    【解决方案1】:

    如果您使用常规数字 ID,则可以向关系添加条件。这将在模型上设置一个自定义关系,该关系将返回成功的最新执行。有一个宏会将“集合”限制为一个。

    has_one :current_succeeded_execution, -> { where(state: "succeeded").reorder(id: :desc) }, class_name: 'Execution'
    

    如果您使用 uuid,我会在这些 mdoel 上设置一个默认范围,以便按在 asc 处创建的顺序来确保第一个始终是最旧的,即第一个创建的。那么你可以重新排序到 desc 以获得最新的

    has_one :current_succeeded_execution, -> { where(state: "succeeded").reorder(created_at: :desc) }, class_name: 'Execution'
    

    【讨论】:

    • 您缺少class_name: 'Execution' 选项。
    • @max 谢谢你:D 我不得不急于完成,因为我在回答时被叫走了:P
    • 答案中的 has_one 范围还缺少 lambda 运算符 (->)
    • 此解决方案似乎不起作用:PeriodicJob.joins(:current_succeeded_execution).pluck(:id) 为每个成功的Execution 返回一个PeriodicJob ID:[14, 14, 14, 14, 14, 15, 15, 15, 15]。它包括上次执行失败的作业的 ID,这不是所要求的(“给我所有 PeriodicJobs 哪个最后(最高 ID)执行状态为 succeed。”)
    【解决方案2】:

    优化读取的一种方法是设置一个单独的外键列和关联作为最新执行的“快捷方式”:

    class AddLatestExecutionToProducts < ActiveRecord::Migration[6.0]
      def change
        add_reference :latest_execution, :execution
      end
    end
    
    class PeriodicJob < ApplicationRecord
      has_many :executions, 
        after_add: :set_latest_execution
      belongs_to :latest_execution, 
        optional: true,
        class_name: 'Execution'
    
      private
    
      def set_latest_execution(execution)
        update_attribute(:latest_execution_id, execution.id)
      end
    end
    

    这使您可以执行PeriodicJob.eager_load(:latest_execution) 并避免 N+1 查询和从执行表中加载所有记录。如果每个周期作业有很多执行,这一点尤其重要。

    成本是每次创建执行时都需要额外的写入查询。

    如果您想将其限制为仅最新的成功/失败,您可以添加两列:

    class AddLatestExecutionToProducts < ActiveRecord::Migration[6.0]
      def change
        add_reference :latest_successful_execution, :execution
        add_reference :latest_failed_execution, :execution
      end
    end
    
    class Execution ​< ApplicationRecord
      enum state: {
        ​succeeded: 'succeeded',
        ​failed:    'failed'
     ​ }
    end
    
    class PeriodicJob < ApplicationRecord
      has_many :executions, 
        after_add: :set_latest_execution
      belongs_to :latest_successful_execution, 
        optional: true,
        class_name: 'Execution'
      belongs_to :latest_failed_execution, 
        optional: true,
        class_name: 'Execution'
    
      private
    
      def set_latest_execution(execution)
        if execution.succeeded?
          update_attribute(:latest_successful_execution_id, execution.id)
        else
          update_attribute(:latest_failed_execution_id, execution.id)
        end
      end
    end
    

    【讨论】:

    • 我在气象站应用程序中使用了这个,每个站都有数千个观测值,它是一英里内性能最高的解决方案。不过,它可能并不完全适合您的用例。
    • 由于读取性能的提升和良好的解释,对此答案+1。然而,我一直在寻找一个没有任何额外迁移的查询,我宁愿不向数据库添加冗余并为每次写入添加回调。
    猜你喜欢
    • 2014-02-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-14
    • 1970-01-01
    • 2021-09-24
    • 2014-07-05
    • 1970-01-01
    相关资源
    最近更新 更多