试图避免n + 1次查询
我正在研究基于Web的双重录入会计应用程序,该应用程序具有以下基本模型;
ruby
class Account < ApplicationRecord
has_many :splits
has_many :entries, through: :splits
end
class Entry < ApplicationRecord
has_many :splits, -> {order(:account_id)}, dependent: :destroy, inverse_of: :entry
attribute :amount, :integer
attribute :reconciled
end
class Split < ApplicationRecord
belongs_to :entry, inverse_of: :splits
belongs_to :account
attribute :debit, :integer
attribute :credit, :integer
attribute :transfer, :string
end
这是一个相当经典的“会计”模型,至少它是在GnuCash之后形成的,但是它会导致一些复杂的查询。 (从古代历史来看,这几乎是第三范式结构!)
第一个Account
是一个层次树结构(一个帐户属于父级(ROOT除外),我有很多孩子,孩子可能也有很多孩子,我称之为家庭)。这些关系大多数都包含在“帐户”模型中,并尽可能优化递归结构。
一个帐户有许多条目(交易),并且条目必须至少具有两个拆分,以使“金额”属性(或“借方/贷方”)之和必须等于0。
此结构的主要用途是生成分类帐,该分类帐只是Entries
及其相关的Splits
的列表,通常按日期范围过滤。如果帐户没有家人/孩子,这很简单
ruby
# self = a single Account
entries = self.entries.where(post_date:@bom..@eom).includes(:splits).order(:post_date,:numb)
如果您想要一个有很多子帐户的分类帐(我想要所有Current Assets
的分类帐),它将变得更加复杂
ruby
def self.scoped_acct_range(family,range)
# family is a single account_id or array of account_ids
Entry.where(post_date:range).joins(:splits).
where(splits: {account_id:family}).
order(:post_date,:numb).distinct
end
虽然这可行,但我想我有一个n + 1查询,因为如果我使用includes instead of joins
,则不会获得条目的所有分割,只有家族中的所有分割-我想要所有分割。这意味着它将重新加载(查询)视图中的拆分。还需要与众不同,因为拆分可能会多次引用一个帐户。
我的问题是否有更好的方法来处理这三个模型查询?
我汇集了一些技巧,其中一项是从分裂中倒退的:
ruby
def self.scoped_split_acct_range(family,range)
# family is a single account_id or array of account_ids
# get filtered Entry ids
entry_ids = Split.where(account_id:family).
joins(:entry).
where(entries:{post_date:range}).
pluck(:entry_id).uniq
# use ids to get entries and eager loaded splits
Entry.where(id:eids).includes(:splits).order(:post_date,:numb)
end
这也有效,并且日志中报告的ms甚至可能更快。正常使用其中一个将要查看一个月左右的50个左右的条目,但是随后您可以过滤掉一年的交易量-但您可以得到所要求的。对于正常使用,一个月的分类帐约为70毫秒,甚至四分之一约为100毫秒。
我在拆分和帐户中都使用了一些属性,这些属性摆脱了一些视图级别的查询。转移基本上是串联的帐户名。
再次,只是看看我是否缺少某些东西,还有更好的方法。
答案 0 :(得分:1)
使用嵌套选择是IMO的正确选项。
您可以使用嵌套选择优化代码以使用以下代码:
entry_ids = Entry.where(post_date: range)
.joins(:splits)
.where(post_date: range, splits: { account_id: family })
.select('entries.id')
.distinct
Entry.where(id: entry_ids).includes(:splits).order(:post_date,:numb)
这将生成带有嵌套select的单个SQL语句,而不是进行2条SQL查询:1条获取Entry ID并将其传递给Rails以及1条其他查询以基于这些ID选择条目。
由前同事开发的以下宝石可以帮助您处理此类事情:https://github.com/MaxLap/activerecord_where_assoc
根据您的情况,它可以使您执行以下操作:
Entry.where_assoc_exists(:splits, account_id: 123)
.where(post_date: range)
.includes(:splits)
.order(:post_date, :numb)
与我建议的相同,但在幕后。