【问题标题】:Rails, use custom SQL query to populate ActiveRecord modelRails,使用自定义 SQL 查询填充 ActiveRecord 模型
【发布时间】:2015-11-21 22:25:34
【问题描述】:

我正在尝试构建 Rails 中的“贷款”模型。有一个相应的“支付”模型。贷款余额是贷款的原始金额减去所有付款的总和。计算余额很容易,但我试图计算大量贷款的余额,同时避免 N+1 查询,同时使“余额”成为“贷款”模型的属性。

当我调用 Loans 控制器的 index 方法时,我可以运行自定义选择查询,这允许我通过直接 SQL 查询返回“余额”属性。

class LoansController < ApplicationController
  def index
    @loans = Loan
    .joins("LEFT JOIN payments on payments.loan_id = loan.id")
    .group("loans.id")
    .select("loans.*, loans.amount - SUM(payments.amount) as balance")
  end
  def index_002
    @loans = Loan.includes(:payments)
  end
  def index_003
    @loans = Loan.includes(:payments)
  end
end

class Loan < ActiveRecord::Base
  has_many :payments
  def balance=(value)
    # I'd like balance to load automatically in the Loan model.
    raise NotImplementedError.new("Balance of a loan cannot be set directly.")
  end
  def balance_002
    # No N+1 query, but iterating through each payment in Ruby
    # is grossly inefficient as well
    amount - payments.map(:amount).inject(0, :+)
  end
  def balance_003
    # Even with the "includes" in the controller, this is N+1
    amount - (payments.sum(:amount) || 0)
  end
end

现在我的问题是如何始终使用我的贷款模型来做到这一点。通常 ActiveRecord 使用以下查询加载一个或多个模型:

SELECT * FROM loans
--where clause optional
WHERE id IN (?)

有没有办法覆盖 Loan 模型,以便它加载以下查询:

SELECT
  loans.*, loans.amount - SUM(payments.amount) as balance
FROM
  loans
LEFT JOIN
  payments ON payments.loan_id = loans.id
GROUP BY
  loans.id

这种方式“平衡”是模型的一个属性,只需要在一个地方声明,但我们也避免了 N+1 查询的低效率。

【问题讨论】:

    标签: ruby-on-rails activerecord orm


    【解决方案1】:

    我喜欢为此使用数据库视图,以便 Rails 认为它​​正在与常规数据库表通信(确保像急切加载这样的事情正常工作),而实际上正在进行聚合或复杂连接。在您的情况下,我可能会定义第二个 loan_balances 视图:

    create view loan_balances as (
      select loans.id as loan_id, loans.amount - sum(payments.amount) as balance
      from loans
      left outer join payments on payments.loan_id = loans.id 
      group by 1
    )
    

    然后只做常规的 Rails 关联:

    class LoanBalance < ActiveRecord::Base
      belongs_to :loan, inverse_of: :loan_balance
    end
    
    class Loan < ActiveRecord::Base 
      has_one :loan_balance, inverse_of: :loan
      delegate :balance, to: :loan_balance, prefix: false
    end
    

    通过这种方式,在您想要余额的操作中,您可以使用includes(:loan_balance) 急切地加载它,但您不会因在围绕 Loan 本身的所有标准 CRUD 内容中违反 Rails 约定而陷入棘手的问题。

    【讨论】:

    • 这种方法的问题是大多数数据库视图不能被索引。这意味着要找到记录,您必须对视图执行线性扫描。我相信 Microsoft SQL Server 允许索引视图。据我所知,Postgres 没有。
    • @SteveZelaznik 这并不完全正确。你不能在视图上创建索引,除非它是物化的,真的,但是 postgres 应该在普通视图中使用基础表上的索引。
    【解决方案2】:

    看来我终于回答了我自己的问题。这里是。我覆盖了默认范围。

    class Loan < ActiveRecord::Base
      validates :funded_amount, presence: true, numericality: {greater_than: 0}
      has_many :payments, dependent: :destroy, inverse_of: :loan
      default_scope {
        joins("LEFT JOIN payments as p ON p.loan_id = loans.id")
        .group("loans.id").select("loans.*, sum(p.amount) as paid")
      }
      def balance
        funded_amount - (paid || 0)
      end
    end
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-09-12
      • 1970-01-01
      • 1970-01-01
      • 2018-08-03
      • 1970-01-01
      • 1970-01-01
      • 2015-09-06
      • 1970-01-01
      相关资源
      最近更新 更多