Most often data being sent or recieved via a webservice is stored in some type of database. For our example we will use Firebase as we have used MongoDB with Parse Server in the past.
We will follow this tutorial on getting started setting up our Firestore database.
Firebase is a NoSQL database, meaning it does not use the standard SQL queries that we are used to. It also does not organize data in the way we are used to. For this exercise we will have 4 collections (tables): Caetgory, User, Post, and PostComment. These collections will contain data based on the structure we created previously. Note: The User collection will contain one previously non-existent field "uid"
We will create 1 example entry for each. Firebase does not allow you to create a data schema, as such until you create an entry the collection cannot exist.
Field | Data Type |
---|---|
content | String |
metaTitle | String |
slug | String |
title | String |
Field | Data Type |
---|---|
uid | String |
username | String |
String | |
firstName | String |
lastName | String |
intro | String |
middleName | String |
mobile | String |
profile | String |
registeredAt | Timestamp |
lastLogin | Timestamp |
Field | Data Type |
---|---|
allowComments | boolean |
authorId | reference (User) |
categoryId | array of references (Category) |
content | String |
createdAt | Timestamp |
published | boolean |
publishedAt | Timestamp |
slug | String |
summary | String |
tags | array of strings |
title | String |
Field | Data Type |
---|---|
authorId | reference (User) |
content | String |
createdAt | Timestamp |
published | boolean |
postId | reference (Post) |
Spring is a Java based framework for building full stack web applications. This includes: frontend, backend, and webservices (SOAP & REST) among other features.
We will be using Spring in different ways. We will look at using ReactJS frontend with Spring as our REST API.
Now let's create our project!
In IntelliJ:
Select New Project > Spring Intializr
Next, add the Spring dependecies. These are the libraries found in Spring that we will need. Select the following:
If you do not have that option, create you project from the site Spring Initializr
To connect to your database, you will need create a Service Account. We create a Key and download the credential file. Save the credential file somewhere on your computer where you will be able to find it later.
Some of you may already have Service Accounts and simply need to create a key.
Next we will add the Google Firebase Libraries to our project. In the POM file add the following into thw <dependencies>
:
We will add code to method main that will allow the program to automatically connect to the Firebase each time we start the application.
Place your key file that you downloaded earlier in the resources folder and rename the file serviceAccountKey.json
.
Next we will add the following code to main above the existing line of code:
Next we will create the required packages and classes. We will be following the Model-View-Controller (MVC) design pattern. Here is a brief explanation of the pattern.
Create a new package below your root package called model
. This package will contain our data models, all the classes for each of the tables. Some of these classes will be abstract. We will create a class for normal use as well as use with the REST Controllers.
We being with our Category collection. Create a variable for each of the values in the database. The names should be identical to those in the database. We will use a few annotations as well:
@Data
: Allows the compiler to generate our getter and setter methods@NoArgsContstructor
: Allows the compiler to gnerate our no argument constructor@AllArgsConstructor
: Allows the compiler to generate a constructor with all the variables as arguments. They will be listed in the order you list the variables in your class@DocumentId
: This is a Firebase annotation that tells the interpreter that when it pulls a DocumentId from Firebase to assign it to the specified variables@Nullable
: Allows for that variable to be null. This is important when we use our models to read in JSON being sent to our API.We make the categoryId nullable so when creating a new category, we are not required to supply a categoryId which we will want Firebase to generate.
Next, we will create the user class. This will be similar to the Category class. We use all the same annotations. We use @Nullable
in a few more places. These are all values that we don't think are absolutely required.
Now, we will move on to our Post Collection. You will note that this is broken into three(3) classes: BasePost, Post, and RestPost. We are using this design pattern because we need to handle posts differently if we are querying it from the database versus creating a new post from the user provided JSON. The larger reason for this is due to our document references.
The BasePost class is an abstract class meaning we will never create an instance of it. It is the super class of the Post and RestPost classes. We put all the common elements of these classes in this file (ie, all the variables not of time DocumentReference).
You will not that we have also explicitly added four(4) methods.
@Data
annotation does not generate getters for boolean variables.Timestamp
is a class in the Firebase library. We use a static method fromProto
to convert the parsed date/time string. Note that this method throws a ParseException. We are choosing not to handle our exceptions in our service classes.The Post class is the POJO implementation of the Post Collection. This is the version of the class that we will use when retrieving a Post from the database. This is a serializable class (can be automatically converted to JSON). Note we do not use the @AllArgsConstructor
annotation rather we write our own verion in which we call our parent constructor.
The RestPost class is the implementation we will use when receiving data via an API call to create a new Post. Note the similarities and differences between the Post and RestPost classes. The main difference is the data type of authorId
and categoryId
. In this class, these are of type DocumentReference
which is the desired data type to be stored in Firebase.
We include our own set methods. These methods expect to receive the string version of the DocumentId being referenced. Because the data type must be a DocumentReference
, we use the provided string to query the database for the particular document reference. In the case of categoryId, it is an array, so we use a loop.
This is our first glimpse of writing a Firebase query. Let's take a look at the parts. First we have the code that gets you access to the database Firestore db = FirestoreClient.getFirestore();
.
FirestoreClient
provides access to Google Cloud Firestore. Use this API to obtain a Firestore instance, which provides methods for updating and querying data in Firestore.
Next we get the DocumentReference
. A DocumentReference
refers to a document location in a Firestore database and can be used to write, read, or listen to the location. The document at the referenced location may or may not exist.
The PostComment collection has similar requirements as the Post collection given the use of DocumentReferences. This is divided into three(3) classes: BasePostComment, PostComment, and RestPostComment.
As mentioned on the MVC explanation, this pattern does not include the business logic. For web-based applications the business logic is often referred to as a service. As such we will be write the logic of how data in handled in our service classes. These classes will contain the logic of retrieving and sending data between the application and Firebase, as well as any data manipulation required.
Service Components are the class file which contains @Service annotation. These class files are used to write business logic in a different layer, separated from @RestController class file.
Create a package under the root package named service
. This package should be on the same level as pacakge model. We will create a service for each of models. Create four (4) service classes CategoryService
, UserService
, PostService
, and PostCommentService
.
Please refer to the Firebase Documentation for help writing queries.
Our CategoryService will include a single method that allows us to get all the categories from the database. Given that we are expecting more than 1 result, we will use the Query
class. The Query class (and its subclass, DatabaseReference) are used for reading data. We will return an ArrayList of Category objects.
We want to return the categories in alphabetical order, so we order it by title orderBy("title", Query.Direction.ASCENDING)
We want to make our calls to the database asynchronous, meaning we don't wait for it to return the answe befor we start a new process. To this, we use the ApiFuture
class. A Future
represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation. Making the calls could cause an exception; we are simply re-throwing the exceptions. We will handle them in our controllers.
This particular Future request return a QuerySnapshot
. A QuerySnapshot
contains zero or more DocumentSnapshot objects representing the results of a query. The documents can be accessed as an array via the docs property or enumerated using the forEach method. The number of documents can be determined via the empty and size properties. A DocumentSnapshot
contains data read from a document in your Cloud Firestore database. The data can be extracted with the getData
or get
methods.
A QueryDocumentSnapshot
offers the same API surface as a DocumentSnapshot
. Since query results contain only existing documents, the exists property will always be true and data()
will never return 'undefined'.
We then iterate over the list of documents and convert each to being a Cateogry object using the toObject
method. Each is added to the ArrayList.
Next is our UserService. This service will include the ability to get all the users, get a specific user, create, update and delete a user.
When retrieving a single object, we use theDocumentReference
class we saw earlier. We pass the userId as a parameter to the document method. Notice the ApiFuture
in this instance as a type of DocumentSnapshot
rather than QueryDocumentSnapshot
When creating a user, we use the POJO User class. We set the registrerdAt by creating a current Timestamp
. We still make the call asynchronously using the add
method. Finally we return the new DocumentId that was generated.
The update method only allows the user to update specific fields. This is managed by the allowed
array. To process the update, we create a HashMap
of the key/value pairs we wish to updated. Once again we do this asynchronously. This is WriteResult
class. This class returns a timestamp of the update.
This version of the getUser
method accepts a DocumentReference
. It is for internal use of the program.
Next we will look at our PostService. This is our largest services. We will be able to get all posts, get a post by user id, get all posts in a category, get a post by the post id and create, update, and delete a post.
Our first method is private. This method is used by several of the methods below. Creating serializable Post object, requires running two(2) additional queries because of the document references. This method allows us not need to write repeatedly. Because of the nature of the Post, we cannot use the toObject
method we have previously, so we are creating the object using the constructor.
The first use of the private method is seen below in the getPosts
method.
This is our first query using a where clause. In this instance we want the records based on a particular user document reference. Because we are comparing document references, we need to first get a DocumentReference
of the specified user. Again, please refer to the Firebase Documentation for a full list of options.
We will use a similar where clause below, however, not with a DocumentReference
.
As was noted when we created our models, we use the RestPost
class when creating new posts. This allows us to simplify this method, as seen below.
This version of getPost
is used internally in other services. This allows for us to pass a DocumentReference
and fetch the Post
object. Note that is uses the private getPost
created at the start of the class.
Finally, we have the PostCommentService. This service allows you get all comments for a specific post, create a comment, and delete a comment.
Before we dive into the final part of our MVC, controllers, we will create a few utility classes. These are classes that we will use to create a standard format for our errors and API responses.
The ErrorMessge class defines the required pieces of error message our program will return.
It is a basic class that requires a message, a className , and a stackTrace.
This class is the basic format for all our API responses. We will use the ResponseEntity
class. ResponseEntity
represents the whole HTTP response: status code, headers, and body. As a result, we can use it to fully configure the HTTP response. ResponseEntity
is a generic type. Consequently, we can use any type as the response body. We are returning a Map (hash map) which can include any type of Java object. Remember, all classes in Java inherit from class Object.
Note that we do no include a no-argument constructor.
Our final utility is the WebConfig
class. This is going to allow us to send request from the same URL (aka CORS). We will learn more about this, and expand this class when we learn to secure our API. For now we will keep it simple.
Final step is to create our controllers. This is where we will put our resource (endpoint) definitions. Create a packagage called controller
. Create the following classes in that package CategoryController
, UserController
, PostController
and PostCommentController
.
Before we begin, in your application.properties
file, add the following:
These are our default values.
The CategoryController will make our CategoryService getCategoies
method available via the API. We will use a few annotations:
@RestController
indicates that the data returned by each method will be written straight into the response body instead of rendering a template.@RequestMapping
annotation for mapping web requests onto methods in request-handling classes with flexible method signatures.@Value
annotation is used to assign default values to variables and method arguments. We can read spring environment variables as well as system variables.@GetMapping
annotation for mapping HTTP GET requests onto specific handler methods.We create class level variables for our service, the HTTP status code, the name, payload, and the ResponseWrapper
class. Also the class name is a static and final variable.
Our getCategories
method is found at the base path of the resource URI. It returns a ResponseEntity
as previously introduced. Here you can see we are using try/catch statements. As mentioned in the model and service sections, we will handle potential exceptions here. As such, the catch statement will handle the occurance of any of these exceptions. If an exception occurs, an ErrorMessage
object is created and returned as the payload. If successful, the result of the query is returned as the payload, and status code is set to 200.
Our UserController will expose the UserServices methods. Note that the controller starts with similar variables and constructor as the previous controller. All of our controllers will look this way. We introduce a few new annotations:
@PathVariable
an be used to handle template variables in the request URI mapping, and set them as method parameters.@RequestBody
annotation maps the HttpRequest body to a transfer or domain object, enabling automatic deserialization of the inbound HttpRequest body onto a Java object.@PostMapping
annotation for mapping HTTP POST requests onto specific handler methods.@PutMapping
annotation for mapping HTTP PUT requests onto specific handler methods.This is also the first time we introduce use of path variable. Path variables, as you may recall, are values that we want to pass to our resouce (eg. user id). In the mapping annotation, the variable name is placed in curly braces to denote that is is a variable and not a string literal. In front of the corresponding method parameter use the @PathVariable
annotation including the name property.The name must match the value in the mapping. (See lines 34-35 for an example)
RequestBody
values are expected to be in JSON format.
The PostController exposes the PostService methods. We add another new annotation:
@DeleteMapping
annotation for mapping HTTP DELETE requests onto specific handler methods.Finally, the PostCommentController exposes the methods of the PostCommentService.
Open Postman and create a new request. In the request URL area enter http://localhost:8080/api/category/
. If you have a different value for the RequestMapping or the GetMapping in your CategoryController, enter what you have there.
If all works correctly, you should be seeing a json list of results from your database. I should look something like this:
You can add additional sort and filter values. We will alter the getPosts
method in the PostService and the asssociated controller method.
We will add the following abilities:
Here is a sample request URI including out sort and filter query parameters:
We will add a variable as a parameter for each option to the getPosts
method.
We will then add the code to sort or filter for each option. This should follow your Query query = db.collection("Post");
line.
Each time you apply a sort/limit/where clause, the method returns a new Query
object. Be sure to reassign your variable each time as seen above.
Now in your post controller, we need to receive these variables and pass them to the the service. We will receive them as query parameters. If you recall from above, a query parameter is added to end of a URL following a question mark(?) and is joined by ampersands (&).
In the above code we added a new annotation @RequestParam
. It is used to extract query parameters, form parameters, and even files from the request. In this example we are using to retrieve our query parameters. This annotations include several variables that can be set. We use the following:
Note that if a parameter is an ArrayList, the @RequestParam annotation will automatically convert comma separated lists into any List type include an array.
Next we edit the method call.
Now test that the filter and sort work by adding query parameters to your Postman call. You can enter them in the table that appears below the URL