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