Spring Boot REST API Using JPA, Hibernate, MySQL Tutorial
- Chinthaka Dinadasa
- 14 Oct, 2020
In this article, I’m going to explain how we can build a REST API using JPA, Hibernate, and MySQL in Spring Boot.
Here JPA (Java Persistence API) is used to access, manage, and persist data between java and relational database while hibernate is doing the ORM(Object Relational Mapping) part.
So basically JPA is the dance, Hibernate is the dancer.
Here I’m going to use MySQL as the database, but with the same structure, you could move this to use with PostgreSQL just changing a few dependencies and configurations.
Technologies Going to Use,
-
Java 1.8
-
Spring Boot: 2.3.4.RELEASE
-
JPA
-
MySQL
-
Lombok
-
Gradle
-
Intellij Idea for IDE
Main topics I’m going to discuss here,
- Adding Required Dependencies
- Defining API Endpoints
- Developing the API
- Testing API
- Further Development
- Moving from MySQL to PostgreSQL in Spring Boot
- Conclusion
Adding Required Dependencies
spring initializr to generate a spring boot project with all the dependencies I need for this tutorial. If you are really new to Spring Boot, Please follow our article on How to Create a Spring Boot Project.
Here I’ve selected following dependencies to create spring boot project using spring initilizr,
- Spring Web – contains common web-specific utilities for both Servlet and Portlet environments.
- Spring Data JPA – JPA with Spring Data
- MySQL Driver – Driver for access MySQL based database
- Lombok – The coolest plugin to spicing up your java. Never write another getter or equals method again, with one annotation your class has a fully-featured builder, Automate your logging variables, and much more.
If you need to learn how we can use lombok in spring boot follow our article Guide to use Lombok In Spring Boot.
If you are using Gradle based project, the following dependencies should be present on your build.gradle.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
or else if you are using maven based project use following dependencies inside your project.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
Now we have our base project which is capable of develop as a REST API which allows end users to access MySQL database over the API.
Base project structure will be like,
Defining API Endpoints
I’m going to develop a simple API that covers CRUD Operations. For the requirement, I’ll use a library scenario where books and author data are stored and members can burrow any book if it is available.
This same API has developed using the DynamoDB and MongoDB databases as well. You can find those tutorials from,
- Spring Boot REST API CRUD With DynamoDB Tutorial.
- Spring Boot REST API Using Spring Data MongoDB Tutorial
So our main API endpoints will be as follow,
Endpoint | HTTP Method | Description |
---|---|---|
/api/library/book | GET | Read all books from database. |
/api/library/book?isbn=1919 | GET | Read book by ISBN |
****/api/library/book/:id | GET | Read book by ID |
****/api/library/book | POST | Register New Book |
****/api/library/book/:id | DELETE | Remove a book |
****/api/library/book/lend | POST | Lend a book to a member |
****/api/library/member | POST | Register member |
****/api/library/member/:id | PATCH | Update a member |
and the base architecture will be like below,
Developing the API
After we created the base project, now we are ready to start configurations then code the REST API using MySQL database. Let’s start with configure MySQL with Spring Boot.
Configure MySQL for Spring Boot Application
Here we need to introduce MySQL connection properties and other properties that needed to connect Spring Boot with MySQL. To do that we just need to add a few properties into the application.properties file.
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/java_to_dev_api_mysql
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
If you are using application.yml just add the following properties into that file, I suggest you use application.yml since its really clean and much easy to use since it allows reusability while defining properties.
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/java_to_dev_api_mysql
username: root
password: password
jpa:
hibernate:
ddl-auto: create
Spring Boot allows you to use a single type of configuration and application.properties are there by default. So if you need to change it to application.yml just remove the application.properties and add application.yml into the src/main/resource folder.
- spring.datasource.url – This is just the connection URL which includes your MySQL server host port and the database name. Just remember to create a database using MySQL server before running this application. otherwise, it will fail to load since the database is missing.
- spring.datasource.username – MySQL username
- spring.datasource.password – MySQL password
- spring.jpa.hibernate.ddl-auto – This is to influence how the schema tool management will manipulate the database schema at startup.
spring.jpa.hibernate.ddl-auto explanation
This property accepts following values and there are different actions get triggered for each value.
- create – This will create the schema with destroying all the previous data on application startup. Basically, it will create the schema as a fresh when every startup.
- create-drop – create a fresh schema and remove it when the application gets stopped. This value is used mostly when run tests for the application. So it will create the schema when starting and persist data when running the tests and get destroyed the whole schema when the test finished.
- update – This will attempt to add new columns, constraints and etc, but will never remove a column or a constraint that is previously added to the database and currently not present in the object model. Developers are most often use this value since it creates all the necessary database-oriented changes automatically while running the application.
- validate – This will only validate the changes but makes no changes to the database.
- none – Commonly used in production or not defining this property at all. That’s because at the production level we use migration scripts like Flyway to manage database migration using scripts.
Writing Model Classes
Author model with simple parameters set which include first name, last name.
package com.javatodev.api.model;
import java.util.Set;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "author")
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
}
Book model with name and isbn to register a book,
package com.javatodev.api.model;
import java.util.Set;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String isbn;
}
Member model with first name and last name and member status parameters to store library member details.
package com.javatodev.api.model;
public enum MemberStatus {
ACTIVE, DEACTIVATED
}
package com.javatodev.api.model;
import java.util.Set;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
@Enumerated(EnumType.STRING)
private MemberStatus status;
}
Lend model to keep track on books borrowed by members from the library with status, startOn and dueOn dates and LendStatus with Enum.
package com.javatodev.api.model;
public enum LendStatus {
AVAILABLE, BURROWED
}
package com.javatodev.api.model;
import java.time.Instant;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "lend")
public class Lend {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String status;
private Instant startOn;
private Instant dueOn;
@Enumerated(EnumType.ORDINAL)
private LendStatus status;
}
Defining Relationships Between Model Classes
One to Many mapping Between Author and Book,
add following into the Book.java,
@ManyToOne
@JoinColumn(name = "author_id")
@JsonManagedReference
private Author author;
and add following to Author.java
@JsonBackReference
@OneToMany(mappedBy = "author",fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Book> books;
One to Many mapping between Book and Lend,
Book.java
@JsonBackReference
@OneToMany(mappedBy = "book",fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Lend> lends;
Lend.java
@ManyToOne
@JoinColumn(name = "book_id")
@JsonManagedReference
private Book book;
One to Many mapping between Member and Lend,
Member.java
@JsonBackReference
@OneToMany(mappedBy = "member",fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Lend> lends;
Lend.java
@ManyToOne
@JoinColumn(name = "member_id")
@JsonManagedReference
private Member member;
All done now we have our relationship mappings and defined all the model classes for our small library application.
Let’s understand the meanings of these parameters which I’ve used in above implementation,
- @OneToMany – This is used to show the specific table has One to many relationships between the defined second table. Additionally, mappedBy should be equivalent to the variable added to the second table. Eg:- mappedBy=”member” and Lend table has member as the variable name.
- @ManyToOne – This is the opposite direction for the OneToMany Relationship.
- @JoinColumn – This is used in order to create a column that stores the parent side id in the child side table.
- cascade – There are multiple values applied here between ALL, PERSIST, MERGE, REMOVE, REFRESH and DETACH
- fetch – This allows to define fetch type between EAGER and LAZY
JPA Cascade Types
Here I’ll discuss the cascade types available in JPA.
Entity relationships often depend on the existence of another entity, for example, Book depends on the Author, There is no need to having a book without an Author, If an author gets deleted from the database, the book should be deleted from the database as well.
That’s where cascading helps us to manage these type of relationships. Basically if something happens for the target entity the same should be applied to the associated entity.
- ALL – This will add all operations happened to the target entity will be reflected on the associated entity.
- PERSIST – If we use this all the saves happened with the parent model will add a save to the child class.Basically, if we save only the Author then it automatically saves all the Books attached to the Author.
- MERGE – This allows us to share the persisted entity for newly created ones by sharing the identifier.
- REMOVE – Remove will remove the child while removing the parent.
- REFRESH – When we use this operation with CascadeType REFRESH, the child entity also gets reloaded from the database whenever the parent entity is refreshed.
- DETACH – When we use CascaseType.DETACH, the child entity will also get removed from the persistent context.
Using Enum in JPA Model Classes
Here I’ve used both @Enumerated(EnumType.STRING) and @Enumerated(EnumType.ORDINAL).
- EnumType.ORDINAL – If we use this for enums in JPA models, JPA will store the Ordinal value for the enum in the database. A problem with this kind of mapping arises when we need to modify our enum. If we add a new value in the middle or rearrange the enum’s order, we’ll break the existing data model. since ordinal numbered with the order of enum values, if we add one in the middle JPA will map wrong value to the enum while reading.
- EnumType.STRING – This store the string value of the enum in the database because of that we can change the order inside enum, But this will give error when we change the enum value which is not compatible with the values inside the database.
What are JsonBackReference and JsonManagedReference
In this example, we are going to face bidirectional relationships in Jackson. and It could cause Jackson JSON infinite recursion problem. So using both JsonBackReference and JsonManagedReference we can resolve that Jackson JSON infinite recursion problem while serializing.
If we don’t use these while defining the realtionships we could end up with dollowing error while reading from database.
nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError)
So we could get rid of that error using @JsonManagedReference the forward part of reference – the one that gets serialized normally, and @JsonBackReference is the back part of reference – it will be omitted from serialization.
There are few more ways to get rid of Jackson JSON infinite recursion problem too.
Defining Repository Layer
Here we are using JPARepository to build the data access layer using JPA. There are two more Repositories which we can use here like CRUDRepository and PagingAndSortingRepository. But JPARepository is a single Repo which included all the methods inside both CRUD and PagingSorting. So Let’s go with JPARepository.
AuthorRepository.java
package com.javatodev.api.repository;
import com.javatodev.api.model.Author;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AuthorRepository extends JpaRepository<Author, Long> {}
All three other Entities like Book, Lend and Member should have Repositories like above, I’m not going to paste those here since it’s the same way to follow as above.
JpaRepository – We need to introduce the Entity and the Data type of the ID param. Here it is Author and Long for the AuthorRepository.
Now we are completed defining our Repository layer with JpaRepository, Then I’m going to introduce Service layer where we do the business logic part in this application.
Adding Service Layer
We only have simple requirement around a simple library system, Hence full business logic could wrap around a single service, Let’s cal it as LibraryService.java. If you have any idea of having multiple services its upto you and free to use as you need.
LibraryService.java
package com.javatodev.api.service;
import com.javatodev.api.repository.AuthorRepository;
import com.javatodev.api.repository.BookRepository;
import com.javatodev.api.repository.LendRepository;
import com.javatodev.api.repository.MemberRepository;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class LibraryService {
private final AuthorRepository authorRepository;
private final MemberRepository memberRepository;
private final LendRepository lendRepository;
private final BookRepository bookRepository;
}
Here I’m using @RequiredArgsConstructor from Lombok to create constructor based dependency injection for repositories.
Basic business logics for LibraryService.java,
public Book readBook(Long id) {
Optional<Book> book = bookRepository.findById(id);
if (book.isPresent()) {
return book.get();
}
throw new EntityNotFoundException("Cant find any book under given ID");
}
public List<Book> readBooks() {
return bookRepository.findAll();
}
public Book readBook(String isbn) {
Optional<Book> book = bookRepository.findByIsbn(isbn);
if (book.isPresent()) {
return book.get();
}
throw new EntityNotFoundException("Cant find any book under given ISBN");
}
public Book createBook(BookCreationRequest book) {
Optional<Author> author = authorRepository.findById(book.getAuthorId());
if (!author.isPresent()) {
throw new EntityNotFoundException("Author Not Found");
}
Book bookToCreate = new Book();
BeanUtils.copyProperties(book, bookToCreate);
bookToCreate.setAuthor(author.get());
return bookRepository.save(bookToCreate);
}
public void deleteBook(Long id) {
bookRepository.deleteById(id);
}
public Member createMember(MemberCreationRequest request) {
Member member = new Member();
BeanUtils.copyProperties(request, member);
return memberRepository.save(member);
}
public Member updateMember (Long id, MemberCreationRequest request) {
Optional<Member> optionalMember = memberRepository.findById(id);
if (!optionalMember.isPresent()) {
throw new EntityNotFoundException("Member not present in the database");
}
Member member = optionalMember.get();
member.setLastName(request.getLastName());
member.setFirstName(request.getFirstName());
return memberRepository.save(member);
}
public Author createAuthor (AuthorCreationRequest request) {
Author author = new Author();
BeanUtils.copyProperties(request, author);
return authorRepository.save(author);
}
public List<String> lendABook (List<BookLendRequest> list) {
List<String> booksApprovedToBurrow = new ArrayList<>();
list.forEach(bookLendRequest -> {
Optional<Book> bookForId = bookRepository.findById(bookLendRequest.getBookId());
if (!bookForId.isPresent()) {
throw new EntityNotFoundException("Cant find any book under given ID");
}
Optional<Member> memberForId = memberRepository.findById(bookLendRequest.getMemberId());
if (!memberForId.isPresent()) {
throw new EntityNotFoundException("Member not present in the database");
}
Member member = memberForId.get();
if (member.getStatus() != MemberStatus.ACTIVE) {
throw new RuntimeException("User is not active to proceed a lending.");
}
Optional<Lend> burrowedBook = lendRepository.findByBookAndStatus(bookForId.get(), LendStatus.BURROWED);
if (!burrowedBook.isPresent()) {
booksApprovedToBurrow.add(bookForId.get().getName());
Lend lend = new Lend();
lend.setMember(memberForId.get());
lend.setBook(bookForId.get());
lend.setStatus(LendStatus.BURROWED);
lend.setStartOn(Instant.now());
lend.setDueOn(Instant.now().plus(30, ChronoUnit.DAYS));
lendRepository.save(lend);
}
});
return booksApprovedToBurrow;
}
Method definitions are as follows.
- readBookById(String id) – This method will read Book using given String ID. and Here I’ve used java.util.Optional to easily check availability after reading any data. This will return a Runtime exception if there is no data present for the given ID.
- readBooks() – Read all books stored in the database.
- createBook(BookCreationRequest book) – This method will bring data from the controller using BookCreationRequest and store those data in the database and return created book after successfully completed.
- deleteBook(String id) – Deletes a book by a given ID.
- createMember(MemberCreationRequest request) – This creates a member using membercreationrequest.
- updateMember (String id, MemberCreationRequest request) – Update the first name and last name of a member under a given ID.
- createAuthor (AuthorCreationRequest request) – Create an author using given author properties.
- lendABook (BookLendRequest request) – Create book lending on after reading book and member. Additionally, this allows lending multiple books for a single member. Additionally this stores
In these services, I’ve used a few more Request classes to map API request coming from controller to service. Additionally, I’m using @Data annotation in Lombok to generate Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode automatically.
AuthorCreationRequest.java
package com.javatodev.api.model.request;
import lombok.Data;
@Data
public class AuthorCreationRequest {
private String firstName;
private String lastName;
}
BookCreationRequest.java
package com.javatodev.api.model.request;
import lombok.Data;
@Data
public class BookCreationRequest {
private String name;
private String isbn;
private Long authorId;
}
BookLendRequest.java
package com.javatodev.api.model.request;
import lombok.Data;
@Data
public class BookLendRequest {
private Long bookId;
private Long memberId;
}
MemberCreationRequest.java
package com.javatodev.api.model.request;
import lombok.Data;
@Data
public class MemberCreationRequest {
private String firstName;
private String lastName;
}
Now we are ready with our business logic with seperate service layer. Let’s add the Rest Controllers to expose these methods as a REST API.
Controller Layer to Expose REST API Endpoints
Here we need to have multiple endpoints to support different aspects. Additionally ‘/api/library‘ is common for every endpoint which defined in this API contract.
Let’s start with adding a controller with @RequestMapping to ‘/api/library’,
package com.javatodev.api.controller;
import com.javatodev.api.service.LibraryService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping(value = "/api/library")
@RequiredArgsConstructor
public class LibraryController {
private final LibraryService libraryService;
}
So every and each endpoint we define inside this rest controller will have ‘/api/library’ as the prefix for the API URL and I’ll use @RequiredArgsConstructor from lombok to inject LibraryService into this controller.
Here as a standard we are not calling any repository directly from controller, every and each request is going through a service and coming back through the service too.
GET mapping for book findAll and find using ISBN will cover this endpoint. Here I’m using @RequestParam and it should be passed through the URL, and It’s an optional value. we can use @RequestParam to get optional parameters like this in Spring Boot REST API. Additionally, I’m using ResponseEntity without passing a type, hence I could return both List
@GetMapping("/book")
public ResponseEntity readBooks(@RequestParam(required = false) String isbn) {
if (isbn == null) {
return ResponseEntity.ok(libraryService.readBooks());
}
return ResponseEntity.ok(libraryService.readBook(isbn));
}
GET mapping to read a book by ID. The ID is passing as a path variable, hence the URL will add the ID in the end, be careful to add the same name in both URL and the variable you are going to use a path variable here.
@GetMapping("/book/{bookId}")
public ResponseEntity<Book> readBook (@PathVariable Long bookId) {
return ResponseEntity.ok(libraryService.readBook(bookId));
}
POST mapping for creating a book. In this endpoint the API accepts a request body, so always it’s defined as an application/json by default in spring boot.
@PostMapping("/book")
public ResponseEntity<Book> createBook (@RequestBody BookCreationRequest request) {
return ResponseEntity.ok(libraryService.createBook(request));
}
DELETE API for delete book by ID. Here same as read by ID we can pass an identification to identify correct value from DB to DELETE.
@DeleteMapping("/book/{bookId}")
public ResponseEntity<Void> deleteBook (@PathVariable Long bookId) {
libraryService.deleteBook(bookId);
return ResponseEntity.ok().build();
}
POST API for create a member
@PostMapping("/member")
public ResponseEntity<Member> createMember (@RequestBody MemberCreationRequest request) {
return ResponseEntity.ok(libraryService.createMember(request));
}
PATCH mapping for update created member. Here we need to have both member id and update request, So we can pass the ID through URL and the updates through the body.
@PatchMapping("/member/{memberId}")
public ResponseEntity<Member> updateMember (@RequestBody MemberCreationRequest request, @PathVariable Long memberId) {
return ResponseEntity.ok(libraryService.updateMember(memberId, request));
}
POST API to do a lending for a book.
@PostMapping("/book/lend")
public ResponseEntity<List<String>> lendABook(@RequestBody BookLendRequest bookLendRequests) {
return ResponseEntity.ok(libraryService.lendABook(bookLendRequests));
}
POST API to create author.
@PostMapping("/author")
public ResponseEntity<Author> createAuthor (@RequestBody AuthorCreationRequest request) {
return ResponseEntity.ok(libraryService.createAuthor(request));
}
Final project structure,
Testing API
Here I’m using Postman to test our API built using MySQL and Spring Boot. Additionally, I’ll share the postman collection with the codebase. So you can import it to your postman application and test the API.
Further Development
Here I have only developed a basic API with connecting Spring Boot and MySQL database using JPA and Hibernate. In addition to that, you can add a global exception handling using our article on Exception Handling Spring Boot REST API to capture and return proper error messages from the API.
Moving from MySQL to PostgreSQL in Spring Boot
If you need to use this same API with PostgreSQL, You just need to add PostgreSQL dependency into the project and set database connection properties to connect with your PostgreSQL server.
Dependency for PostgreSQL,
implementation 'org.postgresql:postgresql:42.2.17'
Database properties for connetion,
spring.datasource.url=jdbc:postgresql://localhost:5432/java_to_dev_api_mysql
spring.datasource.username=postgres
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver
Conclusion
All done, Now I hope you have a good understanding of how to use MySQL database with Spring Boot REST API development. Comment your ideas or issues you are facing while developing your Spring boot API.
You can find source codes for this tutorial from our Github.