Modelling ActiveRecord associations with AirBnB
Today we’re going to be practicing ActiveRecord associations by modelling the popular holiday rental marketplace AirBnB.
ActiveRecord allows us to build models and associate them to each other, and to their associated databases, without having to write a lot of code. By telling ActiveRecord that our models are related, using associations, it makes it much easier to carry out operations that affect multiple models. In this blog, I’m going to be using the belongs_to, has_many and has_many :through
associations provided in ActiveRecord.
(Before we get started, if you need more information on the basics of ActiveRecord associations, check out the relevant section on RailsGuides.)
Now, let’s think about all of the different models, that are involved when a reservation is made on AirBnB. We have:
- Listings — which contain information about each holiday rental, the city, the neighborhood, the host, the address, the price, a description, and more.
- Guests — who browse listings in neighbourhoods and cities, make bookings, and leave reviews.
- Hosts — who post listings, and leave reviews.
- Cities — where all of our neighbourhoods are located.
- Neighbourhoods — are located in a city, and have lots of listings of places to stay.
- Reviews — are left by guests, and displayed on the listing
Phew! There’s a lot going on here. We’ll begin by thinking about users and listings.
User and Listings
AirBnB hosts can have many listings, and have many guests, through listings.
But wait — AirBnB guests are also users! Does this mean that we have to have a separate model for host users and guest users?
Actually, no we don’t! We can tell ActiveRecord which foreign key to look for on the listings table to find the listings associated with a particular host. We need to specify ‘host_id’, as ActiveRecord will default to ‘user_id’.
class Userhas_many :listings, :foreign_key => 'host_id'
We can tell our listing that it belongs_to a particular type of user, a host, using the below syntax.
class Listingbelongs_to :host, :class_name => "User"
Nice!
What other associations do we need for users? As guests, they have many trips, they leave many reviews, and they make many reservations, through listings. Let’s put it all together! (Note, I’m using Rails 4.2, so my classes inherit from ‘ActiveRecord::Base’ — things have changed for Rails 5, you must now inherit from ApplicationController).
class User < ActiveRecord::Base has_many :listings, :foreign_key => 'host_id'
has_many :reservations, through: :listings
has_many :reviews, :foreign_key => 'guest_id'
has_many :trips, :foreign_key => 'guest_id', :class_name => "Reservation"end
Let’s finish listings! We need to associate a listing with a neighbourhood. We also need to tell the listing that it has many reviews and guests through reservations.
Putting it all together, we get:
class Listing < ActiveRecord::Basebelongs_to :host, :class_name => "User"
belongs_to :neighborhood
has_many :reservations
has_many :reviews, through: :reservations
has_many :guests, through: :reservationsend
Now, let’s look at reservations and reviews.
Reservations and Reviews
We know that reservations are made by guests, so our reservation belongs_to a guest. It also belongs_to a listing, as without a listing, no one would be able to find our place to make a booking!
class Reservation < ActiveRecord::Base
belongs_to :listing
belongs_to :guest, :class_name => "User"
has_many :reviewsend
How about our reviews? Well, they belong to a guest, and they also belong to a reservation, as it would be quite unfair if you could leave a review without actually booking somewhere. They belong to a listing too, as each review can only review an individual listing. That gives us:
class Review < ActiveRecord::Base
belongs_to :guest, :class_name => "User"
belongs_to :reservation
belongs_to :listingend
We’ve also got to cover:
Cities and Neighbourhoods
All our listings are geographically located somewhere, and our guests will usually pick where to stay based on the city they want to visit.
Each neighbourhood will have many listings, and will belong to a city. So building associations for our Neighbourhood class should be simple enough:
class Neighbourhood < ActiveRecord::Basehas_many :listings
belongs_to :cityend
In each city, there are several neighbourhoods, and each neighbourhood contains many listings. So our City class will have many listings, through neighbourhoods.
class City < ActiveRecord::Basehas_many :neighbourhoods
has_many :listings, through: :neighbourhoodsend
Alright! We’re now ready to build our migrations.
Migrations
We need to make the database aware of the schema of our tables. We want to make sure that our tables are created in the right order, as ActiveRecord will throw an error if we try to reference a foreign key that doesn’t exist yet.
It seems the safest place to start is with our User class. The database I’m using, SQLite3, will automatically generate a unique identifier for us — it is implicitly created. Therefore, we can get started with:
#001_create_user.rbclass CreateUser < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
end
end
end
The filename needs to contain the name of the class in snake_case, otherwise ActiveRecord will throw an error. Our class definitions should be in ProperCase.
Now we’ve created our Users table, what should our second migration be? Cities are also unreliant on foreign keys, so we can define our second migration as below:
#002_create_city.rbclass CreateCity < ActiveRecord::Migration
def change
create_table :cities do |t|
t.string :name
end
end
end
Now we have cities, we can create our neighbourhoods table. We’ll tell our neighbourhoods which city they belong to through including a ‘city_id’ column.
#003_create_neighbourhood.rbclass CreateNeighbourhood < ActiveRecord::Migration
def change
create_table :neighbourhoods do |t|
t.string :name
t.integer :city_id
end
end
end
Now we have our users, our cities and our neighbourhoods, we can go ahead and create our listings table. We also include references to the id of both the host and the neighborhood.
#004_create_listing.rbclass CreateListing < ActiveRecord::Migration
def change
create_table :listings do |t|
t.string :title
t.string :description
t.string :address
t.string :listing_type
t.integer :price
t.integer :host_id
t.integer :neighborhood_id
t.timestamps
end
end
end
Now we’ve got our listings, it’s time to get away! Let’s define our tables for reservations and trips. Our reservation has a check-in time, a check-out time, belongs to a guest and belongs to a listing. Our trip table is our join table, pulling it all together — this is where we go to find all the ids from our cities, reservations, listings, guests and hosts. Let’s go!
#005_create_reservation.rbclass CreateReservation < ActiveRecord::Migration
def change
create_table :reservations do |t|
t.date :checkin
t.date :checkout
t.integer :listing_id
t.integer :guest_id
t.timestamps
end
end
end#006_create_trip.rbclass CreateTrip < ActiveRecord::Migration
def change
create_table :trips do |t|
t.integer :city_id
t.integer :reservation_id
t.integer :listing_id
t.integer :guest_id
t.integer :host_id
t.timestamps
end
end
end
Are we done? Oh, we’ve still got to add reviews!
#007_create_review.rbclass CreateReview < ActiveRecord::Migration
def change
create_table :reviews do |t|
t.integer :rating
t.string :description
t.integer :reservation_id
t.integer :guest_id
t.integer :listing_id
t.integer :host_id
t.timestamps
end
end
end
It would be handy if our trip table also knew about the reviews. But — it’s bad practice to edit our previous migrations. Let’s create a new, 8th migration, to tell our trips table about reviews. We’ll use the add_column method, which asks us for details in the format of table_name, column_name and type, separated by commas.
#008_add_trips_to_reviews.rbclass AddTripsToReviews < ActiveRecord::Migration
def change
add_column :trips, :review_id, :integer
end
end
And we’re done!
(This blog is based on the “Flatiron Bnb Associations” lab on Learn.co. I am learning so please let me know about any suggested improvements to my code.)