ZetCode

Java Jdbi

上次修改时间:2025 年 6 月 6 日

在本教程中,我们将探讨如何使用 JDBI 有效地处理数据,JDBI 是一个功能强大且轻量级的库,它简化了 Java 中的数据库交互。

JDBI 构建于 JDBC 之上,使数据库编程更加直观且不易出错。 它提供自动异常处理、高效的资源管理以及强大的工具,可将查询结果直接映射到 Java 对象。 这大大减少了样板代码并提高了可维护性。

JDBI 的核心是 DBI 实例,它通过 Handle 实例促进与数据库的连接。 Handle 表示与数据库的活动连接,充当标准 JDBC Connection 对象的包装器。 这种抽象允许更简洁的事务管理和对查询的增强控制。

JDBI 支持两种不同的交互方式

通过利用 JDBI,开发人员可以轻松地执行 CRUD 操作、管理事务以及与高级 SQL 技术集成。 在接下来的章节中,我们将演示实际示例,包括使用两个 API 插入、检索、更新和删除记录。

在示例中,我们使用 H2 和 PostgreSQL 数据库。

<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-core</artifactId>
    <version>3.45.2</version>
</dependency>

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.3</version>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

对于这些示例,我们需要以下构件:jdbi3-corepostgresqlh2

简单查询

在下一个示例中,我们将执行一个简单的查询。

Main.java
import org.jdbi.v3.core.Jdbi;

void main() {

    String jdbcUrl = "jdbc:h2:mem:";

    Jdbi jdbi = Jdbi.create(jdbcUrl);

    int res = jdbi.withHandle(handle -> handle.createQuery("SELECT 2 + 2")
            .mapTo(Integer.class)
            .one());

    System.out.println(res);
}

该示例创建了一个内存中的 H2 数据库并执行 SELECT 2 + 2 语句。

String jdbcUrl = "jdbc:h2:mem:";

这是与内存中的 H2 数据库的连接。

Jdbi jdbi = Jdbi.create(jdbcUrl);

我们使用 Jdbi.create 创建主入口点。 它是 JDBC 数据源的可配置包装器。 使用它来获取 Handle 实例,并为从中获得的所有句柄提供配置。

int res = jdbi.withHandle(handle -> handle.createQuery("SELECT 2 + 2")
        .mapTo(Integer.class)
        .one());

withHandle 是一个方便的函数,它管理句柄的生命周期并将其传递给回调以供客户端使用。 Handle 表示与数据库系统的连接。 它是 JDBC Connection 对象的包装器。 Handle 提供了用于事务管理、语句创建以及与数据库会话相关的其他操作的基本方法。

该查询返回一个整数,因此我们使用 mapTo 将结果映射为一个整数。 由于返回的值是标量,因此我们调用 one。 它返回结果集中唯一的行。 如果行本身为 null,则返回 null。

$ java Main.java
4

调用数据库函数

我们调用 H2VERSION 函数,该函数返回 H2 数据库的版本。

Main.java
import org.jdbi.v3.core.Jdbi;

void main() {

    String jdbcUrl = "jdbc:h2:mem:";

    Jdbi jdbi = Jdbi.create(jdbcUrl);

    String res = jdbi.withHandle(handle -> handle.createQuery("SELECT H2VERSION()")
            .mapTo(String.class)
            .one());

    System.out.println(res);
}

由于该函数返回一个字符串,因此我们使用 mapTo 将结果映射为一个字符串。

$ java Main.java 
2.2.224

对于 PostgreSQL,VERSION 数据库函数返回数据库版本。

Main.java
import org.jdbi.v3.core.Jdbi;

void main() {

    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);

    String res = jdbi.withHandle(handle -> handle.createQuery("SELECT VERSION()")
            .mapTo(String.class)
            .one());

    System.out.println(res);
}

在此示例中,我们还提供了用户名和密码。

$ java Main.java 
PostgreSQL 16.0, compiled by Visual C++ build 1935, 64-bit

批处理

批处理是指作为单个单元提交和执行的一组 SQL 语句。 此功能提供了性能优势,并简化了某些数据库操作的代码。

Main.java
import org.jdbi.v3.core.Jdbi;

void main() {

    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);

    jdbi.withHandle(handle -> handle.createBatch()
            .add("DROP TABLE IF EXISTS cars")
            .add("CREATE TABLE cars(id serial PRIMARY KEY, name VARCHAR(255), price INT)")
            .add("INSERT INTO cars(name, price) VALUES('Audi',52642)")
            .add("INSERT INTO cars(name, price) VALUES('Mercedes',57127)")
            .add("INSERT INTO cars(name, price) VALUES('Skoda',9000)")
            .add("INSERT INTO cars(name, price) VALUES('Volvo',29000)")
            .add("INSERT INTO cars(name, price) VALUES('Bentley',350000)")
            .add("INSERT INTO cars(name, price) VALUES('Citroen',21000)")
            .add("INSERT INTO cars(name, price) VALUES('Hummer',41400)")
            .add("INSERT INTO cars(name, price) VALUES('Volkswagen',21600)")

            .execute());

    System.out.println("Table created and data inserted using JDBI batch.");
}

该示例在数据库中创建一个 cars 表。 使用 createBatch 方法创建一个新批处理。 使用 add 方法添加语句。 最后,使用 execute 运行整个批处理。

one 方法

查询使用 select 执行。 当您期望结果恰好包含一行时,one 方法返回。 仅当返回的行映射到 null 时,此方法才返回 null,如果结果有零行或多行,则抛出异常。

Main.java
import org.jdbi.v3.core.Jdbi;

void main() {

    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);

    String res = jdbi.withHandle(handle -> 
            handle.select("SELECT name FROM cars WHERE id = ?", 3)
                    .mapTo(String.class)
                    .one());


    System.out.println(res);
}

该示例返回指定 Id 的汽车名称。

$ java Main.java
Skoda

findOne 方法

findOne 方法返回一个 Optional

Main.java
import org.jdbi.v3.core.Jdbi;

import java.util.Optional;

void main() {

    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);

    int id = 3;

    Optional<String> res = jdbi.withHandle(handle ->
            handle.select("SELECT name FROM cars WHERE id = ?", id)
                    .mapTo(String.class)
                    .findOne());

    res.ifPresentOrElse(System.out::println, () -> System.out.println("N/A"));
}

该示例使用 Optional 类型作为返回值。

参数绑定

参数绑定是一种防止 SQL 注入漏洞并提高代码可读性的机制。 您可以将值绑定到 SQL 语句中的占位符,JDBI 会处理为数据库执行设置实际值。

绑定类型

以下是一个位置绑定示例

Main.java
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;

import java.util.Optional;

void main() {

    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);

    int id = 3;

    Optional<Car> res = jdbi.withHandle(handle -> 
            handle.select("SELECT * FROM cars WHERE id = ?", id)
                    .registerRowMapper(Car.class, ConstructorMapper.of(Car.class))
                    .mapTo(Car.class)
                    .findOne());

    res.ifPresentOrElse(System.out::println, () -> System.out.println("N/A"));
}

public record Car(int id, String name, int price) {
}

该示例将一行映射到 Car 记录。 registerRowMapper 方法为 Car 注册一个自动映射器。


以下示例使用命名绑定。

Main.java
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;

import java.util.Optional;

void main() {

    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);

    int id = 3;

    Optional<Car> res = jdbi.withHandle(handle -> 
            handle.select("SELECT * FROM cars WHERE id = :id")
                    .registerRowMapper(Car.class, ConstructorMapper.of(Car.class))
                    .bind("id", id)
                    .mapTo(Car.class)
                    .findOne());

    res.ifPresentOrElse(System.out::println, () -> System.out.println("N/A"));
}

public record Car(int id, String name, int price) {
}

此外,我们不是在 select 方法中传递值,而是调用 bind 方法,我们在其中将 id 名称绑定到 id 变量的值。

$ java Main.java
Car[id=3, name=Skoda, price=9000]

将行映射到列表

在下一个示例中,我们将多行映射到汽车列表。

Main.java
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;

import java.util.List;

void main() {

    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);

    List<Car> cars = jdbi.withHandle(handle -> 
            handle.select("SELECT * FROM cars")
                    .registerRowMapper(Car.class, ConstructorMapper.of(Car.class))
                    .mapTo(Car.class)
                    .list());

    if (cars.isEmpty()) {

        System.out.println("No cars found.");
    } else {
        
        System.out.println("List of cars:");
        cars.forEach(System.out::println);
    }
}

public record Car(int id, String name, int price) {
}

在该示例中,我们从表中选择所有汽车。 我们将返回的行映射到汽车列表。

$ java Main.java
List of cars:
Car[id=1, name=Audi, price=52642]
Car[id=2, name=Mercedes, price=57127]
Car[id=3, name=Skoda, price=9000]
Car[id=4, name=Volvo, price=29000]
Car[id=5, name=Bentley, price=350000]
Car[id=6, name=Citroen, price=21000]
Car[id=7, name=Hummer, price=41400]
Car[id=8, name=Volkswagen, price=21600]

事务管理

JDBI 使事务管理变得容易。 您可以在事务中执行多个操作,并在发生错误时回滚。

Main.java
import org.jdbi.v3.core.Jdbi;

void main() {
    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);

    try {
        jdbi.useTransaction(handle -> {
            handle.execute("INSERT INTO cars(name, price) VALUES('Toyota', 25000)");
            handle.execute("INSERT INTO cars(name, price) VALUES('Mazda', 22000)");
            // Uncomment the next line to simulate an error and trigger rollback
            // if (true) throw new RuntimeException("Simulated error");
        });
        System.out.println("Transaction committed.");
    } catch (Exception e) {
        System.out.println("Transaction rolled back: " + e.getMessage());
    }
}

useTransaction 方法确保所有语句都在事务中执行。 如果抛出异常,则事务将回滚。

自定义 RowMapper

您可以创建一个自定义 RowMapper,以将复杂的查询结果映射到 Java 对象,例如在连接表时。

Main.java
package com.zetcode;

import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import org.h2.jdbcx.JdbcDataSource;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

class CarWithOwner {
    public final String carName;
    public final String ownerName;

    public CarWithOwner(String carName, String ownerName) {
        this.carName = carName;
        this.ownerName = ownerName;
    }

    public String toString() {
        return carName + " owned by " + ownerName;
    }
}

class CarWithOwnerMapper implements RowMapper<CarWithOwner> {
    @Override
    public CarWithOwner map(ResultSet rs, StatementContext ctx) throws SQLException {
        return new CarWithOwner(rs.getString("car_name"), rs.getString("owner_name"));
    }
}

public class Main {
    public static void main(String[] args) {
        // Setup H2 Database
        JdbcDataSource dataSource = new JdbcDataSource();
        dataSource.setURL("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1");
        dataSource.setUser("sa");
        dataSource.setPassword("");

        Jdbi jdbi = Jdbi.create(dataSource);

        // Create tables
        jdbi.useHandle(handle -> {
            handle.execute("""
                CREATE TABLE owners (
                    id BIGINT AUTO_INCREMENT PRIMARY KEY,
                    name VARCHAR(255)
                )
            """);

            handle.execute("""
                CREATE TABLE cars (
                    id BIGINT AUTO_INCREMENT PRIMARY KEY,
                    name VARCHAR(255),
                    owner_id BIGINT,
                    FOREIGN KEY (owner_id) REFERENCES owners(id)
                )
            """);

            // Insert test data
            handle.execute("INSERT INTO owners (name) VALUES (?)", "Alice");
            handle.execute("INSERT INTO owners (name) VALUES (?)", "Bob");

            handle.execute("INSERT INTO cars (name, owner_id) VALUES (?, ?)", "Toyota", 1);
            handle.execute("INSERT INTO cars (name, owner_id) VALUES (?, ?)", "Honda", 2);
        });

        // Query data with JDBI
        List<CarWithOwner> list = jdbi.withHandle(handle ->
            handle.createQuery("SELECT c.name AS car_name, o.name AS owner_name FROM cars c JOIN owners o ON c.owner_id = o.id")
                .map(new CarWithOwnerMapper())
                .list()
        );

        // Print results
        list.forEach(System.out::println);
    }
}

本示例演示如何使用自定义 RowMapper 将连接的表结果映射到自定义对象。

SqlObjects

Jdbi SqlObjects 是 Jdbi 库的扩展,提供了一种声明式方式来与 Java 中的关系数据库进行交互。

<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-sqlobject</artifactId>
    <version>3.45.2</version>
</dependency>

我们需要添加 jdbi3-sqlobject 依赖项。

Main.java
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;
import org.jdbi.v3.sqlobject.SqlObject;
import org.jdbi.v3.sqlobject.SqlObjectPlugin;
import org.jdbi.v3.sqlobject.statement.SqlQuery;

import java.util.Optional;

void main() {

    String jdbcUrl = "jdbc:postgresql://:5432/testdb";
    String user = "postgres";
    String password = "s$cret";

    Jdbi jdbi = Jdbi.create(jdbcUrl, user, password);
    jdbi.installPlugin(new SqlObjectPlugin());

    jdbi.registerRowMapper(Car.class, ConstructorMapper.of(Car.class));
    CarDao carDao = jdbi.onDemand(CarDao.class);

    int searchId = 2;
    Optional<Car> car = carDao.findById(searchId);

    if (car.isPresent()) {
        System.out.println("Car found: " + car.get());
    } else {
        System.out.println("Car with id " + searchId + " not found.");
    }

}

public record Car(int id, String name, int price) {
}

public interface CarDao extends SqlObject {

    @SqlQuery("SELECT * FROM cars WHERE id = ?")
    Optional<Car> findById(int id);
}

在该程序中,我们使用 CarDao 接口中函数声明的 @SqlQuery 定义查询。

CarDao carDao = jdbi.onDemand(CarDao.class);

DAO 对象使用 onDemand 创建。

int searchId = 2;
Optional<Car> car = carDao.findById(searchId);

我们在 DAO 对象上调用 findById

来源

JDBI 文档.

在本文中,我们使用了 Java Jdbi 库。

作者

我的名字是 Jan Bodnar,我是一位充满激情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程