Оптимизация запросов в ассоциации has_many_through с тремя моделями - PullRequest
0 голосов
/ 31 января 2019

Пытаясь избежать n + 1 запроса

Я работаю над веб-приложением для учета двойной записи, которое имеет следующие основные модели:

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), и у меня может быть много детей, дети также могуту меня много детей, которых я называю семьей).Большинство из этих отношений описаны в модели Account и оптимизированы, насколько это возможно, для рекурсивной структуры.

У учетной записи много записей (транзакций), и записи должны иметь как минимум два разбиения, равных сумме атрибута Amount.(или Дебит / Кредиты) должны равняться 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

Это также работает, и, согласно сообщению в журнале, может быть даже быстрее.При обычном использовании любой из них будет примерно 50 записей в месяц, но затем вы сможете отфильтровать транзакции по годам - ​​но вы получите то, что просили.При обычном использовании регистр за месяц составляет около 70 мсек. Даже квартал составляет около 100 мсек.

Я использовал несколько атрибутов в разделениях и учетных записях, которые избавили от нескольких запросов уровня представления.Передача в основном связана с именами учетных записей, идущими вверх по дереву.

Опять же, просто смотрю, не пропустил ли я что-то, и есть ли лучший способ.

1 Ответ

0 голосов
/ 31 января 2019

Использование вложенного выбора - это правильный вариант 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)

Это создаст один оператор SQL свложенный выбор вместо двух запросов SQL: 1 для получения идентификаторов Entry и передачи его в Rails и 1 другой запрос для выбора записей на основе этих идентификаторов.


Следующий гем, разработанныйбывший коллега, может помочь вам разобраться с такими вещами: 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)

Что делает то же самоекак я и предлагал, но за кадром.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...