Introduction à jOOQ avec Spring

1. Vue d’ensemble

Cet article présente la requête orientée objet Java - jOOQ - et un moyen simple de la configurer en collaboration avec Spring Framework.

La plupart des applications Java ont une sorte de persistance SQL et accèdent à cette couche à l’aide d’outils de niveau supérieur tels que JPA. Et bien que cela soit utile, dans certains cas, vous avez réellement besoin d’un outil plus fin et plus nuancé pour accéder à vos données ou pour tirer pleinement parti de tout ce que la base de données sous-jacente peut offrir.

jOOQ évite certains modèles ORM typiques et génère du code nous permettant de créer des requêtes dactylographiées et d’obtenir un contrôle complet du SQL généré via une API fluide et puissante

2. Dépendances Maven

Les dépendances suivantes sont nécessaires pour exécuter le code dans ce tutoriel.

2.1. jOOQ

<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.7.3</version>
</dependency>

2.2. Printemps

Plusieurs dépendances de Spring sont requises pour notre exemple. cependant, pour simplifier les choses, il suffit d’en inclure explicitement deux dans le fichier POM:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.2.5.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.2.5.RELEASE</version>
</dependency>

2.3. Base de données

Pour rendre les choses faciles pour notre exemple, nous allons utiliser la base de données intégrée H2:

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

3. Génération de code

3.1. Structure de la base de données

Présentons la structure de la base de données sur laquelle nous allons travailler tout au long de cet article. Supposons que nous devions créer une base de données pour permettre à un éditeur de stocker les informations sur les livres et les auteurs qu’ils gèrent, un auteur pouvant écrire de nombreux livres et un livre pouvant être coécrit par plusieurs auteurs.

Pour simplifier les choses, nous ne générerons que trois tables: book pour les livres, author pour les auteurs et une autre table appelée author book pour représenter la relation plusieurs à plusieurs entre auteurs et livres. La table author a trois colonnes: id , first name et last name. La table book ne contient qu’une colonne title et la clé primaire id__.

Les requêtes SQL suivantes, stockées dans le fichier de ressources intro schema.sql__, seront exécutées sur la base de données que nous avons déjà configurée pour créer les tables nécessaires et les remplir avec des exemples de données:

DROP TABLE IF EXISTS author__book, author, book;

CREATE TABLE author (
  id             INT          NOT NULL PRIMARY KEY,
  first__name     VARCHAR(50),
  last__name      VARCHAR(50)  NOT NULL
);

CREATE TABLE book (
  id             INT          NOT NULL PRIMARY KEY,
  title          VARCHAR(100) NOT NULL
);

CREATE TABLE author__book (
  author__id      INT          NOT NULL,
  book__id        INT          NOT NULL,

  PRIMARY KEY (author__id, book__id),
  CONSTRAINT fk__ab__author     FOREIGN KEY (author__id)  REFERENCES author (id)
    ON UPDATE CASCADE ON DELETE CASCADE,
  CONSTRAINT fk__ab__book       FOREIGN KEY (book__id)    REFERENCES book   (id)
);

INSERT INTO author VALUES
  (1, 'Kathy', 'Sierra'),
  (2, 'Bert', 'Bates'),
  (3, 'Bryan', 'Basham');

INSERT INTO book VALUES
  (1, 'Head First Java'),
  (2, 'Head First Servlets and JSP'),
  (3, 'OCA/OCP Java SE 7 Programmer');

INSERT INTO author__book VALUES (1, 1), (1, 3), (2, 1);

3.2. Propriétés Maven Plugin

Nous allons utiliser trois différents plugins Maven pour générer le code jOOQ. Le premier de ceux-ci est le plugin Properties Maven.

Ce plugin est utilisé pour lire les données de configuration d’un fichier de ressources. Cela n’est pas nécessaire, car les données peuvent être directement ajoutées au POM, mais il est judicieux de gérer les propriétés en externe.

Dans cette section, nous allons définir les propriétés des connexions de base de données, notamment la classe de pilote JDBC, l’URL de la base de données, le nom d’utilisateur et le mot de passe, dans un fichier nommé intro config.properties__. L’externalisation de ces propriétés facilite le basculement de la base de données ou simplement les données de configuration.

L’objectif read-project-properties de ce plug-in doit être lié à une phase précoce afin que les données de configuration puissent être préparées pour une utilisation par d’autres plug-ins. Dans ce cas, il est lié à la phase initialize :

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>properties-maven-plugin</artifactId>
    <version>1.0.0</version>
    <executions>
        <execution>
            <phase>initialize</phase>
            <goals>
                <goal>read-project-properties</goal>
            </goals>
            <configuration>
                <files>
                    <file>src/main/resources/intro__config.properties</file>
                </files>
            </configuration>
        </execution>
    </executions>
</plugin>

3.3. Plugin SQL Maven

Le plugin SQL Maven est utilisé pour exécuter des instructions SQL afin de créer et de renseigner des tables de base de données. Il utilisera les propriétés extraites du fichier intro config.properties par le plug-in Properties Maven et prendra les instructions SQL de la ressource intro schema.sql .

Le plugin SQL Maven est configuré comme suit:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>sql-maven-plugin</artifactId>
    <version>1.5</version>
    <executions>
        <execution>
            <phase>initialize</phase>
            <goals>
                <goal>execute</goal>
            </goals>
            <configuration>
                <driver>${db.driver}</driver>
                <url>${db.url}</url>
                <username>${db.username}</username>
                <password>${db.password}</password>
                <srcFiles>
                    <srcFile>src/main/resources/intro__schema.sql</srcFile>
                </srcFiles>
            </configuration>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.191</version>
        </dependency>
    </dependencies>
</plugin>

Notez que ce plugin doit être placé plus tard que le plugin Properties Maven dans le fichier POM car leurs objectifs d’exécution sont liés à la même phase et Maven les exécutera dans l’ordre indiqué.

3.4. Plugin jOOQ Codegen

Le plugin jOOQ Codegen génère du code Java à partir d’une structure de table de base de données. Son objectif generate doit être lié à la phase generate-sources pour assurer le bon ordre d’exécution. Les métadonnées du plugin ressemblent à ceci:

<plugin>
    <groupId>org.jooq</groupId>
    <artifactId>jooq-codegen-maven</artifactId>
    <version>${org.jooq.version}</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <jdbc>
                    <driver>${db.driver}</driver>
                    <url>${db.url}</url>
                    <user>${db.username}</user>
                    <password>${db.password}</password>
                </jdbc>
                <generator>
                    <target>
                        <packageName>com.baeldung.jooq.introduction.db</packageName>
                        <directory>src/main/java</directory>
                    </target>
                </generator>
            </configuration>
        </execution>
    </executions>
</plugin>

3.5. Code générateur

Pour terminer le processus de génération du code source, nous devons exécuter la phase Maven generate-sources . Dans Eclipse, nous pouvons le faire en cliquant avec le bouton droit de la souris sur le projet et en choisissant Run As Maven generate-sources . Une fois la commande terminée, les fichiers source correspondant aux tables author , book , author book__ (et plusieurs autres pour les classes de support) sont générés.

Explorons les classes de tables pour voir ce que jOOQ a produit. Chaque classe a un champ statique du même nom que la classe, sauf que toutes les lettres du nom sont en majuscule. Les extraits de code suivants sont extraits des définitions des classes générées:

La classe Author :

public class Author extends TableImpl<AuthorRecord> {
    public static final Author AUTHOR = new Author();

   //other class members
}

La classe Book :

public class Book extends TableImpl<BookRecord> {
    public static final Book BOOK = new Book();

   //other class members
}

La classe AuthorBook :

public class AuthorBook extends TableImpl<AuthorBookRecord> {
    public static final AuthorBook AUTHOR__BOOK = new AuthorBook();

   //other class members
}

Les instances référencées par ces champs statiques serviront d’objets d’accès aux données pour représenter les tables correspondantes lors de l’utilisation d’autres couches d’un projet.

4. Configuration de printemps

4.1. Traduire les exceptions jOOQ au printemps

Afin de rendre les exceptions levées depuis l’exécution de jOOQ cohérentes avec la prise en charge de Spring pour l’accès aux bases de données, nous devons les traduire en sous-types de la classe DataAccessException .

Définissons une implémentation de l’interface ExecuteListener pour convertir les exceptions:

public class ExceptionTranslator extends DefaultExecuteListener {
    public void exception(ExecuteContext context) {
        SQLDialect dialect = context.configuration().dialect();
        SQLExceptionTranslator translator
          = new SQLErrorCodeSQLExceptionTranslator(dialect.name());
        context.exception(translator
          .translate("Access database using jOOQ", context.sql(), context.sqlException()));
    }
}

Cette classe sera utilisée par le contexte d’application Spring.

4.2. Configuration du ressort

Cette section décrit les étapes à suivre pour définir un PersistenceContext contenant les métadonnées et les beans à utiliser dans le contexte de l’application Spring.

Commençons par appliquer les annotations nécessaires à la classe:

  • @ Configuration : Faire que la classe soit reconnue comme un conteneur pour

des haricots ** @ ComponentScan : Configurez les directives d’analyse, y compris la value

possibilité de déclarer un tableau de noms de paquets pour rechercher des composants. Dans Dans ce didacticiel, le package à rechercher est celui généré par le Plugin jOOQ Codegen Maven ** @ EnableTransactionManagement : Activer les transactions à gérer

Printemps ** @ PropertySource : Indiquez les emplacements des fichiers de propriétés à

être chargé. La valeur dans cet article pointe vers le fichier contenant les données de configuration et le dialecte de la base de données, qui est le même fichier que celui mentionné dans la sous-section 4.1.

@Configuration
@ComponentScan({"com.baeldung.jooq.introduction.db.public__.tables"})
@EnableTransactionManagement
@PropertySource("classpath:intro__config.properties")
public class PersistenceContext {
   //Other declarations
}

Ensuite, utilisez un objet Environment pour obtenir les données de configuration, qui sont ensuite utilisées pour configurer le bean DataSource :

@Autowired
private Environment environment;

@Bean
public DataSource dataSource() {
    JdbcDataSource dataSource = new JdbcDataSource();

    dataSource.setUrl(environment.getRequiredProperty("db.url"));
    dataSource.setUser(environment.getRequiredProperty("db.username"));
    dataSource.setPassword(environment.getRequiredProperty("db.password"));
    return dataSource;
}

Nous définissons maintenant plusieurs beans pour travailler avec les opérations d’accès à la base de données:

@Bean
public TransactionAwareDataSourceProxy transactionAwareDataSource() {
    return new TransactionAwareDataSourceProxy(dataSource());
}

@Bean
public DataSourceTransactionManager transactionManager() {
    return new DataSourceTransactionManager(dataSource());
}

@Bean
public DataSourceConnectionProvider connectionProvider() {
    return new DataSourceConnectionProvider(transactionAwareDataSource());
}

@Bean
public ExceptionTranslator exceptionTransformer() {
    return new ExceptionTranslator();
}

@Bean
public DefaultDSLContext dsl() {
    return new DefaultDSLContext(configuration());
}

Enfin, nous fournissons une implémentation jOOQ Configuration et le déclarons comme un bean Spring à utiliser par la classe DSLContext :

@Bean
public DefaultConfiguration configuration() {
    DefaultConfiguration jooqConfiguration = new DefaultConfiguration();
    jooqConfiguration.set(connectionProvider());
    jooqConfiguration.set(new DefaultExecuteListenerProvider(exceptionTransformer()));

    String sqlDialectName = environment.getRequiredProperty("jooq.sql.dialect");
    SQLDialect dialect = SQLDialect.valueOf(sqlDialectName);
    jooqConfiguration.set(dialect);

    return jooqConfiguration;
}

5. Utilisation de jOOQ avec Spring

Cette section illustre l’utilisation de jOOQ dans les requêtes d’accès aux bases de données courantes. Il existe deux tests, l’un pour la validation et l’autre pour la restauration, pour chaque type d’opération d’écriture, y compris l’insertion, la mise à jour et la suppression de données. L’utilisation de l’opération «read» est illustrée lors de la sélection de données pour vérifier les requêtes «write».

Nous allons commencer par déclarer un objet DSLContext auto-câblé et des instances des classes générées par jOOQ à utiliser par toutes les méthodes de test:

@Autowired
private DSLContext dsl;

Author author = Author.AUTHOR;
Book book = Book.BOOK;
AuthorBook authorBook = AuthorBook.AUTHOR__BOOK;

5.1. Insérer des données

La première étape consiste à insérer des données dans des tableaux:

dsl.insertInto(author)
  .set(author.ID, 4)
  .set(author.FIRST__NAME, "Herbert")
  .set(author.LAST__NAME, "Schildt")
  .execute();
dsl.insertInto(book)
  .set(book.ID, 4)
  .set(book.TITLE, "A Beginner's Guide")
  .execute();
dsl.insertInto(authorBook)
  .set(authorBook.AUTHOR__ID, 4)
  .set(authorBook.BOOK__ID, 4)
  .execute();

Une requête SELECT pour extraire des données:

Result<Record3<Integer, String, Integer>> result = dsl
  .select(author.ID, author.LAST__NAME, DSL.count())
  .from(author)
  .join(authorBook)
  .on(author.ID.equal(authorBook.AUTHOR__ID))
  .join(book)
  .on(authorBook.BOOK__ID.equal(book.ID))
  .groupBy(author.LAST__NAME)
  .fetch();

La requête ci-dessus génère le résultat suivant:

+----+---------+-----+
|  ID|LAST__NAME|count|
+----+---------+-----+
|   1|Sierra   |    2|
|   2|Bates    |    1|
|   4|Schildt  |    1|
+----+---------+-----+

Le résultat est confirmé par l’API Assert :

assertEquals(3, result.size());
assertEquals("Sierra", result.getValue(0, author.LAST__NAME));
assertEquals(Integer.valueOf(2), result.getValue(0, DSL.count()));
assertEquals("Schildt", result.getValue(2, author.LAST__NAME));
assertEquals(Integer.valueOf(1), result.getValue(2, DSL.count()));

En cas d’échec dû à une requête non valide, une exception est levée et la transaction est annulée. Dans l’exemple suivant, la requête INSERT viole une contrainte de clé étrangère, générant une exception:

@Test(expected = DataAccessException.class)
public void givenInvalidData__whenInserting__thenFail() {
    dsl.insertInto(authorBook)
      .set(authorBook.AUTHOR__ID, 4)
      .set(authorBook.BOOK__ID, 5)
      .execute();
}

5.2. Mise à jour des données

Maintenant, mettons à jour les données existantes:

dsl.update(author)
  .set(author.LAST__NAME, "Baeldung")
  .where(author.ID.equal(3))
  .execute();
dsl.update(book)
  .set(book.TITLE, "Building your REST API with Spring")
  .where(book.ID.equal(3))
  .execute();
dsl.insertInto(authorBook)
  .set(authorBook.AUTHOR__ID, 3)
  .set(authorBook.BOOK__ID, 3)
  .execute();

Obtenez les données nécessaires:

Result<Record3<Integer, String, String>> result = dsl
  .select(author.ID, author.LAST__NAME, book.TITLE)
  .from(author)
  .join(authorBook)
  .on(author.ID.equal(authorBook.AUTHOR__ID))
  .join(book)
  .on(authorBook.BOOK__ID.equal(book.ID))
  .where(author.ID.equal(3))
  .fetch();

Le résultat devrait être:

+----+---------+----------------------------------+
|  ID|LAST__NAME|TITLE                             |
+----+---------+----------------------------------+
|   3|Baeldung |Building your REST API with Spring|
+----+---------+----------------------------------+

Le test suivant vérifiera que jOOQ a fonctionné comme prévu:

assertEquals(1, result.size());
assertEquals(Integer.valueOf(3), result.getValue(0, author.ID));
assertEquals("Baeldung", result.getValue(0, author.LAST__NAME));
assertEquals("Building your REST API with Spring", result.getValue(0, book.TITLE));

En cas d’échec, une exception est levée et la transaction est annulée, ce que nous confirmons par un test:

@Test(expected = DataAccessException.class)
public void givenInvalidData__whenUpdating__thenFail() {
    dsl.update(authorBook)
      .set(authorBook.AUTHOR__ID, 4)
      .set(authorBook.BOOK__ID, 5)
      .execute();
}

5.3. Suppression de données

La méthode suivante supprime certaines données:

dsl.delete(author)
  .where(author.ID.lt(3))
  .execute();

Voici la requête pour lire la table affectée:

Result<Record3<Integer, String, String>> result = dsl
  .select(author.ID, author.FIRST__NAME, author.LAST__NAME)
  .from(author)
  .fetch();

Le résultat de la requête:

+----+----------+---------+
|  ID|FIRST__NAME|LAST__NAME|
+----+----------+---------+
|   3|Bryan     |Basham   |
+----+----------+---------+

Le test suivant vérifie la suppression:

assertEquals(1, result.size());
assertEquals("Bryan", result.getValue(0, author.FIRST__NAME));
assertEquals("Basham", result.getValue(0, author.LAST__NAME));

D’un autre côté, si une requête est invalide, une exception sera levée et la transaction sera annulée. Le test suivant prouvera que:

@Test(expected = DataAccessException.class)
public void givenInvalidData__whenDeleting__thenFail() {
    dsl.delete(book)
      .where(book.ID.equal(1))
      .execute();
}

6. Conclusion

Ce tutoriel présente les bases de jOOQ, une bibliothèque Java permettant de travailler avec des bases de données. Il couvrait les étapes pour générer du code source à partir d’une structure de base de données et comment interagir avec cette base de données à l’aide des classes nouvellement créées.

La mise en œuvre de tous ces exemples et extraits de code est disponible à l’adresse https://github.com/eugenp/tutorials/tree/master/spring-jooq [a.