Rating Movies

The challenges in this course come thick and fast!

In this challenge, you will modify the add() method in the RatingService to save ratings into Neo4j. As part of the challenge, you will:

The Request Lifecycle

Rating a Movie

Before we start, let’s take a look at the request lifecycle when saving a review. If you prefer, you can skip to Save the Rating in a Write Transaction.

On every Movie page, the user is invited to rate a movie on scale of 1 to 5. The form pictured to the right gives the user the ability to select a rating between 1 and 5 and click submit to save the rating.

When the form is submitted, the website sends a POST request to /api/account/ratings/{movieId} and the following will happen:

  1. The server directs the request to the route handler in AccountRoutes.java, whose /ratings/ route gets the authenticated user’s id from the request.

  2. The route handler uses an instance of the RatingService.

  3. The add() method is called on the RatingService, and is passed the id of the current user plus the id of the movie and a rating value from the request body.

  4. It is then the responsibility of the add() method to save this information to the database and return an appropriate response.

A rating is represented in the graph as a relationship going from a :User to a :Movie node with the type :RATED. The relationship has two properties; the rating (an integer) and a timestamp to represent when the relationship was created.

After the data is saved, the UI expects the movie details to be returned, with an additional property called rating, which will be the rating that the current user has given for the movie.

Let’s take a look at the existing method in the RatingService.

java
neoflix/services/RatingService.java
public Map<String,Object> add(String userId, String movieId, int rating) {
    // TODO: Convert the native integer into a Neo4j Integer
    // TODO: Save the rating in the database
    // TODO: Return movie details and a rating

    var copy = new HashMap<>(pulpfiction);
    copy.put("rating",rating);
    return copy;
}

Your challenge is to replace the TODO comments in this method with working code.

Save the Rating in a Write Transaction

For this part of the challenge, open a new session and execute the query within a write transaction. The singular movie result should be converted to a map.

java
// Save the rating in the database

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

    // Run the cypher query
    var movie = session.executeWrite(tx -> {
        String query = """
                MATCH (u:User {userId: $userId})
                MATCH (m:Movie {tmdbId: $movieId})

                MERGE (u)-[r:RATED]->(m)
                SET r.rating = $rating, r.timestamp = timestamp()

                RETURN m { .*, rating: r.rating } AS movie
                """;
        var res = tx.run(query, Values.parameters("userId", userId, "movieId", movieId, "rating", rating));
        return res.single().get("movie").asMap();
    });

By using the MERGE keyword for the relationship, we will reuse an existing rating if one already exists but override its attributes.

This way, we don’t need to worry about duplicates or deleting existing records.

If no movie or user is found, the statement will return no records, then the single() method will throw an NoSuchRecordException error which we convert to a ValidationException.

java
} catch(NoSuchRecordException e) {
    throw new ValidationException("Movie or user not found to add rating", Map.of("movie", movieId, "user", userId));
}

Return the Results

Finally, return the movie.

java
// Return movie details with rating
return movie;

Working Solution

Click here to reveal the Working Solution.
java
src/services/rating.service.js
public Map<String,Object> add(String userId, String movieId, int rating) {
    // Save the rating in the database

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

        // Run the cypher query
        var movie = session.executeWrite(tx -> {
            String query = """
                    MATCH (u:User {userId: $userId})
                    MATCH (m:Movie {tmdbId: $movieId})

                    MERGE (u)-[r:RATED]->(m)
                    SET r.rating = $rating, r.timestamp = timestamp()

                    RETURN m { .*, rating: r.rating } AS movie
                    """;
            var res = tx.run(query, Values.parameters("userId", userId, "movieId", movieId, "rating", rating));
            return res.single().get("movie").asMap();
        });

        // Return movie details with rating
        return movie;
    } catch(NoSuchRecordException e) {
        throw new ValidationException("Movie or user not found to add rating", Map.of("movie", movieId, "user", userId));
    }
}

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._06_RatingMoviesTest#writeMovieRatingAsInt

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

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 06-rating-movies branch by running:

sh
Check out the 06-rating-movies branch
git checkout 06-rating-movies

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

If the test has run successfully, a user with the email address graphacademy.reviewer@neo4j.com will have given the movie Pulp Fiction a rating of 5.

That number should have a type of INTEGER

Hint

You can run the following query to check for the user within the database. If the shouldVerify value returns true, the verification should be successful.

cypher
MATCH (u:User {email: "graphacademy.reviewer@neo4j.com"})-[r:RATED]->(m:Movie {title: "Goodfellas"})
RETURN u.email, m.title, r.rating,
    r.rating = 5 as shouldVerify

Solution

The following statement will mimic the behaviour of the test, merging a new :User node with the email address graphacademy.reviewer@neo4j.com and a :Movie node with a .tmdbId property of '769'. The test then merges a relationship between the user and movie nodes in the graph, giving the relationship a .rating property.

cypher
MERGE (u:User {userId: '1185150b-9e81-46a2-a1d3-eb649544b9c4'})
SET u.email = 'graphacademy.reviewer@neo4j.com'
MERGE (m:Movie {tmdbId: '769'})
MERGE (u)-[r:RATED]->(m)
SET r.rating = 5,
    r.timestamp = timestamp()
RETURN m {
    .*,
    rating: r.rating
} AS movie

Once you have run this statement, click Try again…​* to complete the challenge.

Lesson Summary

In this Challenge, you have updated the RatingService to save a relationship between a User and Movie to represent a Rating.

In the next Challenge, we will implement the My Favorites feature.