Ashbury
Ashbury

Reputation: 2344

has_many through roles and scopes on the third model

Lets say I have movies, people and movies_people

class Person < ActiveRecord::Base
  has_many :movies_people
  has_many :movies, through: :movies_people

class Movies < ActiveRecord::Base
  has_many :movies_people
  has_many :people, through: :movies_people


class MoviesPerson < ActiveRecord::Base
  belongs_to :movie
  belongs_to :person
end

The table movies_people has a role attribute, where I want to store the person's job in the movie. Right now I can do things like this in the console:

u = User.first
m = Movie.first
m.people << u

then find the right movies_people entry and set 'role'

retrieving looks like this:

m.people.where(movies_people: {role: :actor})

Whats the best way to:

  1. Save the role (to the third table) when joining people to movies?
  2. Return all the actors in a movie vs. all the directors vs. all the writers?

Upvotes: 1

Views: 521

Answers (2)

max
max

Reputation: 102174

One solution is to create Roles which contains a list of existing roles and a MovieRole class which joins Movies, People and Roles.

class Movie < ActiveRecord::Base
  has_many :movie_roles, class_name: "MovieRole"
  has_many :roles, through: :movie_roles
  has_many :people, through: :movie_roles
end

class People < ActiveRecord::Base
  has_many :movie_roles, class_name: "MovieRole"
  has_many :movies, through: :movie_roles
  has_many :roles, through: :movie_roles
end

class Role < ActiveRecord::Base
  has_many :movie_roles, class_name: "MovieRole"
  has_many :people, through: :movie_roles
  has_many :movies, through: :movie_roles
end

class MovieRole < ActiveRecord::Base
  belongs_to :movie
  belongs_to :people
  belongs_to :role
end

All the relations are stores in movie_roles which is a three way join table:

class CreateMovieRoles < ActiveRecord::Migration
  def change
    create_table :movie_roles do |t|
      t.references :movie, index: true
      t.references :people, index: true
      t.references :role, index: true
      t.timestamps
    end
  end
end

Some examples of how you could query this association:

@roles = Movie.find_by(title: 'Godzilla').roles
@directors = People.joins(:roles).where(roles: {name: 'Director'})
@directed_by_eastwood = Movie.joins(:people, :roles)
                               .merge(Role.where(name: 'Director'))
                               .merge(People.where(name: 'Clint Eastwood'))

Added:

To associate a person with a movie you would:

MovieRole.create(people: person, movie: movie, role: role)

But you will want to setup convinience methods like:

class People < ActiveRecord::Base
  def add_role(role, movie)
    role = Role.find_by(name: role) unless role.is_a?(Role)
    MovieRole.create(people: self, movie: movie, role: role)
  end 
end

class Movie < ActiveRecord::Base
  def add_cast(role, person)
    role = Role.find_by(name: role) unless role.is_a?(Role)
    MovieRole.create(people: person, movie: self, role: role)
  end 
end

Upvotes: 3

Alejandro Babio
Alejandro Babio

Reputation: 5229

To save the role, just:

person = Person.find(person_id)
movie = Movie.find(movie_id)

movie.movies_people.create(person: person, role: :actor)

To retrieve by role,

movie = Movie.includes(:people).where(id: movie_id).where(movies_people: {role: :actor})

Edit: I don't advice to add a roles table, unless you need it. I follow the agile principles, in this case: "Simplicity--the art of maximizing the amount of work not done--is essential."

Upvotes: 1

Related Questions