Rails запрос ассоциации «многие ко многим» с помощью AND связанных записей

У меня есть проект Rails с моделью продукта и моделью функций и связью «многие ко многим» между ними следующим образом:

# == Schema Information
#
# Table name: products
#
#  id                                   :bigint           not null, primary key
#  created_at                           :datetime         not null
#  updated_at                           :datetime         not null
#
class Product < ApplicationRecord
  has_many :product_features, dependent: :destroy
  has_many :features, through: :product_features
end
# == Schema Information
#
# Table name: features
#
#  id                                   :bigint           not null, primary key
#  created_at                           :datetime         not null
#  updated_at                           :datetime         not null
#
class Feature < ApplicationRecord
  has_many :product_features, dependent: :destroy
  has_many :products, through: :product_features
end
# == Schema Information
#
# Table name: product_features
#
#  id            :bigint           not null, primary key
#  created_at    :datetime         not null
#  updated_at    :datetime         not null
#  product_id    :bigint           not null
#  feature_id    :bigint           not null
#
# Indexes
#
#  index_product_features_on_product_id  (product_id)
#  index_product_features_on_feature_id  (feature_id)
#
# Foreign Keys
#
#  fk_rails_...  (product_id => products.id)
#  fk_rails_...  (feature_id => features.id)
#
class ProductFeature < ApplicationRecord
  belongs_to :product
  belongs_to :feature
end

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

Например:

Product 1 имеет Feature 1, Feature 2 и Feature 3.

Product 2 имеет Feature 2, Feature 3 и Feature 4.

Product 3 имеет Feature 3, Feature 4 и Feature 5.

Product 4 имеет Feature 2 и Feature 5.

Если фильтру заданы Feature 2 и Feature 3, он должен вернуть Product 1 и Product 2, но не Product 3 или Product 4.

Лучшее, что мне удалось придумать на данный момент, это следующее:

def filter_by_features(feature_ids_array)
  product_id_arrays = []
  feature_ids_array.each do |feature_id|
    product_id_arrays << ProductFeature.where(feature_id: feature_id).pluck(:product_id)
  end
  Product.where(id: product_id_arrays.inject(:&))
end

Мне не нравится это решение, потому что оно приводит к запросам N+1. Как я могу реорганизовать фильтр, чтобы избавиться от запросов N+1? Проект работает на Rails 6.0 и PostGres 12.

Вам почти никогда не понадобится или не захочется собирать идентификаторы и передавать их. Это неэффективно. Вместо этого используйте .select(:product_id) и передайте отношение ActiveRecord. Это позволяет AR создавать подзапросы, что позволяет избежать как обращения к базе данных, так и использования памяти.

max 01.05.2024 11:41
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
52
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете использовать комбинацию joins и having, чтобы добиться того, что вам нужно.

Product.joins(:product_features)
       .where(product_features: { feature_id: feature_ids_array })
       .group('products.id')
       .having('count(*) = ?', feature_ids_array.length)

Чтобы это работало правильно, важно гарантировать уникальность комбинации product_id и feature_id в модели ProductFeature. Лучше всего это сделать на уровне базы данных, добавив туда уникальный индекс:

add_index :product_features, [:product_id, :feature_id], unique: true

Наверное, достаточно присоединиться с помощью product_features, не нужно featuresProduct.joins(:product_features).where(product_features: { feature_id: feature_ids_array }).group('products.id').having('count(*) = ?', feature_ids_array.length)

mechnicov 30.04.2024 08:29

Работает отлично. Спасибо за совет по поводу индекса. Мне следовало добавить это независимо от того, какие фильтры я реализовал.

mark.sack 30.04.2024 17:02

Вы можете избежать строк SQL с помощью group(:id) и having(ProductFeature.arel_table[Arel.star].eq(feature_ids_a‌​rray.length)).

max 01.05.2024 11:36

Другие вопросы по теме