DEV Community

samanthamarberger
samanthamarberger

Posted on

Mastering ActiveRecord Associations in Ruby: A Comprehensive Guide

What are database associations?

Database associations are necessary for Ruby applications to manage the relationships between database tables. The associations establish relationships between related tables allowing you to properly link the crossover data within tables. Database associations also allow you to easily retrieve data that is related. With well-defined associations, information from one table can be easily obtained through another table. Along with this, the established relationships make handling CRUD operations and different tasks much more efficient. Lastly, complex relationships also become much easier to handle through database associations, by utilizing many of the different relationships managing your data becomes much more efficient. So with all that said, let's get into using ActiveRecord associations to show how easy managing database relationships can be.

ActiveRecord is a Ruby gem that was created to simplify the binding process between tables within a database. It allows us to use keywords to create relationships rather than building out our own object-relational mapper and manually writing complex SQL. You see, ActiveRecord generated your SQL code behind the scenes allowing you to focus on your application rather than writing out code for your database associations.

Understanding ActiveRecord Associations

Let's create a scenario. We want to create a web application to help us work out. To do this, we need to create a data table with all of the major muscle groups and a separate data table of exercises; each table has many different attributes.

Let's do this by creating each class using ActiveRecord:

class MuscleGroup < ActiveRecord::Base

end
Enter fullscreen mode Exit fullscreen mode
class Exercise < ActiveRecord::Base

end
Enter fullscreen mode Exit fullscreen mode

Once these are created, you are going to create a migration for each table by running:

bundle exec rake db:create_migration NAME=create_muscle_groups

and

bundle exec rake db:create_migration NAME=create_exercises

This migration will create a .rb file with a timestamp at the beginning where you can create your table with all of its attributes. The timestamp is an important aspect of ActiveRecord because it ensures that the migrations are running in the intended order.

In your migration file you can create your tables like so:

class CreateMuscleGroups < ActiveRecord::Migration[6.1]
  def change
    create_table :muscle_groups do |t|
      t.string :name
      t.string :image_url
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
class CreateExercises < ActiveRecord::Migration[6.1]
  def change
    create_table :exercises do |t|
      t.string :name
      t.string :image_url
      t.string :how_to_do
      t.integer :muscle_group_id
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

In order to migrate the table, run bundle exec rake db:migrate after you add a new table or make changes to your table.

You will notice the attribute muscle_group_id. This is our foreign key. Let's define foreign key and primary key.

Primary key

A primary key is a unique identifier for each row in a database table. This provides the uniqueness of the key so that each case can be distinguished. Examples of this would be things like id.

Foreign key

A foreign key is an identifier that refers to the primary key in another table. This establishes the relationship between the two tables. An example of this is the muscle_group_id.

Because of this foreign key, we now have a row in the exercise table that indicates, through the muscle group id, which muscle group the exercise belongs_to.

In order to best make use of these tables, we need to form a relationship. Using ActiveRecord we can very easily do so through one of the types of associations this is the has_many and belongs_to association. Each major muscle group has_many exercises and each exercise belongs_to a muscle group.
To form this relationship, we would need to use these keywords in the class components for those tables. For example:

class MuscleGroup < ActiveRecord::Base
    has_many :exercises
end
Enter fullscreen mode Exit fullscreen mode
class Exercise < ActiveRecord::Base
    belongs_to :muscle_group
end
Enter fullscreen mode Exit fullscreen mode

This indicates to ruby the relationship between muscle groups and exercises. As you can see, the has_many keyword is followed by the pluralized class. That is because there are many exercises whereas, the belongs_to keyword is followed by a singular class because there is only one muscle group for many of the exercises.

Seeding data and testing my code

All of these relationships are great; however, what if we want to test them? We need some data. You can create mock data in db/seeds.rb. Let's go back to our one-to-many relationship and create a muscle group and a couple of exercises within the seeds.rb file.

MuscleGroup.create(name: "Legs", image_url: "https://tse3.mm.bing.net/th?id=OIP.CCaTj7B5HI0e7pIotjn4PAHaE7&pid=Api&P=0&h=180")
Enter fullscreen mode Exit fullscreen mode
Exercise.create(name: "Back Squat", image_url: "https://julielohre.com/wp-content/uploads/2017/11/Barbell-Back-Squat.jpg", 
how_to_do: "Hold the barbell on your shoulders.  Keeping your back straight, squat down until the angel of your legs reaches 90 degrees or greater. Press up until you reach starting position.", muscle_group_id: MuscleGroup.find_by(name: "Legs").id)
Exercise.create(name: "Calf Raise", image_url: "https://i0.wp.com/bootcampmilitaryfitnessinstitute.com/wp-content/uploads/2018/02/Exercise-Calf-Raises-1.jpg?ssl=1", 
how_to_do: "Start with your feet flat on the ground.  Raise up on your toes as far as you can, squeezing the muscles in your calves.  Return to starting position.", 
muscle_group_id: MuscleGroup.find_by(name: "Legs").id)
Enter fullscreen mode Exit fullscreen mode

Now to test my data, I want to run bundle exec rake db:seed. This will seed all of my mock data and I can play around with my code and make sure it works!

To get to the console let's run bundle exec rake console
We can now interact with the console and test our code using... ActiveRecord methods!

ActiveRecord methods

There is a long list of ActiveRecord methods that allow us to interact with the database, here are a few and what they do:

  1. Creating Records:

.create: Creates a new record and saves it to the database.
.new: Creates a new record but does not save it to the database.
.save: Saves changes to an existing record or creates a new record and saves it.

  1. Retrieving Records:

.find: Finds a record by its primary key (usually id).
.find_by: Finds the first record that matches the specified conditions.
.where: Retrieves records that match the specified conditions.
.first: Retrieves the first record from the database.
.last: Retrieves the last record from the database.
.all: Retrieves all records from the database.

  1. Updating Records:

.update: Updates attributes of a record and saves it to the database.
.save: Saves changes to an existing record.

  1. Deleting Records:

.destroy: Deletes a record from the database.

Examples:

Exercise.create(name: "Pull-up", image_url: "https://tse4.mm.bing.net/th?id=OIP.3fI1p5ikgmNfCWjyMGPpAwHaHa&pid=Api&P=0&h=180", 
how_to_do: "Start hanging from the bar with your arms shoulder width apart.  Pull your body upward until your chin is over the bar.  Lower yourself back down to the starting position.", 
muscle_group_id: MuscleGroup.find_by(name: "Back").id) 
Enter fullscreen mode Exit fullscreen mode
Exercises.find(1)
=> #<MuscleGroup:0x0000000109837728
 =>#id: 3,
 =>#name: "Legs",
 =>#image_url: "https://tse3.mm.bing.net/th?=>#id=OIP.CCaTj7B5HI0e7pIotjn4PAHaE7&pid=Api&P=0&h=180"
Enter fullscreen mode Exit fullscreen mode

These are just a few but give you a lot of power to interact with your database.

But what if I wanted to get really specific about the muscle groups?

Many exercises actually hit different muscle groups. What then? Well, we can create a Many to Many relationship .
This is when the has_many :through keyword comes in. Let's make a new database table called SecondaryMuscle, this will be our JOIN table.

class SecondaryMuscle < ActiveRecord::Base

end
Enter fullscreen mode Exit fullscreen mode

and run

bundle exec rake db:create_migration NAME=create_secondary_muscles

We now have created a migration and can create the table:

class CreateSecondaryMuscles < ActiveRecord::Migration[6.1]
  def change
    create_table :secondary_groups do |t|
      t.string :name
      t.string :image_url
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

We now will add/ change our associations using the secondary muscles JOIN table.

The SecondaryMuscles table JOINs the two together in a many-to-many association:

class SecondaryMuscle < ActiveRecord::Base
  belongs_to :muscle_group
  belongs_to :exercise
end
Enter fullscreen mode Exit fullscreen mode

The MuscleGroups associations now become:

class MuscleGroup < ActiveRecord::Base
  has_many :secondary_muscles
  has_many :exercises, through: :secondary_muscles
end
Enter fullscreen mode Exit fullscreen mode

The Exercises associations have become:

class Exercise < ActiveRecord::Base
  has_many :secondary_muscles
  has_many :muscle_groups, through: :secondary_muscles
end
Enter fullscreen mode Exit fullscreen mode

Now, through a small amount of code, you have formed relationships that give you easily accessible information from each table.

ActiveRecord Macros

The has_many, belongs_to, and has_many :through are all ActiveRecord macros. There are a few other types that I won't be going into but could be helpful to know. These macros are:

has_one - this defines a one-to-one association between two models. This is done by setting up an association with a foreign key in one table that references the primary key in the other, very similar to one-to-many.

has_and_belongs_to_many - this defines a many-to-many association between two tables without having to create an intermediate model, for example, no need for our SecondaryMuscle model.

has_one :through- this defines a one-to-one association through an intermediate model. Very similar to using the has_many :through.

To conclude, using ActiveRecord will make your life much easier as a ruby developer. It will allow you to spend more time creating your web application and less time trying to create relationships using handwritten SQL. If you want to dive even deeper, please take a look at Link to ruby on rails: ActiveRecord

Top comments (2)

Collapse
 
andreimaxim profile image
Andrei Maxim • Edited

It's great that people write articles about ActiveRecord. It's one of the better ORMs out there and, if you follow the conventions, it can greatly reduce the time required to build an application!

There's one slight problem in the examples, mainly around the command to create migrations, which is presented as this:

bundle exec rake db:create_migration NAME=create_muscle_groups
Enter fullscreen mode Exit fullscreen mode

The actual command is this:

bundle exec rails generate migration create_muscle_groups
Enter fullscreen mode Exit fullscreen mode

or, if you want to type a bit less:

bin/rails g migration create_muscle_groups
Enter fullscreen mode Exit fullscreen mode

There's also a way to have Rails prefill the columns in the migration:

bin/rails g migration name:string image_url:string
Enter fullscreen mode Exit fullscreen mode

The main difference is that you want Rails to generate a migration file, not to run the actual migrations.

I also feel obliged to mention that associations can be quite complex, but you still need just a few lines of code if you use scopes.

Let's use your examples with MuscleGroup and Exercise:

class MuscleGroup < ActiveRecord::Base
  has_many :exercises
end
Enter fullscreen mode Exit fullscreen mode
class Exercise < ActiveRecord::Base
  belongs_to :muscle_group
end
Enter fullscreen mode Exit fullscreen mode

Now you can list all the exercises for a muscle group by running the following code:

muscle_group.exercises
Enter fullscreen mode Exit fullscreen mode

But it's not really useful to list exercises by name because it won't mean a lot to most people. What if we want to list only the exercises with a photo?

Our code could become something like this:

muscle_group.exercises.where.not(image_url: nil)
Enter fullscreen mode Exit fullscreen mode

This is a bit verbose and you end up exposing the internals of your Exercise class to whoever is trying to get a list, which will make any future code changes a bit harder to do because you will have to search through all your codebase.

Fortunately, there is another way.

Let's define a scope on the Exercise class:

class Exercise < ActiveRecord::Base
  belongs_to :muscle_group

  scope :with_image, -> { where.not(image_url: nil) } 
end
Enter fullscreen mode Exit fullscreen mode

... and change the association on the MuscleGroup class to use the new scope:

class MuscleGroup < ActiveRecord::Base
  has_many :exercises, -> { with_image }
end
Enter fullscreen mode Exit fullscreen mode

Now, when we want to list the exercises for a specific muscle group, we get only the exercises with a image_url:

muscle_group.exercises
Enter fullscreen mode Exit fullscreen mode

But what if you need to list all the exercises in some places of the application (say in an admin area) and in other places you need to list the exercises with an image?

You can define associations between the same models multiple times:

class MuscleGroup < ActiveRecord::Base
  has_many :exercises
  has_many :publishable_exercises, 
           -> { with_image }, 
           class_name: "Exercise"
end
Enter fullscreen mode Exit fullscreen mode

Note that Rails cannot infer the name of the associated model from the association name publishable_exercises so we need to specify it.

Now we can use muscle_group.exercises for listing all the exercises , and muscle_group.publishable_exercises whenever we want to list only exercises with a image.

Collapse
 
samanthamarberger profile image
samanthamarberger

Thank you so much for the feedback. I really appreciate the constructive criticism since I am still fairly new to the world of ORM's.