Chukwuma Anyadike
Posted on March 26, 2023
My latest project involves creating an Eclectic Music database. The purpose is to allow the user to store data about all types of music. Eclectic means deriving ideas, style, or taste from a broad and diverse range of sources. This is why I gave this music database this name. It will allow the user to fully express their musical tastes.
All excitement aside, this is a database for which Active Record was made to handle. Recall that Active Record is a object relational mapper (ORM) which maps classes to tables. Each instance of a class is mapped to a row (aka record). These records can be linked to other records through associations. Here are the common association macros below.
belongs_to
has_many
has_many :through
Here are also some the more esoteric association macros which I will not discuss in any detail except to list these for completeness sake. These include has_one
, has_one :through
, and has_and_belongs_to_many
.
The common active record relationships include one-to-many and many-to-many relationships. One should also be familiar with foreign keys. Foreign keys are columns that refer to the primary key of another table. This is how data in one table is linked to data in another table.
One-to-many relationships:
These are the most common relationships used. Active Record gives us the has_many
and belongs_to
macros for creating instance methods to access data across models in a one-to-many relationship. What in the world does creating instance methods to access data across models mean? The key here is the word macro. In nutrition, a macro includes macronutrients such as carbohydrates, proteins, and fats which are essential for our bodies to function optimally. Macros in programming are code generators (basically code that writes more code). Basically they allow our 'body' of code to run optimally and cut down on the code that we need to write. Let us go through the sample code below.
The models:
class Song < ApplicationRecord
belongs_to :artist
end
class Artist < ApplicationRecord
has_many :songs
end
Corresponding Rails migrations:
class CreateSongs < ActiveRecord::Migration[6.1]
def change
create_table :songs do |t|
t.string :name
t.integer :artist_id
t.timestamps
end
end
end
class CreateArtists < ActiveRecord::Migration[6.1]
def change
create_table :artists do |t|
t.string :name
t.string :genre
t.string :date_established
t.text :interesting_fact
t.string :artist_image_url
t.timestamps
end
end
end
In this example Artist
has many Song
s, and a Song
belongs to Artist
. This allows us to do some really cool things. The artist
method is made available to Song
. This is singular because a Song
can only belong to one Artist
. Likewise, the songs
method is made available to Artist
. Note that this method is pluralized because an Artist
has many Song
s. This method gives access to a collection of the Song
s belonging to a particular Artist
instance.
first_song=Song.first
=> #<Song id: 31, name: "Likey", artist_id: 6, created_at: "2023-03-15 18:36:15.029983000 +0000", updated_at: "2023-03-15 18:36:15.029983...
first_song.artist
=> #<Artist id: 6, name: "Twice", genre: "K-pop", date_established: "2015", interesting_fact: "Twice is the first female Korean act to simultaneo...", artist_image_url: "https://i.pinimg.com/originals/63/19/f5/6319f51f79...", user_id: 1, created_at: "2023-03-15 18:36:14.984921000 +0000", updated_at: "2023-03-22 01:17:56.926138000 +0000">
2.7.4 :004 >
Here I have assigned the first record of Song
s to first_song
. I can now access the Artist
record for first_song
using the artist
method. In plain English, I found the first song named "Likey" by the K-pop artist known as Twice.
class Model < ApplicationRecord
belongs_to :association
end
The belongs_to
association macro gives this class access to a method defined by the symbol passed to the belongs_to
method. The association
method (model_instance.association
) returns the associated object, if any. If no associated object is found, it returns nil
.
There are eight methods granted by the belongs_to
macro which include:
- association
- association=(associate)
- build_association(attributes = {})
- create_association(attributes = {})
- create_association!(attributes = {})
- reload_association
- association_changed?
- association_previously_changed?
In all of these methods, association is replaced with the symbol passed as the first argument to belongs_to. For example, if a Song
record has no existing Artist
, one could be created using song.create_artist({artist_params})
.
blackpink=Artist.first
=> #<Artist id: 5, name: "Blackpink", genre: "K-pop", date_established: "2016", interesting_fact: "Referred to as the biggest girl group in the world.....
2.7.4 :002 >
blackpink.songs
=> #<ActiveRecord::Associations::CollectionProxy [#<Song id: 65, name: "Boombayah", artist_id: 5, created_at: "2023-03-16 08:08:36.097321000 +0000", updated_at: "2023-03-16 08:08:36.097321000 +0000">, #<Song id: 66, name: "Ddu-Du Ddu-Du", artist_id: 5, created_at: "2023-03-16 08:08:36.105075000 +0000", updated_at: "2023-03-16 08:08:36.105075000 +0000">, #<Song id: 67, name: "Whistle", artist_id: 5, created_at: "2023-03-16 08:08:36.112071000 +0000", updated_at: "2023-03-16 08:08:36.112071000 +0000">, #<Song id: 68, name: "Playing with Fire", artist_id: 5, created_at: "2023-03-16 08:08:36.118742000 +0000", updated_at: "2023-03-16 08:08:36.118742000 +0000">, #<Song id: 69, name: "Stay", artist_id: 5, created_at: "2023-03-16 08:08:36.126371000 +0000", updated_at: "2023-03-16 08:08:36.126371000 +0000">, #<Song id: 70, name: "As If It's Your Last", artist_id: 5, created_at: "2023-03-16 08:08:36.132867000 +0000", updated_at: "2023-03-16 08:08:36.132867000 +0000">, #<Song id: 71, name: "Forever Young", artist_id: 5, created_at: "2023-03-16 08:08:36.139672000 +0000", updated_at: "2023-03-16 08:08:36.139672000 +0000">, #<Song id: 72, name: "How You Like That", artist_id: 5, created_at: "2023-03-16 08:08:36.146085000 +0000", updated_at: "2023-03-16 08:08:36.146085000 +0000">, #<Song id: 73, name: "Ice Cream", artist_id: 5, created_at: "2023-03-16 08:08:36.152874000 +0000", updated_at: "2023-03-16 08:08:36.152874000 +0000">, #<Song id: 74, name: "Pretty Savage", artist_id: 5, created_at: "2023-03-16 08:08:36.159772000 +0000", updated_at: "2023-03-16 08:08:36.159772000 +0000">, ...]>
For this case, I have taken the first Artist
record and assigned it to the variable blackpink
. Then the songs
method was used to gain access to the collection of Song
s by blackpink
.
class Model < ApplicationRecord
has_many :collection
end
The has_many
association macro gives this class access to a method defined by the symbol passed to the has_many
method. The collection
method (model_instance.collection
) returns an array of all of the associated objects. If there are no associated objects, it returns an empty array.
smooth_operator=Artist.find_by(name: "Smooth Operator")
=> #<Artist id: 32, name: "Smooth Operator", genre: "hip-hop", date_established: "2020", interesting_fact: "He is smooth", artist_image_url: "https://e...
smooth_operator.songs.create(name: "Smooth Coding using Active Record Associations", artist_id: 32)
=> #<Song id: 10, name: "Smooth Coding using Active Record Associations", artist_id: 32, created_at: "2023-03-16 08:08:36.139672000 +0000", updated_at: "2023-03-16 08:08:36.159772000 +0000">
In this case I have taken a record from a fictional artist "Smooth Operator" and assigned it to the variable smooth_operator
. Then, I used the create method on the collection of Songs from smooth_operator
and created a new Song
called "Smooth Coding using Active Record Associations".
When you declare a has_many
association, the declaring class automatically gains 17 methods related to the association.
- collection
- collection<<(object, ...)
- collection.delete(object, ...)
- collection.destroy(object, ...)
- collection=(objects)
- collection_singular_ids
- collection_singular_ids=(ids)
- collection.clear
- collection.empty?
- collection.size
- collection.find(...)
- collection.where(...)
- collection.exists?(...)
- collection.build(attributes = {})
- collection.create(attributes = {})
- collection.create!(attributes = {})
- collection.reload
In all of these methods, collection is replaced with the symbol passed as the first argument to has_many
, and collection_singular is replaced with the singularized version of that symbol. An example is smooth_operator.songs.create(name: "Smooth Coding using Active Record Associations", artist_id: 32)
that was just illustrated.
Further, one can still use the multitude of Active Record Query Methods on these collections and associations.
blackpink.songs.count
=> 15
blackpink.songs.pluck(:name)
=> ["Boombayah", "Ddu-Du Ddu-Du", "Whistle", "Playing with Fire", "Stay", "As If It's Your Last", "Forever Young", "How You Like That", "Ice Cream", "Pretty Savage", "Love Sick Girls", "Pink Venom", "Shut Down", "Typa Girl", "Ready for Love"]
blackpink.songs.first
=> #<Song id: 65, name: "Boombayah", artist_id: 5, album_id: 34, created_at: "2023-03-16 08:08:36.097321000 +0000", updated_at: "2023-03-16 08:08:36.097321000 +0000">
blackpink.songs.last
=> #<Song id: 79, name: "Ready for Love", artist_id: 5, album_id: 36, created_at: "2023-03-16 08:08:36.198539000 +0000", updated_at: "2023-03-16 08:08:36.198
smooth_operator.songs.order(:name)
=> #<ActiveRecord::AssociationRelation [#<Song id: 137, name: "finally made it smooth", artist_id: 32, created_at: "2023-03-20 10:56:51.733042000 +0000", updated_at: "2023-03-20 10:56:51.733042000 +0000">, #<Song id: 133, name: "make it smooth", artist_id: 32, created_at: "2023-03-20 10:49:08.178298000 +0000", updated_at: "2023-03-20 10:49:08.178298000 +0000">, #<Song id: 134, name: "make it smooth and silky", artist_id: 32, created_at: "2023-03-20 10:51:16.889179000 +0000", updated_at: "2023-03-20 10:51:16.889179000 +0000">, #<Song id: 131, name: "sexy smooth", artist_id: 32, created_at: "2023-03-20 10:38:23.173949000 +0000", updated_at: "2023-03-20 10:38:23.173949000 +0000">, #<Song id: 136, name: "silky and smooth", artist_id: 32, created_at: "2023-03-20 10:54:00.360430000 +0000", updated_at: "2023-03-20 10:54:00.360430000 +0000">, #<Song id: 135, name: "smooth and silky", artist_id: 32, created_at: "2023-03-20 10:52:41.305959000 +0000", updated_at: "2023-03-20 10:52:41.305959000 +0000">, #<Song id: 132, name: "smooth things over", artist_id: 32, created_at: "2023-03-20 10:46:05.463048000 +0000", updated_at: "2023-03-20 12:17:46.602416000 +0000">]>
One last note on foreign keys: The foreign key should be in the belonging table. Please refer to my migrations above in which the foreign key artist_id
is located in the songs
table.
Many-to-many relationships:
Active Record gives us the has_many :through
, in addition to the has_many
and belongs_to
macros for creating instance methods to access data across models in a many-to-many relationship. The concepts described above are still applicable here. However, there are some differences from a one-to-many relationship.
First is the location of the foreign keys. One, should think of a many-to-many relationship as two one-to-many relationships joined together because conceptually that is exactly what it is. Since these relationships are joined together it makes sense to have a join table. This join table contains the foreign keys needed to for a record from each table to reference the other table.
Second is the use of the has_many :through
macro. A has_many :through
association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding through a third model. This third model is the join table.
Here is some sample code.
The models:
class Artist < ApplicationRecord
has_many :songs
has_many :albums, through: :songs
end
class Song < ApplicationRecord
belongs_to :artist
belongs_to :album
end
class Album < ApplicationRecord
has_many :songs
has_many :artists, through: :songs
end
Corresponding Rails migrations:
class CreateArtists < ActiveRecord::Migration[6.1]
def change
create_table :artists do |t|
t.string :name
t.string :genre
t.string :date_established
t.text :interesting_fact
t.string :artist_image_url
t.integer :user_id
t.timestamps
end
end
end
class CreateSongs < ActiveRecord::Migration[6.1]
def change
create_table :songs do |t|
t.string :name
t.integer :artist_id
t.integer :album_id
t.timestamps
end
end
end
class CreateAlbums < ActiveRecord::Migration[6.1]
def change
create_table :albums do |t|
t.string :name
t.string :album_cover_url
t.integer :year_released
t.timestamps
end
end
end
In this case, Artist
has many Song
s and has many Album
s through Song
s. Therefore, Artist
has gained access to the songs
method as well as the albums
method through songs. Album
has many Song
s and has many Artist
s through Song
s. Hence, Album
has gained access to the songs
method as well as the artists
method through songs. Song
belongs to Album
and belongs to Artist
. Song is our join table which contain the foreign keys artist_id
and album_id
, one key to reference one table each.
We should test these methods right? Let's move on from K-pop artists. Where are the rappers?
I heard there was a rapper who resembled "Hammering" Hank Aaron. I am going to find him, his songs, and his albums. I heard he could dance.
Alright, here we go.
mc_hammer=Artist.find_by(name:"MC Hammer")
=> #<Artist id: 19, name: "MC Hammer", genre: "hip-hop", date_established: "1985", interesting_fact: "He was named for his resemblance to \"Hammering\"...
mc_hammer.songs
=> #<ActiveRecord::Associations::CollectionProxy [#<Song id: 138, name: "U Can't Touch This", artist_id: 19, album_id: 56, created_at: "2023-03-20 11:13:12.315773000 +0000", updated_at: "2023-03-20 11:16:08.290758000 +0000">, #<Song id: 113, name: "Too Legit Too Quit", artist_id: 19, album_id: 49, created_at: "2023-03-20 00:56:03.164558000 +0000", updated_at: "2023-03-20 00:56:03.164558000 +0000">]>
mc_hammer.albums
=> #<ActiveRecord::Associations::CollectionProxy [#<Album id: 56, name: "Please Hammer Don't Hurt Em", album_cover_url: "https://upload.wikimedia.org/wikipedia/en/d/d3/Ple...", year_released: 1990, created_at: "2023-03-20 11:13:10.656256000 +0000", updated_at: "2023-03-20 11:14:26.094936000 +0000">, #<Album id: 49, name: "Too Legit to Quit ", album_cover_url: "https://upload.wikimedia.org/wikipedia/en/a/a0/Too...", year_released: 1991, created_at: "2023-03-19 23:31:08.410185000 +0000", updated_at: "2023-03-20 02:06:42.688468000 +0000">]>
Now let's test this in reverse. I am taking the MC Hammer album "Too Legit to Quit " and finding the songs and the artist who released the album. I predict it will come back to the Hammer man.
too_legit_to_quit=Album.find(49)
=> #<Album id: 49, name: "Too Legit to Quit ", album_cover_url: "https://upload.wikimedia.org/wikipedia/en/a/a0/Too...", year_released: 1991, created_a...
too_legit_to_quit.songs
=> #<ActiveRecord::Associations::CollectionProxy [#<Song id: 113, name: "Too Legit Too Quit", artist_id: 19, album_id: 49, created_at: "2023-03-20 00:56:03.164558000 +0000", updated_at: "2023-03-20 00:56:03.164558000 +0000">]>
too_legit_to_quit.artists
=> #<ActiveRecord::Associations::CollectionProxy [#<Artist id: 19, name: "MC Hammer", genre: "hip-hop", date_established: "1985", interesting_fact: "He was named for his resemblance to \"Hammering\" Ha...", artist_image_url: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...", user_id: 1, created_at: "2023-03-19 19:34:56.268802000 +0000", updated_at: "2023-03-19 23:02:13.796906000 +0000">]>
It's all good when you know how to use Active Record Associations to make music but let's get something on the page. Our search for an artist using mc_hammer=Artist.find_by(name:"MC Hammer")
.
Now we can see his albums using mc_hammer.albums
.
Check out these songs by MC Hammer using mc_hammer.songs
.
We just made music together using the power of Active Record Associations.
Recap:
- The common relationships are one-to-many and many-to-many.
- One-to-many relationships use
has_many
andbelong_to
macros. - Many-to-many relationships use
has_many :through
in addition tohas_many
andbelong_to
macros. - The use of foreign keys are integral to these relationships. In a one-to-many relationship the foreign key is located in the belonging table. In a many-to-many relationship it is located in the join table.
- The macros
belong_to
,has_many
andhas_many :through
add additional methods to your models.
In short Active Record associations with their macros allow us work with complex networks of related models to create rich and dynamic applications. Now that is music to my ears.
Source: Ruby on Rails Guides
Posted on March 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.