Um guia para o Apache Commons DbUtils

Um guia para o Apache Commons DbUtils

1. Visão geral

O Apache Commons DbUtils é uma pequena biblioteca que facilita muito o trabalho com o JDBC.

Neste artigo, implementaremos exemplos para mostrar seus recursos e capacidades.

2. Configuração

2.1. Dependências do Maven

Primeiro, precisamos adicionar as dependênciascommons-dbutilseh2 ao nossopom.xml:


    commons-dbutils
    commons-dbutils
    1.6


    com.h2database
    h2
    1.4.196

Você pode encontrar a versão mais recente decommons-dbutilseh2 no Maven Central.

2.2. Banco de dados de teste

Com nossas dependências no lugar, vamos criar um script para criar as tabelas e registros que usaremos:

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]');
// ...

Todos os casos de teste de exemplo neste artigo usarão uma conexão recém-criada com um banco de dados na memória 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);
    }
    // ...
}

2.3. POJOs

Finalmente, precisaremos de duas classes simples:

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. Introdução

A biblioteca DbUtils fornecethe QueryRunner class as the main entry point para a maioria das funcionalidades disponíveis.

Essa classe funciona recebendo uma conexão com o banco de dados, uma instrução SQL a ser executada e uma lista opcional de parâmetros para fornecer valores para os espaços reservados da consulta.

Como veremos mais tarde, alguns métodos também recebem uma implementaçãoResultSetHandler - que é responsável por transformar as instânciasResultSet nos objetos que nosso aplicativo espera.

Obviamente, a biblioteca já fornece várias implementações que lidam com as transformações mais comuns, como listas, mapas e JavaBeans.

4. Consultando dados

Agora que sabemos o básico, estamos prontos para consultar nosso banco de dados.

Vamos começar com um exemplo rápido de obtenção de todos os registros no banco de dados como uma lista de mapas usando umMapListHandler:

@Test
public void givenResultHandler_whenExecutingQuery_thenExpectedList()
  throws SQLException {
    MapListHandler beanListHandler = new MapListHandler();

    QueryRunner runner = new QueryRunner();
    List> 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");
}

A seguir, aqui está um exemplo usando umBeanListHandler para transformar os resultados em instânciasEmployee:

@Test
public void givenResultHandler_whenExecutingQuery_thenEmployeeList()
  throws SQLException {
    BeanListHandler beanListHandler
      = new BeanListHandler<>(Employee.class);

    QueryRunner runner = new QueryRunner();
    List employeeList
      = runner.query(connection, "SELECT * FROM employee", beanListHandler);

    assertEquals(employeeList.size(), 5);
    assertEquals(employeeList.get(0).getFirstName(), "John");
    assertEquals(employeeList.get(4).getFirstName(), "Christian");
}

Para consultas que retornam um único valor, podemos usar umScalarHandler:

@Test
public void givenResultHandler_whenExecutingQuery_thenExpectedScalar()
  throws SQLException {
    ScalarHandler scalarHandler = new ScalarHandler<>();

    QueryRunner runner = new QueryRunner();
    String query = "SELECT COUNT(*) FROM employee";
    long count
      = runner.query(connection, query, scalarHandler);

    assertEquals(count, 5);
}

Para aprender todas as implementações deResultSerHandler, você pode consultarResultSetHandler documentation.

4.1. Manipuladores personalizados

Também podemos criar um manipulador personalizado para passar para os métodos deQueryRunner quando precisarmos de mais controle sobre como os resultados serão transformados em objetos.

Isso pode ser feito implementando a interfaceResultSetHandler ou estendendo uma das implementações existentes fornecidas pela biblioteca.

Vamos ver como fica a segunda abordagem. Primeiro, vamos adicionar outro campo à nossa classeEmployee:

public class Employee {
    private List emails;
    // ...
}

Agora, vamos criar uma classe que estende o tipoBeanListHandler e define a lista de e-mail para cada funcionário:

public class EmployeeHandler extends BeanListHandler {

    private Connection connection;

    public EmployeeHandler(Connection con) {
        super(Employee.class);
        this.connection = con;
    }

    @Override
    public List handle(ResultSet rs) throws SQLException {
        List employees = super.handle(rs);

        QueryRunner runner = new QueryRunner();
        BeanListHandler handler = new BeanListHandler<>(Email.class);
        String query = "SELECT * FROM email WHERE employeeid = ?";

        for (Employee employee : employees) {
            List emails
              = runner.query(connection, query, handler, employee.getId());
            employee.setEmails(emails);
        }
        return employees;
    }
}

Observe que estamos esperando um objetoConnection no construtor para que possamos executar as consultas para obter os e-mails.

Por fim, vamos testar nosso código para ver se tudo está funcionando conforme o esperado:

@Test
public void
  givenResultHandler_whenExecutingQuery_thenEmailsSetted()
    throws SQLException {
    EmployeeHandler employeeHandler = new EmployeeHandler(connection);

    QueryRunner runner = new QueryRunner();
    List employees
      = runner.query(connection, "SELECT * FROM employee", employeeHandler);

    assertEquals(employees.get(0).getEmails().size(), 2);
    assertEquals(employees.get(2).getEmails().size(), 3);
}

4.2. Processadores de linha personalizados

Em nossos exemplos, os nomes das colunas da tabelaemployee correspondem aos nomes dos campos de nossa classeEmployee (a correspondência não diferencia maiúsculas de minúsculas). No entanto, nem sempre é esse o caso - por exemplo, quando os nomes das colunas usam sublinhados para separar palavras compostas.

Nessas situações, podemos aproveitar a interfaceRowProcessor e suas implementações para mapear os nomes das colunas para os campos apropriados em nossas classes.

Vamos ver como fica. Primeiro, vamos criar outra tabela e inserir alguns registros nela:

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'));
// ...

Agora, vamos modificar nossa classeEmployeeHandler:

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

Observe que estamos usandoBeanProcessor para fazer o mapeamento real de colunas para campos e apenas para aqueles que precisam ser endereçados.

Finalmente, vamos testar se está tudo bem:

@Test
public void
  givenResultHandler_whenExecutingQuery_thenAllPropertiesSetted()
    throws SQLException {
    EmployeeHandler employeeHandler = new EmployeeHandler(connection);

    QueryRunner runner = new QueryRunner();
    String query = "SELECT * FROM employee_legacy";
    List employees
      = runner.query(connection, query, employeeHandler);

    assertEquals((int) employees.get(0).getId(), 1);
    assertEquals(employees.get(0).getFirstName(), "John");
}

5. Inserindo Registros

A classeQueryRunner fornece duas abordagens para criar registros em um banco de dados.

A primeira é usar o métodoupdate()e passar a instrução SQL e uma lista opcional de parâmetros de substituição. O método retorna o número de registros inseridos:

@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);
}

O segundo é usar o métodoinsert() que, além da instrução SQL e dos parâmetros de substituição, precisa de umResultSetHandler para transformar as chaves geradas automaticamente resultantes. O valor de retorno será o que o manipulador retorna:

@Test
public void
  givenHandler_whenInserting_thenExpectedId() throws SQLException {
    ScalarHandler 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. Atualizando e Excluindo

O métodoupdate() da classeQueryRunner também pode ser usado para modificar e apagar registros de nosso banco de dados.

Seu uso é trivial. Aqui está um exemplo de como atualizar o salário de um funcionário:

@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);
}

E aqui está outro para excluir um funcionário com a id fornecida:

@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. Operações assíncronas

DbUtils fornece a classeAsyncQueryRunner para executar operações de forma assíncrona. Os métodos nesta classe têm uma correspondência com aqueles da classeQueryRunner, exceto que eles retornam uma instânciaFuture.

Aqui está um exemplo para obter todos os funcionários no banco de dados, esperando até 10 segundos para obter os resultados:

@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> future
      = runner.query(connection, query, employeeHandler);
    List employeeList = future.get(10, TimeUnit.SECONDS);

    assertEquals(employeeList.size(), 5);
}

8. Conclusão

Neste tutorial, exploramos os recursos mais notáveis ​​da biblioteca Apache Commons DbUtils.

Consultamos dados e os transformamos em diferentes tipos de objetos, inserimos registros obtendo as chaves primárias geradas e atualizamos e excluímos dados com base em um determinado critério. Também aproveitamos a classeAsyncQueryRunner para executar uma operação de consulta de maneira assíncrona.

E, como sempre, o código-fonte completo deste artigo pode ser encontradoover on Github.