Java JDBI
last modified July 6, 2020
In this tutorial, we explore how to efficiently work with data using JDBI, a powerful, lightweight library that simplifies database interaction in Java. For this example, we use MySQL as our database, but JDBI works with any JDBC-compatible database.
JDBI is built on top of JDBC, making database programming more intuitive and less error-prone. It provides automatic exception handling, efficient resource management, and robust tools for mapping query results directly to Java objects. This significantly reduces boilerplate code and improves maintainability.
At the core of JDBI is the DBI instance, which facilitates
connections to the database through Handle instances. A
Handle represents an active connection to the database, acting as a
wrapper around the standard JDBC Connection object. This
abstraction allows for cleaner transaction management and enhanced control over
queries.
JDBI supports two distinct styles of interaction:
- Fluent API: Provides a more dynamic, expressive way to build queries in a chainable manner, reducing redundant code.
- Object API: Maps SQL query results directly to Java objects, eliminating the need for manual result set processing.
By leveraging JDBI, developers can perform CRUD operations, manage transactions, and integrate with advanced SQL techniques effortlessly. In the upcoming sections, we’ll demonstrate practical examples, including inserting, retrieving, updating, and deleting records using both APIs.
Creating a database in MySQL
In this section, we create a new testdb database in MySQL.
We use the mysql monitor to do the job, but we could use
the NetBeans database tool as well.
DROP TABLE IF EXISTS Cars;
CREATE TABLE Cars(Id INT PRIMARY KEY AUTO_INCREMENT,
Name TEXT, Price INT) ENGINE=InnoDB;
INSERT INTO Cars(Name, Price) VALUES('Audi', 52642);
INSERT INTO Cars(Name, Price) VALUES('Mercedes', 57127);
INSERT INTO Cars(Name, Price) VALUES('Skoda', 9000);
INSERT INTO Cars(Name, Price) VALUES('Volvo', 29000);
INSERT INTO Cars(Name, Price) VALUES('Bentley', 350000);
INSERT INTO Cars(Name, Price) VALUES('Citroen', 21000);
INSERT INTO Cars(Name, Price) VALUES('Hummer', 41400);
INSERT INTO Cars(Name, Price) VALUES('Volkswagen', 21600);
This is the SQL to create the Cars table in MySQL.
To create the database and the table, we use the mysql
monitor tool.
$ sudo service mysql start
MySQL is started with sudo service mysql start command.
$ mysql -u testuser -p
We connect to the database with the mysql monitor.
mysql> CREATE DATABASE testdb; Query OK, 1 row affected (0.02 sec)
With the CREATE DATABASE statement, a new database is created.
mysql> USE testdb; mysql> SOURCE cars_mysql.sql
With the source command, we load and execute the cars_mysql.sql
file.
mysql> SELECT * FROM Cars; +----+------------+--------+ | Id | Name | Price | +----+------------+--------+ | 1 | Audi | 52642 | | 2 | Mercedes | 57127 | | 3 | Skoda | 9000 | | 4 | Volvo | 29000 | | 5 | Bentley | 350000 | | 6 | Citroen | 21000 | | 7 | Hummer | 41400 | | 8 | Volkswagen | 21600 | +----+------------+--------+ 8 rows in set (0.00 sec)
We verify the data.
The pom.xml file
The examples will use the following Maven POM file:
<?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.zetcode</groupId>
<artifactId>JDBIEx</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi</artifactId>
<version>2.73</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
</dependencies>
</project>
We have defined dependencies for the JDBI library and the MySQL driver.
Fluent API
In the following examples, we will use the JDBI Fluent API to work with the MySQL database.
Retrieving all cars
In the first example, we fetch all cars from the Cars table.
package com.zetcode;
import java.util.List;
import java.util.Map;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.Query;
public class JDBIEx {
public static void main(String[] args) {
Handle handle = null;
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
String sql = "SELECT * FROM Cars";
try {
handle = dbi.open();
Query<Map<String, Object>> q = handle.createQuery(sql);
List<Map<String, Object>> l = q.list();
for (Map<String, Object> m : l) {
System.out.printf("%d ", m.get("Id"));
System.out.printf("%s ", m.get("Name"));
System.out.println(m.get("Price"));
}
} finally {
if (handle != null) {
handle.close();
}
}
}
}
The example connects to the testdb database and retrieves all
cars from the Cars table.
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
An access point is created to the database with the DBI class.
handle = dbi.open();
A Handle to the database is created with the DBI's
open method. It represents a connection to the database.
A connection to the database is created with the DriverManager.
Query<Map<String, Object>> q = handle.createQuery(sql);
A Query object is created with the createQuery method.
List<Map<String, Object>> l = q.list();
From the query object, we get a list of key/value pairs.
for (Map<String, Object> m : l) {
System.out.printf("%d ", m.get("Id"));
System.out.printf("%s ", m.get("Name"));
System.out.println(m.get("Price"));
}
We go through the list and print all columns.
} finally {
if (handle != null) {
handle.close();
}
}
In the end, we close the handle.
1 Audi 52642 2 Mercedes 57127 3 Skoda 9000 4 Volvo 29000 5 Bentley 350000 6 Citroen 21000 7 Hummer 41400 8 Volkswagen 21600
This is the output of the example.
Retrieving a car by its ID
In the next example, we fetch a car name from the Cars
table by its ID.
package com.zetcode;
import java.util.Map;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.Query;
import org.skife.jdbi.v2.util.StringColumnMapper;
public class JDBIEx2 {
public static void main(String[] args) {
Handle handle = null;
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
try {
handle = dbi.open();
String sql = "SELECT Name FROM Cars WHERE Id = ?";
Query<Map<String, Object>> q = handle.createQuery(sql);
q.bind(0, 1);
String carName = q.map(StringColumnMapper.INSTANCE).first();
System.out.println(carName);
} finally {
if (handle != null) {
handle.close();
}
}
}
}
In the example, we select a car name from the Cars table. The
SQL query takes a parameter which is bound later.
String sql = "SELECT Name FROM Cars WHERE Id = ?";
This is SQL code for selecting a car name from the table. The question mark is a token to be filled later in code.
Query<Map<String, Object>> q = handle.createQuery(sql);
A new Query object is created from the SQL statement.
q.bind(0, 1);
With the bind method, we bind the missing parameter.
The parameter is bound positionally.
String carName = q.map(StringColumnMapper.INSTANCE).first();
We map a column of the result set with the StringColumnMapper
to the string type. The first method is used for returning one
value.
System.out.println(carName);
The name of the car is printed to the console.
Data source
In this example, we connect to the database using a data source. The usage of a data source improves application's performance and scalability.
# mysql properties mysql.driver=com.mysql.jdbc.Driver mysql.url=jdbc:mysql://localhost:3306/testdb mysql.username=testuser mysql.password=test623
In the db.properties file, we have the connection properties.
The file is placed in the project's Resources directory.
package com.zetcode;
import com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.Query;
import org.skife.jdbi.v2.util.IntegerColumnMapper;
public class JDBIEx3 {
public static MysqlDataSource getMySQLDataSource() throws
FileNotFoundException, IOException {
Properties props = new Properties();
FileInputStream fis = null;
MysqlDataSource ds = null;
fis = new FileInputStream("src/main/Resources/db.properties");
props.load(fis);
ds = new MysqlConnectionPoolDataSource();
ds.setURL(props.getProperty("mysql.url"));
ds.setUser(props.getProperty("mysql.username"));
ds.setPassword(props.getProperty("mysql.password"));
return ds;
}
public static void main(String[] args) throws IOException {
Handle handle = null;
MysqlDataSource ds = getMySQLDataSource();
DBI dbi = new DBI(ds);
try {
handle = dbi.open();
String sql = "SELECT Price FROM Cars WHERE Id = ?";
Query<Map<String, Object>> q = handle.createQuery(sql);
q.bind(0, 1);
Integer price = q.map(IntegerColumnMapper.WRAPPER).first();
System.out.println(price);
} finally {
if (handle != null) {
handle.close();
}
}
}
}
The example selects the price of a car found by its ID.
fis = new FileInputStream("src/main/Resources/db.properties");
props.load(fis);
We load the properties from the Resources directory.
ds = new MysqlConnectionPoolDataSource();
ds.setURL(props.getProperty("mysql.url"));
ds.setUser(props.getProperty("mysql.username"));
ds.setPassword(props.getProperty("mysql.password"));
A MysqlConnectionPoolDataSource is created. We set the parameters
from the properties file.
Integer price = q.map(IntegerColumnMapper.WRAPPER).first();
Since the SQL query returns an integer, we use the IntegerColumnMapper class.
The withHandle method
The DBI class has a convenience method called withHandle, which
manages the lifecycle of a handle and yields it to a callback for use by clients.
package com.zetcode;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.util.IntegerColumnMapper;
public class JDBIEx4 {
public static void main(String[] args) {
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
String sql = "SELECT Price FROM Cars WHERE Id = :id";
int id = 3;
Integer price = dbi.withHandle((Handle h) -> {
return h.createQuery(sql)
.map(IntegerColumnMapper.WRAPPER)
.bind("id", id)
.first();
});
System.out.println(price);
}
}
The example selects a price of a car identified by its ID.
String sql = "SELECT Price FROM Cars WHERE Id = :id";
This SQL query uses a named parameter.
Integer price = dbi.withHandle((Handle h) -> {
return h.createQuery(sql)
.map(IntegerColumnMapper.WRAPPER)
.bind("id", id)
.first();
});
A query is created and executed without us having to worry about closing the handle.
Mapping custom classes
It is possible to map custom classes to the result sets. The mapping
class must implement the ResultSetMapper<T> interface.
package com.zetcode;
public class Car {
private Long Id;
private String Name;
private int Price;
public Car(Long Id, String Name, int Price) {
this.Id = Id;
this.Name = Name;
this.Price = Price;
}
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 int getPrice() {
return Price;
}
public void setPrice(int Price) {
this.Price = Price;
}
@Override
public String toString() {
return "Car{" + "Id=" + Id + ", Name=" + Name + ", Price=" + Price + '}';
}
}
This is a custom Car class to which we are going to
map the result set.
package com.zetcode;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
public class CarMapper implements ResultSetMapper<Car> {
@Override
public Car map(int idx, ResultSet rs, StatementContext ctx) throws SQLException {
return new Car(rs.getLong("Id"), rs.getString("Name"), rs.getInt("Price"));
}
}
We provide the mapping class. It returns a new Car object filled with
the data from the result set.
package com.zetcode;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
public class JDBIEx5 {
public static void main(String[] args) {
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
String sql = "SELECT * FROM Cars WHERE Id = :id";
int id = 3;
Car car = dbi.withHandle((Handle h) -> {
return h.createQuery(sql)
.map(new CarMapper())
.bind("id", id)
.first();
});
System.out.println(car);
}
}
The example selects a Car object from the table
identified by its ID.
Car car = dbi.withHandle((Handle h) -> {
return h.createQuery(sql)
.map(new CarMapper())
.bind("id", id)
.first();
});
A custom CarMapper object is passed to the map
method.
Batch operations
Batch processing allows us to group related SQL statements into a batch and submit them with one call to the database. This can significantly improve the performance of our application.
Batch operations are not atomic; they do not provide an all or nothing solution. For instance, if we create an incorrect INSERT statement, it fails, but other INSERT statements are executed.
package com.zetcode;
import org.skife.jdbi.v2.Batch;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
public class JDBIEx6 {
public static void main(String[] args) {
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
Handle handle = dbi.open();
Batch batch = handle.createBatch();
batch.add("DROP TABLE IF EXISTS Friends");
batch.add("CREATE TABLE Friends(Id INT AUTO_INCREMENT PRIMARY KEY, Name TEXT)");
batch.add("INSERT INTO Friends(Name) VALUES ('Monika')");
batch.add("INSERT INTO Friends(Name) VALUES ('Tom')");
batch.add("INSERT INTO Friends(Name) VALUES ('Jane')");
batch.add("INSERT INTO Friends(Name) VALUES ('Robert')");
batch.execute();
}
}
The example creates a new Friends table. The SQL commands are
grouped into one batch operation.
Batch batch = handle.createBatch();
A Batch represents a group of non-prepared statements; it is
created with the createBatch method.
batch.add("DROP TABLE IF EXISTS Friends");
The add method adds a statement to the batch.
batch.execute();
The batch is executed with the execute method.
Transactions
A transaction is an atomic unit of database operations against the data in one or more databases. The effects of all the SQL statements in a transaction can be either all committed to the database or all rolled back.
Also note that in MySQL, DDL statements such as DROP TABLE and CREATE TABLE cause an implicit commit to a transaction.
package com.zetcode;
import org.skife.jdbi.v2.Batch;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.TransactionStatus;
import org.skife.jdbi.v2.VoidTransactionCallback;
public class JDBIEx7 {
public static void main(String[] args) {
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
dbi.inTransaction(new VoidTransactionCallback() {
@Override
protected void execute(Handle handle, TransactionStatus status)
throws Exception {
Batch batch = handle.createBatch();
batch.add("DROP TABLE IF EXISTS Friends");
batch.add("CREATE TABLE Friends(Id INT AUTO_INCREMENT PRIMARY KEY, Name TEXT)");
batch.add("INSERT INTO Friends(Name) VALUES ('Monika')");
batch.add("INSERT INTO Friends(Name) VALUES ('Tom')");
batch.add("INSERT INTO Friends(Name) VALUES ('Jane')");
batch.add("INSERT INTO Friends(Name) VALUES ('Robert')");
batch.execute();
}
});
}
}
The example places a batch operation in a transaction. Due to the implicit commits of DDL statements in MYSQL, only the INSERT statements are in a all or nothing mode.
dbi.inTransaction(new VoidTransactionCallback() {
@Override
protected void execute(Handle handle, TransactionStatus status)
throws Exception {
...
}
});
A transaction is created with the inTransaction method. The VoidTransactionCallback
is a transaction callback that does not return a value.
SQL Object API
The SQL Object API provides a declarative mechanism for common JDBI operations.
To use the SQL Object API, we create an interface or an abstract class with annotations
such as @SqlQuery or @SqlUpdate.
Simple example
We create an example where we will create simple queries utilizing the SQL Object API.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.8</version>
</dependency>
In the example, we also use the lombok library, which
reduces some boilerplate code.
package com.zetcode;
import lombok.Data;
@Data
public class Car {
private final Long Id;
private final String Name;
private final int Price;
}
The Car class is decorated with the lombok's @Data
annotation. It will automatically create the getter and setter methods,
equals method, toString method,
hashCode method, and an arguments constructor.
package com.zetcode;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
public class CarMapper implements ResultSetMapper<Car> {
@Override
public Car map(int idx, ResultSet rs, StatementContext ctx) throws SQLException {
return new Car(rs.getLong("Id"), rs.getString("Name"), rs.getInt("Price"));
}
}
The CarMapper maps the result set to the Car class.
package com.zetcode;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
public interface MyDAO {
@SqlQuery("SELECT * FROM Cars WHERE Id = :id")
@Mapper(CarMapper.class)
Car findById(@Bind("id") int id);
@SqlQuery("SELECT COUNT(Id) FROM Cars")
int countCars();
}
Here we have a MyDAO interface decorated with two
@SqlQuery annotations. The methods find a car by its ID
and count all cars in the table.
@SqlQuery("SELECT * FROM Cars WHERE Id = :id")
The @SqlQuery annotation indicates that the method
executes the specified query.
@Mapper(CarMapper.class)
The @Mapper specifies the result set mapper on a query method.
Car findById(@Bind("id") int id);
The @Bind annotation binds the method's argument to the SQL
query parameter.
package com.zetcode;
import org.skife.jdbi.v2.DBI;
public class JDBIEx8 {
public static void main(String[] args) {
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
int id = 3;
MyDAO dao = dbi.onDemand(MyDAO.class);
Car car = dao.findById(id);
System.out.println(car);
int nCars = dao.countCars();
System.out.printf("There are %d cars in the table", nCars);
}
}
In this client application, we find a car with ID equal to 3 and count all cars in the table.
MyDAO dao = dbi.onDemand(MyDAO.class);
The onDemand method creates a new sql object which will obtain and
release connections from this dbi instance, as it needs to, and can, respectively.
We should not explicitly close this sql object.
Car car = dao.findById(id);
We get a car with the specified ID.
int nCars = dao.countCars();
We count the number of cars in the database table.
Transactions
In SQL Object API we can use the @Transaction annotation
to create a transaction.
CREATE TABLE IF NOT EXISTS Authors(Id BIGINT PRIMARY KEY AUTO_INCREMENT,
Name VARCHAR(25)) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS Books(Id BIGINT PRIMARY KEY AUTO_INCREMENT,
AuthorId BIGINT, Title VARCHAR(100),
FOREIGN KEY(AuthorId) REFERENCES Authors(Id) ON DELETE CASCADE)
ENGINE=InnoDB;
For this example, we create two tables: Authors and Books.
package com.zetcode;
import java.util.List;
import org.skife.jdbi.v2.exceptions.TransactionFailedException;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction;
public abstract class MyDAO {
@SqlUpdate("INSERT INTO Authors(Name) VALUES(:author)")
public abstract void createAuthor(@Bind("author") String author);
@SqlQuery("SELECT Id FROM Authors WHERE Name = :name")
abstract long getAuthorId(@Bind("name") String name);
@SqlUpdate("INSERT INTO Books(AuthorId, Title) VALUES(:authorId, :title)")
abstract void insertBook(@Bind("authorId") Long authorId, @Bind("title") String title);
@Transaction
public void insertBooksForAuthor(String author, List<String> titles) {
Long authorId = getAuthorId(author);
if (authorId == null) {
throw new TransactionFailedException("No author found");
}
for (String title : titles) {
insertBook(authorId, title);
}
}
}
We have an abstract MyDAO class, where we utilize the @SqlUpdate, @SqlQuery,
and @Transaction annotations.
@SqlUpdate("INSERT INTO Authors(Name) VALUES(:author)")
public abstract void createAuthor(@Bind("author") String author);
This method adds a new author.
@SqlQuery("SELECT Id FROM Authors WHERE Name = :name")
abstract long getAuthorId(@Bind("name") String name);
The getAuthorId is used to get the ID of an author.
The ID is needed when we insert new books into the Books table.
@SqlUpdate("INSERT INTO Books(AuthorId, Title) VALUES(:authorId, :title)")
abstract void insertBook(@Bind("authorId") Long authorId, @Bind("title") String title);
The insertBook methods inserts a single book into the Books
table.
@Transaction
public void insertBooksForAuthor(String author, List<String> titles) {
Long authorId = getAuthorId(author);
if (authorId == null) {
throw new TransactionFailedException("No author found");
}
for (String title : titles) {
insertBook(authorId, title);
}
}
The @Transaction annotation causes the insertBooksForAuthor
to be run within a transaction. So either all books are inserted or none.
package com.zetcode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.skife.jdbi.v2.DBI;
public class JDBIEx9 {
public static void main(String[] args) {
DBI dbi = new DBI("jdbc:mysql://localhost:3306/testdb",
"testuser", "test623");
List<Map<String, List<String>>> authorsBooks = new ArrayList<>();
Map<String, List<String>> autMap1 = new HashMap<>();
List<String> books1 = new ArrayList<>();
books1.add("Call of the Wild");
books1.add("Martin Eden");
books1.add("The Iron Heel");
books1.add("White Fang");
autMap1.put("Jack London", books1);
Map<String, List<String>> autMap2 = new HashMap<>();
List<String> books2 = new ArrayList<>();
books2.add("Father Goriot");
books2.add("Colonel Chabert");
books2.add("Cousing Pons");
autMap2.put("Honore de Balzac", books2);
authorsBooks.add(autMap1);
authorsBooks.add(autMap2);
MyDAO dao = dbi.onDemand(MyDAO.class);
for (Map<String, List<String>> map : authorsBooks) {
Set<String> ks = map.keySet();
for (String author : ks) {
dao.createAuthor(author);
List<String> titles = map.get(author);
dao.insertBooksForAuthor(author, titles);
}
}
}
}
The example inserts two authors and their books into the database.
In this tutorial, we have presented the JDBI library. ZetCode has the following related tutorials: Java tutorial, MySQL Java tutorial, and MySQL tutorial.