Skip to content
Steve Robinson edited this page Jan 24, 2015 · 2 revisions

ActiveRecord

Let's create a MovieDB app to learn AR :)

Setup a new Rails app

rails new moviedb

rails server

The Tables

datamodel

The Models

$ rails generate model genre name:string

$ rails g model movie name:string release_date:date revenue:decimal genre_id:integer

$ rails g model actor name:string gender:string age:integer

$ rails g model role name:string actor_id:integer movie_id:integer

Open up and see the model files.

Let's define associations.

The Associations

Associations are defined in the model classes (inherited from ActiveRecord::Base). Let's define our associations:

  • A genre has_many movies and a movie belongs_to a genre.
class Genre < ActiveRecord::Base
    has_many :movies
end

class Movie < ActiveRecord::Base
    belongs_to :genre
end
  • A movie has_many roles and a role belongs_to a movie.
  • What about association between roles and actors?
  • A movie has_many actors through roles.
class Movie < ActiveRecord::Base
    belongs_to :genre
    has_many :roles
    has_many :actors, through: :roles
end
  • And vice-versa for actors.

Let's create some data

Fire up rails console.

Playing with the models

Create a new Genre..

genre = Genre.new
genre.name = 'Action'
genre.save

In one line... Genre.create({name: 'Action'})

Now create a movie :)

Let's associate a movie with a genre.

movie = Movie.find(1)
genre = Genre.first
movie.genre = genre
movie.save

**Ex: ** Create a new Actor

**Ex: ** Create a new Role for the movie and the new actor

MOOAAR Data.

Genre..

["Drama", "Science Fiction", "Horror", "Fantasy", "Romance", "Comedy"].each do |name|
    Genre.create name: name
 end

Movies..

gem 'ffaker'

$ bundle install

Inside Rails console..

genres = Genre.all
10.times do 
    Movie.create name: Faker::Movie.title, genre: genres.sample, release_date: rand(5.years).seconds.ago, revenue: rand(10_00_000..100_00_00_000) 
end

Actors...

20.times do
    Actor.create name: Faker::Name.name, gender: ['Male', 'Female'].sample, age: rand(19..75)
end

Roles...

actors = Actor.all
Movie.all.each do |movie|
    Role.create name: Faker::Name.first_name, movie: movie, actor: actors.sample
end

Lets run some queries!

  • All female actresses: Actor.where(gender: 'Female')
  • All movies after 2013: Movie.where('release_date > ?', '1-1-2013') # Sanitizing data from users
  • Actors below 25 years of age
  • Movies released after 2013 and with revenue more than 50 lakhs
  • Top 3 movies by revenue Hint: .order() & limit.()

a bit more advanced...

  • find genres and their total revenue.

Genre.joins(:movies).select("genres.name, SUM(movies.revenue) as total_revenue").group('genres.name').order('total_revenue')

  • All movies featuring female roles

Movie.joins(roles: :actors).where('actors.gender = "Female"')

the N+1 query problem:

  • Print movie names along with genre..
    Movie.all.each do |movie|
        puts movie.name, movie.genre.name
        puts '----------------'
    end

How many queries were ran? How to fix this? Movie.all.includes(:genre)

This is called eager loading.

Few gotchas
  • Use limit to limit records. Do not use all blindly.
  • Use eager_loading. bullet gem.
  • Use select() to specify only the fields needed when table is large.
  • If you want just the values from the db - like genre names, use pluck. Genre.pluck :name
  • Create indexes on all association fields (foreign keys) in migrations. For eg. Our app needs a database index on movies.genre_id and so on.
  • Use operations like sum, group and order on the database rather than in Ruby

Bonus: Polymorphic Association

Your users want to rate movies and actors. We could go with two tables as 'movie_ratings' and 'actor_ratings'.

But we get a much simpler implementation when we use polymorphic associations.

Create model Ratable.

$ rails g model rating value:integer ratable_id:integer ratable_type:string migrate... Update models:

# ratable.rb
class Rating < ActiveRecord::Base
    belongs_to :ratable, polymorphic: true
end

# movie.rb
class Movie < ActiveRecord::Base
    has_many :ratings, as: :ratable
end

Add ratings to movies and actors

movie.ratings.new(value: 5)
movie.save