Reputation: 73
Ruby 2.6.5 on Rails 5.2.4.1
I've got a table of Products
, and I'd like to associate products as compatible with one another. For example, Product 1 is compatible with Product 2.
When creating the first ProductCompatibility
, I populate it as follows:
#<ProductLibrary::ProductCompatibility id: 1, product_id: 1, compatible_product_id: 2>
At the moment, I can perform the following:
0> ProductLibrary::Product.find(1).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 2>]
But I would also like to be able to perform the following, without creating an additional record:
0> ProductLibrary::Product.find(2).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 1>]
Currently, the above returns the following:
0> ProductLibrary::Product.find(2).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 2>]
My models look like this:
module ProductLibrary
class Product < ApplicationRecord
has_many :product_compatibilities, ->(p) {
unscope(where: :product_id)
.where(product_id: p.id)
.or(ProductLibrary::ProductCompatibility.where(compatible_product_id: p.id))
}
has_many :compatible_products, through: :product_compatibilities
end
end
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
has_one :compatible_product,
primary_key: :compatible_product_id,
foreign_key: :id,
class_name: 'ProductLibrary::Product'
end
end
The primary_key
in compatible_product
is why I'm getting Product 2 when I request Product 2's compatible products (instead of Product 1).
What I'd like is for the has_one :compatible_product
association to return products where the primary key is both :compatible_product_id
and :product_id
, but I can't figure out how to do that without writing multiple associations and compiling them in a helper method (which feels clunky and unconventional).
I'm not even sure it's possible, but it seems like it's along the lines of a
ProductLibrary::Product.where(id: [:product_id, :compatible_product_id])
which I couldn't get to work as association logic.
Upvotes: 1
Views: 1190
Reputation: 73
Here's what I ended up with, thanks to some help from @max
module ProductLibrary
class Product < ApplicationRecord
has_many :product_compatibilities, ->(p) {
unscope(where: :product_id)
.where(product_id: p.id)
.or(ProductLibrary::ProductCompatibility.where(compatible_product_id: p.id))
}
has_many :compatible_products, through: :product_compatibilities
has_many :inverse_compatible_products, through: :product_compatibilities
def all_compatible
(self.compatible_products + self.inverse_compatible_products).uniq.sort - [self]
end
end
end
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
belongs_to :compatible_product,
class_name: 'ProductLibrary::Product'
belongs_to :inverse_compatible_product,
foreign_key: :product_id,
class_name: 'ProductLibrary::Product'
end
end
I'll probably rename some things, and we may need to implement a boolean to drive whether a product can be compatible with itself (for now I assume not).
It's kind of what I was trying to avoid, but it looks like this is a correct solution.
Upvotes: 1
Reputation: 102443
You should be using belongs_to
instead of has_one
.
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
belongs_to :compatible_product,
class_name: 'ProductLibrary::Product'
end
end
The semantics of has_one
and belongs_to
are a really common source of confusion but the difference is with belongs_to
the foreign key column is on this models table and with has_one
the FKC is on the other model.
What you are creating here is really just a join table and join model with the slight difference that both foreign keys happen to point to the same table instead of two different tables.
Upvotes: 3