In this posting, we will see the structure of the Spring Boot application and how to make the API server using Spring Boot. for CRUD operations. We will also see how we can persist data using the H2 database
List of Contents
- Spring Boot Structure
- Dependencies
- Setting Up the Project Structure
- Implementation
- Adding Database(H2)
- Exceptions
Spring Boot Structure
To begin with, here is the visual presentation of the Spring Boot application structure. There are three layers for handling different actions
API data transmission is done using HTTP and among many data formats for sending the data, JSON format is the most popular. For Spring to work with the JSON data, a JSON format must be converted to the matching JAVA POJO class. Converting between the two is automatically done by a program called Jackson using getters and setters in the POJO class. It uses the setters to convert the request JSON data to POJO and getters for the other way around
Dependencies
Add the codes below to the pom.xml file and click the reload icon on the top right side (refer to the picture below).
Spring-web
Handles the HTTP request and runs the web server
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
H2
In memory database dependency
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Maps an entity into a table in the database
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Setting Up the Project Structure
In the main project folder, add the controller, entities, and service packages with the classes under each package.
Implementation
Entity Class
The entity class specifies the fields to handle data. Each filed name should match that of a table and should have Getter, and Setter to get allow access to them
package com.example.employee.entity;
public class Employee {
private int employeeId;
private String employeeName;
private String employeeCity;
public Employee(int employeeId, String employeeName, String employeeCity) {
this.employeeId = employeeId;
this.employeeName = employeeName;
this.employeeCity = employeeCity;
}
public int getEmployeeId() {
return employeeId;
}
public void setEmployeeId(int employeeId) {
this.employeeId = employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public String getEmployeeCity() {
return employeeCity;
}
public void setEmployeeCity(String employeeCity) {
this.employeeCity = employeeCity;
}
}
Annotations
Code marked with an annotation can use the functions that the annotation has.
Annotations in the Controller Class
▶ @Controller
Makes a class a controller
@Controller
▶ @ResponseBody
Used with the controller annotation to transmit the data through the HTTP body.
@ResponseBody
▶ @RestController
Combination of the @Controller and @ResponseBody
@RestController
▶ @Autowired
Used to inject the service class dependencies.
@Autowired
Annotations in the Service Class
▶ @Service
Makes a class a service
@Service
Annotations in the Entity Class
▶ @Entity
Used to create JPA class table
@Entity
▶ @Table
By default, the JPA class table uses the class name as its name. We can change the default name with this annotation
@Table(name="<name>")
▶ @Id
Sets the primary key in the relational database
@Id
▶ @GeneratedValue
An option to automatically create the primary key
@GeneratedValue(strategy = GenerationType.IDENTITY)
▶ @OneToOne
1 to 1 relationship annotation
@OneToOne
▶ @OneTo Many
1 to many relationship annotations
@OneToMany
▶ @ManyTo One
Many to 1 relationship annotation
@ManyToOne
▶ @ManyToMany
Many to many relationship annotation
@ManyToMany
▶ @JoinColumn
By default, the field name uses the property name as its name. We can change the default name with this annotation.
@JoinColumn(name = "<fk_spouse>")
▶ @JoinTable
An option to change the field names that connect two tables in the many-to-many relationship
@JoinTable(name = "<tableName>", joinColumns = @JoinColumn(name = "<fieldName>"),
inverseJoinColumns = @JoinColumn(name = "<fieldName>"))
▶ @JsonIgnore
Prevents the infinite loop when creating a relationship that has a many relationship on any side
@JsonIgnore
Service Class
The service layer takes requests from the presentation layer and passes the requests to the data access layer to get or update data and sends back the data to the presentation layer when it receives the data from the data access layer.
▶ POST (For this example, data are created in the service class)
List<Employee> employeeList = new ArrayList<>(Arrays.asList(
new Employee(1, "John", "Washington"),
new Employee(2, "Tom", "Waterloo")
));
▶ GET (Item list)
public List<Employee> getEmployees() {
return employeeList;
}
▶ GET (An item)
public Employee getEmployee(int employeeId) {
return employeeList.stream().filter(e -> (
e.getEmployeeId() == employeeId
)).findFirst().get();
}
▶ POST
public void addEmployee(Employee employee) {
employeeList.add(employee);
}
▶ PUT
public void updateEmployee(Employee employee) {
List<Employee> tempList = new ArrayList<>();
for (Employee e: employeeList) {
if(e.getEmployeeId() == employee.getEmployeeId()) {
e.setEmployeeName(employee.getEmployeeName());
e.setEmployeeCity(employee.getEmployeeCity());
}
tempList.add(e);
}
this.employeeList = tempList;
}
▶ DELETE
public void deleteEmployee(int id) {
List<Employee> tempList = new ArrayList<>();
for (Employee e: employeeList) {
if(e.getEmployeeId() == id) {
continue;
}
tempList.add(e);
}
this.employeeList = tempList;
}
▶ Completed Code
package com.example.employee.service;
import com.example.employee.entity.Employee;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class EmployeeService {
List<Employee> employeeList = new ArrayList<>(Arrays.asList(
new Employee(1, "John", "Washington"),
new Employee(2, "Tom", "Waterloo")
));
public List<Employee> getEmployees() {
return employeeList;
}
public Employee getEmployee(int employeeId) {
return employeeList.stream().filter(e -> (
e.getEmployeeId() == employeeId
)).findFirst().get();
}
public void addEmployee(Employee employee) {
employeeList.add(employee);
}
public void updateEmployee(Employee employee) {
List<Employee> tempList = new ArrayList<>();
for (Employee e: employeeList) {
if(e.getEmployeeId() == employee.getEmployeeId()) {
e.setEmployeeName(employee.getEmployeeName());
e.setEmployeeCity(employee.getEmployeeCity());
}
tempList.add(e);
}
this.employeeList = tempList;
}
public void deleteEmployee(int id) {
List<Employee> tempList = new ArrayList<>();
for (Employee e: employeeList) {
if(e.getEmployeeId() == id) {
continue;
}
tempList.add(e);
}
this.employeeList = tempList;
}
}
Controller Class
The controller resides in the presentation layer. It creates endpoints sends a request to the service layer and sends a response to the front when the data is processed.
▶ GET (Item list)
@RequestMapping("/employees")
public List<Employee> getEmployees() {
return employeeService.getEmployees();
}
Or
@GetMapping("/employees")
public List<Employee> getEmployees() {
return employeeService.getEmployees();
}
▶ GET (An item)
@RequestMapping("/employees/{id}")
public Employee getEmployee(@PathVariable int id) {
return employeeService.getEmployee(id);
}
Or
@GetMapping("/employees/{id}")
public Employee getEmployee(@PathVariable int id) {
return employeeService.getEmployee(id);
}
▶ POST
@RequestMapping(value = "/employees", method = RequestMethod.POST)
public void addEmployee(@RequestBody Employee employee) {
employeeService.addEmployee(employee);
}
Or
@PostMapping("/employees")
public void addEmployee(@RequestBody Employee employee) {
employeeService.addEmployee(employee);
}
▶ PUT
@RequestMapping(value = "/employees/{id}", method = RequestMethod.PUT)
public void updateEmployee(@PathVariable int id, @RequestBody Employee employee) {
employeeService.updateEmployee(employee);
}
Or
@PutMapping("/employees/{id}")
public void updateEmployee(@PathVariable int id, @RequestBody Employee employee) {
employeeService.updateEmployee(employee);
}
▶ DELETE
@RequestMapping(value = "/employees/{id}", method = RequestMethod.DELETE)
public void deleteEmployee(@PathVariable int id) {
employeeService.deleteEmployee(id);
}
Or
@DeleteMapping("/employees/{id}")
public void deleteEmployee(@PathVariable int id) {
employeeService.deleteEmployee(id);
}
▶ Completed Code
package com.example.employee.controller;
import com.example.employee.entity.Employee;
import com.example.employee.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@ResponseBody
//@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@RequestMapping("/employees")
//@GetMapping("/employees")
public List<Employee> getEmployees() {
return employeeService.getEmployees();
}
@RequestMapping("/employees/{id}")
//@GetMapping("/employees/{id}")
public Employee getEmployee(@PathVariable int id) {
return employeeService.getEmployee(id);
}
@RequestMapping(value = "/employees", method = RequestMethod.POST)
//@PostMapping("/employees")
public void addEmployee(@RequestBody Employee employee) {
employeeService.addEmployee(employee);
}
@RequestMapping(value = "/employees/{id}", method = RequestMethod.PUT)
//@PutMapping("/employees/{id}")
public void updateEmployee(@PathVariable int id, @RequestBody Employee employee) {
employeeService.updateEmployee(employee);
}
@RequestMapping(value = "/employees/{id}", method = RequestMethod.DELETE)
//@DeleteMapping("/employees/{id}")
public void deleteEmployee(@PathVariable int id) {
employeeService.deleteEmployee(id);
}
}
Adding Database(H2)
Property Setup
Open the application.properties and add the code shown below.
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:<dbName>
spring.datasource.username=<name>
spring.datasource.password=<password>
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
※ Use these to show the SQL command in the console and add a format
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Adding Dependencies (Refer to the dependency section above)
Adding Entity Annotaion
For JPA to automatically create a table from the entity, we need the @Entity, and @Id annotations along with a constructor without parameters as shown below.
package com.example.testemployee.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int employeeId;
private String employeeName;
private String employeeCity;
public Employee() {
}
public Employee(int employeeId, String employeeName, String employeeCity) {
this.employeeId = employeeId;
this.employeeName = employeeName;
this.employeeCity = employeeCity;
}
public int getEmployeeId() {
return employeeId;
}
public void setEmployeeId(int employeeId) {
this.employeeId = employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public String getEmployeeCity() {
return employeeCity;
}
public void setEmployeeCity(String employeeCity) {
this.employeeCity = employeeCity;
}
}
Connection Test
Run the app and open a browser with the below address
http://localhost:8080/h2-console
Type in the same URL, User Name, and Password as the ones you used in the application.properties
Adding a Repository Class
Create a repository package and add an interface and extend the JPA repository
package com.example.employee.repository;
import com.example.employee.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}
Updating the Service Class
Add the repository class in the service class and tag the @Autowired annotation. Change the code inside each method as shown below
package com.example.employee.service;
import com.example.employee.entity.Employee;
import com.example.employee.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class EmployeeService {
@Autowired
EmployeeRepository employeeRepository;
public List<Employee> getEmployees() {
return employeeRepository.findAll();
}
public Employee getEmployee(int employeeId) {
return employeeRepository.findById(employeeId).orElseThrow(() -> new RuntimeException("Not found"));
}
public void addEmployee(Employee employee) {
employeeRepository.save(employee);
}
public void updateEmployee(Employee employee) {
/*
* jpa only has the save command.
* If the ID sent exists in the DB,
* it will be updated otherwise new item will be saved
*/
employeeRepository.save(employee);
}
public void deleteEmployee(int id) {
employeeRepository.delete(employeeRepository.getReferenceById(id));
}
}
Cascade
The cascade is an option to manage the consistency of data between the parent and the children. It has three options
▶ ALL
All the operations from the the parent run on the child (Create, Delete, Update,...)
@OneToOne(cascade = CascadeType.ALL)
▶ PERSIST
Only the create operation from the parent can run on the child
@OneToOne(cascade = CascadeType.PERSIST)
▶ REMOVE
Only the delete operation from the parent can run on the child
@OneToOne(cascade = CascadeType.REMOVE)
▶ REFRESH
Only the refresh operation from the parent can run on the child
@OneToOne(cascade = CascadeType.REFRESH)
▶ DETACH
Only the detach operation from the parent can run on the child
@OneToOne(cascade = CascadeType.DETACH)
▶ MERGE
Only the merge operation from the parent can run on the child
@OneToOne(cascade = CascadeType.MERGE)
※ Use the curly braces to add multiple options
@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
Fetch Types
Fetch types are used to decide whether to fetch the data from a child entity when its parent is fetched. It has two options
▶ EAGER
Fetches the data from a child when the parent is fetched (Default for one-to-one, many-to-one relationship)
@OneToMany(fetch = FetchType.EAGER)
▶ LAZY
Do not fetch the data from a child when the parent is fetched ( Default for one-to-many, many-to-many )
@OneToOne(FetchType.LAZY)
Adding Relationships
Let's see how we can add relationships
▶ 1 to 1 relationship
Add a child entity and add constructors and properties. Add @OneToOne annotation with the 'mappedBy' option to connect with the parent entity (Without this option it will be a unidirectional relationship meaning only the parent can fetch the data from the child). Add Getters and Setters for all the properties
@OneToOne(mappedBy = "spouse")
private Employee employee;
Open the primary entity and add the child as a property and put @OneToOne annotation. Use the 'cascade' option to maintain the consistency of the data (@JoinColumn is for changing the filed name). Add Getters and Setters for the property
When a bi-direction relationship is added, we can add, delete, and so on the primary entity using the child
@OneToOne(mappedBy = "spouse", cascade = CascadeType.ALL)
private Employee employee;
If you want to keep the primary data while deleting the child, exclude the cascade delete
@OneToOne(mappedBy = "employeeDetail", cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
private Employee employee;
In the delete method break the link between them by setting null value
public void deleteEmployeeDetail(int id) {
EmployeeDetail employeeDetail = entityManager.find(EmployeeDetail.class, id);
employeeDetail.getEmployee().setEmployeeDetail(null);
entityManager.remove(employeeDetail);
}
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "fk_spouse")
private Spouse spouse;
▶ Many to 1 relationship
Add a child entity and add constructors and properties. Add @ManyToOne annotation to connect with the parent entity. Add Getters and Setters for all the properties. For a relationship that one or both side has many relationship, add @JsonIgnore annotation to prevent the infinite loop
Add a constructor that includes the added field
▶ 1 to Many relationship
On the 1 side add a list of the many side filed with @OneToMany annotation and add a cascade option. Add Getters and Setters for the property
@OneToMany(cascade = CascadeType.ALL)
private List<Address> addresses;
Update the service class to add many side data to the 1 side
package com.example.employee.service;
import com.example.employee.entity.Address;
import com.example.employee.entity.Employee;
import com.example.employee.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class EmployeeService {
@Autowired
EmployeeRepository employeeRepository;
public List<Employee> getEmployees() {
return employeeRepository.findAll();
}
public Employee getEmployee(int employeeId) {
return employeeRepository.findById(employeeId).orElseThrow(() -> new RuntimeException("Not found"));
}
//updated
public void addEmployee(Employee employee) {
ArrayList<Address> addresses = new ArrayList<>();
for (Address address : employee.getAddresses()) {
addresses.add(new Address(
address.getZip(),
address.getCity(),
address.getState(),
address.getCountry()
));
}
employee.setAddresses(addresses);
employeeRepository.save(employee);
}
public void updateEmployee(Employee employee) {
employeeRepository.save(employee);
}
public void deleteEmployee(int id) {
employeeRepository.delete(employeeRepository.getReferenceById(id));
}
}
Or use the primary entity to update both
@OneToMany(mappedBy = "employee", cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
private List<Project> projects;
public void add(Project project) {
if (project == null) {
projects = new ArrayList<>();
}
projects.add(project);
project.setEmployee(this);
}
▶ Many to Many relationship
Add a child entity and add constructors and properties. Add @ManyToMany annotation with the 'mappedBy' option to connect with the parent entity (Without this option it will be a unidirectional relationship meaning only the parent can fetch the data from the child). Add Getters and Setters for all the properties
Open the primary entity and add the child as a property in a list and put @ManyToMany. Use the 'cascade' option to maintain the consistency of the data (@JoinColumn is for changing the filed name). Add Getters and Setters for the property
@ManyToMany(cascade = CascadeType.ALL)
private List<Project> projects;
※ Use this option to change the filed names
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "employee_project", joinColumns = @JoinColumn(name = "fk_employee"),
inverseJoinColumns = @JoinColumn(name = "fk_project"))
private List<Project> projects;
For the many-to-many relationship, we need an additional method to update both tables. Add the code shown below in the primary entity
public void removeProject(Project project) {
this.projects.remove(project);
project.getEmployee().remove(project);
}
public void addProject(Project project) {
this.projects.add(project);
project.getEmployee().add(this);
}
Exceptions
When exceptions are thrown, the messages are sent in HTML format by default. This reduces readability and usability so let's see how we can change the exception message to a JSON format response
Annotations
▶ @ExceptionHandler
Handles errors
@ExceptionHandler
▶ @ControllerAdvice
Catches global exceptions
@ControllerAdvice
Entities
Create a POJO that will hold the error information
▶ Code
package com.example.employee.common;
public class ErrorRes {
private int status;
private String message;
private long timestamp;
public ErrorRes() {
}
public ErrorRes(int status, String message, long timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
Exception Handler Class
Create an exception handler class and extend the 'RuntimeException'
Add constructors
▶ Code
package com.example.employee.common;
public class NotFound extends RuntimeException {
public NotFound(String message) {
super(message);
}
public NotFound(String message, Throwable cause) {
super(message, cause);
}
public NotFound(Throwable cause) {
super(cause);
}
}
Add the below error handling methods to the controller
@ExceptionHandler
public ResponseEntity<ErrorRes> handleError(NotFound ex) {
ErrorRes err = new ErrorRes();
err.setStatus(HttpStatus.NOT_FOUND.value());
err.setMessage(ex.getMessage());
err.setTimestamp(System.currentTimeMillis());
return new ResponseEntity<>(err, HttpStatus.NOT_FOUND);
}
@ExceptionHandler
public ResponseEntity<ErrorRes> handleException(Exception ex) {
ErrorRes err = new ErrorRes();
err.setStatus(HttpStatus.BAD_REQUEST.value());
err.setMessage(ex.getMessage());
err.setTimestamp(System.currentTimeMillis());
return new ResponseEntity<>(err, HttpStatus.BAD_REQUEST);
}
Global Exception Handling
Adding error handling methods to a controller as shown above is cumbersome because the code only covers that particular controller on which the methods are added. Using the @ControllerAdvice annotation we can make a global error handler
Create a class and add @ControllerAdvice annotation. Move the exception handler methods to this class
▶ Code
package com.example.employee.common;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public ResponseEntity<ErrorRes> handleError(NotFound ex) {
ErrorRes err = new ErrorRes();
err.setStatus(HttpStatus.NOT_FOUND.value());
err.setMessage(ex.getMessage());
err.setTimestamp(System.currentTimeMillis());
return new ResponseEntity<>(err, HttpStatus.NOT_FOUND);
}
@ExceptionHandler
public ResponseEntity<ErrorRes> handleException(Exception ex) {
ErrorRes err = new ErrorRes();
err.setStatus(HttpStatus.BAD_REQUEST.value());
err.setMessage(ex.getMessage());
err.setTimestamp(System.currentTimeMillis());
return new ResponseEntity<>(err, HttpStatus.BAD_REQUEST);
}
}
In this writing, we had a chance to create a web API using the Spring Boot and to add the H2 database and I hope you enjoyed it.
References
Commands
www.h2database.com
'Backend > Java' 카테고리의 다른 글
Spring Boot Security (1) | 2023.10.01 |
---|---|
Spring Boot Actuator (0) | 2023.10.01 |
Data Persistency Tools (JPA, Hybernate, Mybatis) (3) | 2023.09.09 |
Spring Boot View Template Tools (0) | 2023.09.02 |
Creating a Spring Boot Project (1) | 2023.08.05 |