Like Model
Let's add the ability for users to like products. This way, users can keep track of products they may be interested in buying later.
Tracking liked products
To set this up, we need to keep track of all the products a user likes. How can we store this information in the database?
First Attempt
To keep track of all the products in a category, we just added a category_id
to products. In this case, we could we add a user_id
to products which will be used to record the user that likes a given product.
You should be able to see why that won't work. Each product only belonged to one category, so we could add a category_id to products to track this relationship. But each product may be liked by many users, so one user_id
won't be able to track all the liking users. (We also cannot just add one product_id
to users, since a user can like many products.)
Solution
We need a way to track all the likes between products and users. Each user can like many products and each product can be liked by many users. This is called a many-to-many relationship. Instead of adding a single column to one model, we should create a new model to represent this relationship between users and products.
Let's create a new model called Like to keep track of all the likes. What custom columns should we put in likes? We just need two: product_id
and user_id
. Each like
item will store the id of the liking user and the id the liked product.
To get all the products liked by a user #1, the computer will get all the Likes where user_id
is 1, and then it can use the accompanying product_id's to get all the liked products.
In the above diagram, User #1 liked 3 products, with ID's 1,3, and 4.
Creating an Indexed Like Model
Go ahead and generate the model Like.
Enter the following in your terminal:
rails generate model Like user_id:integer product_id:integer
This will output:
invoke active_record create db/migrate/20150326201616_create_likes.rb create app/models/like.rb
After entering the above code, Rails will create a new Like model and migration file. Usually, we would run rake db:migrate
now to create the Likes database table, but let's first make some changes to the migration file.
Indexing
How does the computer get all the likes for a specific user_id? By default, the computer simply goes through all the likes from beginning to end and pulls out the ones with the specified user_id. This is fine for a small site, but can get slow when there are millions of likes. To speed things up, you should add an index
to columns that get looked up a lot. An index takes up some space, but it lets the database look up items much more quickly. You can tell Rails to create a database index for you by using the add_index method in the migration file.
The user_id will be looked up frequently, so we should add an index to it.
To do this, add the following line to your migration file:
add_index :likes, :user_id
This says to add an index to the likes table for the user_id column.
It will also be useful to lookup the likes for a given product, so add another line that does that.
add_index :likes, :product_id
Finally, we can add one more index to quickly lookup a specific combination of product_id and user_id:
add_index :likes, [:product_id, :user_id]
A user shouldn't be able to like the same product twice, so let's add a condition that prevents that from happening:
add_index :likes, [:product_id, :user_id], unique: true
Your migration file should now look like this:
class CreateLikes < ActiveRecord::Migration
def change
create_table :likes do |t|
t.integer :user_id
t.integer :product_id
t.timestamps null: false
end
add_index :likes, :user_id
add_index :likes, :product_id
add_index :likes, [:product_id, :user_id], unique: true
end
end
You should now run
rake db:migrate
to create the likes table with the specified columns and indices.
Rails Relationship
Now that the database is set up, it's time to tell Rails about the relationships we want to create so we can easily get the products liked by a user. We'll create a more complex association than the one we did with categories.
A user has_many products that they like, so let's start by adding this line to the user model:
has_many :products, through: likes
This tells Rails that a user has many products, and these products can be found by looking at the user's likes. Now we just need to specify that the user has_many :likes
too:
user.rb
class User
has_many :likes
has_many :products, through: :likes
#...
end
Rails can now get a user's liked products by looking at a user's likes and then getting the product from each like. To get this to work, we need to setup the like association so a Like returns the product it belongs to. Can you add the code to the like model to setup that relationship?
Simply add the belong_to code to specify the relationship:
class Like < ActiveRecord::Base
belongs_to :product
end
Our current code works, but the naming is a little confusing. What does it mean when we say user.products
? These could be products the user is buying or selling, there's nothing to indicate these are the products the user likes.
To fix this, let's change the name from has_many :products
to has_many :liked_products
. We'll also need to add a source
to explain the actual source model of liked_products:
user.rb
class User
has_many :likes
has_many :liked_products, through: :likes, source: :product
#...
end
Console Time
Now that the relationship is set up, start your rails console
to try it out.
u = User.find 1 Like.create(user_id: 1, product_id: 1) u.liked_products
This should return a collection that contains the product you just liked, product #1.
Try creating the same like
again:
Like.create(user_id: 1, product_id: 1)
This should fail:
ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: likes.product_id, likes.user_id: INSERT INTO "likes" ("user_id", "product_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)...
This like
violated the unique
condition that you applied to the database, so it was not added to the database.
Challenge
What's the name of the only product liked by user #4?
Please sign in or sign up to submit answers.
Alternatively, you can try out Learneroo before signing up.