Technologies: Java 21 (LTS), JDBC 4.3, Maven, MySQL 8, Docker Level: Beginner to intermediate
Table of Contents
- 2.1 Introduction and used versions
- 2.2 What you will build
- 2.3 Business focus — the verbose JDBC problem
- 2.4 Creating the project with the Maven Archetype
- 2.5 Project structure
- 2.6 Configuring the database with Docker
- 2.7 docker-compose.yml file
- 2.8 Module 2 Summary
- 3.1 The JDBC driver
- 3.2 Adding the MySQL dependency in the pom.xml
- 3.3 DriverManager
- 3.4 AbstractDao — centralize the connection
- 3.5 The Dao Interface
- 3.6 Model Book
- 3.7 BookDao — first implementation with Statement
- 3.8 Main application — App.java
- 3.9 Creating the table in MySQL Workbench
- 3.10 Module 3 Summary
- 4.1 The ResultSet in detail
- 4.2 PreparedStatement — why use it
- 4.3 Optionals — handle null values
- 4.4 Dao interface updated with findById
- 4.5 BookDao with PreparedStatement and Optional
- 4.6 App.java with findById
- 4.7 Module 4 Summary
- 5.1 Insert records — generated keys
- 5.2 Update records
- 5.3 Batch updates — grouped update
- 5.4 Complete Dao interface (create + update + batch)
- 5.5 Book model with rating field
- 5.6 BookDao complet — create, update, batch update
- 5.7 App.java — inserts and updates demo
- 5.8 Module 5 Summary
- 6.1 Delete a record
- 6.2 Batch deletes
- 6.3 Final Dao interface with delete
- 6.4 BookDao with delete method
- 6.5 App.java — delete demonstration
- 6.6 Module 6 Summary
- 7.1 The problem — JDBC code duplication
- 7.2 The Template Method Pattern — the solution
- 7.3 JdbcQueryTemplate — generic abstract class
- 7.4 BookDao refactored with JdbcQueryTemplate
- 7.5 App.java final
- 7.6 Summary of module 7 and conclusion
1. Course Overview
Data access is fundamental to almost all Java applications. Even if you use JPA or Hibernate, ultimately everything relies on JDBC. This course covers the fundamentals of data access in the most recent Long-Term Support (LTS) version of Java SE, Java 21.
Major topics covered:
- Complete CRUD operations on a database: Create, Read, Update, Delete
- Using try-with-resources to properly handle closing connections and other resources
- Using Optionals to handle null results
- The Template Method Pattern to clean up the architecture and reduce code duplication
Prerequisites:
- Basic knowledge of Java
- Knowledge of Maven is useful but not required
2. Project configuration and business case
2.1 Introduction and versions used
Data access in Java has been stable since before Java 21, but several independently released features will be collected in this course. Version matching is one of the most common causes of problems in the help forums.
| Technology | Version used in the course |
|---|---|
| Java (LTS) | Java 21 |
| JDBC | 4.3 |
| Maven | Latest stable version |
| IDE | IntelliJ IDEA (any compatible IDE works) |
| Database | MySQL 8.0 |
| JDBC driver | mysql-connector-java 8.0.28 |
Important: Versions must match. Most support questions come from version incompatibility.
2.2 What you will build
The demonstration application is focused on the business case of a library. The application will allow you to retrieve, create, update and delete books (Book), categories and other associated entities. This is a simple use case, but sufficient to illustrate all database operations.
Source code is available via the exercise files or a GitHub repository.
2.3 Business focus — the verbose JDBC problem
Here is the fundamental problem with naive JDBC code. For a simple SELECT, the developer ends up with a very verbose block where the business logic is buried in infrastructure code:
// Exemple de JDBC verbeux — à NE PAS reproduire tel quel
Connection con = null;
PreparedStatement stmt = null;
ResultSet rset = null;
try {
con = DriverManager.getConnection(url, username, password);
stmt = con.prepareStatement("SELECT ID, TITLE FROM BOOK WHERE ID = ?");
stmt.setLong(1, id);
rset = stmt.executeQuery();
if (rset.next()) {
Book book = new Book();
book.setId(rset.getLong("ID"));
book.setTitle(rset.getString("TITLE"));
return book;
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (rset != null) try { rset.close(); } catch (SQLException e) {}
if (stmt != null) try { stmt.close(); } catch (SQLException e) {}
if (con != null) try { con.close(); } catch (SQLException e) {}
}
The actual business code — what the business really wants — comes down to this:
SELECT ID, TITLE FROM BOOK WHERE ID = ?
And extracting the returned object. Everything else is boilerplate. This course shows how to gradually improve this situation.
2.4 Creating the project with Maven Archetype
The following Maven command generates the standard project structure. It must be run in the parent directory where you want the project to be created.
mvn archetype:generate \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DgroupId=com.pluralsight \
-DartifactId=library-app
The archetype automatically generates:
- A basic
pom.xmlfile - The
src/main/javaandsrc/test/javadirectory structure - A startup
App.javaclass - A test
AppTest.java
2.5 Project structure
library-app/
├── pom.xml
└── src/
├── main/
│ └── java/
│ └── com/
│ └── pluralsight/
│ └── App.java
├── test/
│ └── java/
│ └── com/
│ └── pluralsight/
│ └── AppTest.java
└── site/
└── site.xml
2.6 Database configuration with Docker
To create the database, this course uses Docker. All the companies consulted by the trainer in the last three years install their databases via Docker. If you don’t have it yet, download Docker Desktop for your operating system. It is also possible to install MySQL natively, but Docker is simpler.
Essential Docker commands:
# Démarrer le conteneur (depuis le répertoire contenant docker-compose.yml)
docker compose up -d
# Vérifier que le conteneur tourne
docker ps
# Arrêter le conteneur
docker compose down
2.7 Docker-compose.yml file
This YAML file must be created at the root of the project. It defines the MySQL container with all the necessary parameters.
version: '3.8'
networks:
default:
services:
db:
image: mysql:8.0
container_name: library
ports:
- 3306:3306
volumes:
- "./.data/db:/var/lib/mysql"
environment:
MYSQL_ROOT_PASSWORD: pass
MYSQL_DATABASE: library_db
Key points:
- image:
mysql:8.0— official MySQL version 8 image - container_name:
library— container name - ports:
3306:3306— exposure of standard MySQL port - volumes: Data is persisted in
./.data/dbto survive reboots - MYSQL_ROOT_PASSWORD:
pass— root password (for local development only) - MYSQL_DATABASE:
library_db— the database automatically created on startup
Security Note: These credentials are for local development only. Never use such a simple password in production.
2.8 Module 2 Summary
- Checking versions (Java 21 LTS, JDBC 4.3, Maven)
- Running the Maven archetype to create the
library-appproject - Discovering the Java Maven standard structure
- Configuring and starting MySQL database via Docker Compose
- Verify that the container is operational before moving to the next module
3. Connecting and querying the database
3.1 The JDBC driver
Regardless of the connection method — JNDI lookup, connection pool, or direct connection — the first step is always to obtain the JDBC driver. It is the library that interprets all commands sent to the specific database engine (here MySQL).
The drivers are versioned and must match the database version:
- For MySQL 8 →
mysql-connector-java:8.0.28 - For MySQL 5.7 → use the driver corresponding to version 5.x
Since JDBC 4.0, it is no longer necessary to use
Class.forName(...)to manually load the driver. TheDriverManagerautomatically loads it from the classpath.
3.2 Added MySQL dependency in pom.xml
The initial pom.xml generated by the archetype must be updated:
- Change source/target compiler versions from
1.7to17 - Remove obsolete JUnit dependency (version 3.8.1)
- Add dependency for MySQL driver
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pluralsight</groupId>
<artifactId>library-app</artifactId>
<version>1.0-SNAPSHOT</version>
<name>library-app</name>
<description>A simple library-app.</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
After adding, IntelliJ will offer to load Maven changes (“Load Maven Changes” button). Once loaded, the mysql-connector-java jar will be visible in the External Libraries of the project.
3.3 DriverManager
The DriverManager class is used to obtain a database connection. It takes a JDBC URL, a username and a password. It automatically searches for the appropriate driver in the classpath.
JDBC URL format for MySQL:
jdbc:mysql://<host>:<port>/<database>
Example: jdbc:mysql://localhost:3306/library_db
3.4 AbstractDao — centralize the connection
Rather than copy/paste the connection logic into each DAO, we create an AbstractDao class with a getConnection() method. All DAOs will extend this class to achieve a connection uniformly.
This centralizes connection information. If we later switch to a JNDI or a connection pool, we only need to modify one place.
package com.pluralsight.repository;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class AbstractDao {
protected Connection getConnection() throws SQLException {
String url = "jdbc:mysql://localhost:3306/library_db";
String username = "root";
String password = "pass";
return DriverManager.getConnection(url, username, password);
}
}
Important points:
- Method is
protected— accessible only by subclasses - It declares
throws SQLException— subclasses handle this exception or propagate it - The hard-coding of credentials here is intentional for simplicity; in production, we would use a properties file, JNDI, or environment variables
3.5 The Dao interface
The Dao interface defines the contract that all Data Access Object classes must respect. It is generic (<T>) to be reusable with any type of entity.
package com.pluralsight.repository;
import java.util.List;
public interface Dao<T> {
List<T> findAll();
}
This interface is inspired by the way JPA, Hibernate, TopLink and EclipseLink define their data access contracts.
3.6 Model Book
package com.pluralsight.model;
public class Book {
private long id;
private String title;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
3.7 BookDao — first implementation with Statement
BookDao extends AbstractDao and implements the Dao<Book> interface. It uses a simple Statement and a ResultSet to query the BOOK table.
package com.pluralsight.repository;
import com.pluralsight.model.Book;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class BookDao extends AbstractDao implements Dao<Book> {
@Override
public List<Book> findAll() {
List<Book> books = Collections.emptyList();
String sql = "SELECT * FROM BOOK";
try (
Connection con = getConnection();
Statement stmt = con.createStatement();
ResultSet rset = stmt.executeQuery(sql);
) {
books = new ArrayList<>();
while (rset.next()) {
Book book = new Book();
book.setId(rset.getLong("id"));
book.setTitle(rset.getString("title"));
books.add(book);
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return books;
}
}
Key points:
- try-with-resources:
Connection,StatementandResultSetare all declared in thetry(...)clause. They will be automatically closed at the end of the block, even if an exception is thrown. - Reading by column name:
rset.getLong("id")andrset.getString("title")— more robust than reading by index. Collections.emptyList(): safe initialization of the list before the try.
3.8 Main application — App.java
package com.pluralsight;
import com.pluralsight.model.Book;
import com.pluralsight.repository.BookDao;
import com.pluralsight.repository.Dao;
import java.util.List;
public class App {
public static void main(String[] args) {
Dao<Book> bookDao = new BookDao();
List<Book> books = bookDao.findAll();
for (Book book : books) {
System.out.println("Id: " + book.getId());
System.out.println("Title: " + book.getTitle());
}
}
}
3.9 Creating the table in MySQL Workbench
To create the table via MySQL Workbench:
- Open MySQL Workbench and create a connection (host:
localhost, port:3306, user:root, default schema:library_db) - Test the connection (
Test Connection) then click OK - In the left panel, right click on Tables → Create Table
- Table name:
BOOK - Columns:
id: INT, Primary Key, Not NULL, Auto Incrementtitle: VARCHAR(255)
- Click Apply — MySQL Workbench displays the generated SQL — click Apply again
The equivalent SQL generated:
CREATE TABLE `library_db`.`BOOK` (
`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NULL,
PRIMARY KEY (`id`)
);
3.10 Module 3 Summary
- Added JDBC MySQL driver to
pom.xml - Creating
AbstractDaowith centralizedgetConnection()method - Creation of the
Dao<T>interface withfindAll() - Implementing
BookDaowithStatementandResultSetin a try-with-resources - Creating table
BOOKin MySQL Workbench - Running the application and checking the returned data
4. Statements, PreparedStatements and ResultSets
4.1 The ResultSet in detail
The ResultSet interface (provided by the JDBC driver) contains the result of the operation executed against the database. The ResultSet cursor starts before the first line; we call rset.next() to move forward.
Best practice — read by column name rather than index:
// ✅ Par nom de colonne — préféré, moins sujet aux erreurs
book.setId(rset.getLong("ID"));
book.setTitle(rset.getString("TITLE"));
// ⚠️ Par index — fragile si l'ordre des colonnes change
book.setId(rset.getLong(1));
book.setTitle(rset.getString(2));
4.2 PreparedStatement — why use it
The PreparedStatement is the improved version of the Statement:
| Criterion | Statement | PreparedStatement |
|---|---|---|
| Protection against SQL injection | ❌ No | ✅ Yes |
| Pre-compilation | ❌ No | ✅ Yes (more efficient) |
| Settings with automatic escape | ❌ No | ✅ Yes |
| Recommended Use | Queries without parameters | Always if possible |
Security: A generic
Statementallows SQL injection attacks. ThePreparedStatementautomatically escapes parameters and compiles them before execution.
4.3 Optionals — handle null values
Optional<T> are a Java feature (introduced in Java 8) allowing you to properly handle objects that could be null. In the JDBC context, they are particularly useful for queries that may not return any results.
// Sans Optional — risque de NullPointerException
Book book = bookDao.findById(99); // peut retourner null
System.out.println(book.getTitle()); // NullPointerException si book est null !
// Avec Optional — gestion explicite du cas null
Optional<Book> optBook = bookDao.findById(99);
if (optBook.isPresent()) {
System.out.println(optBook.get().getTitle());
}
// Ou avec ifPresent()
optBook.ifPresent(b -> System.out.println(b.getTitle()));
4.4 Dao interface updated with findById
package com.pluralsight.repository;
import java.util.List;
import java.util.Optional;
public interface Dao<T> {
Optional<T> findById(long id);
List<T> findAll();
}
4.5 BookDao with PreparedStatement and Optional
package com.pluralsight.repository;
import com.pluralsight.model.Book;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class BookDao extends AbstractDao implements Dao<Book> {
@Override
public Optional<Book> findById(long id) {
Optional<Book> book = Optional.empty();
String sql = "SELECT ID, TITLE FROM BOOK WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setLong(1, id); // Liaison du paramètre — protège contre SQL injection
try (ResultSet rset = prepStmt.executeQuery()) {
Book resBook = new Book();
if (rset.next()) {
resBook.setId(rset.getLong("ID"));
resBook.setTitle(rset.getString("TITLE"));
}
book = Optional.of(resBook);
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public List<Book> findAll() {
List<Book> books = Collections.emptyList();
String sql = "SELECT * FROM BOOK";
try (
Connection con = getConnection();
Statement stmt = con.createStatement();
ResultSet rset = stmt.executeQuery(sql);
) {
books = new ArrayList<>();
while (rset.next()) {
Book book = new Book();
book.setId(rset.getLong("id"));
book.setTitle(rset.getString("title"));
books.add(book);
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return books;
}
}
Key points of findById:
Optional.empty()— safe default valuecon.prepareStatement(sql)— SQL is compiled with?as placeholderprepStmt.setLong(1, id)— binding the parameter to index 1 (indexes start at 1 in JDBC)ResultSetnested inside a second try-with-resources to ensure it closesif (rset.next())— checks if a record was found (nowhileloop for unique ID search)
4.6 App.java with findById
package com.pluralsight;
import com.pluralsight.model.Book;
import com.pluralsight.repository.BookDao;
import com.pluralsight.repository.Dao;
import java.util.List;
import java.util.Optional;
public class App {
public static void main(String[] args) {
Dao<Book> bookDao = new BookDao();
List<Book> books = bookDao.findAll();
for (Book book : books) {
System.out.println("Id: " + book.getId());
System.out.println("Title: " + book.getTitle());
}
Optional<Book> optBook = bookDao.findById(1);
if (optBook.isPresent()) {
Book book = optBook.get();
System.out.println("Id: " + book.getId());
System.out.println("Title: " + book.getTitle());
}
}
}
4.7 Summary of Module 4
- The
ResultSetis an interface whose implementation is provided by the JDBC driver - Reading columns by name is more robust than by index
PreparedStatementis preferred over genericStatement— it protects against SQL injections, is compiled ahead of time, and escapes parametersOptional<T>allow elegantly handling the case where no record is found
5. Batch inserts and updates
5.1 Insert records — generated keys
Data insertion uses the same mechanisms as an update. An important real-world aspect: returning the automatically generated ID from the database after an INSERT.
To retrieve the generated keys, we pass Statement.RETURN_GENERATED_KEYS when preparing the statement:
PreparedStatement prepStmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
After executing executeUpdate(), an additional ResultSet containing the generated keys is available via prepStmt.getGeneratedKeys().
5.2 Update records
Update is almost the same as insert — only the SQL changes. We use executeUpdate() in both cases.
UPDATE BOOK SET TITLE = ? WHERE ID = ?
The method can also return the number of rows affected, which is useful for validating that the operation completed as expected.
5.3 Batch updates
batch update is the most efficient technique for updating a large number of records. The most expensive part of database operations is establishing the connection and preparing the statement. By grouping several operations in a single batch, this cost is minimized.
Steps of a batch update:
- Create the connection and prepare the statement
- For each record: configure the parameters then call
prepStmt.addBatch() - Call
prepStmt.executeBatch()to send all records at once executeBatch()returns anint[]array with the number of rows affected for each operation
Before implementing the batch update in Java, a new rating column (of type INT) was added to the BOOK table via MySQL Workbench:
ALTER TABLE `library_db`.`BOOK`
ADD COLUMN `rating` INT NULL;
5.4 Complete Dao interface (create + update + batch)
package com.pluralsight.repository;
import java.util.List;
import java.util.Optional;
public interface Dao<T> {
Optional<T> findById(long id);
List<T> findAll();
T create(T t);
T update(T t);
int[] update(List<T> t);
}
5.5 Book model with rating field
package com.pluralsight.model;
public class Book {
private long id;
private String title;
private int rating;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getRating() {
return rating;
}
public void setRating(int rating) {
this.rating = rating;
}
}
5.6 BookDao complete — create, update, batch update
package com.pluralsight.repository;
import com.pluralsight.model.Book;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class BookDao extends AbstractDao implements Dao<Book> {
@Override
public Optional<Book> findById(long id) {
Optional<Book> book = Optional.empty();
String sql = "SELECT ID, TITLE FROM BOOK WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setLong(1, id);
try (ResultSet rset = prepStmt.executeQuery()) {
Book resBook = new Book();
if (rset.next()) {
resBook.setId(rset.getLong("ID"));
resBook.setTitle(rset.getString("TITLE"));
}
book = Optional.of(resBook);
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public List<Book> findAll() {
List<Book> books = Collections.emptyList();
String sql = "SELECT * FROM BOOK";
try (
Connection con = getConnection();
Statement stmt = con.createStatement();
ResultSet rset = stmt.executeQuery(sql);
) {
books = new ArrayList<>();
while (rset.next()) {
Book book = new Book();
book.setId(rset.getLong("id"));
book.setTitle(rset.getString("title"));
books.add(book);
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return books;
}
@Override
public Book create(Book book) {
String sql = "INSERT INTO BOOK (TITLE) VALUES (?)";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(
sql, Statement.RETURN_GENERATED_KEYS); // Demande le retour des clés générées
) {
prepStmt.setString(1, book.getTitle());
prepStmt.executeUpdate();
try (ResultSet genKeys = prepStmt.getGeneratedKeys()) {
if (genKeys.next()) {
book.setId(genKeys.getLong(1)); // L'ID généré est assigné à l'objet retourné
}
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public Book update(Book book) {
String sql = "UPDATE BOOK SET TITLE = ? WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setString(1, book.getTitle()); // Paramètre 1 : nouveau titre
prepStmt.setLong(2, book.getId()); // Paramètre 2 : ID du livre à modifier
prepStmt.executeUpdate();
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public int[] update(List<Book> books) {
int[] records = {};
String sql = "UPDATE BOOK SET TITLE = ?, RATING = ? WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
for (Book book : books) {
prepStmt.setString(1, book.getTitle());
prepStmt.setInt(2, book.getRating());
prepStmt.setLong(3, book.getId());
prepStmt.addBatch(); // Ajoute chaque opération au batch
}
records = prepStmt.executeBatch(); // Exécute toutes les opérations en une seule fois
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return records;
}
}
5.7 App.java — demo inserts and updates
package com.pluralsight;
import com.pluralsight.model.Book;
import com.pluralsight.repository.BookDao;
import com.pluralsight.repository.Dao;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class App {
public static void main(String[] args) {
Dao<Book> bookDao = new BookDao();
// Lister tous les livres
List<Book> books = bookDao.findAll();
for (Book book : books) {
System.out.println("Id: " + book.getId());
System.out.println("Title: " + book.getTitle());
}
// Trouver et mettre à jour un livre par ID
Optional<Book> optBook = bookDao.findById(1);
if (optBook.isPresent()) {
Book book = optBook.get();
System.out.println("Id: " + book.getId());
System.out.println("Title: " + book.getTitle());
book.setTitle("Effective Java: Second Edition");
bookDao.update(book);
}
// Créer un nouveau livre (commenté dans la démo finale)
// Book newBook = new Book();
// newBook.setTitle("The River Why");
// newBook = bookDao.create(newBook);
// System.out.println("Id: " + newBook.getId());
// System.out.println("Title: " + newBook.getTitle());
// Batch update — mettre à jour le rating de tous les livres avec Java Streams
books = bookDao.findAll();
List<Book> updatedEntries = books.stream()
.peek(b -> b.setRating(5)) // peek() modifie chaque livre sans changer le stream
.collect(Collectors.toList());
bookDao.update(updatedEntries);
}
}
Using Stream.peek() for batch update:
peek()allows you to modify each element of the stream without creating a new stream- This is an elegant way to configure the
ratingon all objects before sending them in batch to the database
5.8 Module 5 Summary
- Insert uses
PreparedStatementwithStatement.RETURN_GENERATED_KEYSto retrieve generated ID - Unit update is almost identical to insert (only the SQL differs)
- The batch update groups together several operations to minimize the cost of connections and preparation of statements
- Using Java Streams with
peek()is a modern approach to preparing data before a batch
6. Deleting records
6.1 Delete a record
Deleting a record is basically the same as updating — we use executeUpdate(). The main difference is that the SQL is a DELETE and you can choose to return the number of rows affected.
Returning the number of rows affected is useful for:
- Validate that the deletion has taken place
- Detect unexpected deletions (e.g.: several lines deleted when only one was expected)
- Potentially trigger a rollback if the result is unexpected
DELETE FROM BOOK WHERE ID = ?
6.2 Batch deletes
Just like updates, deletions can be grouped in batches. This use case occurs in particular when:
- Cleaning reporting data after processing
- Merge accounts and remove duplicates
- Archiving and purging temporary data
The code is identical to the batch update, with a DELETE FROM BOOK WHERE ID = ? in place of the UPDATE.
6.3 Final Dao interface with delete
package com.pluralsight.repository;
import java.util.List;
import java.util.Optional;
public interface Dao<T> {
Optional<T> findById(long id);
List<T> findAll();
T create(T t);
T update(T t);
int[] update(List<T> t);
int delete(T t); // Retourne le nombre de lignes supprimées
}
6.4 BookDao with delete method
package com.pluralsight.repository;
import com.pluralsight.model.Book;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class BookDao extends AbstractDao implements Dao<Book> {
@Override
public Optional<Book> findById(long id) {
Optional<Book> book = Optional.empty();
String sql = "SELECT ID, TITLE FROM BOOK WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setLong(1, id);
try (ResultSet rset = prepStmt.executeQuery()) {
Book resBook = new Book();
if (rset.next()) {
resBook.setId(rset.getLong("ID"));
resBook.setTitle(rset.getString("TITLE"));
}
book = Optional.of(resBook);
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public List<Book> findAll() {
List<Book> books = Collections.emptyList();
String sql = "SELECT * FROM BOOK";
try (
Connection con = getConnection();
Statement stmt = con.createStatement();
ResultSet rset = stmt.executeQuery(sql);
) {
books = new ArrayList<>();
while (rset.next()) {
Book book = new Book();
book.setId(rset.getLong("id"));
book.setTitle(rset.getString("title"));
books.add(book);
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return books;
}
@Override
public Book create(Book book) {
String sql = "INSERT INTO BOOK (TITLE) VALUES (?)";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(
sql, Statement.RETURN_GENERATED_KEYS);
) {
prepStmt.setString(1, book.getTitle());
prepStmt.executeUpdate();
try (ResultSet genKeys = prepStmt.getGeneratedKeys()) {
if (genKeys.next()) {
book.setId(genKeys.getLong(1));
}
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public Book update(Book book) {
String sql = "UPDATE BOOK SET TITLE = ? WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setString(1, book.getTitle());
prepStmt.setLong(2, book.getId());
prepStmt.executeUpdate();
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public int[] update(List<Book> books) {
int[] records = {};
String sql = "UPDATE BOOK SET TITLE = ?, RATING = ? WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
for (Book book : books) {
prepStmt.setString(1, book.getTitle());
prepStmt.setInt(2, book.getRating());
prepStmt.setLong(3, book.getId());
prepStmt.addBatch();
}
records = prepStmt.executeBatch();
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return records;
}
@Override
public int delete(Book book) {
int rowsAffected = 0;
String sql = "DELETE FROM BOOK WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setLong(1, book.getId());
rowsAffected = prepStmt.executeUpdate(); // Retourne le nombre de lignes supprimées
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return rowsAffected;
}
}
6.5 App.java — delete demonstration
package com.pluralsight;
import com.pluralsight.model.Book;
import com.pluralsight.repository.BookDao;
import com.pluralsight.repository.Dao;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class App {
public static void main(String[] args) {
Dao<Book> bookDao = new BookDao();
List<Book> books = bookDao.findAll();
for (Book book : books) {
System.out.println("Id: " + book.getId());
System.out.println("Title: " + book.getTitle());
}
Optional<Book> optBook = bookDao.findById(1);
if (optBook.isPresent()) {
Book book = optBook.get();
System.out.println("Id: " + book.getId());
System.out.println("Title: " + book.getTitle());
book.setTitle("Effective Java: Second Edition");
bookDao.update(book);
}
// Créer un livre puis le supprimer immédiatement
Book newBook = new Book();
newBook.setTitle("The River Why");
newBook = bookDao.create(newBook);
System.out.println("Id: " + newBook.getId());
System.out.println("Title: " + newBook.getTitle());
int numDel = bookDao.delete(newBook);
System.out.println("Number of records deleted: " + numDel);
// Batch update commenté pour cette démo
// books = bookDao.findAll();
// List<Book> updatedEntries = books.stream()
// .peek(b -> b.setRating(5))
// .collect(Collectors.toList());
// bookDao.update(updatedEntries);
}
}
6.6 Summary of module 6
- The
DELETEusesexecuteUpdate()— exactly like anUPDATE - Returning the number of rows affected (
int rowsAffected) validates the operation - Deletions can also be grouped in batch (
addBatch()+executeBatch()) Dao<T>interface is now complete with all CRUD operations
7. Architecture and the Template Method Pattern
7.1 The problem — JDBC code duplication
By observing the code written throughout the course, several architectural problems emerge:
1. Code duplication (copy and paste)
For each new JDBC method, the same try-with-resources block is copied: getConnection(), prepareStatement(), ResultSet handling, catch(SQLException). This is the first red flag that something needs to be refactored.
2. The business focus is drowned out Business doesn’t care if you get a connection or prepare a statement. What interests him is the logic of data recovery.
3. Exception handling clutters the code Try-with-resources is a good solution for automatically closing resources, but it visibly burdens each method.
4. Testability is low The current implementation mixes JDBC infrastructure and business logic, making unit tests very difficult to write.
7.2 The Template Method Pattern — the solution
The Template Method Pattern (or a pattern close to the Strategy Pattern) consists of encapsulating the entire redundant JDBC infrastructure in an abstract class, delegating only the variable part (the data mapping) to an abstract method that the subclasses implement.
Benefits obtained:
| Initial problem | Solution provided |
|---|---|
| Code copied and pasted | Wrapped only once in JdbcQueryTemplate |
| Blurred business focus | The abstract method mapItem() contains only business logic |
| Cumbersome Exceptions | Managed only once in the template class |
| Low testability | Better adherence to SOLID principles |
This pattern is directly inspired by what Spring JDBC does internally with JdbcTemplate.
7.3 JdbcQueryTemplate — generic abstract class
package com.pluralsight.repository;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
public abstract class JdbcQueryTemplate<T> extends AbstractDao {
public JdbcQueryTemplate() {
// Constructeur sans arguments — contrôle le contrat de création
}
/**
* Exécute la requête SQL et retourne une liste d'objets T
* en déléguant le mapping de chaque ligne à mapItem().
*/
public List<T> queryForList(String sql) {
List<T> items = new ArrayList<>();
try (
Connection con = getConnection();
Statement stmt = con.createStatement();
ResultSet rset = stmt.executeQuery(sql);
) {
while (rset.next()) {
items.add(mapItem(rset)); // Délègue le mapping à la sous-classe
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return items;
}
/**
* Méthode abstraite que les sous-classes DOIVENT implémenter.
* Elle définit comment transformer une ligne du ResultSet en objet T.
*/
public abstract T mapItem(ResultSet rset) throws SQLException;
}
Architecture:
JdbcQueryTemplate<T>is abstract and generic- It extends
AbstractDaoto accessgetConnection() queryForList()contains all JDBC boilerplatemapItem()is the only variable part — it must be implemented by anonymous classes or subclasses- All try-with-resources, exception handling and iteration of the
ResultSetare encapsulated here
7.4 BookDao refactored with JdbcQueryTemplate
package com.pluralsight.repository;
import com.pluralsight.model.Book;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class BookDao extends AbstractDao implements Dao<Book> {
@Override
public Optional<Book> findById(long id) {
Optional<Book> book = Optional.empty();
String sql = "SELECT ID, TITLE FROM BOOK WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setLong(1, id);
try (ResultSet rset = prepStmt.executeQuery()) {
Book resBook = new Book();
if (rset.next()) {
resBook.setId(rset.getLong("ID"));
resBook.setTitle(rset.getString("TITLE"));
}
book = Optional.of(resBook);
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
/**
* findAll() refactorisé avec JdbcQueryTemplate.
* Tout le boilerplate JDBC disparaît — il ne reste que la logique métier dans mapItem().
*/
@Override
public List<Book> findAll() {
List<Book> books = Collections.emptyList();
// Création d'une instance anonyme de JdbcQueryTemplate<Book>
// On n'implémente QUE la méthode mapItem() — le reste est dans la classe template
JdbcQueryTemplate<Book> template = new JdbcQueryTemplate<Book>() {
@Override
public Book mapItem(ResultSet rset) throws SQLException {
Book book = new Book();
book.setId(rset.getLong("ID"));
book.setTitle(rset.getString("TITLE"));
book.setRating(rset.getInt("RATING"));
return book;
}
};
books = template.queryForList("SELECT ID, TITLE, RATING FROM BOOK");
return books;
}
@Override
public Book create(Book book) {
String sql = "INSERT INTO BOOK (TITLE) VALUES (?)";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(
sql, Statement.RETURN_GENERATED_KEYS);
) {
prepStmt.setString(1, book.getTitle());
prepStmt.executeUpdate();
try (ResultSet genKeys = prepStmt.getGeneratedKeys()) {
if (genKeys.next()) {
book.setId(genKeys.getLong(1));
}
}
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public Book update(Book book) {
String sql = "UPDATE BOOK SET TITLE = ? WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setString(1, book.getTitle());
prepStmt.setLong(2, book.getId());
prepStmt.executeUpdate();
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return book;
}
@Override
public int[] update(List<Book> books) {
int[] records = {};
String sql = "UPDATE BOOK SET TITLE = ?, RATING = ? WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
for (Book book : books) {
prepStmt.setString(1, book.getTitle());
prepStmt.setInt(2, book.getRating());
prepStmt.setLong(3, book.getId());
prepStmt.addBatch();
}
records = prepStmt.executeBatch();
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return records;
}
@Override
public int delete(Book book) {
int rowsAffected = 0;
String sql = "DELETE FROM BOOK WHERE ID = ?";
try (
Connection con = getConnection();
PreparedStatement prepStmt = con.prepareStatement(sql);
) {
prepStmt.setLong(1, book.getId());
rowsAffected = prepStmt.executeUpdate();
} catch (SQLException sqe) {
sqe.printStackTrace();
}
return rowsAffected;
}
}
Before/after comparison for findAll():
| Before (without template) | After (with JdbcQueryTemplate) |
|---|---|
| Connection, Statement, ResultSet, catch | Only mapItem() to implement |
| ~20 boilerplate lines | ~8 lines focused on the profession |
| Infrastructure logic mixed with business | Clear separation of responsibilities |
7.5 App.java final
package com.pluralsight;
import com.pluralsight.model.Book;
import com.pluralsight.repository.BookDao;
import com.pluralsight.repository.Dao;
import java.util.List;
public class App {
public static void main(String[] args) {
Dao<Book> bookDao = new BookDao();
// findAll() utilise maintenant JdbcQueryTemplate en interne
List<Book> books = bookDao.findAll();
for (Book book : books) {
System.out.println("Id: " + book.getId());
System.out.println("Title: " + book.getTitle());
System.out.println("Rating: " + book.getRating());
}
}
}
7.6 Summary of module 7 and conclusion
The last module addressed the architectural issues inherent in naively written JDBC code:
- Duplication — try-with-resources copied and pasted into each method
- Fuzzy business focus — infrastructure logic obscures business logic
- Excessive verbosity — exception handling clutters the code
- Low testability — mixing responsibilities complicates unit testing
The solution — the Template Method Pattern implemented via JdbcQueryTemplate<T> — encapsulates the JDBC boilerplate only once. The abstract method mapItem() becomes the only point of variation, focusing only on what interests the business.
This pattern is directly inspired by what Spring JDBC does with its JdbcTemplate, and the way Hibernate and JPA encapsulate JDBC internally.
8. Summary — Evolution of the project architecture
Final package structure
src/
└── main/
└── java/
└── com/
└── pluralsight/
├── App.java
├── model/
│ └── Book.java
└── repository/
├── AbstractDao.java
├── Dao.java (interface générique)
├── BookDao.java
└── JdbcQueryTemplate.java (module 7)
Evolution of the Dao interface
| Module | Available methods |
|---|---|
| 3 | findAll() |
| 4 | findAll(), findById(long id) |
| 5 | findAll(), findById(), create(T), update(T), update(List<T>) |
| 6 | findAll(), findById(), create(T), update(T), update(List<T>), delete(T) |
Class hierarchy
AbstractDao
└── BookDao implements Dao<Book>
JdbcQueryTemplate<T> extends AbstractDao (abstract)
└── Classes anonymes dans BookDao.findAll()
Docker Commands Reference
# Démarrer la base de données
docker compose up -d
# Vérifier l'état
docker ps
# Voir les logs du conteneur
docker logs library
# Arrêter et supprimer les conteneurs (les données persistent dans .data/)
docker compose down
Reference SQL — BOOK table structure
CREATE TABLE `library_db`.`BOOK` (
`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NULL,
`rating` INT NULL,
PRIMARY KEY (`id`)
);
9. Key Takeaways
- JDBC is the foundation of all data access in Java, even with JPA or Hibernate
- Since JDBC 4.0,
Class.forName()is no longer necessary — the driver is loaded automatically - Always prefer PreparedStatement to generic
Statement— SQL injection protection and better performance - try-with-resources ensures automatic closing of
Connection,StatementandResultSet - Optionals avoid
NullPointerExceptionon query results - batch update (
addBatch()+executeBatch()) is the most efficient solution for bulk operations — establishing the connection and preparing the statement is the most expensive operation - Access to
ResultSetcolumns by name is more robust than by index - The Template Method Pattern applied to JDBC separates the infrastructure from the business code — it is the direct inspiration of
JdbcTemplatein Spring JDBC
Search Terms
data · access · java · se · fundamentals · backend · architecture · full-stack · web · app.java · batch · bookdao · dao · interface · delete · book · jdbc · method · records · update · updates · business · class · configuration