提取满足Rails中模型函数的记录

时间:2015-12-03 07:11:55

标签: ruby-on-rails activerecord arel

我在名为CashTransaction的模型中有以下方法。

def is_refundable?
    self.amount > self.total_refunded_amount
end

def total_refunded_amount
   self.refunds.sum(:amount)
end

现在我需要提取满足上述功能的所有记录,即返回true的记录。

我通过以下声明得到了它:

CashTransaction.all.map { |x| x if x.is_refundable? }

但结果是Array。我正在寻找ActiveRecord_Relation对象,因为我需要对结果执行join

我觉得我在这里遗漏了一些东西,因为它看起来并不那么困难。无论如何,它让我陷入困境。建设性的建议会很棒。

注意:amount只是CashTransaction列。

修改

关注SQL完成工作。如果我可以将其更改为ORM,它仍然会完成这项工作。

SELECT `cash_transactions`.* FROM `cash_transactions` INNER JOIN `refunds` ON `refunds`.`cash_transaction_id` = `cash_transactions`.`id` WHERE (cash_transactions.amount > (SELECT SUM(`amount`) FROM `refunds` WHERE refunds.cash_transaction_id = cash_transactions.id GROUP BY `cash_transaction_id`));

分享进度

我设法通过遵循ORM来实现它:

CashTransaction
  .joins(:refunds)
  .group('cash_transactions.id')
  .having('cash_transactions.amount > sum(refunds.amount)')

但我实际上看起来像是:

CashTransaction.joins(:refunds).where(is_refundable? : true)

其中is_refundable?是模型函数。最初我认为将is_refundable?设置为attr_accesor会起作用。但我错了。

只是一个想法,可以使用Arel以优雅的方式解决问题。

4 个答案:

答案 0 :(得分:3)

有两种选择。

1)完成,你已经开始了(当涉及到更大量的数据时效率非常低,因为它在处理之前全部被带入内存):

CashTransaction.all.map(&:is_refundable?) # is the same to what you've written, but shorter.

得到ids:

ids = CashTransaction.all.map(&:is_refundable?).map(&:id)

现在,要获得ActiveRecord Relation:

CashTransaction.where(id: ids) # will return a relation

2)将计算移动到SQL:

CashTransaction.where('amount > total_refunded_amount')

第二种选择以各种可能的方式更快更有效。

当您处理数据库时,尝试在数据库级别处理它,尽可能使用最少的Ruby。

修改

根据编辑的问题,您将如何实现预期的结果:

CashTransaction.joins(:refunds).where('amount > SUM(refunds.amount)')

编辑#2

至于您的相关更新 - 我真的不明白,为什么你作为实例方法锁定到is_refundable?,可以在查询中使用,这在AR中基本上是不可能的,但是..

我的建议是创建一个范围is_refundable

scope :is_refundable, -> { CashTransaction
  .joins(:refunds)
  .group('cash_transactions.id')
  .having('cash_transactions.amount > sum(refunds.amount)')
}

现在可以用

作为简短表示法
CashTransaction.is_refundable

比目标更短更清晰

CashTransaction.where('is_refundable = ?', true)

答案 1 :(得分:1)

你可以这样做:

cash_transactions = CashTransaction.all.map { |x| x if x.is_refundable? } # Array
CashTransaction.where(id: cash_transactions.map(&:id)) # ActiveRecord_Relation

但是,正如其他回答者所提到的那样,这是一种无效的方式。

如果amounttotal_refunded_amount是数据库中cash_transactions表的列,效率和性能更高,则可以使用SQL执行此操作:

CashTransaction.where('amount > total_refunded_amount')

但是,如果amounttotal_refunded_amount不是数据库中的实际列,那么就不能这样做。然后,我猜你已经采用了比使用原始SQL更有效的另一种方式。

答案 2 :(得分:0)

我认为你应该在使用回调更新CashTransaction和他的退款(假设has_many?)时预先计算is_refundable结果(在新列中):

class CashTransaction
  before_save :update_is_refundable
  def update_is_refundable
    is_refundable = amount > total_refunded_amount
  end
  def total_refunded_amount
     self.refunds.sum(:amount)
  end
end

class Refund
  belongs_to :cash_transaction
  after_save :update_cash_transaction_is_refundable
  def update_cash_transaction_is_refundable
    cash_transaction.update_is_refundable
    cash_transaction.save!
  end
end

注意:必须优化上面的代码以防止一些查询

他们可以查询is_refundable列:

CashTransaction.where(is_refundable: true)

答案 3 :(得分:0)

我认为在两个查询而不是连接表上执行此操作并不错,就像这样

def refundable
  where('amount < ?', total_refunded_amount)
end

这将执行单个求和查询,然后在第二个查询中使用sum,当表变大时,您可能会发现这比在数据库中进行连接更快。