Making Music With Active Record Associations

chukwuma1976

Chukwuma Anyadike

Posted on March 26, 2023

Making Music With Active Record Associations

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In this example Artist has many Songs, 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 Songs. This method gives access to a collection of the Songs 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 > 
Enter fullscreen mode Exit fullscreen mode

Here I have assigned the first record of Songs 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.

Likey by Twice

class Model < ApplicationRecord
    belongs_to :association
end
Enter fullscreen mode Exit fullscreen mode

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">, ...]>
Enter fullscreen mode Exit fullscreen mode

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 Songs by blackpink.

Blackpink

class Model < ApplicationRecord
    has_many :collection
end
Enter fullscreen mode Exit fullscreen mode

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"> 
Enter fullscreen mode Exit fullscreen mode

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">]> 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In this case, Artist has many Songs and has many Albums through Songs. Therefore, Artist has gained access to the songs method as well as the albums method through songs. Album has many Songs and has many Artists through Songs. 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.

MC Hammer 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">]>
Enter fullscreen mode Exit fullscreen mode

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">]> 
Enter fullscreen mode Exit fullscreen mode

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").

MC Hammer Search

Now we can see his albums using mc_hammer.albums.

MC Hammer Albums

Check out these songs by MC Hammer using mc_hammer.songs.

Image description

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 and belong_to macros.
  • Many-to-many relationships use has_many :through in addition to has_many and belong_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 and has_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

💖 💪 🙅 🚩
chukwuma1976
Chukwuma Anyadike

Posted on March 26, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related