Un guide pour Apache Commons DbUtils

Un guide pour Apache Commons DbUtils

1. Vue d'ensemble

Apache Commons DbUtils est une petite bibliothèque qui facilite beaucoup le travail avec JDBC.

Dans cet article, nous allons mettre en œuvre des exemples pour présenter ses fonctionnalités et capacités.

2. Installer

2.1. Dépendances Maven

Tout d'abord, nous devons ajouter les dépendancescommons-dbutils eth2 à nospom.xml:


    commons-dbutils
    commons-dbutils
    1.6


    com.h2database
    h2
    1.4.196

Vous pouvez trouver la dernière version decommons-dbutils eth2 sur Maven Central.

2.2. Base de données de test

Une fois nos dépendances en place, créons un script pour créer les tables et les enregistrements que nous utiliserons:

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

Tous les exemples de scénarios de test dans cet article utiliseront une connexion nouvellement créée à une base de données H2 en mémoire:

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

Enfin, nous aurons besoin de deux 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. introduction

La bibliothèque DbUtils fournitthe QueryRunner class as the main entry point pour la plupart des fonctionnalités disponibles.

Cette classe fonctionne en recevant une connexion à la base de données, une instruction SQL à exécuter et une liste facultative de paramètres permettant de fournir des valeurs pour les espaces réservés de la requête.

Comme nous le verrons plus tard, quelques méthodes reçoivent également une implémentation deResultSetHandler - qui est responsable de la transformation des instances deResultSet en objets attendus par notre application.

Bien entendu, la bibliothèque fournit déjà plusieurs implémentations qui gèrent les transformations les plus courantes, telles que les listes, les cartes et les JavaBeans.

4. Requête de données

Maintenant que nous connaissons les bases, nous sommes prêts à interroger notre base de données.

Commençons par un exemple rapide d'obtention de tous les enregistrements de la base de données sous forme de liste de cartes à l'aide d'unMapListHandler:

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

Ensuite, voici un exemple utilisant unBeanListHandler pour transformer les résultats en instancesEmployee:

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

Pour les requêtes qui retournent une seule valeur, nous pouvons utiliser unScalarHandler:

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

Pour apprendre toutes les implémentations deResultSerHandler, vous pouvez vous référer auxResultSetHandler documentation.

4.1. Gestionnaires personnalisés

Nous pouvons également créer un gestionnaire personnalisé à transmettre aux méthodes deQueryRunner lorsque nous avons besoin de plus de contrôle sur la façon dont les résultats seront transformés en objets.

Cela peut être fait en implémentant l'interfaceResultSetHandler ou en étendant l'une des implémentations existantes fournies par la bibliothèque.

Voyons à quoi ressemble la deuxième approche. Tout d'abord, ajoutons un autre champ à notre classeEmployee:

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

Maintenant, créons une classe qui étend le typeBeanListHandler et définit la liste de diffusion pour chaque employé:

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

Notez que nous attendons un objetConnection dans le constructeur afin que nous puissions exécuter les requêtes pour obtenir les e-mails.

Enfin, testons notre code pour voir si tout fonctionne comme prévu:

@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. Processeurs de lignes personnalisés

Dans nos exemples, les noms de colonne de la tableemployee correspondent aux noms de champ de notre classeEmployee (la correspondance est insensible à la casse). Cependant, ce n’est pas toujours le cas, par exemple lorsque les noms de colonnes utilisent des traits de soulignement pour séparer les mots composés.

Dans ces situations, nous pouvons profiter de l'interfaceRowProcessor et de ses implémentations pour mapper les noms de colonnes aux champs appropriés dans nos classes.

Voyons à quoi cela ressemble. Commençons par créer une autre table et y insérer des enregistrements:

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

Maintenant, modifions notre 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;
    }
    // ...
}

Notez que nous utilisons unBeanProcessor pour faire le mappage réel des colonnes aux champs et uniquement pour ceux qui doivent être adressés.

Enfin, testons tout va bien:

@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. Insérer des enregistrements

La classeQueryRunner propose deux approches pour créer des enregistrements dans une base de données.

La première consiste à utiliser la méthodeupdate() et à transmettre l'instruction SQL et une liste facultative de paramètres de remplacement. La méthode retourne le nombre d'enregistrements insérés:

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

La deuxième consiste à utiliser la méthodeinsert() qui, en plus de l'instruction SQL et des paramètres de remplacement, nécessite unResultSetHandler pour transformer les clés générées automatiquement. La valeur de retour sera ce que le gestionnaire retourne:

@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. Mise à jour et suppression

La méthodeupdate() de la classeQueryRunner peut également être utilisée pour modifier et effacer des enregistrements de notre base de données.

Son utilisation est triviale. Voici un exemple de mise à jour du salaire d’un employé:

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

Et en voici une autre pour supprimer un employé avec l'identifiant donné:

@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. Opérations Asynchrones

DbUtils fournit la classeAsyncQueryRunner pour exécuter des opérations de manière asynchrone. Les méthodes de cette classe ont une correspondance avec celles de la classeQueryRunner, sauf qu'elles renvoient une instanceFuture.

Voici un exemple pour obtenir tous les employés de la base de données, en attendant jusqu'à 10 secondes pour obtenir les résultats:

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

Dans ce tutoriel, nous avons exploré les fonctionnalités les plus remarquables de la bibliothèque Apache Commons DbUtils.

Nous avons interrogé des données et les avons transformées en différents types d'objets. Nous avons inséré des enregistrements pour obtenir les clés primaires générées et des données mises à jour et supprimées en fonction d'un critère donné. Nous avons également profité de la classeAsyncQueryRunner pour exécuter de manière asynchrone une opération de requête.

Et, comme toujours, le code source complet de cet article peut être trouvéover on Github.