Last active
May 15, 2016 22:12
-
-
Save thomasklemm/49483bbd708888056641 to your computer and use it in GitHub Desktop.
Simplifying a has_many :through (ActiveRecord / Rails). Also a useful boilerplate sharing complex ActiveRecord scenarios.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Run this script with `$ ruby has_much_fun.rb` | |
require 'sqlite3' | |
require 'active_record' | |
# Use `binding.pry` anywhere in this script for easy debugging | |
require 'pry' | |
# Connect to an in-memory sqlite3 database | |
ActiveRecord::Base.establish_connection( | |
adapter: 'sqlite3', | |
database: ':memory:' | |
) | |
# Define a minimal database schema | |
ActiveRecord::Schema.define do | |
create_table :shows do |t| | |
t.string :name | |
end | |
create_table :episodes do |t| | |
t.string :name | |
t.belongs_to :show, index: true | |
end | |
create_table :contributions do |t| | |
t.belongs_to :episode, index: true | |
t.belongs_to :person, index: true | |
t.belongs_to :role, index: true | |
end | |
create_table :people do |t| | |
t.string :name | |
end | |
create_table :roles do |t| | |
t.string :name | |
end | |
end | |
# Define the models | |
class Show < ActiveRecord::Base | |
has_many :episodes, inverse_of: :show | |
has_many :contributions, through: :episodes | |
has_many :people, through: :contributions | |
has_many :actors, -> { joins(:roles).merge(Role.actors) }, | |
through: :contributions, source: :person | |
end | |
class Episode < ActiveRecord::Base | |
belongs_to :show, inverse_of: :episodes, required: true | |
has_many :contributions, inverse_of: :episode | |
has_many :roles, through: :contributions | |
has_many :people, through: :contributions | |
end | |
class Contribution < ActiveRecord::Base | |
belongs_to :episode, inverse_of: :contributions, required: true | |
belongs_to :person, inverse_of: :contributions, required: true | |
belongs_to :role, inverse_of: :contributions, required: true | |
end | |
class Person < ActiveRecord::Base | |
has_many :contributions, inverse_of: :person | |
has_many :episodes, through: :contributions | |
has_many :roles, through: :contributions | |
end | |
class Role < ActiveRecord::Base | |
has_many :contributions, inverse_of: :role | |
has_many :people, through: :contributions | |
has_many :episodes, through: :contributions | |
scope :actors, -> { where(name: 'Actor') } | |
end | |
# Create a few people... | |
actor_names = [ | |
'Jim Parsons', | |
'Johnny Galecki', | |
'Kaley Cuoco', | |
'Simon Helberg', | |
'Kunal Nayyar' | |
] | |
writer_names = [ | |
'Chuck Lorre', | |
'Bill Prady' | |
] | |
director_names = [ | |
'Mark Cendrowski' | |
] | |
people_names = actor_names + writer_names + director_names | |
people_names.each do |person_name| | |
Person.create!(name: person_name) | |
end | |
# ...and the roles | |
writer = Role.create!(name: 'Writer') | |
director = Role.create!(name: 'Director') | |
actor = Role.create!(name: 'Actor') | |
# ...and the show and episodes | |
show = Show.create!(name: 'Big Bang Theory') | |
episode = show.episodes.create!(name: 'Pilot') | |
# ... and the contributions. | |
people_by_role = { | |
actor => actor_names, | |
writer => writer_names, | |
director => director_names | |
} | |
people_by_role.each do |role, person_names| | |
person_names.each do |person_name| | |
person = Person.find_by!(name: person_name) | |
Contribution.create!(episode: episode, person: person, role: role) | |
end | |
end | |
# Now let's find all actors on the show | |
# Let's try it like this, only using the associations | |
show.people.joins(:roles).where(roles: { name: 'Actor' }).pluck(:name) | |
# => ["Jim Parsons", "Johnny Galecki", "Kaley Cuoco", "Simon Helberg", "Kunal Nayyar"] | |
show.people.joins(:roles).where(roles: { name: 'Actor' }).to_sql | |
# => | |
# "SELECT \"people\".* FROM \"people\" | |
# INNER JOIN \"contributions\" \"contributions_people_join\" ON \"contributions_people_join\".\"person_id\" = \"people\".\"id\" | |
# INNER JOIN \"roles\" ON \"roles\".\"id\" = \"contributions_people_join\".\"role_id\" | |
# INNER JOIN \"contributions\" ON \"people\".\"id\" = \"contributions\".\"person_id\" | |
# INNER JOIN \"episodes\" ON \"contributions\".\"episode_id\" = \"episodes\".\"id\" | |
# WHERE \"episodes\".\"show_id\" = 1 | |
# AND \"roles\".\"name\" = 'Actor'" | |
show.people.joins(:roles).merge(Role.actors).pluck(:name) | |
# => ["Jim Parsons", "Johnny Galecki", "Kaley Cuoco", "Simon Helberg", "Kunal Nayyar"] | |
names_of_actors = show.actors.pluck(:name) | |
# => ["Jim Parsons", "Johnny Galecki", "Kaley Cuoco", "Simon Helberg", "Kunal Nayyar"] | |
puts "The actors in #{show.name} are #{ names_of_actors.join(', ') }." | |
# => The actors in Big Bang Theory are Jim Parsons, Johnny Galecki, Kaley Cuoco, Simon Helberg, Kunal Nayyar. | |
# Use `binding.pry` here to experiment with this setup. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment