Let's add a Song
class that will give us full CRUD functionality for adding songs to our application. We'll start by adding song.rb
to our lib folder and song_spec.rb
to our spec folder. The methods listed in the table below will be implemented in almost the exactly same way we added functionality to our Album
class.
In addition to that, we'll need a few more methods as well:
What follows are the two "tables" that we'll haves in our mock database, one for albums, and one for songs. In a SQL database, they actually will be tables.
Now if we want to find all the songs that belong to "Giant Steps", we just need to look for songs that have an album_id
that matches the id
of "Giant Steps." We'll find songs associated with an album using code that looks similar to this:
album = Album.find(id)
songs = Song.find_by_album(album.id)
As long as we have an album's id
, it will be straightforward to find songs associated with it. However, we must always make sure that songs have an album_id
— otherwise, they'd be in limbo and not connected to an album!
If this isn't fully clear yet, don't worry — we will cover relationships more in the next course section when we start using databases. For now, we want to stay focused on routing.
Many of our Song
methods will be similar to Album
methods so we won't go through each test and method individually. Here is the code for most of our specs:
require 'rspec'
require 'song'
require 'album'
require 'pry'
describe '#Song' do
before(:each) do
Album.clear()
Song.clear()
@album = Album.new("Giant Steps", nil)
@album.save()
end
describe('#==') do
it("is the same song if it has the same attributes as another song") do
song = Song.new("Naima", @album.id, nil)
song2 = Song.new("Naima", @album.id, nil)
expect(song).to(eq(song2))
end
end
describe('.all') do
it("returns a list of all songs") do
song = Song.new("Giant Steps", @album.id, nil)
song.save()
song2 = Song.new("Naima", @album.id, nil)
song2.save()
expect(Song.all).to(eq([song, song2]))
end
end
describe('.clear') do
it("clears all songs") do
song = Song.new("Giant Steps", @album.id, nil)
song.save()
song2 = Song.new("Naima", @album.id, nil)
song2.save()
Song.clear()
expect(Song.all).to(eq([]))
end
end
describe('#save') do
it("saves a song") do
song = Song.new("Naima", @album.id, nil)
song.save()
expect(Song.all).to(eq([song]))
end
end
describe('.find') do
it("finds a song by id") do
song = Song.new("Giant Steps", @album.id, nil)
song.save()
song2 = Song.new("Naima", @album.id, nil)
song2.save()
expect(Song.find(song.id)).to(eq(song))
end
end
describe('#update') do
it("updates an song by id") do
song = Song.new("Naima", @album.id, nil)
song.save()
song.update("Mr. P.C.", @album.id)
expect(song.name).to(eq("Mr. P.C."))
end
end
describe('#delete') do
it("deletes an song by id") do
song = Song.new("Giant Steps", @album.id, nil)
song.save()
song2 = Song.new("Naima", @album.id, nil)
song2.save()
song.delete()
expect(Song.all).to(eq([song2]))
end
end
end
All of these specs are for methods that we covered for the Album
class as well, so they should mostly be review. A few small differences:
before(:each)
block to DRY up our code;album_id
which we set to the value of @album.id
;require
both our Song
and Album
class since our specs will rely on both.Let's get the specs above to pass:
class Song
attr_reader :id
attr_accessor :name, :album_id
@@songs = {}
@@total_rows = 0
def initialize(name, album_id, id)
@name = name
@album_id = album_id
@id = id || @@total_rows += 1
end
def ==(song_to_compare)
(self.name() == song_to_compare.name()) && (self.album_id() == song_to_compare.album_id())
end
def self.all
@@songs.values
end
def save
@@songs[self.id] = Song.new(self.name, self.album_id, self.id)
end
def self.find(id)
@@songs[id]
end
def update(name, album_id)
self.name = name
self.album_id = album_id
@@songs[self.id] = Song.new(self.name, self.album_id, self.id)
end
def delete
@@songs.delete(self.id)
end
def self.clear
@@songs = {}
end
end
Once again, this should all be review; we wrote the exact same methods for our Album
class.
We also need to add some new logic:
Album#songs()
will be an instance method for returning a list of an album's songs;Song.find_by_album()
class method to actually find those songs that belong to an album;Song#album()
to return the album associated with a song.Why do we need both of these methods? Well, it would be convenient to be able to call album.songs()
and have access to all of an album's songs. However, the Album
class doesn't have access to @@songs
— and nor should it. Our Song
class will do the filtering for us.
Let's start with the Song.find_by_album()
method because Album#songs()
will rely on it.
Here's a test for our new method:
...
describe '#Song' do
...
describe('.find_by_album') do
it("finds songs for an album") do
album2 = Album.new("Blue", nil)
album2.save
song = Song.new("Naima", @album.id, nil)
song.save()
song2 = Song.new("California", album2.id , nil)
song2.save()
expect(Song.find_by_album(album2.id)).to(eq([song2]))
end
end
end
We create two new songs, each associated with a different album. We should expect to find only the song that belongs to album2
.
Here's our new method:
class Song
...
def self.find_by_album(alb_id)
songs = []
@@songs.values.each do |song|
if song.album_id == alb_id
songs.push(song)
end
end
songs
end
end
This method loops through @@songs
and pushes items with the correct album_id
to an array. It is not a performant method, but it's suitable for our purposes. Fortunately, SQL databases handle these kinds of searches much more efficiently.
Now let's add an Album#songs
test:
require 'rspec'
require 'album'
require 'song'
describe '#Album' do
...
describe('#songs') do
it("returns an album's songs") do
album = Album.new("Giant Steps", nil)
album.save()
song = Song.new("Naima", album.id, nil)
song.save()
song2 = Song.new("Cousin Mary", album.id, nil)
song2.save()
expect(album.songs).to(eq([song, song2]))
end
end
end
Here's the method:
class Album
...
def songs
Song.find_by_album(self.id)
end
end
It's not ideal that we need to rely on another class to find songs, but it's better than using global variables — and our Album
class shouldn't have access to @@songs
.
We'll do something similar with our Song#album()
method. First, a test:
describe '#Song' do
...
describe('#album') do
it("finds the album a song belongs to") do
song = Song.new("Naima", @album.id, nil)
song.save()
expect(song.album()).to(eq(@album))
end
end
end
Here's our new method:
class Song
...
def album
Album.find(self.album_id)
end
end
Once again, we need to rely on another class. We now have all the backend methods we need to add routing for songs to our record store application. We'll cover that in the next lesson.
Lesson 26 of 37
Last updated August 7, 2022