Переопределить ActiveRecord ::Relation::CollectionProxy только для конкретной модели

#ruby #activerecord #ruby-on-rails-5 #query-optimization

#ruby #activerecord #ruby-on-rails-5 #оптимизация запросов

Вопрос:

В настоящее время я пытаюсь минимизировать количество SQL-запросов в приложении rails. И использовал область для фильтрации записей и обнаружил, что в моем журнале сервера область запускает SQL-запрос, хотя он показывает КЭШ (0,00 мс), даже если он фильтрует запись с использованием атрибутов записи. (например. фильтрация с использованием obj.status)

Я попытался использовать метод класса в модели, снова, когда я когда-либо вызываю

 def pdf_files
  all.select(amp;:pdf?)
end
  

области,

 scope :pdf_files, -> { all.select(amp;:pdf?) }
  

это запускает sql-запрос.

Я попытался переопределить ActiveRecord ::Associations::CollectionProxy, чтобы включить этот метод, например,

 class ActiveRecord::Associations::CollectionProxy
  def pdf_files
    to_a.select(amp;:pdf?)
  end
end
  

Сработало, но этот метод также может быть вызван из другой модели.

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

 has_many :files, do
  def pdf_files
    to_a.select(amp;:pdf?)
  end
end
  

Это работает для меня, он выбирает файлы без какого-либо запроса sql.

Но дело в том, что. Мне нужны эти методы, доступные только для файловой модели, поскольку эти методы требуются для всех других моделей, связанных с файловой моделью, и я не хочу нарушать соглашение rails, например, @email.files.pdf .

Также обнаружено, что в консоли rails, когда я набираю File::ProxyCollection, он выдает мне ActiveRecord ::Associations::CollectionProxy. Но не знаю, как добавить к нему метод.

Я хочу иметь возможность вызывать @email.files.pdf?, @email.files.doc ?, чтобы отфильтровать результаты без выполнения какого-либо запроса вообще, даже если он вызывает SQL-запрос КЭША.

Определения моделей

 class TestFile < ApplicationRecord
  belongs_to :fileable, polymorphic: true, optional: true
  scope :document_files, -> { all.select(amp;:document?) }
  scope :ebook_files, -> { all.select(amp;:ebook?) }

  enum file_type: [:document, :ebook, :paper, :article, :picture]
end

class TestEmail < ApplicationRecord
  has_many :test_files, as: :fileable
end

class TestPost < ApplicationRecord
  has_many :test_files, as: :fileable
end
  

В консоли

 email = TestEmail.includes(:test_files).first

document_files = email.test_files.document_files
  

Запрос, выполняемый для области document_files, равен
введите описание изображения здесь

Но если я сделаю что-то вроде,

 class TestFile < ApplicationRecord
  belongs_to :fileable, polymorphic: true, optional: true

  enum file_type: [:document, :ebook, :paper, :article, :picture]
end

class TestEmail < ApplicationRecord
  has_many :test_files, as: :fileable do
    def document_files
      to_a.select(amp;:document?)
    end
  end
end

class TestPost < ApplicationRecord
  has_many :test_files, as: :fileable do
    def document_files
      to_a.select(amp;:document?)
    end
  end
end
  

результат загружается без какого-либо запроса
введите описание изображения здесь

Комментарии:

1. Честно говоря, вы пытаетесь сделать что-то очень странное. Не могли бы вы предоставить определения модели (с ассоциацией) и пример запроса, для которого вы хотите избавиться от ненужных запросов?

2. @KonstantinStrukov Я отредактировал свой вопрос, включив в него определения моделей и снимок экрана, показывающий состояние sql-запроса для каждого случая. Это то, что вы ищете?

Ответ №1:

Итак, у вас есть полиморфная ассоциация, и вы хотите получить связанные данные с минимально возможными запросами, верно?

Что касается меня, сама задача («минимизировать количество запросов») имеет смысл только с разумными ограничениями. Всегда полезно избавиться от N 1, но не обязательно пытаться выполнить все в 1 запросе или что-то в этом роде — сложный запрос может быть медленнее, чем несколько последующих тривиальных. Кроме того, сложные сложные запросы, как правило, подвержены ошибкам.

Теперь давайте вернемся к вашему коду. Прежде всего, эта идея

 scope :document_files, -> { all.select(amp;:document?) }
  

выглядит довольно плохо для меня по двум причинам:

  1. все загружается в память, и в случае большой таблицы это вредит. Иногда это необходимый компромисс, но не в этом случае (вам не нужна какая-либо тяжелая обработка и т. Д. — Просто фильтрация данных)

  2. эта «область» не является обычной — ее вызов дает вам Array вместо отношения AR, поэтому ее нельзя связать, как это делают надлежащие области AR и т. Д. Это просто вводящий в заблуждение код, который пахнет. Просто попробуйте что-нибудь. нравится TestFile.document_files.where(<some_extra_conditions>) , и вы получите NoMethodError — вероятно, не тот результат, который можно было ожидать от простого и понятного кода…

Использование расширений ассоциации также бесполезно в вашем случае — это делает ваш код более загадочным, но на самом деле не дает вам никаких преимуществ. Вы ошибаетесь, предполагая, что он работает каким-то особым образом — фактически, он выполняет точно такую же работу, как если бы вы явно вызывали to_a.select... прокси-сервер ассоциации.

Я бы предложил что-то более простое и идиоматичное:

 class TestFile < ApplicationRecord
  belongs_to :fileable, polymorphic: true, optional: true
  scope :document_files, -> { where(file_type: :document) }
  scope :ebook_files, -> { where(file_type: :ebook) }

  enum file_type: [:document, :ebook, :paper, :article, :picture]
end

...
  

Затем, используя предварительную загрузку, где это уместно, вы можете получить небольшое количество запросов (без N 1).

Хотите оптимизировать это еще больше? Что ж, есть много способов сделать это, используя более низкоуровневый API: например, вы можете использовать connection#select_all с произвольно сложным запросом, а затем создавать экземпляры необходимых записей вручную ActiveRecord::Result без дополнительных запросов… Но AR — довольно упрямый фреймворк ORM, и если вы собираетесь «бороться» с ним, вы, скорее всего, довольно быстро получите грязный и не поддерживаемый код.

Если вам действительно нужно что-то более гибкое, я бы посоветовал попробовать другие варианты (продолжение, ROM)…

Комментарии:

1. Я уже пробовал эту опцию тоже, для области document_files , -> { where(file_type: :document) } также запускает SQL-запрос, которого я хочу избежать, поскольку у нас уже есть необходимые тестовые файлы, которые уже загружены, и нам просто нужно отфильтровать требуемые файлы из этих загруженных записей.

2. И каждый раз, когда я делаю что-то вроде documents = @emal.test_files.document_files , каждый раз, когда он выполняет новый SQL-запрос

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