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

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-jdbcclient-example. Here I selected the Maven project - language Java 17 - Spring Boot 3.2.0 , JDBC API, PostgreSQL Driver and Testcontainers(for testing purpose).


Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(spring-jdbcclient-example) 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-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

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 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

Create Java record named 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"));
}
}
}
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.

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

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