Spring JdbcClient Example
In this section, we're gonna build a Spring Boot application that use Spring JdbcClient to interact with PostgreSQL database.
Spring Framework 6.1 introduced the JdbcClient API. As we all know, Spring Framework 6.1 is part of Spring Boot version 3.2.
We can use the Spring JdbcClient interface to define the query, set the parameters, and perform the DB operation, using a fluent API. Fluent APIs are designed to make the code more readable and therefore easier to use. Wiring objects together using method chaining helps accomplish these readability and usability goals.
An example for retrieving a Student as a Optional:
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();
}
What we will build?
We will build a spring boot, JDBC API application, create DAO layer and finally we will write test against PostgreSQL database as container instance.
We are using Postgresql database here, but you can use any other relational database too, such as MySQL and Oracle. All you need is to change the database configurations and it should work.
Creating spring boot application
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-jdbcclient-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-jdbcclient-example</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.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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</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.
#Real database(PostgreSQL) configuration
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 test-student-data.sql
INSERT INTO students (id,name, email, age, gender) VALUES
(100,'Pro', 'pro@knf.com', 60, 'Male'),
(101,'Alpha', 'alpha@knf.com', 50, 'Male'),
(102,'Beta', 'beta@knf.com', 40, 'Female'),
(103,'Gama', 'gama@knf.com', 30, 'Male'),
(104,'Pekka', 'pekka@knf.com', 20, 'Female'),
(105,'Noob', 'noob@knf.com', 10, 'Male'),
(106,'Noob2', 'noob2@knf.com', 5, 'Male'),
(107,'Noob3', 'noob3@knf.com', 5, 'Male');
Load some initial test data. Later, we will will use this script inside out test class to load data.
Create Model - Student
package com.knf.dev.demo.model;
public record Student
(Long id,
String name,
String email,
Integer age,
String 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);
Integer updateStudent(Student student);
Integer deleteStudent(Long id);
List<Student> findStudentByAgeAndGender(Integer age, String gender);
Optional<Student> findStudentById(Long id);
List<Student> findStudentByGenderAndAgeGreaterThan(Integer age, String gender);
}
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.name(), Types.VARCHAR)
.param("email", student.email(), Types.VARCHAR)
.param("gender", student.gender(), Types.VARCHAR)
.param("age", student.age(), Types.INTEGER)
.update();
}
@Override
public Integer updateStudent(Student student) {
String sql = "update students set name = :name, email = :email, " +
"gender= :gender, age = :age where id = :id";
return jdbcClient.sql(sql)
.param("name", student.name())
.param("email", student.email())
.param("gender", student.gender())
.param("age", student.age())
.param("id", student.id())
.update();
}
@Override
public Integer deleteStudent(Long id) {
String sql = "delete from students where id = :id";
return jdbcClient.sql(sql).param("id", id).update();
}
//Using Varargs
@Override
public List<Student> findStudentByAgeAndGender(Integer age, String gender) {
String sql = "select id, name, age, email, gender, age from students"
+ " where age = ? and gender = ?";
return jdbcClient.sql(sql)
.params(age, gender)
.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();
}
//Using Positional Params
@Override
public List<Student> findStudentByGenderAndAgeGreaterThan
(Integer age, String gender) {
String sql = "select id, name, age, email, gender from students"
+ " where age > ? and gender = ?";
return jdbcClient.sql(sql)
.param(age)
.param(gender)
.query(new StudentRowMapper()).list();
}
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"));
}
}
}
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);
}
}
Implementing the Tests
For running the test, install docker in your local machine if you not yet installed docker. Go to docker official https://docs.docker.com/engine/install/#desktop and download docker and then install.
Create StudentDAOTests
When using JUnit 4, @SpringBootTest 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.knf.dev.demo.dao.StudentDAOImpl;
import com.knf.dev.demo.model.Student;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.test.context.jdbc.Sql;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@JdbcTest
@Testcontainers
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
@Sql({"/test-student-data.sql"})
public class StudentDAOTests {
@Autowired
private JdbcClient jdbcClient;
private StudentDAOImpl studentDAO;
@Container
@ServiceConnection
public static PostgreSQLContainer postgreSQLContainer =
new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
@BeforeEach
void setUp() {
studentDAO = new StudentDAOImpl(jdbcClient);
}
@Test
void saveStudent_ReturnsTheInteger() {
Integer row = studentDAO.
insertStudent(
new Student(1l,"rahul",
"rahul@email.com",19,"Male"));
assertThat(row).isEqualTo(1);
}
@Test
void updateStudent_ReturnsTheInteger() {
Integer row = studentDAO.
updateStudent(
new Student(104l,"rahul",
"rahulg@email.com",25,"Male"));
Student student = studentDAO.findStudentById(104l).get();
assertThat(row).isEqualTo(1);
}
@Test
void deleteStudent_ReturnsTheInteger() {
Integer row = studentDAO.
deleteStudent(105l);
assertThat(row).isEqualTo(1);
}
@Test
void findStudentByGenderAndAgeGreaterThan_ReturnsTheListStudents() {
List<Student> students = studentDAO.findStudentByGenderAndAgeGreaterThan(31,"Male");
//Convert list of students to list of id(Integer)
List<Integer> ids = students.stream()
.map(o -> o.id().intValue())
.collect(Collectors.toList());
assertThat(students.size()).isEqualTo(2);
assertThat(ids).hasSameElementsAs(Arrays.asList(100,101));
}
@Test
void findStudentByAgeAndGender_ReturnsTheListStudents() {
List<Student> students = studentDAO.findStudentByAgeAndGender(5,"Male");
//Convert list of students to list of id(Integer)
List<Integer> ids = students.stream()
.map(o -> o.id().intValue())
.collect(Collectors.toList());
assertThat(students.size()).isEqualTo(2);
assertThat(ids).hasSameElementsAs(Arrays.asList(106,107));
}
@Test
void findStudentById_ReturnsTheStudent() {
Student student = studentDAO.findStudentById(102l).get();
assertThat(student).isNotNull();
assertThat(student.email()).isEqualTo("beta@knf.com");
assertThat(student.name()).isEqualTo("Beta");
assertThat(student.age()).isEqualTo(40);
assertThat(student.id()).isEqualTo(102l);
assertThat(student.gender()).isEqualTo("Female");
}
}
Run the test
Since we are using Testcontainers , make sure to start Docker in your local machine.
After that, run the test,
Or you can run the test using following command:
mvn test -Dtest=StudentDAOTests
or
mvn test