Step 2: Adding artist and track features

Enhance the Playlist application by adding the ability to search the song catalog by track and artist name, with corresponding tables and CQL queries.

In Step 1, you connected to Cassandra from a web application and displayed some data stored in the system table. Now we will enhance Playlist by importing song data from a comma-separated value (CSV) file into tables, organized by the types of queries required by the Playlist application.

Setting up Cassandra keyspaces and tables 

You will create a new keyspace in Cassandra called playlist. A Cassandra keyspace is similar to a named database in a relational database, but is slightly different. A Cassandra keyspace is a namespace that defines how data is replicated throughout the Cassandra cluster. You typically create one keyspace per application. When creating a keyspace, you must configure how the data is replicated by specifying a replication strategy. In this tutorial, we are using a simple cluster with a single node, so we are using the SimpleStrategy, and a replication factor of 1. The replication factor specifies how many nodes on which the data will be replicated. If our cluster had several nodes, we could set a higher replication factor to ensure that the table data would still be available if one of the nodes in the cluster failed.

After creating the keyspace, you will then create a table named artists_by_first_letter and populate it with data from a CSV file. The artists_by_first_letter table is designed for a query used in Playlist to list the artists by the first letter of their names, and consists of two columns, first_letter and artist. The artists_by_first_letter table's primary key is a composite primary key, also called a compound primary key, made up of these columns. The first part of the definition of a composite primary key is the partition key, which is used by Cassandra to distribute the table data across nodes. The second part of the definition is the clustering key, or clustering column or columns, which is used by Cassandra to sort the data within the partition. You can use multiple columns when setting the partition key and clustering key of a composite primary key. Designing the table in this way allows Cassandra to easily distribute the table data throughout the nodes in the cluster. This type of table design is quite different than a fully normalized relational database, but allows for increased performance and failover in a clustered database. See the CQL documentation for more information on composite primary keys.

artist_by_first_letter
first_letter text PK
artist text PK

The songs are stored in two tables, track_by_artists and track_by_genre, to allow searching by artist name and type of music. These tables are also populated from a CSV file.

track_by_artist
track text PK
artist text PK
track_id UUID PK
track_length_in_seconds int  
genre text  
music_file text  
track_by_genre
track text PK
artist text PK
track_id UUID PK
track_length_in_seconds int  
genre text PK
music_file text  

Data access objects for artists and tracks 

There are two new data-access object classes and their corresponding controller servlets, playlist.model.ArtistsDAO and playlist.controller.ArtistServlet, and playlist.model.TracksDAO and playlist.controller.TrackServlet.

The ArtistsDAO class is used to return a list of the artists in the music service. It has a single finder method, listArtistsByLetter, which is used to find all artists whose names begin with the specified letter. The listArtistsByLetter method can return the list ordered in ascending alphabetic order, or optionally in descending alphabetic order. The CQL query is a prepared statement bound by the first letter parameter. A prepared statement acts like a template to improve the performance of similar queries. The prepared statement is then bound with the parameter data before execution.

String queryText = "SELECT * FROM artists_by_first_letter WHERE first_letter = ?" + (desc ? " ORDER BY artist DESC" : "");
PreparedStatement preparedStatement = getSession().prepare(queryText);
BoundStatement boundStatement = preparedStatement.bind(first_letter);

The CQL query will use the ORDER BY artist DESC fragment only if the desc parameter is true.

The query is executed and the results are returned as a com.datastax.driver.core.ResultSet object.

ResultSet results = getSession().execute(boundStatement);

Each row in the results is parsed and added to a List<String> object, and then the list is returned for display in the view.

List<String> artists = new ArrayList<>();
for (Row row : results) {
   artists.add(row.getString("artist"));
}
return artists;

The TracksDAO class manages the display of track information and the ability to add new tracks to the catalog. Like ArtistsDAO, it has finder methods, listSongsByArtist and listSongsByGenre. It also has an add method to add new tracks using the information entered into a form in the view.

The SELECT queries used in TracksDAO are similar to the query used in ArtistsDAO.

public static List<TracksDAO> listSongsByArtist(String artist) {

  String queryText = "SELECT * FROM track_by_artist WHERE artist = ?";
  PreparedStatement preparedStatement = getSession().prepare(queryText);
  BoundStatement boundStatement = preparedStatement.bind(artist);
  ResultSet results = getSession().execute(boundStatement);

  List<TracksDAO> tracks = new ArrayList<>();

  for (Row row : results) {
    tracks.add(new TracksDAO(row));
  }

  return tracks;
}

public static List<TracksDAO> listSongsByGenre(String genre) {

  String queryText = "SELECT * FROM track_by_genre WHERE genre = ?";
  PreparedStatement preparedStatement = getSession().prepare(queryText);
  BoundStatement boundStatement = preparedStatement.bind(genre);
  ResultSet results = getSession().execute(boundStatement);


  List<TracksDAO> tracks = new ArrayList<>();

  for (Row row : results) {
    tracks.add(new TracksDAO(row));
  }

  return tracks;
}

These finder methods use a private constructor for TracksDAO that parses the Row object and extracts the information from the columns using getter methods according to the type of data stored in the column.

private TracksDAO(Row row) {
  track_id = row.getUUID("track_id");
  artist = row.getString("artist");
  track = row.getString("track");
  genre = row.getString("genre");
  music_file = row.getString("music_file");
  track_length_in_seconds = row.getInt("track_length_in_seconds");
}

The add method uses INSERT statements to add new rows to the three tables used: artists_by_first_letter, track_by_artist, and track_by_genre.

public void add() {

  // Compute the first letter of the artists name for the artists_by_first_letter table
  String artist_first_letter = this.artist.substring(0,1).toUpperCase();

  // insert into artists_by_first_letter
  PreparedStatement preparedStatement =
          getSession().prepare("INSERT INTO artists_by_first_letter (first_letter, artist) VALUES (?, ?)");
  BoundStatement boundStatement = preparedStatement.bind(artist_first_letter, this.artist);
  getSession().execute(boundStatement);

  // insert into track_by_artist
  preparedStatement = getSession().prepare("INSERT INTO track_by_artist (genre, track_id, artist, track, track_length_in_seconds) VALUES (?, ?, ?, ?, ?)");
  boundStatement = preparedStatement.bind(this.genre, this.track_id, this.artist, this.track, this.track_length_in_seconds);
  getSession().execute(boundStatement);

  // insert into track_by_genre
  preparedStatement = getSession().prepare("INSERT INTO track_by_genre (genre, track_id, artist, track, track_length_in_seconds) VALUES (?, ?, ?, ?, ?)");
  boundStatement = preparedStatement.bind(this.genre, this.track_id, this.artist, this.track, this.track_length_in_seconds);
  getSession().execute(boundStatement);

}

The controller and view 

The two new controller servlets, ArtistServlet and TrackServlet are similar to the HomeServlet used in step 1, managing the requests and forwarding them on to the proper view for a response. ArtistServlet examines the request to determine if the user wants to display the artists in descending order, and if so, calling listArtistsByFirstLetter with the desc parameter set to true. TrackServlet responds to HTTP POST requests when users create a new track, extracting the track information from the request.

Changes from Step 1 

To see all code changes from the step1 branch, enter the following command in a terminal in the playlist directory:

git diff step1..step2