Association Help In Rails - ruby-on-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.

Related

Rails query has_many through association

I have three models associated with the following ways:
User model
has_many :project_memberships
has_many :projects, through: :project_memberships
Project model
has_many :project_memberships
has_many :users, through: :project_memberships
Project membership model
belongs_to :user
belongs_to :project
The project membership model also has additional fields like user_role, invitation_accepted etc.
I want to get all the users in a specified project, with all the project membership fields.
Example:
# user json response
[
{
id: user_id,
name: user_name,
user_role: "admin",
invitation_accepted: true
},
{
// Etc
}
]
Currently, I have something like:
def index
#project = Project.find(params[:project_id])
#team_members = #project.project_memberships
end
The team_members only returns
#<ActiveRecord::Associations::CollectionProxy [#<ProjectMembership id: "42087cd2-31f5-4453-b620-5b47a82de422", user_id: "4f428880-48d0-40d0-b6d6-eed9172ce78d", project_id: "3e758d26-7625-4cbd-8980-77085f8d38a0", role: "admin", invitation_accepted: true, job_title: nil, created_at: "2020-10-24 05:48:38", updated_at: "2020-10-24 05:48:38">]>
I am getting the user_id, but don't know how to merge the actual user fields in the above query.
You can use includes to preload data (behind the scenes, it is performing a join).
#team_members = #project.project_memberships.includes(:user)
Now you can call #team_members[0].user.name (or extract the user name from all of them) and it doesn't fire an additional database query to load the user.
Note that this does work without includes, but it will be slower, and introduces a common pitfall known as "N+1 queries"

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

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.

Creating a habtm relationship

I'm creating a movie watchlist app in rails/angular. For this I have a table with users and a table with movies.
At the moment I create a unique record for each movie added, this results in several copies of existing records but all with different user id's. So I'm looking into the has_and_belongs_to_many relationship in rails. But I'm having some trouble using it in my app.
As I see it a user can have multiple movies, and a movie can have multiple users. So I've added this to the models,
movie.rb
has_and_belongs_to_many :users
user.rb
has_and_belongs_to_many :movies
I've also created a migration,
class AddMoviesUsersJoinTable < ActiveRecord::Migration
def self.up
create_table :movies_users, :id => false do |t|
t.integer :movie_id
t.integer :user_id
end
end
def self.down
drop_table :movies_users
end
end
But I'm unsure on how this is all connected. With the migration I've added a join table with 2 colums. Movie_id and user_id. Does the movie_id value in the movie_users table correspond to the movie_id in the movie table? Same goes for the user_id, does that correspond to the user_id in the user table?
Also, how do I connect the two id's together. Do I have to add "something" to the movies_users join table when I create a new movie?
This is my current movies_controller.rb
def create
respond_with Movie.create(movie_params.merge(user_id: current_user.id))
end
private
def movie_params
params.require(:movie).permit(:title, :image, :release_date, :movie_id)
end
//EDIT//
I've added the has_many_belongs_to relations to my movie and user model and I created a join table called movies_users with 2 colums user_id and movie_id.
I've created 2 accounts on my page and added the movie Creed with them. This is the result in my rails console Movie.all
#<Movie id: 1, title: "Creed", user_id: 1, movie_id: "312221">,
#<Movie id: 2, title: "Creed", user_id: 2, movie_id: "312221">
As you can see it still creates 2 different movies although they have the same values, except the user_id. So it looks like there's no checking to see if an value (the movie_id) already exists. I thought this was a part of the habtm relation.
Rather than habtm i would use has_many :through
class User
has_many :movies_users
has_many :movies, :through => :movies_users
class Movie
has_many :movies_users
has_many :users, :through => :movies_users
class MoviesUser
belongs_to :movie
belongs_to :user
Now you will have a single movie record per movie and create a movie_user record when a user watches a movie. You can do this RESTfully with a MoviesUsersController, which you call the create action, passing through params = {:movies_user => {:user_id => 123, :movie_id => 456}}
It is fine to use habtm in your case if you need the join table only for relationships and don't need to store extra information in the joining table. In this c\\ase records of the joining table (movies_users) are created automatically, while you are using given by the has_and_belongs_to_many methods.
Your create action may look like this:
def create
movie = Movie.create(movie_params)
current_user.movies << movie
...
end
or like this:
def create
current_user.movies.create(movie_params)
...
end
Added after the question updated
You don't need movie_id and user_id fields in your Movie model. This all should look like this:
movie = Movie.last
#<Movie id: 1, title: "Creed">
user = User.last
#<User id: 1, name: "Username", ...>
# add the movie to the user's watch list
user.movies << movie
# a record in the joining table will be created automatically
#<user_id: 1, movie_id: 1>

How to display unique records from a has_many through relationship?

I'm wondering what is the best way to display unique records from a has_many, through relationship in Rails3.
I have three models:
class User < ActiveRecord::Base
has_many :orders
has_many :products, :through => :orders
end
class Products < ActiveRecord::Base
has_many :orders
has_many :users, :through => :orders
end
class Order < ActiveRecord::Base
belongs_to :user, :counter_cache => true
belongs_to :product, :counter_cache => true
end
Lets say I want to list all the products a customer has ordered on their show page.
They may have ordered some products multiple times, so I'm using counter_cache to display in descending rank order, based on the number of orders.
But, if they have ordered a product multiple times, I need to ensure that each product is only listed once.
#products = #user.products.ranked(:limit => 10).uniq!
works when there are multiple order records for a product, but generates an error if a product has only been ordered once. (ranked is custom sort function defined elsewhere)
Another alternative is:
#products = #user.products.ranked(:limit => 10, :select => "DISTINCT(ID)")
I'm not confident that I'm on the right approach here.
Has anyone else tackled this? What issues did you come up against? Where can I find out more about the difference between .unique! and DISTINCT()?
What is the best way to generate a list of unique records through a has_many, through relationship?
Thanks
Have you tried to specify the :uniq option on the has_many association:
has_many :products, :through => :orders, :uniq => true
From the Rails documentation:
:uniq
If true, duplicates will be omitted from the collection. Useful in conjunction with :through.
UPDATE FOR RAILS 4:
In Rails 4, has_many :products, :through => :orders, :uniq => true is deprecated. Instead, you should now write has_many :products, -> { distinct }, through: :orders. See the distinct section for has_many: :through relationships on the ActiveRecord Associations documentation for more information. Thanks to Kurt Mueller for pointing this out in his comment.
Note that uniq: true has been removed from the valid options for has_many as of Rails 4.
In Rails 4 you have to supply a scope to configure this kind of behavior. Scopes can be supplied through lambdas, like so:
has_many :products, -> { uniq }, :through => :orders
The rails guide covers this and other ways you can use scopes to filter your relation's queries, scroll down to section 4.3.3:
http://guides.rubyonrails.org/association_basics.html#has-many-association-reference
On Rails 6 I got this to work perfectly:
has_many :regions, -> { order(:name).distinct }, through: :sites
I couldn't get any of the other answers to work.
in rails6 use -> { distinct } in scope it will work
class Person
has_many :readings
has_many :articles, -> { distinct }, through: :readings
end
person = Person.create(name: 'Honda')
article = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 7, name: "a1">]
Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
You could use group_by. For example, I have a photo gallery shopping cart for which I want order items to be sorted by which photo (each photo can be ordered multiple times and in different size prints). This then returns a hash with the product (photo) as the key and each time it was ordered can be listed in context of the photo (or not). Using this technique, you could actually output an order history for each given product. Not sure if that's helpful to you in this context, but I found it quite useful. Here's the code
OrdersController#show
#order = Order.find(params[:id])
#order_items_by_photo = #order.order_items.group_by(&:photo)
#order_items_by_photo then looks something like this:
=> {#<Photo id: 128>=>[#<OrderItem id: 2, photo_id: 128>, #<OrderItem id: 19, photo_id: 128>]
So you could do something like:
#orders_by_product = #user.orders.group_by(&:product)
Then when you get this in your view, just loop through something like this:
- for product, orders in #user.orders_by_product
- "#{product.name}: #{orders.size}"
- for order in orders
- output_order_details
This way you avoid the issue seen when returning only one product, since you always know that it will return a hash with a product as the key and an array of your orders.
It might be overkill for what you're trying to do, but it does give you some nice options (i.e. dates ordered, etc.) to work with in addition to the quantity.

Resources