Spring Boot JdbcClient CRUD Example

In this section, we're gonna build CRUD REST APIs using Spring Boot application that use Spring JdbcClient to interact with PostgreSQL database.

We are recommending you to cover Spring JdbcClient Example tutorial to get better understanding about JdbcClient


What we will build?

We will build CRUD RESTFul APIs for a Simple Student Management System using Spring Boot, JDBC API, and the PostgreSQL database. In this application we use JdbcClient to interact with PostgreSQL database.


Creating a spring boot application

First, open the Spring initializr https://start.spring.io

Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact spring-boot-jdbcclient-crud. Here I selected the Maven project - language Java 17 - Spring Boot 3.2.0, Spring Web,JDBC API, and PostgreSQL Driver.

Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(spring-boot-jdbcclient-crud) file and downloads the project. Then, Extract the Zip file. 

Then, import the project on your favourite IDE.

Final Project Directory:


Complete pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>spring-boot-jdbcclient-crud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-jdbcclient-crud</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

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

</project>


application.yaml 

For creating table via schema.sql, we need to set the spring.sql.init.mode=always.

If you are running the application with real postgresql database, then configure Spring Boot to use PostgreSQL as data source. We are simply adding PostgreSQL database URL, username, and password in the src/main/resources/application.yaml.

spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: root
sql:
init:
mode: always


Create schema.sql

To generate a database schema at application start, the file schema.sql with the corresponding SQL commands for schema creation must be stored in the resources folder.

CREATE TABLE IF NOT EXISTS students (
id SERIAL PRIMARY KEY,
name VARCHAR(250) NOT NULL,
email VARCHAR(250) NOT NULL,
gender VARCHAR(250) NOT NULL,
age INT
);


Create Model - Student.java

Let's create a new package called model inside com.knf.dev.demo package and then create the Student class inside the model package with the following contents - 

package com.knf.dev.demo.model;

public class Student{

private Long id;
private String name;
private String email;
private Integer age;
private String gender;

public Student() {
}

public Student(Long id, String name,
String email, Integer age, String gender) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
this.gender = gender;
}

public Long getId() {
return id;
}

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

public String getName() {
return name;
}

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

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}
}


Create StudentDAO.java

Let's create a new package called dao inside com.knf.dev.demo package and then create the following interface inside the repository package - 

package com.knf.dev.demo.dao;

import com.knf.dev.demo.model.Student;

import java.util.List;
import java.util.Optional;

public interface StudentDAO {

Integer insertStudent(Student student);

Student updateStudent(Student student);

void deleteStudent(Long id);

List<Student> getAllStudents();

Optional<Student> findStudentById(Long id);

}


Create StudentDAOImpl.java

Create the StudentDAOImpl class inside the dao package with the following contents - 

package com.knf.dev.demo.dao;

import com.knf.dev.demo.model.Student;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import java.util.Optional;

@Repository
public class StudentDAOImpl implements StudentDAO {

private JdbcClient jdbcClient;

public StudentDAOImpl(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}

@Override
public Integer insertStudent(Student student) {

String sql = "INSERT INTO students (name, email, gender, age)"
+ "VALUES (:name, :email, :gender, :age)";
return this.jdbcClient.sql(sql)
.param("name", student.getName(), Types.VARCHAR)
.param("email", student.getEmail(), Types.VARCHAR)
.param("gender", student.getGender(), Types.VARCHAR)
.param("age", student.getAge(), Types.INTEGER)
.update();
}

@Override
public Student updateStudent(Student student) {

String sql = "update students set name = :name, email = :email, " +
"gender= :gender, age = :age where id = :id";

jdbcClient.sql(sql)
.param("name", student.getName())
.param("email", student.getEmail())
.param("gender", student.getGender())
.param("age", student.getAge())
.param("id", student.getId())
.update();
return findStudentById(student.getId()).get();
}

@Override
public void deleteStudent(Long id) {

String sql = "delete from students where id = :id";
jdbcClient.sql(sql).param("id", id).update();
}

//Using Varargs
@Override
public List<Student> getAllStudents() {

String sql = "select id, name, age, email, gender, age from students";

return jdbcClient.sql(sql)
.query(new StudentRowMapper()).list();
}

@Override
public Optional<Student> findStudentById(Long id) {

String sql = "select id, name, age, gender, " +
"email from students where id = :id";

return jdbcClient.sql(sql)
.param("id", id)
.query(Student.class)
.optional();
}

static class StudentRowMapper implements RowMapper<Student> {
@Override
public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Student(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email"),
rs.getInt("age"),
rs.getString("gender"));
}
}
}
JdbcClient is auto-configured for us in Spring Boot.
Above JdbcClient is used and injected in the component class. Initialise a final JdbcClient object and create a constructor injection using this field.

Here, we are wiring objects together using method chaining helps accomplish readability and usability.


Create StudentController.java

Let's create a new package called controller inside com.knf.dev.demo package and then create the StudentController class inside the controller package with the following contents - 

package com.knf.dev.demo.controller;

import com.knf.dev.demo.dao.StudentDAOImpl;
import com.knf.dev.demo.model.Student;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/v1/students")
public class StudentController {

private final StudentDAOImpl studentDAO;

public StudentController(StudentDAOImpl studentDAO) {
this.studentDAO = studentDAO;
}

@PostMapping
public ResponseEntity<Integer> createStudent(
@RequestBody Student student) {

studentDAO.insertStudent(student);
return ResponseEntity.ok().build();
}

@GetMapping("/{id}")
public ResponseEntity<Student> getStudent(@PathVariable Long id) {

return studentDAO.findStudentById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

@GetMapping
public ResponseEntity<List<Student>> getAllStudents() {

return ResponseEntity.ok(studentDAO.getAllStudents());
}

@PutMapping("/{id}")
public ResponseEntity<Student> updateStudent(@PathVariable Long id,
@RequestBody Student student) {

Student _student = studentDAO.findStudentById(id).orElseThrow();
_student.setAge(student.getAge());
_student.setName(student.getName());
_student.setEmail(student.getEmail());
_student.setGender(student.getGender());

return ResponseEntity.ok(studentDAO.updateStudent(_student));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteStudent(@PathVariable Long id) {

studentDAO.deleteStudent(id);
return ResponseEntity.noContent().build();
}
}


Application.java

package com.knf.dev.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

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

}


Write Unit test for StudentController 

The StudentController is a @RestController and thus part of the Web Layer that we want to test.

Create StudentControllerTests

When using JUnit 4, this annotation should be used in combination with @RunWith(SpringRunner.class). But for this example  we are using JUnit 5, there’s no need to add the equivalent @ExtendWith(SpringExtension.class).
package com.knf.dev.demo;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.knf.dev.demo.controller.StudentController;
import com.knf.dev.demo.dao.StudentDAOImpl;
import com.knf.dev.demo.model.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(StudentController.class)
public class StudentControllerTests {

@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private StudentDAOImpl studentDAO;

@Test
void shouldUpdateStudent() throws Exception {

long id = 1L;

Student student = new Student(id, "Alpha", "alpha@tmail.com",23,"male");
Student updatedStudent = new Student(id, "Beta", "alpha@tmail.com",23,"male");

when(studentDAO.findStudentById(id)).thenReturn(Optional.of(student));
when(studentDAO.updateStudent(any(Student.class))).thenReturn(updatedStudent);

mockMvc.perform(put("/api/v1/students/{id}", id).
contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updatedStudent)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value(updatedStudent.getName()))
.andExpect(jsonPath("$.email").value(updatedStudent.getEmail()))
.andExpect(jsonPath("$.age").value(updatedStudent.getAge()))
.andExpect(jsonPath("$.gender").value(updatedStudent.getGender()))
.andDo(print());
}

@Test
void shouldReturnListOfStudents() throws Exception {

List<Student> students = new ArrayList<>(
Arrays.asList(new Student(1l, "Alpha", "alpha@tmail.com",12,"male"),
new Student(2l, "Beta", "beta@tmail.com",22,"female"),
new Student(3l, "Gama", "gama@tmail.com",50,"male")));

when(studentDAO.getAllStudents()).thenReturn(students);

mockMvc.perform(get("/api/v1/students"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.size()").value(students.size()))
.andDo(print());
}

@Test
void shouldReturnStudent() throws Exception {

long id = 1L;
Student student = new Student(1l, "Alpha", "alpha@tmail.com",23,"male");

when(studentDAO.findStudentById(id)).thenReturn(Optional.of(student));

mockMvc.perform(get("/api/v1/students/{id}", id)).andExpect(status().isOk())
.andExpect(jsonPath("$.name").value(student.getName()))
.andExpect(jsonPath("$.email").value(student.getEmail()))
.andExpect(jsonPath("$.age").value(student.getAge()))
.andExpect(jsonPath("$.gender").value(student.getGender()))
.andDo(print());
}

@Test
void shouldCreateStudent() throws Exception {

Student student = new Student(1l, "Alpha", "alpha@tmail.com",23,"male");

mockMvc.perform(post("/api/v1/students").
contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(student)))
.andExpect(status().isOk())
.andDo(print());
}

@Test
void shouldDeleteStudent() throws Exception {

long id = 1L;

doNothing().when(studentDAO).deleteStudent(id);
mockMvc.perform(delete("/api/v1/students/{id}", id))
.andExpect(status().isNoContent())
.andDo(print());
}
}


Verify our system is working as expected

Run the Spring Boot application - mvn spring-boot:run


OR 


Run this Spring boot application from 

  • IntelliJ IDEA IDE by right click - Run 'Application.main()' 
  • Eclipse/STS - You can right click the project or the Application.java file and run as java application or Spring boot application.

Add Student- POST


Update Student- PUT


Fetch all Student- GET




Run the test

mvn test -Dtest=StudentControllerTests

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Spring Boot + Mockito simple application with 100% code coverage

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete