Author: Brian Sam-Bodden
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:
- The progress made in this lesson is available on the redi2read github repository at https://github.com/redis-developer/redi2read/tree/course/milestone-4
Now that we’ve created the Roles let’s load the
Users from the provided JSON data in
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
src/main/java/com/redislabs/edu/redi2read/repositories let's create the
UserRepository interface as follows:
findFirstByEmail method takes advantage of the index we previously created on the email field of the
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:
- Create an input stream from the user’s JSON data file
- Using Jackson, read the input stream into a collection of users
- 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
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:
- Create the
"by name"index: Created as a Redis Set with the key
com.redislabs.edu.redi2read.models.Role:name:superusercontaining one entry; the id of the indexed object
- 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
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
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
@Autowiredannotation to inject the
UserRepository, and the
- As with the
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
- 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
Classobject, we load the JSON file from the resources directory
- Then we use the
ObjectMapperto convert the incoming input stream into a
- 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., roles., etc.) with a value being the key for a given role. This is the result of annotating the Java Set of Role using
- The password field is hashed correctly.
Now that we have
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
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
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: