code.life

Rose Weixel’s technical blog

Join Tables in ActiveRecord: Complex Associations, Simple Code

Up until a few weeks ago, my entire coding toolbox consisted of a small sample of some built in Ruby methods. Two of these became blog post topics (#find and #slice), and one—the << method—became the central character of my first coding dream/nightmare, with reserved words while and true playing key supporting roles. I used these methods to write other pretty simple methods.

After just a few weeks at The Flatiron School, I’m quickly learning that building up complex programs, like web apps, can’t (or maybe really just shouldn’t) be accomplished by writing out method after method in one big file. You need the right structures and tools to create complex objects, persist them in a database, and express relationships between these objects. Using ActiveRecord, you get some really simple and powerful tools for all this that hide a huge amount of complexity away, allowing you focus on designing your app to do what you want it to do.

While ActiveRecord makes it easy to create models and associations, some of the concepts involved were hard for me to get my head around at first. One such concept was the join table. I ran up against a problem in need of a join table while working with my classmates on a lab involving modeling characters and actors.

In ActiveRecord speak, actors have many characters and a character belongs to an actor. Here, in all of its elegant simplicity, is how you’d create the models that express this relationship:

app/models/actor.rb
1
2
3
class Actor < ActiveRecord::Base
  has_many :characters
end
app/models/character.rb
1
2
3
class Character < ActiveRecord::Base
  belongs_to :actor
end

Now all we need are two tables (actors and characters) to make this association work. The characters table, being on the belongs_to side of the relationship, must have a column for the foreign key, actor_id. In ActiveRecord, this can be accomplished in remarkably few lines of code:

db/migrations/001_create_actors.rb
1
2
3
4
5
6
7
8
class CreateActors < ActiveRecord::Migration
  def change
    create_table :actors do |t|
      # ActiveRecord provides an id (primary key) column by default, for free!
      t.string :name
    end
  end
end
db/migrations/002_create_characters.rb
1
2
3
4
5
6
7
8
9
class CreateCharacters < ActiveRecord::Migration
  def change
    create_table :characters do |t|
      t.string :name
      # Here is where the magic happens. ActiveRecord knows that this references the actors table.
      t.integer :actor_id
    end
  end
end

This is the basic pattern you’d follow to model any has_many belongs_to relationship in ActiveRecord. What seems like “magic” here is made possible by ActiveRecord Ruby methods that either give our associated classes more methods that let them interact with each other, or wrap SQL statements and hide them away (in the case of migrations). But that is a topic for another post.

Back to actors and characters. As my classmates and I were creating these models, two big things came to the surface:

1) We could not recall any of Tom Cruise’s many characters’ names.

2) What happens when a character has more than one actor? One of my favorite characters would most certainly break our has many/belongs to association—The Doctor.

images

How can we fit The Doctor into our current schema??? The more I thought about it, the more impossible it seemed. Here is an illustration:

images

The above is our simple model, before The Doctor comes along and breaks everything. Just to be sure I was grasping how NOT to try to include The Doctor and his many actors, here is how it might look:

images

Our actors table seems fine, but our characters table is definitely not okay, and we haven’t even included the eight doctors from the first twenty-six seasons yet.

With our current setup, we have to change the entire schema every time we add another Doctor. This is very, very bad. A database was designed for adding lots of rows. Not so for columns.

In order to make things right, we need a different association: many-to-many. Actors have many characters, and a character has many actors. In order to set our database up for this association, we need a join table. First the visual:

images

Now for the migration (assuming your actors table is still intact and you’ve already removed all but the name column from characters):

004_create_actors_characters_join_table.rb
1
2
3
4
5
6
7
8
class CreateCharacterActorRelationships < ActiveRecord::Migration
  def change
    create_table :character_actor_relationships do |t|
      t.integer :character_id
      t.integer :actor_id
    end
  end
end

We also need to change our actor and character models. In order for the association to work, we need to somehow connect our actors and characters table through the character_actor_relationships table. The association we need in our actor and character models is has_many :through:

app/models/actor.rb
1
2
3
4
class Actor < ActiveRecord::Base
  has_many :character_actor_relationships
  has_many :characters, through: :character_actor_relationships
end
app/models/character.rb
1
2
3
4
class Character < ActiveRecord::Base
  has_many :character_actor_relationships
  has_many :actors, through: :character_actor_relationships
end

Finally, we need a model for CharacterActorRelationship:

app/models/character_actor_relationship.rb
1
2
3
4
class CharacterActorRelationship < ActiveRecord::Base
  belongs_to :character
  belongs_to :actor
end

Now we have the right associations and the right database schema to add all of the Doctors that ever were and ever will be throughout all time.

images