有没有一种简单的方法来检查具有完全相同关联的现有 ActiveRecord 对象?
Is there an easy way to check for an existing ActiveRecord object with exactly the same associations?
我正在尝试创建一组代表一组产品的模型。当我创建一个新的包时,我想检查匹配的包是否已经存在,以便我可以重新使用它。
型号
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
Rails 中没有 built-in 方法,您必须自己编写查询。你已经差不多明白了,我只建议清理它:
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
P. S. 如果这成为性能瓶颈,您总是可以将缓存列添加到“捆绑包”table。但您需要确保始终填写它。
# 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
你可以做到这一点。
BundlesProducts.where(product_id: product_ids)
.group_by(&:bundle_id)
.select { |k, v| v.map(&:product_id) == product_ids}.keys
但这需要定义 BundlesProducts
模型,这里的问题也是我对检索到的记录进行内存处理。
我正在尝试创建一组代表一组产品的模型。当我创建一个新的包时,我想检查匹配的包是否已经存在,以便我可以重新使用它。
型号
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
Rails 中没有 built-in 方法,您必须自己编写查询。你已经差不多明白了,我只建议清理它:
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
P. S. 如果这成为性能瓶颈,您总是可以将缓存列添加到“捆绑包”table。但您需要确保始终填写它。
# 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
你可以做到这一点。
BundlesProducts.where(product_id: product_ids)
.group_by(&:bundle_id)
.select { |k, v| v.map(&:product_id) == product_ids}.keys
但这需要定义 BundlesProducts
模型,这里的问题也是我对检索到的记录进行内存处理。