User Roles & Secondary Indexes

Brian Sam-Bodden
Brian Sam-Bodden, Developer Advocate at Redis

To finish creating the user-role domain, load and transform JSON data, and begin crafting the Redi2Read API.

In this lesson, you'll learn:

  • How to load JSON data using Jackson.
  • How to create and work with secondary indexes
  • How to use the repositories with a REST controller.

If you get stuck:

Now that we’ve created the Roles let’s load the Users from the provided JSON data in src/main/resources/data/users/users.json. The file contains an array of JSON user objects as shown below:

The JSON fields map exactly to the JavaBean names for our User POJO properties.

First, we’ll create the UserRepository; just like we did with the RoleRepository, we’ll extend CrudRepository. Under the src/main/java/com/redislabs/edu/redi2read/repositories let's create the UserRepository interface as follows:

The findFirstByEmail method takes advantage of the index we previously created on the email field of the User model. The Spring Repository will provide an implementation of the finder method at runtime. We will use this finder when we tackle our application's security.

Let’s create another CommandLineRunner under the boot package to load the users. We’ll follow a similar recipe for the Roles, except that we will load the JSON data from disk and use Jackson (https://github.com/FasterXML/jackson), one of the most popular Java JSON libraries.

The recipe to load the user goes as follows:

  1. 1.Create an input stream from the user’s JSON data file
  2. 2.Using Jackson, read the input stream into a collection of users
  3. 3.For each user:
  • Encode the plain text password
  • Add the customer role

Based on the loading recipe above, there are two things our application can’t currently do that it needs:

  • A way to encode plain text user password
  • A way to find a role by name

Our implementation of PasswordEncoder will use the BCrypt strong hashing function. In the Redi2readApplication class add:

With the corresponding import:

As we learned in the previous lesson, the @Indexed annotation can be used to create a secondary index. Secondary indexes enable lookup operations based on native Redis structures. The index is maintained on every save/update of an indexed object. To add a secondary index to the Role model, we’ll simply add the @Indexed annotation:

Don’t forget to add the corresponding import:

Now when a new Role instance is created, with ID as "abc-123" and role as "superuser", Spring Data Redis will do the following:

  1. 1.Create the "by name" index: Created as a Redis Set with the key com.redislabs.edu.redi2read.models.Role:name:superuser containing one entry; the id of the indexed object "abc-123"
  2. 2.A list of indexes for the Role "superuser": Create a Redis Set with the key "com.redislabs.edu.redi2read.models.Role:abc-123:idx" containing one entry; the key of the index "com.redislabs.edu.redi2read.models.Role:name:superuser"

Unfortunately, to index the already created Roles, we’ll need to either retrieve them and resave them or recreate them. Since we already have automated the seeding of the Roles and we haven’t yet created any associated objects, we can simply delete them using the Redis CLI and the DEL command and restart the server:

The DEL command takes one or more keys. We’ll pass the three current keys for the Role hashes and the Role key set.

With the secondary index on the name for roles created, we can add a finder method to the RoleRepository:

Under the src/main/java/com/redislabs/edu/redi2read/boot let's create the CreateUsers.java file with the following contents:

Let’s break it down:

  • At the top, we use the @Autowired annotation to inject the RoleRepository, the UserRepository, and the BCryptPasswordEncoder.
  • As with the CreateRoles CommandLineRunner, we only execute the logic if there are no database users.
  • We then load the admin and customer roles by using the Repository custom finder method findFirstByName.
  • To process the JSON, we create a Jackson ObjectMapper and a TypeReference, which will serve as a recipe for serializing the JSON into Java objects.
  • Using the getResourceAsStream from the Class object, we load the JSON file from the resources directory
  • Then we use the ObjectMapper to convert the incoming input stream into a List of User objects
  • For each user, we encode the password and add the customer role
  • Near the end of the file, we create a single user with the admin role, which we will use in a later Lesson

On application restart, we should now see:

If you were watching the Redis CLI in MONITOR mode you probably saw a barrage of the Redis commands executing for the 1001 users we’ve just created. Let’s use the CLI to explore the data:

We now have a Redis Set holding the collection of user keys for the Redis Hashes containing user instances. We use the SCARD command to get the set’s cardinality (1001, the 1000 users from the JSON plus the admin user). Using the SRANDMEMBER command, we can pull a random member from a Set. We then use that and the User Hashes prefix to retrieve the data for a random User hash. A few things to point out:

  • The user’s set of roles are stored using indexed hash fields (roles.[0], roles.[1], etc.) with a value being the key for a given role. This is the result of annotating the Java Set of Role using @Reference
  • The password field is hashed correctly.

Now that we have Users and Roles, let’s create an UserController to expose some user management functionality.

We can now issue a GET request to retrieve all users:

The output should be an array of JSON object like:

Let’s be good RESTful citizens and filter out the password and passwordConfirm fields on the way out. To accomplish this we take advantage of the fact the Jackson is the default serializer in Spring Web which mean we can annotate the User class with the @JsonIgnoreProperties only allowing setters (so that we can load the data) but hiding the getters during serialization as shown next:

With the import statement:

Issuing the request again should reflect the changes on the JSON response:

Let’s add one more method to our UserController. We’ll add the ability to retrieve a user by its email address, which will take advantage of the secondary index on email in the User object. We’ll implement it as a filter on the GET root endpoint of the controller:

We use a request parameter for the email, and if it is present, we invoke the findFirstByEmail finder. We wrap the result in a list to match the result type of the method. We use Optional to handle a null result from the finder. And don’t forget your imports:

Invoking the endpoint with curl:

Returns the expected result: