The Home Page

Now for another challenge.

In this challenge, you will use the knowledge gained so far in this course to add new functionality to the API. You will modify the all() method of the MovieService to do the following:

Once you have completed the challenge, you will be asked to run a unit test to verify that the code has been correctly implemented. If the test runs correctly, the title of the highest rated movie will be logged.

You will need this value to verify that the test has run correctly.

Exploring the Code

Before you start, let’s take a look at the code.

If you are not interested in the code, you can skip straight to Implementing Read Transactions.

If you start the application with mvn compile exec:java and access the app at http://localhost:3000, you will see two lists on the home page; one for Popular Movies and one for Latest Releases.

Both of these lists are populated by a request to http://localhost:3000/api/movies with some additional request query parameters, feel free to open that URL in your browser, you should see the JSON response.

Route Handler

You can find the route handler, the function that handles the request, in src/main/java/neoflix/routes/MovieRoutes.java:

java
neoflix/routes/MovieRoutes.java
movieService = new MovieService(driver);  // (1)
get("",  ctx -> {
    var params = Params.parse(ctx, Params.MOVIE_SORT); // (2)
    String userId = AppUtils.getUserId(ctx);  // (3)
    var movies = movieService.all(params, userId);  // (4)
    ctx.result(gson.toJson(movies));
});

Within the route handler, you can see that:

  1. A new instance of the MovieService is created with the Driver instance that you created in Adding the Driver passed to the constructor.

  2. The sort, order, limit and skip values are extracted from the request. This allows us to apply sorting and pagination to the results.

  3. The userId is also extracted from the request.

  4. The query results are retrieved via the all() method and are returned by the handler as JSON.

The Movie Service

The magic happens in the MovieService, located at src/main/java/neoflix/services/MovieService.java. For this route, we are concerned with the all() method.

If we take a closer look at the all() method, we can see that it currently returns a hardcoded list of popular movies from the popular.json fixture.

java
neoflix/services/MovieService.java
public List<Map<String,Object>> all(Params params, String userId) {
    // TODO: Open an Session
    // TODO: Execute a query in a new Read Transaction
    // TODO: Get a list of Movies from the Result
    // TODO: Close the session

    return AppUtils.process(popular, params);
}

You will need to replace these TODO comments with working code to complete the challenge.

Implementing Read Transactions

As you learned in the Sessions and Transactions lesson, you will complete code to open a new session and run the query within a Read Transaction.

Then, finally you add code to extract and return the results.

Open a new Session

Within the all() method, first open a new session:

java
// Open a new session
try (var session = driver.session()) {

  // Do something with the session...

  // Close the session automatically in try-with-resources block
}

Execute a Cypher statement within a new Read Transaction

The Cypher statement itself is similar to this one, only that the property that’s ordered by will be dynamically inserted based on user interaction from the API query parameters.

cypher
// ordering will be dynamic in implementation
MATCH (m:Movie)
WHERE m.title IS NOT NULL
RETURN m {
  .*
} AS movie
ORDER BY m.title ASC
SKIP $skip
LIMIT $limit

This session then provides an executeRead() method for which you pass a callback/lambda to represent the unit of work.

The function will have one argument passed to it, a Transaction instance that you can use to execute a Cypher statement using the run() method.

The run() method accepts two arguments:

  1. The Cypher statement as a string, using query parameters (placeholders prefixed with $).

  2. An object containing the names and values of the parameters.

java
// Execute a query in a new Read Transaction
var movies = session.executeRead(tx -> {
    // Retrieve a list of movies with the
    // favorite flag appened to the movie's properties
    Params.Sort sort = params.sort(Params.Sort.title);
    String query = String.format("""
        MATCH (m:Movie)
        WHERE m.`%s` IS NOT NULL
        RETURN m {
          .*
        } AS movie
        ORDER BY m.`%s` %s
        SKIP $skip
        LIMIT $limit
        """, sort, sort, params.order());
    var res= tx.run(query, Values.parameters( "skip", params.skip(), "limit", params.limit()));
    // Get a list of Movies from the Result
    return res.list(row -> row.get("movie").asMap());
});

Two of the parameters, namely the sort field and sort order cannot be provided as Cypher statement parameters, so we need to use string substitution with %s instead, which we would love to avoid.

Extract a list of Movies from the Result

Now that you have a complete result assigned to the res variable, you

  1. use the list() function to convert each row,

  2. grab each movie node from the row Record

  3. turn the node properties into a Java Map with the asMap() method.

java
// Get a list of Movies from the Result
return res.list(row -> row.get("movie").asMap());

Return the Results

Finally, update the return statement to return the movies list extracted above.

java
return movies;

Working Solution

Click here to reveal the completed all() method
java
neoflix/services/MovieService.java
public List<Map<String,Object>> all(Params params, String userId) {
    // Open a new session
    try (var session = this.driver.session()) {
        // Execute a query in a new Read Transaction
        var movies = session.executeRead(tx -> {
            // Retrieve a list of movies with the
            // favorite flag appened to the movie's properties
            Params.Sort sort = params.sort(Params.Sort.title);
            String query = String.format("""
                MATCH (m:Movie)
                WHERE m.`%s` IS NOT NULL
                RETURN m {
                  .*
                } AS movie
                ORDER BY m.`%s` %s
                SKIP $skip
                LIMIT $limit
                """, sort, sort, params.order());
            var res= tx.run(query, Values.parameters( "skip", params.skip(), "limit", params.limit()));
            // Get a list of Movies from the Result
            return res.list(row -> row.get("movie").asMap());
        });

        return movies;
    }
}

Testing

To test that this functionality has been correctly implemented, run the following code in a new terminal session:

sh
Running the test
mvn test -Dtest=neoflix._02_MovieListTest

The test file is located at src/test/java/neoflix/_02_MovieListTest.java.

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 02-movie-lists branch by running:

sh
Check out the 02-movie-lists branch
git checkout 02-movie-lists

You may have to commit or stash your changes before checking out this branch. You can also click here to expand the Support pane.

Verifying the Test

Highest Rated Movie

The final test in the suite logs out the name of the highest rated movie according to the imdbRating property on each movie. Enter the title of the highest rated movie.

  • ✓ Band of Brothers

Hint

You can also find the answer by running the following Cypher statement:

cypher
MATCH (m:Movie)
WHERE m.imdbRating IS NOT NULL
RETURN m.title
ORDER BY m.imdbRating DESC LIMIT 1

Copy the answer without any quotes or whitespace.

Solution

The answer is Band of Brothers.

Lesson Summary

In this Challenge, you used your knowledge of sessions and transactions to retrieve a list of Movie nodes from the database.

If you now open the API endpoint or the application UI for popular movies you should see the list coming from your database. Congratulations 🎉.

In the next Challenge about user management, you will learn to write data to the database.