Post

Udemy Spring boot course: Section 4 REST Crud Api

Specialized Component annotations

  • alt-text
  • Spring has provided a few specialized stereotype annotations: @Controller, @Service and @Repository. They all provide the same function as @Component.
  • They all act the same because they are all composed annotations with @Component as a meta-annotation for each of them. They are like @Component aliases with specialized uses and meaning outside Spring auto-detection or dependency injection.

Github repo for this section

Github repo

References

@RestController, @RequestMapping & @GetMapping

  • @RestController tells spring it is a rest controller class which will hold the endpoints in the class
  • @RequestMapping tells spring the route for all of the sub method which will be under it
  • @GetMapping tells spring this method is a GET endpoint

Sample endpoint /test/hi

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/test")
public class MyControllerClass {

  @GetMapping("/hi")
  public String hiMethod(){
    return "you've reached the /test/hi endpoint";
  }

}

Why have a service layer

  • The service layer is used so you can use all of the needed dao to implment some business logic
  • alt-text

Best Practices

Best Practice to apply transactional boundaries at the service layer

  • it is the service layer’s responsibility to manage transaction boundaries
  • The @Transactional annotation should be in the service and not the dao

Java JSON Data binding

  • alt-text
  • java json binding is the process of converting JSON data to a java POJO and vice versa
  • AKA
    • Mapping
    • Serialization / Deserialization
    • Marshalling / Unmarshalling
  • Spring uses the Jackson Project behind the scenes
    • Convert JSON to POJO –> jackson calls setter methods
    • Convert POJO to JSON –> jackson calls getter methods
  • By default Jackson will call the appropiate getter / setter methods
  • Java POJO must have getter and setter methods

@PathVariable

  • The get mapping will have a variable placed between curly braces{}
  • @PathVariable is placed before the parameters
  • the variable in the {} and specified in the parameters MUST BE IDENTICAL
1
2
3
4
    @GetMapping("/student/{studentId}")
    public Student getStudent(@PathVariable int studentId){
      // implmenetation
    }

Exception handling

Controller specific

@ExceptionHandler - goes above the exception handler method - Controller specific - returns a ResonseEntity which is just a http wrapped response

POJO what will be converted to JSON when returned to the caller

1
2
3
4
5
6
7
8
public class StudentErrorReponse {
    private int status;
    private String message;
    private long timeStamp;

    // constructors
    // getters and setters
}

Custom Exception

1
2
3
4
5
6
7
public class StudentNotFoundException extends RuntimeException{

    public StudentNotFoundException(String message) {
        super(message);
    }
    // other constructors
}

Controller class where the top endpoint throws an exception if an invalid studentId is passed. The second method is the @ExceptionHandler where if any StudentNotFoundExceptions are thrown it will handle it according to the method body. The last method is to handle generic exceptions that are thrown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@GetMapping("/student/{studentId}")
public Student getStudent(@PathVariable int studentId){
    if(studentId < 0 || studentId >= students.size()){
        throw new StudentNotFoundException("Student with id: " +studentId + " was not found");
    }
    return students.get(studentId);
}


// StudentNotFoundExceptions
@ExceptionHandler
public ResponseEntity<StudentErrorReponse> handleException(StudentNotFoundException studentNotFoundException){
    StudentErrorReponse studentErrorReponse = new StudentErrorReponse(
            404,
            studentNotFoundException.getMessage(),
            System.currentTimeMillis());

    return new ResponseEntity<>(studentErrorReponse, HttpStatus.NOT_FOUND);
}

// Generic exceptions
@ExceptionHandler ResponseEntity<StudentErrorReponse> genericException(Exception exception){
    StudentErrorReponse studentErrorReponse = new StudentErrorReponse(
            HttpStatus.I_AM_A_TEAPOT.value(),
            "something bad generic happened",
            System.currentTimeMillis()
    );
    return new ResponseEntity<>(studentErrorReponse, HttpStatus.I_AM_A_TEAPOT);
}

Global handling

  • the controller specific exception handlers are great but not able to be used outside of that specific class

@ControllerAdvice

  • goes above a class declaration
  • like a interceptor / filter
  • pre-processes requests to controllers
  • post-process responses to handle exceptions
  • perfect for global handling
  • No other configuration is needed to tell the controllers to use these exception handlers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ControllerAdvice
public class StudentRestExceptionHandler {

    // StudentNotFound exceptions
    @ExceptionHandler
    public ResponseEntity<StudentErrorReponse> handleException(StudentNotFoundException studentNotFoundException){
        StudentErrorReponse studentErrorReponse = new StudentErrorReponse(
                404,
                studentNotFoundException.getMessage(),
                System.currentTimeMillis());

        return new ResponseEntity<>(studentErrorReponse, HttpStatus.NOT_FOUND);
    }

    // Generic exceptions
    @ExceptionHandler
    public ResponseEntity<StudentErrorReponse> genericException(Exception exception){
        StudentErrorReponse studentErrorReponse = new StudentErrorReponse(
                HttpStatus.I_AM_A_TEAPOT.value(),
                exception.getMessage(),
                System.currentTimeMillis()
        );
        return new ResponseEntity<>(studentErrorReponse, HttpStatus.I_AM_A_TEAPOT);
    }
}

Employee Example API

Requirements

  • Create a REST Api for employee directory
  • Rest clients should be able to
    • get a list of employees
    • get a single employee
    • add a new employee
    • update an employee
    • delete an employee

Endpoints

HTTP MethodEndpointDescription
GET/api/employeesget all employees
GET/api/employees/{employee_id}get a single employee
POST/api/employeesinsert a new employee
PUT/api/employees/{employee_id}update a employee
DELETE/api/employees/{employee_id}remove a employee

Model / Entity

This class represents the employee table in the database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity
@Table(name = "employee")
public class Employee {
    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "first_name")
    private String first_name;

    @Column(name = "last_name")
    private String last_name;

    @Column(name = "email")
    private String email;

    // constructors

    // tostring

    // getters and setters
}

DAO interface

1
2
3
4
5
6
public interface EmployeeDao {
   List<Employee> findAll();
   Employee findById(int id);
   Employee save(Employee employee);
   Employee deleteById(int id);
}

DAO implementation

the DAO implementation uses the EntityManager that is created automatically by the spring container with all of the settings that were setup in the application.properties file.

  • The enity manager has all of the methods needed to interact with the Employee database
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Repository
public class EmployeeDaoImpl implements EmployeeDao{
    private EntityManager entityManager;

    @Autowired
    public EmployeeDaoImpl(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Override
    public List<Employee> findAll() {
        // get all of the employees from the db
        TypedQuery<Employee> typedQuery = entityManager.createQuery("from Employee", Employee.class);
        return typedQuery.getResultList();
    }

    @Override
    public Employee findById(int id) {
        return entityManager.find(Employee.class, id);
    }

    @Override
    public Employee save(Employee employee) {
        entityManager.merge(employee);
        return employee;
    }

    @Override
    public Employee deleteById(int id) {
        Employee employee = entityManager.find(Employee.class, id);
        entityManager.remove(employee);
        return employee;
    }
}

Service

1
2
3
4
5
6
public interface EmployeeService {
    List<Employee> findAll();
    Employee findById(int id);
    Employee save(Employee employee);
    Employee deleteById(int id);
}

Service implementation

What is the service used for

  • The service is used for the business logic with the employee table
  • if there were multiple daos it would inject it into the constructor dependency and be utilitzed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
@Transactional // all methods will be wrapped in a transaction
public class EmployeeServiceImpl implements EmployeeService{
    private EmployeeDaoImpl employeeDao;

    @Autowired
    public EmployeeServiceImpl(EmployeeDaoImpl employeeDao) {
        this.employeeDao = employeeDao;
    }

    @Override
    public List<Employee> findAll() {
        return employeeDao.findAll();
    }

    @Override
    public Employee findById(int id) {
        return employeeDao.findById(id);
    }

    @Override
    public Employee save(Employee employee) {
        return employeeDao.save(employee);
    }

    @Override
    public Employee deleteById(int id) {
        return employeeDao.deleteById(id);
    }
}

Rest controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@RestController
@RequestMapping("/api")
public class EmployeeController {
    private EmployeeService employeeService;

    @Autowired
    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    // get all employees
    @GetMapping("/employees")
    public List<Employee> findAll(){
        return employeeService.findAll();
    }

    // get a single employee by id
    @GetMapping("/employees/{employee_id}")
    public Employee getEmployee(@PathVariable int employee_id){
        Employee employee = employeeService.findById(employee_id);
        if(employee == null){
           throw new RuntimeException("Employee id not found - " + employee_id);
        }
        return employee;
    }

    // insert a new employee
    @PostMapping("/employees")
    public Employee addEmployee(@RequestBody Employee employee){
        employee.setId(0);
        Employee dbEmployee = employeeService.save(employee);
        return dbEmployee;
    }

    // updates an employee or adds it if not in the db
    @PutMapping("/employees/{employee_id}")
    public Employee updateEmployee(@RequestBody Employee employee, @PathVariable int employee_id){
        // the id does not exist
        if(employeeService.findById(employee_id) == null){
            throw new RuntimeException("No employee with id: " +employee_id + " was found");
        }
        employee.setId(employee_id);
        employeeService.save(employee);
        return employee;
    }

    // delete an employee by id
    @DeleteMapping("/employees/{employee_id}")
    public Employee deleteEmployee(@PathVariable int employee_id){
        if(employeeService.findById(employee_id) == null){
            throw new RuntimeException("Employee with id: " + employee_id + " was not found...");
        }
        return employeeService.deleteById(employee_id);
    }
}

Creating the CRUD DAO automatically by spring instead of creating different DAO for different entities

  • This is achieved by using Spring Data JPA
  • You create a DAO by just plugging in your entity type and primary key
    • Spring will give you a CRUD implmenentaton for free instead of having to create a DAO interface and DAO implementation for every entity like the example above

JPARepository

  • Spring Data JPA provides the interface JpaRepository
  • Exposes methods
    • findAll()
    • findById()
    • save()
    • deleteById()
    • others…
  • All you have to do is extend the JpaRepository and you get all of the implmemented methods for free!
  • Works by using generics
  • JpaRepository<Entity, PrimaryKey>
  • Provides the @Transactional so you don’t have to specify in the service class
1
2
3
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
  // that's it, no need to write code!!!
}
1
2
3
4
5
6
public interface EmployeeService {
    Employee save(Employee employee);
    List<Employee> findAll();
    Employee findById(int id);
    Employee removeById(int id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Service
public class EmployeeServiceImpl implements EmployeeService{
    private EmployeeRepository employeeRepository;

    @Autowired
    public EmployeeServiceImpl(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Override
    public Employee save(Employee employee) {
        return employeeRepository.save(employee);
    }

    @Override
    public List<Employee> findAll() {
        return employeeRepository.findAll();
    }

    @Override
    public Employee findById(int id) {
        if(employeeRepository.findById(id).isPresent()){
            return employeeRepository.findById(id).get();
        }
        else{
            return null;
        }
    }

    @Override
    public Employee removeById(int id) {
        Optional<Employee> employee = employeeRepository.findById(id);
        if(employee.isPresent()){
            employeeRepository.deleteById(id);
            return employee.get();
        }
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@RestController
@RequestMapping("/api")
public class Controller {
    private EmployeeService employeeService;

    @Autowired
    public Controller(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    // get all employees
    @GetMapping("/employees")
    public List<Employee> getEmployees(){
        return employeeService.findAll();
    }

    // get single employee
    @GetMapping("/employees/{employee_id}")
    public Employee getEmployee(@PathVariable int employee_id){
        Employee employee = employeeService.findById(employee_id);
        if(employee == null){
            throw new RuntimeException("No employee with id: " +employee_id +" was found");
        }
        return employee;
    }

    // add new employee
    @PostMapping("/employees")
    public Employee newEmployee(@RequestBody Employee employee){
       return employeeService.save(employee);
    }

    // update single employee
    @PutMapping("/employees/{employee_id}")
    public Employee newEmployee(@RequestBody Employee employee, @PathVariable int employee_id){
        if(employeeService.findById(employee_id) == null){
            throw new RuntimeException("Employee with id: " +employee_id +" was not found...");
        }
        employee.setId(employee_id);
        return employeeService.save(employee);
    }

    // delete single employee
    @DeleteMapping("employees/{employee_id}")
    public Employee deleteEmployee(@PathVariable int employee_id){
        Employee employee = employeeService.removeById(employee_id);
        if(employee == null){
            throw new RuntimeException("No employee with id: " +employee_id +" was found...");
        }
        return employee;
    }

}

Extended features

  • you can add additonal things like
    • extending and adding custom queries with JPQl
    • Query Domain Specific Lanauge (Query DSL)
    • Defining custom methods (low-level coding)

Spring Data REST

  • Spring Data REST does what Spring Data Jpa does for DAO’s but for the REST controller
  • leverages JpaRepository to give you a REST CRUD implementation for free
  • minimizes boiler-plate REST code

Endpints exposed for you

HTTP MethodEndpointCRUD Action
GET/employeesRead a list of employees
POST/employeesCreate a new employee
GET/employees/{employeeId}Get a single employee
PUT/employees/{employeeId}Update an existing employee
DELETE/employees/{employeeId}Delete an existing employee

How does it work

  • Spring Data REST will scan your project for JpaRepository
  • Expose REST APIs for each enity type for you JpaRepository
  • alt-text
1
2
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}

How do you implement this?

  • All you do is have to add the dependency in the pom.xml file and add your JpaRepository entity interfaces and spring will automatically scan them and implement the CRUD API

Architectur before/after

  • alt-text

HATEOAS compliant

  • Spring Data REST endpoints are HATEOAS compliant
  • HATEOAS - Hypermedia as the Engine of Application State
  • think of it as meta-data for REST data
  • alt-text
    • example of hateoas compliant for a get request of a single entity
  • alt-text
    • example of hateoas compliant for a collection response which contains
      • page size
      • total elements
      • pages, etc..

Spring Data REST Advanced Features

  • Pagination, sorting and searching

Base path for the endpoints

  • Can be specified in the applicaton.properties file with the following
1
spring.data.rest.base-path=/api

Manually specifying plural names for the endpoints

  • Done with the @RepositoryRestResource(path=”endpointname”)
1
2
3
@RepositoryRestResource(path = "members")
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}

Accessing different pages

  • spring data-rest defaults to 20 results
  • we can change that with the following appendage to the url
  • Zero based

http://localhost:3000/api/memebers?page=0

Spring Data REST configurations

NameDescription
spring.data.rest.base-pathBase path used to expose repository resources
spring.data.rest.default-page-sizeDefault size of pages
spring.data.rest.max-page-sizeMaximum size of pages

Sorting

  • We can sort by maniupulating the url
  • For some reason there is a bug where it won’t work if you try to sort with a field that is using underscores
    • example http://localhost:8080/employees?sort=last_name – does not work
    • but if you camelCase it would work with lastName
  • stack overflow underscores issue sorting
  • Sort by last name
    • http://localhost:8080/employees?sort=last_name
  • Sort by first name
  • http://localhost:8080/employees?sort=last_name
  • Sort first name descending
  • http://localhost:8080/employees?sort=last_name, desc
This post is licensed under CC BY 4.0 by the author.