Rails validation on a joint table - ruby-on-rails

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?

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.
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.

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

Related

Rails has_one association with multiple primary keys

Ruby 2.6.5 on Rails 5.2.4.1
Current Situation
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>]
Current Code
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
Intention
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.
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.
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.

rails has_many :through relationship query returns nothing, though records are saved in database

I'm testing out some code in the Rails (v 6.0 if that matters) console, and having difficulty properly querying records that have a has_many :through relationship with each other. This makes me think I've setup my models incorrectly.
Here are my models:
subscriber.rb
has_many :subscriber_rewards, :dependent => :destroy
has_many :rewards, through: :subscriber_rewards
reward.rb
has_many :subscriber_rewards
has_many :subscribers, through: :subscriber_rewards
subscriber_reward.rb
belongs_to :reward
belongs_to :subscriber
I run this code in the console to create a new SubscriberReward assigned to a subscriber:
SubscriberReward.create(reward_id: 3, subscriber_id: 73)
But when I query the rewards of that subscriber...
#subscriber = Subscriber.find(73)
#subscriber.rewards
The console returns this:
#<ActiveRecord::Relation []>
When I would expect it to return an array of rewards associated with that subscriber through the just created SubscriberReward.
And if I look in the database for the last SubscriberReward, I see that it exists and appears to be assigned to the correct subscriber and reward.
SubscriberReward.last
=> #<SubscriberReward id: 3, reward_id: 3, subscriber_id: 73, created_at: "2020-11-15 19:42:58", updated_at: "2020-11-15 19:42:58">
Any idea why I can't just run #subscriber.rewards to access the rewards array that corresponds to that subscriber?
Thanks in advance!
EDIT: Formatting error excluded a line from my subscriber model

Rails and polymorphic associations

I have three models: User, Company, and Subscription. What I am trying to accomplish is a Subscription belongs to either a User OR a Company.
To try accomplish this, I referenced this guide, but I have been unsuccessful as the record creation keeps rolling back.
here's my Company model:
# app/models/company.rb
class Company < ApplicationRecord
has_many :subscriptions, dependent: :destroy, as: :imageable
end
here's my User model:
# app/models/user.rb
class User < ApplicationRecord
has_many :subscriptions, dependent: :destroy, as: :imageable
end
and finally, here's my Subscription model:
class Subscription < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
Now as far as the migration file, this is my Subscription migration file:
class CreateSubscriptions < ActiveRecord::Migration[5.1]
def change
create_table :subscriptions do |t|
t.references :imageable, polymorphic: true, index: true
t.date :start_date
t.date :stop_date
t.timestamps
end
end
end
As far as what I can see, this is pretty much exactly like the guide shows, but it keeps rolling back. Here's the output of the rails console:
Loading development environment (Rails 5.1.6)
2.5.1 :001 > Subscription.create(imageable_id: 1, start_date: Time.now, stop_date: 2.days.from_now)
(8.6ms) SET NAMES utf8, ##SESSION.sql_mode = CONCAT(CONCAT(##sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), ##SESSION.sql_auto_is_null = 0, ##SESSION.wait_timeout = 2147483
(0.2ms) BEGIN
(0.3ms) ROLLBACK
=> #<Subscription id: nil, imageable_type: nil, imageable_id: 1, start_date: "2018-10-10", stop_date: "2018-10-12", created_at: nil, updated_at: nil>
2.5.1 :002 >
Here are the questions that I have:
Why is there an imageable_type field? Is that created by t.references and if so, do I need this? Can I just use imageable_id instead of t.references as the other part of the suggestion shows?
Why is it rolling back? Are polymorphic associations done differently in Rails 5.x or something by chance?
According to the graph shown in the guide, it looks like if a picture belongs to imageable_id 4, then if there is an employee AND a production with the ID of 4, then a picture would belongs to both instead of one or the other like I'm trying to accomplish. Correct?
In your association, Imageable type will contain the class name and imageble id will contain the id of that class. So if you want to create subscription for user you can do like below
User.first.subcriptions.create(start_date: Time.now, stop_date: 2.days.from_now)
So it will automatically pick up First user's id in imageable id and take "User" as imageable type.
If you want to create subscription manually, you must have to pass both fields imageable type and imageble id like below,
Subscription.create(imageable_id: 1, imageable_type: "User", start_date: Time.now, stop_date: 2.days.from_now)
Why is there an imageable_type field? Is that created by
t.references and if so, do I need this? Can I just use imageable_id
instead of t.references as the other part of the suggestion shows?
=> imageable_type will contain the class of associate model like "User" or "Company"
Why is it rolling back? Are polymorphic associations done
differently in Rails 5.x or something by chance?
=> No, you setup it correctly
According to the graph shown in the guide, it looks like if a picture belongs to imageable_id 4, then if there is an employee AND a production with the ID of 4, then a picture would belongs to both instead of one or the other like I'm trying to accomplish. Correct ?
=> It depends on both imageable_id and imageble_type , so by combination of both this you will get record. If imageable_id is 4 and imageable_type is "Picture" then it will take Picture with id 4.
Please check this link for understaing
For polymorphic association, you should also pass imageable_type along with imageable_id. You don't do it and that's why it doesn't work, most probably (i.e. there might be other reasons, I don't know, but this one is pretty obvious).
imageable_type holds the name of the class of the record given Subscription is associated to.

Association Help In Rails

I'm making a movie review application. A user can scroll through the different reviews that have been created and save them to a list. In my console, I'm unable to access the user's list reviews. User.list.reviews. What am I missing? Thanks in advance!!
Here are my current models and associations.
User Model
has_one :list
has_many reviews, :through => :list
List Model
belongs_to :user
has_many :reviews
Review Model
has_many :lists
has_many :users, :through => :lists
Schema: List
user_id
review_id
In your schema, a List has one user ID and one review ID. So a List can only ever have one of those things. But you want a User to have just one List, while the List has many Reviews.
It then gets more complex, because a List can have many Reviews. But since many different Users can put Reviews into their own lists, one Review might appear in several Lists. In short, List has_and_belongs_to_many :reviews and Review has_and_belongs_to_many :lists. This means you need somewhere to put the List ID and Review ID pair that express this relationship - it's called a join table. The convention is just concatenate the two names of the related two tables to get the name of the join table - so if we have tables lists and reviews, we need a join table called lists_reviews (you can override this but it's easier to just go with the convention).
A bare minimum Rails migration would be:
create_table :users do |t|
end
create_table :lists do |t|
t.belongs_to :user # Leads to "user_id" column
end
create_table :reviews do |t|
end
create_table :lists_reviews do |t|
t.belongs_to :list # Leads to a "list_id" column
t.belongs_to :review # Leads to a "review_id" column
end
Given this, and given that with #has_one you're supposed to put #belongs_to in the thing it has, too, so List should belong_to :user, we get:
class User < ActiveRecord::Base # Rails <= v4
has_one :list
has_many :reviews, :through => :list
end
class List < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :reviews
end
class Review < ActiveRecord::Base
has_and_belongs_to_many :lists
has_many :users, :through => :lists
end
And with all this dumped into an empty Rails shell we can test it at the console:
u1 = User.new; u1.save!
u2 = User.new; u2.save!
l1 = List.new( user: u1 ); l1.save!
l2 = List.new( user: u2 ); l2.save!
r1 = Review.new; r1.save!
r2 = Review.new; r2.save!
r3 = Review.new; r3.save!
l1.reviews << r1
l1.reviews << r2
l1.save!
l2.reviews << r2
l2.reviews << r3
l2.save!
u1.list
# => #<List id: 1, user_id: 1>
u1.list.reviews
# => #<ActiveRecord::Associations::CollectionProxy [#<Review id: 1>, #<Review id: 2>]>
u2.list
# => #<List id: 2, user_id: 2>
u2.list.reviews
# => #<ActiveRecord::Associations::CollectionProxy [#<Review id: 2>, #<Review id: 3>]>
l1.user
# => #<User id: 1>
l2.user
# => #<User id: 2>
r1.users
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 1>]>
r1.lists
=> #<ActiveRecord::Associations::CollectionProxy [#<List id: 1, user_id: 1>]>
r2.users
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 1>, #<User id: 2>]>
r2.lists
=> #<ActiveRecord::Associations::CollectionProxy [#<List id: 1, user_id: 1>, #<List id: 2, user_id: 2>]>
r3.users
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2>]>
r3.lists
=> #<ActiveRecord::Associations::CollectionProxy [#<List id: 2, user_id: 2>]>
...it works.
Schema: List
user_id
review_id
This implies that your List belongs-to one user and also belongs-to one review... but you've defined your list associations like this:
belongs_to :user
has_many :reviews
So Rails is getting confused as to why a list has a review_id (which is a belongs_to association thing). and is looking at the Review model, hoping it will have a list_id column... because that's how you'd solve that one... and yet your Review model has many list, so it can't resolve the issue.
Can you tell us what you'd actually like to happen? How should these things be related? should we change the associations you've defined to match the id-columns or can you more completely specify the relationships between models (eg with an object diagram) and then we can tell you how to alter your id-columns/associations to match your object-diagram?
You'll need to use a has_and_belongs_to_many for this. Take a look at http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association.
The problem is that your List model has_many reviews, and your Review model has_many lists. If two models both have a has_many relation to each other, how can you model this in the database? That is, where would you put the foreign key? Whichever table has the foreign key can only have a belongs_to relation to the other table. i.e. it can only belong to one record on the other table.
The solution to this is to use a join table. In rails this will usually have a name of something like lists_reviews. This join table would have two foreign keys, one for the lists table, and one for reviews. This way, each table can have a has_many relation to the other. In rails, you can use has_and_belongs_to_many to do this. Check out the link above for more.

Rails Postgres many to many relationship with custom foreign key

I have a fact table, clients with a bunch of businesses:
bus_id, sales, date
1, $986, 1/1/2016
1, $543, 1/2/2016
2, $921, 1/1/2016
2, $345, 1/2/2016
I want to create a table opportunities
bus_id, opportunity
1, "Upsell"
1, "Upsell More"
How do I create the opportunities table so that I could display the opportunities per bus_id?
Here is the migration command:
bin/rails g model opportunity custom_foreign_bus_id:integer:index description
businesses.rb
has_many :opportunities, foreign_key: :custom_foreign_bus_id
opportunity.rb
belongs_to :business, foreign_key: :custom_foreign_bus_id
Then to get the Business Opportunities:
Business.find(1).opportunities
or simply:
Opportunity.where(custom_foreign_bus_id: 1)
To do a many-to-many you need a has_and_belongs_to_many association or a join model, is that what you really want?
2.8 Choosing Between has_many :through and has_and_belongs_to_many

Resources