Есть ли простой способ проверить существующий объект ActiveRecord с точно такими же ассоциациями? - PullRequest
1 голос
/ 06 августа 2020

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

Модели

class Bundle < ApplicationRecord
  has_and_belongs_to_many :products
end
class Product < ApplicationRecord
  has_and_belongs_to_many :bundles
end

Миграция

class CreateBundles < ActiveRecord::Migration[5.2]
  def change
    create_table :bundles
    create_table :products
    create_join_table :bundles, :products
    add_index :bundles_products, [:bundle_id, :product_id], unique: true
  end
end

Что я пробовал

Сначала я попытался использовать find_or_create_by, но ему не понравилась ассоциация HABTM:

Bundle.find_or_create_by!(products: Product.where(id: [1, 2, 3]))
# ActiveRecord::UnknownPrimaryKey (Unknown primary key for table bundles_products in model Bundles::HABTM_Products.)

На основании некоторых других вопросов, которые мне удалось создать несколько методов, которые достигают того же поведения, что и я. Это работает, но, учитывая, насколько это ужасно выглядит, это, безусловно, неправильный путь:

class Bundle < ApplicationRecord
  has_and_belongs_to_many :products

  def self.containing_exactly(products)
    product_ids = products.pluck(:id)
    where('bundles.id = (
      SELECT bundles_products.bundle_id
      FROM bundles_products 
      JOIN products on bundles_products.product_id = products.id
      GROUP BY bundles_products.bundle_id
      HAVING COUNT(DISTINCT CASE WHEN products.id IN (?) THEN products.id END) = ? 
      AND COUNT(CASE WHEN products.id NOT IN (?) THEN 1 END) = 0
      LIMIT 1)', product_ids, product_ids.size, product_ids).first
    end
  
  def self.create_for(products)
    containing_exactly(products) || create!(products: products)
  end
end
Bundle.create_for(Product.first(2)) # Creates a new bundle with the first two products
Bundle.create_for(Product.first(2)) # Returns the previous bundle since it has the exact same products

Есть ли более простой способ найти существующие пакеты, которые соответствуют определенному набору продуктов, или есть другой способ подойти эти модели?

Использование mysql для базы данных

Ответы [ 2 ]

3 голосов
/ 06 августа 2020

В Rails нет встроенного способа, вам придется написать свой собственный запрос. Вы в значительной степени поняли, я бы предложил только очистить его:

where(<<~SQL, product_ids).first
  bundles.id = (
    select bundle_id
    from bundles_products
    group by bundle_id
    having count(product_id) = count(case when product_id in (?) then 1 else 0 end)
    limit 1
  )
SQL

PS Если это когда-либо станет узким местом производительности, вы всегда можете добавить столбец кеша в таблицу «пакетов». Но вам нужно будет всегда заполнять его.

# Migration
add_column :bundles, :products_hash, :string

# Model
def self.create_for(products)
  products_hash = Digest::SHA1.base64digest(products.map(&:id).join(","))
  Bundle.find_or_create_by!(products_hash: products_hash) do
    bundle.products = products
  end
end
0 голосов
/ 06 августа 2020

, вы можете это сделать.

BundlesProducts.where(product_id: product_ids)
  .group_by(&:bundle_id)
  .select { |k, v| v.map(&:product_id) == product_ids}.keys

но это потребовало бы определения модели BundlesProducts, также проблема здесь в том, что я выполняю обработку памяти для извлеченных записей.

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