Lesson Weekend

In the last lesson, we updated almost all of our Album methods to use a database. We didn't have to change a single test in the process. Now we'll do the same for the Song class. In fact, the methods are so similar that we won't walk through each one separately. After walking through the previous lesson with a pair, we recommend trying to write out each method for the Song class on your own and getting tests passing one at a time. This provides important SQL practice and helps solidify these concepts.

The entire updated class is below:

song.rb
class Song
  attr_reader :id
  attr_accessor :name, :album_id

  def initialize(attributes)
    @name = attributes.fetch(:name)
    @album_id = attributes.fetch(:album_id)
    @id = attributes.fetch(:id)
  end

  def ==(song_to_compare)
    (self.name() == song_to_compare.name()) && (self.album_id() == song_to_compare.album_id())
  end

  def self.all
    returned_songs = DB.exec("SELECT * FROM songs;")
    songs = []
    returned_songs.each() do |song|
      name = song.fetch("name")
      album_id = song.fetch("album_id").to_i
      id = song.fetch("id").to_i
      songs.push(Song.new({:name => name, :album_id => album_id, :id => id}))
    end
    songs
  end

  def save
    result = DB.exec("INSERT INTO songs (name, album_id) VALUES ('#{@name}', #{@album_id}) RETURNING id;")
    @id = result.first().fetch("id").to_i
  end

  def self.find(id)
    song = DB.exec("SELECT * FROM songs WHERE id = #{id};").first
    name = song.fetch("name")
    album_id = song.fetch("album_id").to_i
    id = song.fetch("id").to_i
    Song.new({:name => name, :album_id => album_id, :id => id})
  end

  def update(name, album_id)
    @name = name
    @album_id = album_id
    DB.exec("UPDATE songs SET name = '#{@name}', album_id = #{@album_id} WHERE id = #{@id};")
  end

  def delete
    DB.exec("DELETE FROM songs WHERE id = #{@id};")
  end

  def self.clear
    DB.exec("DELETE FROM songs *;")
  end

  def self.find_by_album(alb_id)
    songs = []
    returned_songs = DB.exec("SELECT * FROM songs WHERE album_id = #{alb_id};")
    returned_songs.each() do |song|
      name = song.fetch("name")
      id = song.fetch("id").to_i
      songs.push(Song.new({:name => name, :album_id => alb_id, :id => id}))
    end
    songs
  end

  def album
    Album.find(@album_id)
  end
end

The only key difference worth mentioning here is our new self.find_by_album method. It looks very similar to our self.all method. The key difference is that we have a select statement with a WHERE query included. Specifically, we are looking for songs where the album_id matches the argument passed into alb_id. This is a very common query for finding the "many" in a one-to-many relationship. In this case, we're using it to find all the Songs that belong to an Album.

At this point, we can also uncomment the remaining test in album_spec.rb as well as any integration specs. All tests should pass and we can navigate through our application just as we did before. This is a great thing about modular, clean code. We were able to update all of our backend logic without touching our tests, app.rb, or our views. Ultimately, our application is doing the exact same thing it was doing before but with one key difference: we now have persistence through a database. From here on out, class variables should be avoided if possible.

Lesson 9 of 29
Last updated August 7, 2022