An Introduction to Spring Boot: MySQL, JPA and Hibernate

An Introduction to Spring Boot: MySQL, JPA and Hibernate

Before we start, this post is mostly aimed at Java developers, specifically Java developers who use the Spring umbrella of frameworks for development. If you are not a Java developer, you can still read on, chances are you haven't found the love of your life - in terms of languages - yet.

First off, what really is Spring?

If you are a java developer, you've probably stumbled upon the term 'Spring' at some point in your journey, and possibly fell face first at either its learning curve, or its complexity. One thing most developers fail to understand - I also struggled with this at first - is that Spring represents a collection of frameworks that are tailored to meet specific development needs. As an example, if you're a java web developer, Spring provides the Web Servlet Framework for web development where Spring MVC (Included in this framework) is built on top of the Servlet API. Therefore, you need not learn all the frameworks that Spring provides, but rather the frameworks that fit your specific use case. Yeah, that's a shortcut, and yes, you're welcome.

If you've never heard about Spring before, Spring is an Inversion of Control and Dependency injection framework. This are fairly big terms but this comprehensive post will help you understand the meanings of these two concepts: IoC and Dependency Injection

Now onto Spring Boot

If you have used Spring MVC before, you've definitely have had to wrestle with Spring MVC's pre-configurations like Setting up the Dispatcher Servlet etc. etc. before you were able to get the framework up and running. This is where Spring Boot comes in. Spring Boot is an auto-configuration tool for setting up your Spring-powered applications. You can now put away those boxing gloves cause you might not need to wrestle with Spring Boot.

To help you understand Spring Boot further, and shine a light on why you should be using it if you already aren't, we'll build a simple Netflix API that allows client devices to register themselves, suggest movies and query movies.

Let us begin

Step 1: Setting up Spring Boot on your application.

Spring offers a project initializer, Spring Initialzr that allows you to select your project specifications and download an already configured Spring Boot project as a zip file or a maven build file. You could skip to step 2 if you have done this.

If you're a more of a hands on type of person who enjoys understanding what's happening under the hood, you can continue with this step.

Folder Structure.

Create a new Java project with you favourite IDE and configure your folder structure to mimic the following design:

└── src
	└── main
		└── controllers
		└── models
		└── repositories
		└── resources
			└── templates
				└── error.html
			└── application.properties
		└── Application.java

contollers - This folder will contain the controllers we define for this project

repositories - This folder will contain the repositories we'll define for our models that will be used to fetch data from the database.

resources - this folder will contain our project resources. The templates folder contains our template files that will be rendered by Spring. You can include other folders like static which will be used to server static content like javascript and css files.

Maven dependencies

Spring Boot allows us to include in our pom.xml file all the Spring dependencies that we'll use in our project. Copy paste the following dependencies, together with the Spring Boot Maven Plugin to your pom.xml.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-spring-boot</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <dependencies>
        <!--Spring dependencies-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
<!--Spring JPA -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
    </dependencies>

    <properties>
        <java.version>1.8</java.version>
    </properties>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Our dependencies overview:

1. spring-boot-starter-web - When building web applications using java, we often need other external dependencies that we include in our pom.xml like tomcat and Spring MVC. What spring-boot-starter-web does is add all these dependencies through one single dependency.

2. spring-boot-starter-thymeleaf - If you've never used thymeleaf before, thymeleaf is a templating engine for processing and creating HTML, XML, JavaScript, CSS, and text whose template files retain the .html extension and therefore a better alternative to JSPs (Java Server Pages). What this basically means is that you can run thymeleaf template files like normal web pages without a backend server for template processing as in the case of JSPs.

3. spring-boot-devtools - These tools grease your gears of development therefore making the overall development process more bearable. To learn more about what these tools offer, you can check out this link: spring-boot-devtools

4. mysql-connector-java - These is the MySQL JDBC implementation that we'll use to make connections to our MySQL database.

5. spring-boot-starter-data-jpa - Most if not all web applications need some form of persistence, which in java cases, is often JPA (Java Persistence API). If spring-boot-data-jpa is in the classpath, Spring boot will automatically configure our data-source through reading our database configuration from the application.properties file that we will configure next.

Note that we've set our java version to 1.8 since JDK 11 does not offer a lot of things out of the box and therefore you may run into errors like: springboot: org.hibernate.MappingException: Could not get constructor for org.hibernate.persister.entity.SingleTableEntityPersister

Application.properties file

Spring boot automatically reads configuration settings from this file and configures our spring boot environment accordingly. We'll configure our database here and also at the same time disable Spring boot's whitelabel error page which we'll replace with our own custom error page. You can copy paste all this into your own application.properties file if you do not intended to make any changes.

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url = jdbc:mysql://localhost:3306/netflix?useSSL=false
spring.datasource.username = netflix
spring.datasource.password = netflix


## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

#Disabling the whitelabel error page
server.error.whitelabel.enabled=false

In the above application.properties file, We've configured our database, username and password to netflix . You can configure this if you want to. Spring JPA automatically uses the Hibernate implementation of JPA. We've set spring.jpa.hibernate.ddl-auto to update which will ensure that any changes we make to our models will be reflected in our Database, which also includes creating a new model. Please note that this option is only suitable for development environments rather than production environments. For more information, you can check this link: Database Initialization. We've also set server.error.whitelabel.enabled to false to disable Spring boot's whitelabel error pages which we'll replace with our own custom error page.

Configuring our Application.java file

This file will contain the main method which we'll use to ignite our Spring Application with. Copy paste the following to your Application.java file:

package main;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableJpaRepositories(basePackages="main.repositories")
@EnableTransactionManagement
@EnableJpaAuditing
@EntityScan(basePackages={"main.entities","main.models"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

@SpringBootApplication is a combination of the following more specific spring annotations -

1. @Configuration : Any class annotated with @Configuration annotation is bootstrapped by Spring and is also considered as a source of other bean definitions.

2. @EnableAutoConfiguration : This annotation tells Spring to automatically configure your application based on the dependencies that you have added in the pom.xml file. For example, If spring-data-jpa or spring-jdbc is in the classpath, then it automatically tries to configure a DataSource by reading the database properties from application.properties file.

3. @ComponentScan : It tells Spring to scan and bootstrap other components defined in the current package (main) and all the sub-packages.

@EnableJpaAuditing is used to support the automatic filling of fields that we'll annotate with @CreatedDate.

@EnableJpaRepositories tells Spring where to find our defined Repositories, since we'll not be using the @Repository annotation.

Step 2. Coding our Controllers.

We'll create only 3 Contollers namely: CustomErrorController that we'll use to format and serve our custom error page, MoviesController that will perform movie related functions and UsersContoller that will perform user related functions.

CustomErrorController

In this controller, we'll register a route error that will be mapped to our renderErrorPage method. Therefore all requests made through the error route will be recieved by our method.

Note that here we'll use the @Controller annotation since we'd like to return a view rather than plain text and therefore our method returning a string will return the name of the view. To return plain text rather than views, use the @RestController annotation.

We will also format our error messages to make them more user friendly when we display them on our error page.

We've also implemented the ErrorController interface and overridden the getErrorPath() method which will automatically be invoked when Spring encounters an error.

@Controller
public class CustomErrorController implements ErrorController {


    @RequestMapping(value = "error",produces = "application/json;charset=UTF-8")
    public String renderErrorPage(HttpServletRequest request, Model model) {
         String errorMsg = "";
        Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        int httpErrorCode = 404;
        if(status != null){
            httpErrorCode = Integer.valueOf(status.toString());
        }
        switch (httpErrorCode) {
            case 400: {
                errorMsg = "Http Error Code: 400. Bad Request";
                break;
            }
            case 401: {
                errorMsg = "Http Error Code: 401. Unauthorized";
                break;
            }
            case 404: {
                errorMsg = "Http Error Code: 404. Resource not found";
                break;
            }
            case 500: {
                errorMsg = "Http Error Code: 500. Internal Server Error";
                break;
            }
        }
        model.addAttribute("error",errorMsg);
        return "error";
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }
}

MoviesController

As we have stated earlier, this Controller will store functionalities related to our movies. Since we are creating an api, we'll map api requests to url patterns that start with /api. Therefore, we add a @RequestMapping annotation on top of the class, rather than the method so that every request url we map on our methods will be appended to /api.

@RestController
@RequestMapping(value = "/api",produces = "application/json;charset=UTF-8") //All our api request URLs will start with /api and will return Json
public class MoviesController {

    private MoviesRepository moviesRepository;
    private CategoriesRepository categoriesRepository;
    private UserRepository userRepository;

    @Autowired
    public MoviesController(MoviesRepository moviesRepository, CategoriesRepository categoriesRepository, UserRepository userRepository){
        this.moviesRepository = moviesRepository;
        this.categoriesRepository = categoriesRepository;
        this.userRepository = userRepository;
    }

    //Suggest A movie
    @GetMapping(value = "/suggestMovie")
    public String suggestMovie(@RequestParam(name = "category_id") Long categoryId,@RequestParam(name = "name")String name
    ,@RequestParam(name = "suggested_by")Long suggestedBy){
        //Movies added through this API route are automatically marked as suggested.
        String movieType = Movies.MovieType.SUGGESTED.getMovieType();
        Movies movies = new Movies();

        //Provided category id should be in our categories table.
        if(categoriesRepository.findById(categoryId).isPresent()){

            if(userRepository.findById(suggestedBy).isPresent()){
                movies.setCategoryId(categoryId);
                movies.setName(name);
                movies.setType(movieType);
                movies.setSuggestedBy(suggestedBy);
                return moviesRepository.save(movies).toString();
            } else {
                return "{'error':'The specified user id does not exist.'}";
            }

        } else {
            return "{'error':'The specified category id does not exist.'}";
        }



    }

    //delete a suggested movie
    @GetMapping(value = "/deleteMovie")
    public String deleteMovie(@RequestParam(name = "movie_id") Long movieId,@RequestParam(name = "user_id")Long userId) {
        if(userRepository.findById(userId).isPresent()){
            Optional<Movies> movies = moviesRepository.findById(movieId);
            if(movies.isPresent()){
                List<Movies> movie = moviesRepository.findBySuggestedByEqualsAndIdEquals(userId,movieId);
                if(movie.size()>0){
                    moviesRepository.delete(movie.get(0));
                    return movie.toString();
                } else {
                    return generateErrorResponse("The user specified cannot delete this movie");
                }


            } else {
                return  generateErrorResponse("Specified movie id does not exist");
            }

        } else {
            return generateErrorResponse("Specified user id does not exist");
        }
    }

    //update a suggested movie. Supports only updating of the movie name or category.
    @GetMapping(value = "/updateMovie/{movie_id}")
    public String updateMovie(@PathVariable(name = "movie_id") Long movieId,@RequestParam(name = "user_id")Long userId,
                              @RequestParam(name = "movie_name",required = false)String movieName, @RequestParam(name = "movie_category",required = false) Long movieCategory) {
        List<Movies> movie = moviesRepository.findBySuggestedByEqualsAndIdEquals(userId,movieId);
        if(!(movie.size()>0)){
            return generateErrorResponse("The user specified cannot update this movie");
        }

        if(moviesRepository.findById(movieId).isPresent()){
            Movies movies = moviesRepository.findById(movieId).get();
            if(movieName != null && !movieName.isEmpty()){
                movies.setName(movieName);
            }
            if(movieCategory != null && categoriesRepository.findById(movieCategory).isPresent()){
                movies.setCategoryId(movieCategory);
            }

            return moviesRepository.save(movies).toString();
        } else {
            return generateErrorResponse("The specified movie id does not exist");
        }
    }

    //query available movies
    @GetMapping(value = "/queryMovies/{categoryId}")
    public String queryMovies(@PathVariable Long categoryId,@RequestParam(name = "type") String type){
        JsonObjectBuilder jsonResponse = Json.createObjectBuilder();
        JsonObjectBuilder temp = Json.createObjectBuilder();
        int count = 0;
        for(Movies movie:moviesRepository.findAllByCategoryIdEqualsAndTypeEquals(categoryId,type)){
            temp.add("id",movie.getId());
            temp.add("name",movie.getName());
            temp.add("type",movie.getType());
            temp.add("category_id",movie.getCategoryId());
            temp.add("created_at",movie.getCreatedAt().toString());
            jsonResponse.add(count + "",temp);
            temp = Json.createObjectBuilder();
            count++;
        }

        return jsonResponse.build().toString();
    }

    private String generateErrorResponse(String message){
        return "{\"error\":\"" + message + "\"";
    }

    //add categories
    @GetMapping(value = "/addCategories")
    public String addCategories(@RequestParam(name = "name") String name){
        Categories categories = new Categories();
        categories.setName(name);

        return categoriesRepository.save(categories).toString();
    }

}

In this Class, you may have noticed annotations that you might have not seen before. Let's go through them quickly:

1. @Autowired - As the annotation itself suggests, this annotation automatically injects an implementation of the movies, users and categories repository interface which we assign the the field variables we have declared. As we mentioned earlier, you need a repository to be able to access database contents, which explains these three repositories. I'll explain this further when we reach the repositories section.

2. @GetMapping - This annotation is the same as @RequestMapping except that it only maps get requests to the specified url.

3. @RequestParam - This annotation automatically injects the specified query parameter name to this variable.

4. @PathVariable` - This annotation automatically injects the path value - enclosed in curly braces - to this variable.

Users Controller

This controller will contain functionalities related to users. In this case, we'll define only a single method that will be responsible for creating a user.

@RestController
@RequestMapping(value = "/api",produces = "application/json;charset=UTF-8") //All our api request URLs will start with /api and return Json
public class UsersController {

    private UserRepository userRepository;

    @Autowired
    public UsersController(UserRepository userRepository){
        this.userRepository = userRepository;
    }


    @GetMapping(path = "/addUser")
        public String addUser(@RequestParam(name = "id")Long id, @RequestParam(name="name") String name) {
        Users users = new Users();
        users.setId(id);
        users.setName(name);

        users = userRepository.save(users);
        return users.toString();

    }


}

Our user IDs in this case will not be auto-generated but instead, we'll provide users with an option to define their own IDs.

Step 3. Defining our Repositories

Repositories will be used by our models to query data from the Database. spring-jpa comes with a JpaRepository interface that defines all CRUD operations that we can perform on an Entity. We'll use the CrudRepository implementation of JpaRespository as it offers many CRUD operations out of the box through methods like findAll(), save() etc. At the same time, CrudRepository automatically generated for us dynamic queries based on method names as we'll see in the following example.

We'll define three repositories for our three entities: CategoriesRepository , MoviesRepository and UsersRepository, which will all be interfaces extending CrudRepository.

###CategoriesRepository

public interface CategoriesRepository extends CrudRepository<Categories,Long> {
}

MoviesRepository

public interface MoviesRepository extends CrudRepository<Movies,Long> {

    List<Movies> findAllByCategoryIdEqualsAndTypeEquals(Long categoryId,String type);

    List<Movies> findBySuggestedByEqualsAndIdEquals(Long suggestedBy,Long movieId);
}

In this repository, notice the abstract methods we have defined. Extending CrudRepository will automatically compel Spring to create an implementation of these methods automatically at run-time just from the definition of the method name. To add Custom methods, we can add them in the following ways:

  1. We can start our query method names with find...By, read...By, query...By, count...By, and get...By. Before By we can add expression such as Distinct . After By we need to add property names of our entity.

  2. To get data on the basis of more than one property we can concatenate property names using And and Or while creating method names.

  3. If we want to use completely custom name for our method, we can use @Query annotation to write query.

UsersRepository

@Repository
public interface UserRepository extends CrudRepository<Users,Long> {

}

Final Step: Defining our models.

The models (Entities) that we define will be used to store our table structures as will be defined in the database. We will therefore have three models for our three tables: Categories , Movies and Users.

Categories Model

@Entity
@Table(name = "categories")
public class Categories {

    @Id
    @GeneratedValue
    private Long id;

    @NotBlank
    private String name;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        JsonObjectBuilder builder = Json.createObjectBuilder();

        //serialize to Json only if the data was persisted.
        if(!Objects.isNull(id)){
            builder.add("id",id);
        }
        if(!Objects.isNull(name)){
            builder.add("name",name);
        }

        return builder.build().toString();
    }


}

An entity is a plain old Java object (POJO) class that is mapped to the database and configured for usage through JPA using annotations and/or XML. Note that we've included a @Table annotation to explicitly define the name of our table. The @Id annotation automatically declares the created field as a primary key for our table in our database. At the same time, the @GeneratedValue annotation will automatically generate a value and store it in the database during saving of a record, pretty much like an auto-increment field. The @NotBlank annotation will automatically validate values that will be inserted into the name variable we defined and ensure that this field is not blank.

We've also defined our own toString method (overriding the superclass's toString method) that will convert our model to a Json string that we return as a response in our controllers.

Movies Model


@Entity
@Table(name = "movies")
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(value = {"createdAt"},allowGetters = true)
public class Movies implements Serializable {

    @Id
    @GeneratedValue
    private Long id;

    private Long categoryId;

    @NotBlank
    private String  type;

    @NotBlank
    private String name;

    private Long suggestedBy;

    @Column(nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @CreatedDate
    private Date createdAt; //Stores the date at which a user was created.

    @PrePersist
    public void prePersist(){
        createdAt = new Date();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getCategoryId() {
        return categoryId;
    }

    public void setCategoryId(Long categoryId) {
        this.categoryId = categoryId;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getCreatedAt() {
        return createdAt;
    }


    @Override
    public String toString() {
        JsonObjectBuilder builder = Json.createObjectBuilder();

        //serialize to Json only if the data was persisted.
        if(!Objects.isNull(id)){
            builder.add("id",id);
        }
        if(!Objects.isNull(name)){
            builder.add("name",name);
        }

        if(!Objects.isNull(categoryId)){
            builder.add("category_id",categoryId);
        }

        if(!Objects.isNull(createdAt)) {
            builder.add("created_at",createdAt.toString());
        }
        return builder.build().toString();
    }

    public Long getSuggestedBy() {
        return suggestedBy;
    }

    public void setSuggestedBy(Long suggestedBy) {
        this.suggestedBy = suggestedBy;
    }

    public enum MovieType{
        SUGGESTED("suggested"),ORIGINAL("original");

        private String movieType;

         MovieType(String movieType){
            this.movieType = movieType;
        }

        public String getMovieType() {
            return movieType;
        }

    }

}

In this model, note the annotations below: 1. @EntityListeners(AuditingEntityListener.class) - This will attach an entity listener to our model class that will automatically fill the fields we've annotated with @CreatedAt. 2. `@PrePersist - This annotation will ensure that the automatically generated value for the createdAt field is stored in this field whenever we'll need access. For more information on Database Auditing you can check this link: Database Auditing

Users Model

@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(value = {"createdAt"},
        allowGetters = true)
public class Users implements Serializable {
    private static final long serialVersionUID = 2L;

    @Column(updatable = false)
    @Id
    private Long id;

    @NotBlank(message = "The field 'name' is mandatory.")
    private String name;

    @Column(nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @CreatedDate
    private Date createdAt; //Stores the date at which a user was created.

    @PrePersist
    public void prePersist(){
        createdAt = new Date();
    }

    public void setId(long id) {
        this.id = id;
    }

    public long getId() {
        return id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }



    @Override
    public String toString() {
        JsonObjectBuilder builder = Json.createObjectBuilder();

        //serialize to Json only if the data was persisted.
        if(!Objects.isNull(id)){
            builder.add("id",id);
        }
        if(!Objects.isNull(name)){
            builder.add("name",name);
        }

        if(!Objects.isNull(createdAt)) {
            builder.add("created_at",createdAt.toString());
        }
        return builder.build().toString();
    }


    public Date getCreatedAt() {
        return createdAt;
    }

}

The Custom Error Page Template

In the templates folder we defined, create a html page and name it error.html and copy paste the following code into it:

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Error</title>
</head>
<body>
    <div>Web Application. Error : th:text="${error}"</div>
</body>
</html>

thymeleaf will automatically parse this html page and render our error message by replacing the th:text attribute.

Finally

Run your Application.java's main method and test out your netflix api on your browser by navigation to localhost:8080/. You should be able to see your json responses on your browser. Alternatively, you can check out my git repository for the source code and a client you can test your code with: github repo

Conclusion

You've successfully made a netflix api using Spring boot, mysql and JPA. Congrats! For more posts like this, you could check out my personal blog @ balysnotes.com