JOOQ et JOOλ :

Comment écrire du code propre dans vos DAO

Mathieu Gandin

Tech lead @LesFurets.com

Problèmes

  • En 2017, SQL est toujours présent dans le code des DAO
  • Le mapping objet-relationnel en Java est toujours la combination de deux concepts
  • Comment éviter d'écrire du code spaghetti dans les DAO ?

Contexte @lesfurets.com

  • Migration de "tout MySQL" à "Cassandra et un peu de MySQL"
  • Framework maison en JDBC
  • Le code existant doit rester proche du SQL
  • Le code existant doit être refactoré dans un style moins spaghetti

Java ne manque pas de solutions à ces problèmes

  • JDBC
  • JPA / Hibernate
  • JDBC Template
  • MyBatis, framework maison, ...
  • Essayons Java Object Oriented Query a.k.a JOOQ

Live demo

Comment simplifier le mapping ? Essayons Spring JDBC Template

            
@Autowired
public InsuranceParamRepository(DataSource dataSource) {
  this.jdbcTemplate = new JdbcTemplate(dataSource);
}

public List<Provider> findProvidersBy(String agencyId) {
  return jdbcTemplate.query(FIND_PROVIDER_BY_AGENCY_ID,
    new Object[] { agencyId },
    (resultSet, i) -> new Provider(resultSet.getString("CODE"),
                                    resultSet.getString("NAME"),
                                    resultSet.getString("COUNTRY")));
}
            
            

Mapping simplifié avec Spring JDBC Template

  • Encapsule de nombreux comportements de JDBC
  • Mais le mapping est toujours problématique

Si vous vous rappelez de ce code ?

            
return DSL.using(connection)
  .select(YEAR_RESULT.DEPARTEMENT,
            YEAR_RESULT.MANAGER,
            YEAR_RESULT.NETPROFIT,
            YEAR_RESULT.OPERATINGEXPENSE,
            YEAR_RESULT.TURNOVER,
            YEAR_RESULT.CREATION_DATE)
  .from(YEAR_RESULT)
  .where(YEAR_RESULT.DEPARTEMENT.eq(departement))
  .orderBy(YEAR_RESULT.CREATION_DATE.asc())
  .stream()
  .map(r -> new YearReport(r.get(YEAR_RESULT.DEPARTEMENT),
            r.get(YEAR_RESULT.MANAGER),
            r.get(YEAR_RESULT.NETPROFIT),
            r.get(YEAR_RESULT.OPERATINGEXPENSE),
            r.get(YEAR_RESULT.TURNOVER),
            r.get(YEAR_RESULT.CREATION_DATE, LocalDate.class)))
  .collect(toList());
            
        

Un "Extract Method" très simple peut rendre le code encore plus clair

            
return DSL.using(connection)
    .select(YEAR_RESULT.DEPARTEMENT,...)
    .from(YEAR_RESULT)
    .where(YEAR_RESULT.DEPARTEMENT.eq(departement))
    .orderBy(YEAR_RESULT.CREATION_DATE.asc())
    .stream()
    .map(this::map)
    .collect(toList());
            
            

Un "Extract Method" très simple peut rendre le code encore plus clair

            
private YearReport map(Record r) {
  return new YearReport(r.get(YEAR_RESULT.DEPARTEMENT),
            r.get(YEAR_RESULT.MANAGER),
            r.get(YEAR_RESULT.NETPROFIT),
            r.get(YEAR_RESULT.OPERATINGEXPENSE),
            r.get(YEAR_RESULT.TURNOVER),
            r.get(YEAR_RESULT.CREATION_DATE, LocalDate.class));
}
            
            

Et pourquoi pas un ORM avec JPA ?

            
@Entity
@Table(name = "marques")
public class Marque {
@Id
private String code;
private String name;
private String logoUrl;
@Transient
private byte[] logo;
@Transient
private String logoName;
@OneToMany(mappedBy = "marque", fetch = FetchType.EAGER)
@Fetch(FetchMode.SELECT)
private List<Brochure> brochures;
            
            

Et pourquoi pas un ORM avec JPA ?

            
public class MarqueDao {
    @PersistenceContext
    private EntityManager entityManager;

    public Marque findByCode(String code) {
        try {
            Query query = entityManager.createQuery("from Marque to where to.code=:code")
                                        .setParameter("code", code);
            return (Marque) query.getSingleResult();
        } catch (Exception e) {
            throw new ResourceNotFoundException(e);
    }
}
            
            

ORM avec JPA, pros

  • Approche POJO, orientée objet
  • Navigation dans une grappe d'object
  • CriteriaQuery

ORM avec JPA, cons pour notre code

  • Beaucoup de configuration pour faire le premier pas
  • JPA encourage une approche top-down, chez les furets.com on est plutôt sur une approche yoyo
  • Nous avons pas mal de requêtes SQL complexes, une base dénormalisée, ce qui rend plus complexe l'utilisation de JQL/HQL

Fortement typé et valide en SQL

            
public Set<Commission> findCommissionsBy(Connection connection, ESite site, EModule module) throws SQLException {
    return DSL.using(connection)
                .selectDistinct(field(COL_REFMODULE),
                    field(COL_REFCUSTOMER),
                    field(COL_COMMISSION),
                    field(COL_CREATIONDATETIME, LocalDateTime.class))
                .from(new TableImpl<>(BO_PRICE_LIST.toString()))
                .where(COL_REFCUSTOMER.eq(site.getIntValue()))
                .and(COL_REFMODULE.eq(module.getIntValue()))
                .orderBy(field(COL_CREATIONDATETIME).desc())
                .stream()
                .map(record -> new Commission(EModule.fromIntValue(record.get(COL_REFMODULE, Integer.class)),
                    ESite.fromIntValue(record.get(COL_REFCUSTOMER, Integer.class)),
                    ECommissionType.valueOf(record.get(COL_COMMISSION, String.class)),
                    record.get(COL_CREATIONDATETIME, LocalDateTime.class)))
                .collect(toSet());
}
            
            

Fortement typé et valide en SQL

            
SELECT distinct
    COL_REFMODULE,
    COL_REFCUSTOMER,
    COL_COMMISSION,
    COL_CREATIONDATETIME
FROM BO_PRICE_LIST
WHERE COL_REFCUSTOMER = 'amaguiz'
AND COL_REFMODULE = 'auto'
ORDER BY COL_CREATIONDATETIME
DESC;
            
            

Et les dates ?

            
Date creationDate = resultSet.getDate("creation_date");
LocalDateTime date = Instant.ofEpochMilli(creationDate.getTime())
                                            .atZone(ZoneId.systemDefault())
                                            .toLocalDateTime();
            
            

Et les dates ?

            
public List<Tuple2<LocalDateTime, String>> findPlate(Connection connection)
    throws SQLException {
  return DSL.using(connection)
    .fetch(FIND_PLATE_AND_CREATION_DATE)
    .stream()
    .map(r -> {
      String plate = r.getValue(PLATE, String.class);
      LocalDateTime date = r.getValue(CREATION_DATE, LocalDateTime.class);
      return new Tuple2<>(date, plate);
    }).collect(toList());
}
            
            

Nous utilisons aussi JOOλ avec JOOQ

  • Classes Helper pour un style de code plus fonctionnel et déclaratif
  • De Function1 à Function16
  • De Tuple1 à Tuple16
  • Sequences
  • Unchecked Function, Supplier et Consumer

De Tuple1 à Tuple16

            
Tuple2<LocalDate, String> tuple =Tuple.tuple(now(), "Yolo");
            
            

Sequences

            
Seq.of(1, 2, 3).concat(Seq.of(4, 5, 6)); // == Seq.of(1, 2, 3, 4, 5, 6);
            
            

Function, Supplier et Consumer en Java 8

            
Arrays.stream(dir.listFiles()).forEach(file -> {
  try {
    System.out.println(file.getCanonicalPath());
  }
  catch (IOException e) {
    throw new RuntimeException(e);
  }
});
            
            

Unchecked Function, Supplier et Consumer

            
Arrays.stream(dir.listFiles()).forEach(
  Unchecked.consumer(file -> System.out.println(file.getCanonicalPath()))
);
            
            

JOOλ, précautions d'usage

  • Ne pas utiliser les Tuples pour remplacer vos POJOS
  • Si vous utilisez Function16, vous avez peut être besoin de refactorer quelques choses avant
  • Rappelez vous que Java n'est pas un langage fonctionnel

Generation de code avec JOOQ

  • Supporte Ant, Maven et Gradle
            
<configuration>
    <jdbc>
      <driver>${db.driver}</driver>
      <url>${db.url}</url>
      <user>${db.username}</user>
      <password>${db.password}</password>
    </jdbc>
    <generator>
      <database>
          <name>org.jooq.util.h2.H2Database</name>
          <includes>.*</includes>
          <excludes></excludes>
          ...
        </database>
        <generate>
          <instanceFields>true</instanceFields>
        </generate>
        <target>
          <packageName>com.lesfurets.db</packageName>
          <directory>target/generated-sources</directory>
        </target>
      </generator>
</configuration>
            
            

Base de données supportées

  • Oracle : dialect spécifique pour les types de données propriétaires, les procédures stockées, ...
  • SQL-Server Dialect support SQL-Server 2012 and 2008
  • Support of MariaDB, PostgreSQL, SQLite, ...

Java Object Oriented Query

  • Mapping SQL basé sur un DSL
  • Support de l'API stream et java.time
  • Fortement typé et valide en SQL
  • Génération de code
  • On peut utiliser JOOλ avec JOOQ pour simplifier du code
  • Supporte MariaDB, MySQL, Oracle, Oracle, PostgreSQL, SQLite, SQL Server ...

Questions