tomdonarski
tomdonarski

Reputation: 49

Rails validation on a joint table

I have following models:

Product (id, name):

    has_many :prices

Product_price (id, product_id, price): The thing is that each product can have different prices

    belongs_to :product

Subscription (id, name):

    has_many :subscription_price_sets,
             foreign_key: :subscription_price_set_id,
             inverse_of: :subscription
    has_many :product_prices, through: :subscription_price_sets

Subscription_price_set (id, product_price_id, subscription_id):

    belongs_to :subscription,
               foreign_key: :subscription_id
    belongs_to :product_price,
               foreign_key: :product_price_id

How do I validate it, so that for a given subscription it's impossible to have a product with two different prices?

For example:

I have two products: Notebook (id: 1) and Pencil (id: 2) And their prices are:

Product_prices:

(id: 1, product_id: 1, price: 4)
(id: 2, product_id: 1, price: 12)
(id: 3, product_id: 1, price: 10)
(id: 4, product_id: 2, price: 3)
(id: 5, product_id: 2, price: 2)

And a Basic subscription:

(id: 1, name: "Basic")

Let's say I have Subscription_price_set:

(id: 1, product_price_id: 1, subscription_id: 1)

Now I should be able to create another Subscription_price_set with subscription_id: 1, but the only allowable product_price_ids should be id: 4 and id: 5.

Any hints on how to achieve that?

Upvotes: 0

Views: 765

Answers (2)

tomdonarski
tomdonarski

Reputation: 49

I've created a custom validation method in Subscription_price_set model, and it did the trick :)

validate :product_uniqness

private

def product_uniqness
  return unless subscription.product_prices.pluck(:product_id)
                         .include?(product_price.product_id)

  errors.add(:product_price_id, 'You can\'t add the same product twice')
end

Upvotes: 1

max
max

Reputation: 102443

Use scope to make a uniqueness validation on multiple columns:

validates_uniqueness_of :subscription_id, scope: :product_price_id

However this does not actually guarantee uniqueness.

Image courtesy of Thoughtbot

To safeguard against race conditions you need to compliment the validation with a database index:

class AddIndexToSubscriptionPriceSets < ActiveRecord::Migration[6.0]
  def change
    add_index :subscription_price_sets, [:subscription_id, :product_price_id] , unique: true
  end
end

Your also using the foreign_key option all wrong. Rails is driven by convention over configuration and will derive the foreign key from the name of the association. You only ever need to specify foreign_key if the name of the association does not match.

belongs_to :subscription
belongs_to :product_price

On the has_many association it will actually cause an error:

has_many :subscription_price_sets,
         foreign_key: :subscription_price_set_id,
         inverse_of: :subscription

This will result in the following join

JOINS subscription_price_sets ON subscription_price_sets.subscription_price_set_id = subscriptions.id

Which of course will blow up as there is no such column. The foreign_key option on a has_many association is used to specify which column on the other table that corresponds to this table. All you really need is:

has_many :subscription_price_sets

Rails can also deduce the inverse of an association based and you only need to specify when you are "going off the rails" and the names don't match up.

Upvotes: 2

Related Questions