본문 바로가기

Backend/Java

Spring Boot API

반응형

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

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>

Dependency Update Button

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 (h2database.com)

 

Commands

 

www.h2database.com

 

728x90
반응형

'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