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
class Exercise < ActiveRecord::Base
end
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
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
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
class Exercise < ActiveRecord::Base
belongs_to :muscle_group
end
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")
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)
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:
- 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.
- 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.
- Updating Records:
.update: Updates attributes of a record and saves it to the database.
.save: Saves changes to an existing record.
- 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)
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"
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
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
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
The MuscleGroups associations now become:
class MuscleGroup < ActiveRecord::Base
has_many :secondary_muscles
has_many :exercises, through: :secondary_muscles
end
The Exercises associations have become:
class Exercise < ActiveRecord::Base
has_many :secondary_muscles
has_many :muscle_groups, through: :secondary_muscles
end
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)
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:
The actual command is this:
or, if you want to type a bit less:
There's also a way to have Rails prefill the columns in the migration:
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
andExercise
:Now you can list all the exercises for a muscle group by running the following code:
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:
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:... and change the association on the
MuscleGroup
class to use the new scope:Now, when we want to list the exercises for a specific muscle group, we get only the exercises with a
image_url
: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:
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 , andmuscle_group.publishable_exercises
whenever we want to list only exercises with a image.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.