Contents

Apache Commons DbUtils 简介

1.概述

Apache Commons DbUtils 是一个小型库,它使使用 JDBC 变得更加容易。

在本文中,我们将实施示例来展示其特性和功能。

2. 设置

2.1. Maven 依赖项

首先,我们需要将commons-dbutilsh2依赖添加到我们的pom.xml中:

<dependency>
    <groupId>commons-dbutils</groupId>
    <artifactId>commons-dbutils</artifactId>
    <version>1.6</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.196</version>
</dependency>

你可以在 Maven Central 上找到最新版本的commons-dbutilsh2 。

2.2. 测试数据库

有了我们的依赖关系,让我们创建一个脚本来创建我们将使用的表和记录:

CREATE TABLE employee(
    id int NOT NULL PRIMARY KEY auto_increment,
    firstname varchar(255),
    lastname varchar(255),
    salary double,
    hireddate date,
);
CREATE TABLE email(
    id int NOT NULL PRIMARY KEY auto_increment,
    employeeid int,
    address varchar(255)
);
INSERT INTO employee (firstname,lastname,salary,hireddate)
  VALUES ('John', 'Doe', 10000.10, to_date('01-01-2001','dd-mm-yyyy'));
// ...
INSERT INTO email (employeeid,address)
  VALUES (1, '[[email protected]](/cdn_cgi/l/email_protection)');
// ...

本文中的所有示例测试用例都将使用新创建的与 H2 内存数据库的连接:

public class DbUtilsUnitTest {
    private Connection connection;
    @Before
    public void setupDB() throws Exception {
        Class.forName("org.h2.Driver");
        String db
          = "jdbc:h2:mem:;INIT=runscript from 'classpath:/employees.sql'";
        connection = DriverManager.getConnection(db);
    }
    @After
    public void closeBD() {
        DbUtils.closeQuietly(connection);
    }
    // ...
}

23. POJO

最后,我们需要两个简单的类:

public class Employee {
    private Integer id;
    private String firstName;
    private String lastName;
    private Double salary;
    private Date hiredDate;
    // standard constructors, getters, and setters
}
public class Email {
    private Integer id;
    private Integer employeeId;
    private String address;
    // standard constructors, getters, and setters
}

3. 简介

DbUtils 库提供QueryRunner类作为大多数可用功能的主要入口点

此类通过接收到数据库的连接、要执行的 SQL 语句以及为查询的占位符提供值的可选参数列表来工作。

正如我们稍后将看到的,一些方法还接收一个ResultSetHandler实现——它负责将ResultSet实例转换为我们的应用程序期望的对象。

当然,该库已经提供了几种处理最常见转换的实现,例如列表、映射和 JavaBean。

4. 查询数据

现在我们知道了基础知识,我们已经准备好查询我们的数据库了。

让我们从一个使用MapListHandler获取数据库中所有记录作为示例开始:

@Test
public void givenResultHandler_whenExecutingQuery_thenExpectedList()
  throws SQLException {
    MapListHandler beanListHandler = new MapListHandler();
    QueryRunner runner = new QueryRunner();
    List<Map<String, Object>> list
      = runner.query(connection, "SELECT * FROM employee", beanListHandler);
    assertEquals(list.size(), 5);
    assertEquals(list.get(0).get("firstname"), "John");
    assertEquals(list.get(4).get("firstname"), "Christian");
}

接下来,这是一个使用BeanListHandler将结果转换为Employee实例的示例:

@Test
public void givenResultHandler_whenExecutingQuery_thenEmployeeList()
  throws SQLException {
    BeanListHandler<Employee> beanListHandler
      = new BeanListHandler<>(Employee.class);
    QueryRunner runner = new QueryRunner();
    List<Employee> employeeList
      = runner.query(connection, "SELECT * FROM employee", beanListHandler);
    assertEquals(employeeList.size(), 5);
    assertEquals(employeeList.get(0).getFirstName(), "John");
    assertEquals(employeeList.get(4).getFirstName(), "Christian");
}

对于返回单个值的查询,我们可以使用ScalarHandler

@Test
public void givenResultHandler_whenExecutingQuery_thenExpectedScalar()
  throws SQLException {
    ScalarHandler<Long> scalarHandler = new ScalarHandler<>();
    QueryRunner runner = new QueryRunner();
    String query = "SELECT COUNT(*) FROM employee";
    long count
      = runner.query(connection, query, scalarHandler);
    assertEquals(count, 5);
}

要了解所有ResultSerHandler实现,可以参考ResultSetHandler文档

4.1. 自定义处理程序

当我们需要更多地控制如何将结果转换为对象时,我们还可以创建一个自定义处理程序以传递给QueryRunner的方法。

这可以通过实现ResultSetHandler接口或扩展库提供的现有实现之一来完成。

让我们看看第二种方法的外观。首先,让我们在Employee类中添加另一个字段:

public class Employee {
    private List<Email> emails;
    // ...
}

现在,让我们创建一个扩展BeanListHandler类型并为每个员工设置电子邮件列表的类:

public class EmployeeHandler extends BeanListHandler<Employee> {
    private Connection connection;
    public EmployeeHandler(Connection con) {
        super(Employee.class);
        this.connection = con;
    }
    @Override
    public List<Employee> handle(ResultSet rs) throws SQLException {
        List<Employee> employees = super.handle(rs);
        QueryRunner runner = new QueryRunner();
        BeanListHandler<Email> handler = new BeanListHandler<>(Email.class);
        String query = "SELECT * FROM email WHERE employeeid = ?";
        for (Employee employee : employees) {
            List<Email> emails
              = runner.query(connection, query, handler, employee.getId());
            employee.setEmails(emails);
        }
        return employees;
    }
}

请注意,我们期望构造函数中有一个Connection对象,以便我们可以执行查询以获取电子邮件。

最后,让我们测试一下我们的代码,看看是否一切都按预期工作:

@Test
public void
  givenResultHandler_whenExecutingQuery_thenEmailsSetted()
    throws SQLException {
    EmployeeHandler employeeHandler = new EmployeeHandler(connection);
    QueryRunner runner = new QueryRunner();
    List<Employee> employees
      = runner.query(connection, "SELECT * FROM employee", employeeHandler);
    assertEquals(employees.get(0).getEmails().size(), 2);
    assertEquals(employees.get(2).getEmails().size(), 3);
}

4.2. 自定义行处理器

在我们的示例中,employee表的列名与Employee类的字段名匹配(匹配不区分大小写)。然而,情况并非总是如此——例如当列名使用下划线来分隔复合词时。

在这些情况下,我们可以利用RowProcessor接口及其实现将列名映射到我们类中的适当字段。

让我们看看这是什么样子。首先,让我们创建另一个表并在其中插入一些记录:

CREATE TABLE employee_legacy (
    id int NOT NULL PRIMARY KEY auto_increment,
    first_name varchar(255),
    last_name varchar(255),
    salary double,
    hired_date date,
);
INSERT INTO employee_legacy (first_name,last_name,salary,hired_date)
  VALUES ('John', 'Doe', 10000.10, to_date('01-01-2001','dd-mm-yyyy'));
// ...

现在,让我们修改我们的EmployeeHandler类:

public class EmployeeHandler extends BeanListHandler<Employee> {
    // ...
    public EmployeeHandler(Connection con) {
        super(Employee.class,
          new BasicRowProcessor(new BeanProcessor(getColumnsToFieldsMap())));
        // ...
    }
    public static Map<String, String> getColumnsToFieldsMap() {
        Map<String, String> columnsToFieldsMap = new HashMap<>();
        columnsToFieldsMap.put("FIRST_NAME", "firstName");
        columnsToFieldsMap.put("LAST_NAME", "lastName");
        columnsToFieldsMap.put("HIRED_DATE", "hiredDate");
        return columnsToFieldsMap;
    }
    // ...
}

请注意,我们正在使用BeanProcessor进行列到字段的实际映射,并且仅用于需要处理的那些。

最后,让我们测试一切是否正常:

@Test
public void
  givenResultHandler_whenExecutingQuery_thenAllPropertiesSetted()
    throws SQLException {
    EmployeeHandler employeeHandler = new EmployeeHandler(connection);
    QueryRunner runner = new QueryRunner();
    String query = "SELECT * FROM employee_legacy";
    List<Employee> employees
      = runner.query(connection, query, employeeHandler);
    assertEquals((int) employees.get(0).getId(), 1);
    assertEquals(employees.get(0).getFirstName(), "John");
}

5. 插入记录

QueryRunner类提供了两种在数据库中创建记录的方法。

第一个是使用*update()*方法并传递 SQL 语句和可选的替换参数列表。该方法返回插入的记录数:

@Test
public void whenInserting_thenInserted() throws SQLException {
    QueryRunner runner = new QueryRunner();
    String insertSQL
      = "INSERT INTO employee (firstname,lastname,salary, hireddate) "
        + "VALUES (?, ?, ?, ?)";
    int numRowsInserted
      = runner.update(
        connection, insertSQL, "Leia", "Kane", 60000.60, new Date());
    assertEquals(numRowsInserted, 1);
}

第二种是使用insert()方法,除了 SQL 语句和替换参数之外,还需要一个ResultSetHandler来转换生成的自动生成的键。返回值将是处理程序返回的内容:

@Test
public void
  givenHandler_whenInserting_thenExpectedId() throws SQLException {
    ScalarHandler<Integer> scalarHandler = new ScalarHandler<>();
    QueryRunner runner = new QueryRunner();
    String insertSQL
      = "INSERT INTO employee (firstname,lastname,salary, hireddate) "
        + "VALUES (?, ?, ?, ?)";
    int newId
      = runner.insert(
        connection, insertSQL, scalarHandler,
        "Jenny", "Medici", 60000.60, new Date());
    assertEquals(newId, 6);
}

6. 更新和删除

QueryRunner类的*update()*方法也可用于修改和删除数据库中的记录。

它的用法是微不足道的。以下是如何更新员工工资的示例:

@Test
public void givenSalary_whenUpdating_thenUpdated()
 throws SQLException {
    double salary = 35000;
    QueryRunner runner = new QueryRunner();
    String updateSQL
      = "UPDATE employee SET salary = salary * 1.1 WHERE salary <= ?";
    int numRowsUpdated = runner.update(connection, updateSQL, salary);
    assertEquals(numRowsUpdated, 3);
}

这是另一个删除具有给定 id 的员工的方法:

@Test
public void whenDeletingRecord_thenDeleted() throws SQLException {
    QueryRunner runner = new QueryRunner();
    String deleteSQL = "DELETE FROM employee WHERE id = ?";
    int numRowsDeleted = runner.update(connection, deleteSQL, 3);
    assertEquals(numRowsDeleted, 1);
}

7. 异步操作

DbUtils 提供了AsyncQueryRunner类来异步执行操作。该类的方法与QueryRunner类的方法有对应关系,只是它们返回一个Future实例。

下面是一个获取数据库中所有员工的示例,最多等待 10 秒才能得到结果:

@Test
public void
  givenAsyncRunner_whenExecutingQuery_thenExpectedList() throws Exception {
    AsyncQueryRunner runner
      = new AsyncQueryRunner(Executors.newCachedThreadPool());
    EmployeeHandler employeeHandler = new EmployeeHandler(connection);
    String query = "SELECT * FROM employee";
    Future<List<Employee>> future
      = runner.query(connection, query, employeeHandler);
    List<Employee> employeeList = future.get(10, TimeUnit.SECONDS);
    assertEquals(employeeList.size(), 5);
}