У меня есть проект 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.
Вы можете использовать комбинацию 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
, не нужно features
Product.joins(:product_features).where(product_features: { feature_id: feature_ids_array }).group('products.id').having('count(*) = ?', feature_ids_array.length)
Работает отлично. Спасибо за совет по поводу индекса. Мне следовало добавить это независимо от того, какие фильтры я реализовал.
Вы можете избежать строк SQL с помощью group(:id)
и having(ProductFeature.arel_table[Arel.star].eq(feature_ids_array.length))
.
Вам почти никогда не понадобится или не захочется собирать идентификаторы и передавать их. Это неэффективно. Вместо этого используйте
.select(:product_id)
и передайте отношение ActiveRecord. Это позволяет AR создавать подзапросы, что позволяет избежать как обращения к базе данных, так и использования памяти.