使用三种模型在has_many_through关联中优化查询

时间:2019-01-31 15:42:55

标签: ruby-on-rails

试图避免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毫秒。

我在拆分和帐户中都使用了一些属性,这些属性摆脱了一些视图级别的查询。转移基本上是串联的帐户名。

再次,只是看看我是否缺少某些东西,还有更好的方法。

1 个答案:

答案 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)

与我建议的相同,但在幕后。