ZetCode

Java JDBI

last modified July 6, 2020

In this tutorial, we show how to work with data with JDBI. We chose MySQL for our database. ZetCode has a complete e-book for MySQL Java, which contains a JDBI chapter: MySQL Java programming e-book.

JDBI is a convenience library built on top of JDBC. It makes database programming much easier. It manages exceptions. It has tools for automatic resource management and mapping result sets to classes. JDBI is conceptually similar to the Spring's JdbcTemplate, for which ZetCode has a tutorial.

The DBI instance provides connections to the database via Handle instances. Handle represents a connection to the database system; it is a wrapper around a JDBC Connection object.

JDBI provides two different style APIs: fluent style and an object style.

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.

cars_mysql.sql
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:

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

JDBIEx.java
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.

JDBIEx2.java
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.

db.properties
# 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.

Database properties
Figure: Database properties

The file is placed in the project's Resources directory.

JDBIEx3.java
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.

JDBIEx4.java
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.

Car.java
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.

CarMapper.java
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.

JDBIEx5.java
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.

JDBIEx6.java
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.

JDBIEx7.java
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.

Car.java
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.

CarMapper.java
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.

MyDAO.java
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.

JDBIEx8.java
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.

authors_books.sql
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.

MyDAO.java
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.

JDBIEx9.java
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.