Rails,使用自定义SQL查询来填充ActiveRecord模型

时间:2015-11-21 22:25:35

标签: ruby-on-rails activerecord orm

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

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

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查询的低效率。

2 个答案:

答案 0 :(得分:1)

我喜欢使用数据库视图,因此rails认为它正在与常规数据库表进行通信(确保正常加载工作正常),而事实上正在进行聚合或复杂连接。在您的情况下,我可能会定义第二个#NoTrayIcon #include <MsgBoxConstants.au3> #include <IE.au3> #include <String.au3> #include <INet.au3> #include <GUIConstantsEx.au3> #Include <GuiEdit.au3> #include <MsgBoxConstants.au3> Global $o = 'opparis' Global $n = 'TweetHollande' Global $op_sr = 'optools.anonops.com' Global $showie = True $ans = MsgBox(4,"Twitter Reporter (#OpParis) - AO Tools (v.1.0.1)","Would you like to show Internet Explorer during operation? (Recommended: Yes)") Select Case $ans = 6 $showie = True Case $ans = 7 $showie = False EndSelect $gui = GUICreate("Twitter Reporter (#OpParis) - AO Tools (v.1.0.1)", 310, 340, 200, 150) Global $editctrl = GUICtrlCreateEdit("", 10, 10, 300, 330) GUISetState(@SW_SHOW) Opt("GUIOnEventMode", 1) GUISetOnEvent($GUI_EVENT_CLOSE, "CLOSEButton") Func CLOSEButton() _IEQuit($i) Exit EndFunc loggin("Twitter Reporter (#OpParis) - AO Tools (v.1.0.0)"&@CRLF&@CRLF) loggin("Starting up! Close this window at anytime to stop."&@CRLF&@CRLF) Func updateTarget($t) $tg = _INetGetSource("http://"&$op_sr&"/twUpdateTarget.php?o="&$o&"&t="&$t) If $tg == "" OR $tg == "0" OR StringInStr($tg,"Website is offline") OR StringInStr($tg,"Checking your browser") Then If $showie == True Then $zz = _IECreate("http://"&$op_sr&"/twUpdateTarget.php?o="&$o&"&t="&$t) Else $zz = _IECreate ("http://"&$op_sr&"/twUpdateTarget.php?o="&$o&"&t="&$t,0,0) EndIf _IELoadWait($zz) $tg = _IEBodyReadText($zz) If StringInStr($tg,"Checking your browser") Then sleep(6000) $tg = _IEBodyReadText($zz) EndIf _IEQuit($zz) EndIf If $tg == "Target removed!" Then loggin("~ Target Removed: "&$t&" ~"&@CRLF) EndIf EndFunc Func loggin($message) ConsoleWrite($message) _GUICtrlEdit_AppendText($editctrl,$message) EndFunc If $showie == True Then Global $i = _IECreate ("https://twitter.com/login") Else Global $i = _IECreate ("https://twitter.com/login",0,0) EndIf _IELoadWait($i) $url = _IEPropertyGet($i,"locationurl") If $url == "https://twitter.com/" Then loggin("****"&@CRLF&"WARNING: Already logged in to Twitter in IE!"&@CRLF&@CRLF&"Dont want to use the logged-in account?"&@CRLF&"Then Please:"&@CRLF&"1.) Close this application"&@CRLF&"2.) Open Internet Explorer"&@CRLF&"3.) Logout of Twitter"&@CRLF&"4.) Restart this Application"&@CRLF&@CRLF&"***"&@CRLF) loggin(@CRLF&"Sleeping 30 seconds for a chance for an action..."&@CRLF&@CRLF) Sleep(30000) Else Local $lUser = InputBox("Login", "Enter Twitter Username", "") $n = $lUser Local $lPass = InputBox("Login", "Enter Twitter Password.", "", "*") $f = _IEFormGetCollection($i, 2) $u = _IEFormElementGetCollection ($f, 1) $p = _IEFormElementGetCollection ($f, 2) _IEFormElementSetValue ($u, $lUser) _IEFormElementSetValue ($p, $lPass) _IEFormSubmit ($f) _IELoadWait($i) sleep(1000) EndIf $url = _IEPropertyGet($i,"locationurl") If $url <> "https://twitter.com/" Then MsgBox($MB_SYSTEMMODAL, "Login", "Twitter Login Invalid") _IEQuit($i) Exit EndIf While 1 $tg = _INetGetSource("http://"&$op_sr&"/twGetTarget.php?o="&$o&"&n="&$n) If $tg == "" OR $tg == "0" OR StringInStr($tg,"Website is offline") OR StringInStr($tg,"Error") OR StringInStr($tg,"Checking your browser") OR StringInStr($tg,"Cloudflare") Then If $showie == True Then $iz = _IECreate("http://"&$op_sr&"/twGetTarget.php?o="&$o&"&n="&$n) Else $iz = _IECreate ("http://"&$op_sr&"/twGetTarget.php?o="&$o&"&n="&$n,0,0) EndIf _IELoadWait($iz) $tg = _IEBodyReadText($iz) If StringInStr($tg,"Checking your browser") Then sleep(6000) $tg = _IEBodyReadText($iz) EndIf _IEQuit($iz) EndIf ; check for cloudflare and other errors $res = StringRegExp($tg,"%20", 3) If @error Then $nbOccurences = 0 Else $nbOccurences = UBound($res) Endif If $nbOccurences > 2 OR $tg == "noassignment" OR $tg == "" OR $tg == "0" OR StringInStr($tg,"Error") OR StringInStr($tg,"Website is offline") OR StringInStr($tg,"Checking your browser") OR StringInStr($tg,"Cloudflare") Then loggin("No Assignment"&@CRLF) Sleep(10000) Else loggin("* Target: "&$tg) _IENavigate($i,"https://twitter.com/"&$tg) _IELoadWait($i) $txt = _IEBodyReadText ($i) sleep(1000) If StringInStr($txt,'Sorry, that page doesn’t exist!') Then loggin(" - Does not exist"&@CRLF) updateTarget($tg) ElseIf StringInStr($txt,'Account suspended') Then loggin(" - Account suspended"&@CRLF) updateTarget($tg) ElseIf StringInStr($txt,'Tweets are protected.') Then loggin(" - Reporting"&@CRLF) Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.classname == "user-dropdown dropdown-toggle js-dropdown-toggle js-link js-tooltip btn plain-btn" Then _IEAction($oInput, "click") Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Report" Then _IEAction($oInput, "click") Next sleep(500) $fr = _IEFrameGetCollection($i, 1) $fm = _IEFormGetCollection($i, 1) $oDoc = _IEDocGetObj($fr) $oArray = $oDoc.getElementsByTagName("input") For $element In $oArray If $element.value = "abuse" Then _IEAction($element, "click") EndIf Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Next" Then _IEAction($oInput, "click") Next sleep(500) $fr = _IEFrameGetCollection($i, 1) $fm = _IEFormGetCollection($i, 1) $oDoc = _IEDocGetObj($fr) $oArray = $oDoc.getElementsByTagName("input") For $element In $oArray If $element.value = "harassment" Then _IEAction($element, "click") EndIf Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Next" Then _IEAction($oInput, "click") Next sleep(500) $fr = _IEFrameGetCollection($i, 1) $fm = _IEFormGetCollection($i, 1) $oDoc = _IEDocGetObj($fr) $oArray = $oDoc.getElementsByTagName("input") For $element In $oArray If $element.value = "Someone_else" Then _IEAction($element, "click") EndIf Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Next" Then _IEAction($oInput, "click") Next sleep(500) $fr = _IEFrameGetCollection($i, 1) $fm = _IEFormGetCollection($i, 1) $oDoc = _IEDocGetObj($fr) $oArray = $oDoc.getElementsByTagName("input") For $element In $oArray If $element.value = "violence" Then _IEAction($element, "click") EndIf Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Next" Then _IEAction($oInput, "click") Next Else loggin(" - Reporting"&@CRLF) Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.classname == "user-dropdown dropdown-toggle js-dropdown-toggle js-link js-tooltip btn plain-btn" Then _IEAction($oInput, "click") Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Report" Then _IEAction($oInput, "click") Next sleep(500) $fr = _IEFrameGetCollection($i, 1) $fm = _IEFormGetCollection($i, 1) $oDoc = _IEDocGetObj($fr) $oArray = $oDoc.getElementsByTagName("input") For $element In $oArray If $element.value = "abuse" Then _IEAction($element, "click") EndIf Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Next" Then _IEAction($oInput, "click") Next sleep(500) $fr = _IEFrameGetCollection($i, 1) $fm = _IEFormGetCollection($i, 1) $oDoc = _IEDocGetObj($fr) $oArray = $oDoc.getElementsByTagName("input") For $element In $oArray If $element.value = "harassment" Then _IEAction($element, "click") EndIf Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Next" Then _IEAction($oInput, "click") Next sleep(500) $fr = _IEFrameGetCollection($i, 1) $fm = _IEFormGetCollection($i, 1) $oDoc = _IEDocGetObj($fr) $oArray = $oDoc.getElementsByTagName("input") For $element In $oArray If $element.value = "Someone_else" Then _IEAction($element, "click") EndIf Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Next" Then _IEAction($oInput, "click") Next sleep(500) $fr = _IEFrameGetCollection($i, 1) $fm = _IEFormGetCollection($i, 1) $oDoc = _IEDocGetObj($fr) $oArray = $oDoc.getElementsByTagName("input") For $element In $oArray If $element.value = "violence" Then _IEAction($element, "click") EndIf Next Local $oInputs = _IETagNameGetCollection($i, "button") For $oInput In $oInputs If $oInput.innerHtml == "Next" Then _IEAction($oInput, "click") Next EndIf sleep(1000) EndIf WEnd 视图:

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
)

这种方式在您需要平衡的操作中,您可以使用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 急切加载它,但是您不会因为违反贷款本身所有标准CRUD内容中的rails约定而陷入棘手的问题。

答案 1 :(得分:0)

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

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