Skip to content

Instantly share code, notes, and snippets.

@thomasklemm
Last active May 15, 2016 22:12
Show Gist options
  • Save thomasklemm/49483bbd708888056641 to your computer and use it in GitHub Desktop.
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.
# 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