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
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
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
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
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"));
}
}
}
Create StudentController.java
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
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.
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
Find Student by ID- GET
Delete Student- DELETE
Run the test
mvn test -Dtest=StudentControllerTests