JDBC avec Groovy

JDBC avec Groovy

1. introduction

Dans cet article, nous allons voir comment interroger des bases de données relationnelles avecJDBC, en utilisant Groovy idiomatique.

JDBC, bien que relativement bas niveau, est la base de la plupart des ORM et autres bibliothèques d’accès aux données de haut niveau sur la JVM. Et nous pouvons utiliser JDBC directement dans Groovy, bien sûr; Cependant, son API est plutôt lourde.

Heureusement pour nous, la bibliothèque standard Groovy s'appuie sur JDBC pour présenter une interface propre, simple et puissante. Nous allons donc explorer le module Groovy SQL.

Nous allons examiner JDBC dans Groovy simple, sans considérer aucun framework tel que Spring, pour lequel nous avonsother guides.

2. Configuration JDBC et Groovy

Nous devons inclure le module ssqlgroovy-parmi nos dépendances:


    org.codehaus.groovy
    groovy
    2.4.13


    org.codehaus.groovy
    groovy-sql
    2.4.13

Il n'est pas nécessaire de le lister explicitement si nous utilisons groovy-all:


    org.codehaus.groovy
    groovy-all
    2.4.13

Nous pouvons trouver la dernière version degroovy, groovy-sql etgroovy-all sur Maven Central.

3. Connexion à la base de données

La première chose à faire pour travailler avec la base de données est de s'y connecter.

Introduisons la classegroovy.sql.Sql, que nous utiliserons pour toutes les opérations sur la base de données avec le module Groovy SQL.

Une instance deSql représente une base de données sur laquelle nous voulons opérer.

Cependant,an instance of Sqlisn’t a single database connection. Nous parlerons des connexions plus tard, ne nous en préoccupons pas maintenant; supposons simplement que tout fonctionne comme par magie.

3.1. Spécification des paramètres de connexion

Tout au long de cet article, nous allons utiliser une base de données HSQL, qui est une base de données relationnelle légère qui est principalement utilisée dans les tests.

Une connexion à une base de données nécessite une URL, un pilote et des informations d'identification d'accès:

Map dbConnParams = [
  url: 'jdbc:hsqldb:mem:testDB',
  user: 'sa',
  password: '',
  driver: 'org.hsqldb.jdbc.JDBCDriver']

Ici, nous avons choisi de spécifier ceux utilisant unMap, bien que ce ne soit pas le seul choix possible.

On peut alors obtenir une connexion à partir de la classeSql:

def sql = Sql.newInstance(dbConnParams)

Nous verrons comment l'utiliser dans les sections suivantes.

Lorsque nous avons terminé, nous devons toujours libérer les ressources associées:

sql.close()

3.2. Utilisation d'unDataSource

Il est courant, en particulier dans les programmes exécutés sur un serveur d'applications, d'utiliser une source de données pour se connecter à la base de données.

De plus, lorsque nous souhaitons mettre en pool des connexions ou utiliser JNDI, une source de données est l'option la plus naturelle.

La classeSql de Groovy accepte très bien les sources de données:

def sql = Sql.newInstance(datasource)

3.3. Gestion automatique des ressources

Se souvenir d'appelerclose() lorsque nous en avons terminé avec une instanceSql est fastidieux; les machines se souviennent des choses beaucoup mieux que nous, après tout.

AvecSql, nous pouvons envelopper notre code dans une fermeture et faire en sorte que Groovy appelleclose() automatiquement lorsque le contrôle le quitte, même en cas d'exceptions:

Sql.withInstance(dbConnParams) {
    Sql sql -> haveFunWith(sql)
}

4. Émettre des déclarations sur la base de données

Maintenant, nous pouvons passer aux choses intéressantes.

La méthode la plus simple et non spécialisée pour émettre une instruction sur la base de données est la méthodeexecute:

sql.execute "create table PROJECT (id integer not null, name varchar(50), url varchar(100))"

En théorie, cela fonctionne aussi bien pour les instructions DDL / DML que pour les requêtes; Cependant, le formulaire simple ci-dessus n'offre pas un moyen de récupérer les résultats de la requête. Nous laisserons les requêtes pour plus tard.

La méthodeexecute a plusieurs versions surchargées, mais, encore une fois, nous examinerons les cas d'utilisation plus avancés de cette méthode et d'autres dans les sections suivantes.

4.1. Insertion de données

Pour insérer des données en petites quantités et dans des scénarios simples, la méthodeexecute évoquée plus haut convient parfaitement.

Cependant, pour les cas où nous avons généré des colonnes (par exemple, avec des séquences ou auto-incrémentation) et que nous voulons connaître les valeurs générées, une méthode dédiée existe:executeInsert.

En ce qui concerneexecute, nous allons maintenant examiner la surcharge de méthode la plus simple disponible, en laissant des variantes plus complexes pour une section ultérieure.

Supposons donc que nous ayons une table avec une clé primaire à incrémentation automatique (identité dans le jargon HSQLDB):

sql.execute "create table PROJECT (ID IDENTITY, NAME VARCHAR (50), URL VARCHAR (100))"

Insérons une ligne dans le tableau et enregistrons le résultat dans une variable:

def ids = sql.executeInsert """
  INSERT INTO PROJECT (NAME, URL) VALUES ('tutorials', 'github.com/eugenp/tutorials')
"""

executeInsert se comporte exactement commeexecute, mais que renvoie-t-il?

Il s'avère que la valeur de retour est une matrice: ses lignes sont les lignes insérées (rappelez-vous qu'une seule instruction peut entraîner l'insertion de plusieurs lignes) et ses colonnes sont les valeurs générées.

Cela semble compliqué, mais dans notre cas, qui est de loin le plus courant, il existe une seule ligne et une seule valeur générée:

assertEquals(0, ids[0][0])

Une insertion ultérieure renverrait une valeur générée de 1:

ids = sql.executeInsert """
  INSERT INTO PROJECT (NAME, URL)
  VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
"""

assertEquals(1, ids[0][0])

4.2. Mise à jour et suppression de données

De même, une méthode dédiée pour la modification et la suppression des données existe:executeUpdate.

Encore une fois, cela diffère deexecute uniquement par sa valeur de retour, et nous ne regarderons que sa forme la plus simple.

La valeur de retour, dans ce cas, est un entier, le nombre de lignes affectées:

def count = sql.executeUpdate("UPDATE PROJECT SET URL = 'https://' + URL")

assertEquals(2, count)

5. Interroger la base de données

Les choses commencent à devenir Groovy lorsque nous interrogeons la base de données.

Traiter avec la classe JDBCResultSet n'est pas vraiment amusant. Heureusement pour nous, Groovy offre une belle abstraction à ce sujet.

5.1. Itération des résultats de la requête

Alors que les boucles sont si anciennes ... nous sommes tous dans les fermetures de nos jours.

Et Groovy est là pour satisfaire nos goûts:

sql.eachRow("SELECT * FROM PROJECT") { GroovyResultSet rs ->
    haveFunWith(rs)
}

La méthodeeachRow émet notre requête sur la base de données et appelle une fermeture sur chaque ligne.

Comme nous pouvons le voir,a row is represented by an instance of GroovyResultSet, qui est une extension des anciensResultSet avec quelques goodies supplémentaires. Lisez la suite pour en savoir plus.

5.2. Accès aux jeux de résultats

En plus de toutes les méthodesResultSet,GroovyResultSet offre quelques utilitaires pratiques.

Principalement, il expose les propriétés nommées correspondant aux noms de colonne:

sql.eachRow("SELECT * FROM PROJECT") { rs ->
    assertNotNull(rs.name)
    assertNotNull(rs.URL)
}

Notez que les noms de propriété ne sont pas sensibles à la casse.

GroovyResultSet offre également l'accès aux colonnes en utilisant un index de base zéro:

sql.eachRow("SELECT * FROM PROJECT") { rs ->
    assertNotNull(rs[0])
    assertNotNull(rs[1])
    assertNotNull(rs[2])
}

5.3. Pagination

Nous pouvons facilement paginer les résultats, c’est-à-dire charger uniquement un sous-ensemble à partir d’un décalage jusqu’à un nombre maximum de lignes. Ceci est une préoccupation commune dans les applications Web, par exemple.

eachRow et les méthodes associées ont des surcharges acceptant un décalage et un nombre maximum de lignes renvoyées:

def offset = 1
def maxResults = 1
def rows = sql.rows('SELECT * FROM PROJECT ORDER BY NAME', offset, maxResults)

assertEquals(1, rows.size())
assertEquals('REST with Spring', rows[0].name)

Ici, la méthoderows renvoie une liste de lignes plutôt que de les itérer commeeachRow.

6. Requêtes et instructions paramétrées

Le plus souvent, les requêtes et les instructions ne sont pas complètement corrigées au moment de la compilation; ils ont généralement une partie statique et une partie dynamique, sous la forme de paramètres.

Si vous pensez à la concaténation de chaînes, arrêtez maintenant et allez lire sur l'injection SQL!

Nous avons mentionné précédemment que les méthodes que nous avons vues dans les sections précédentes comportent de nombreuses surcharges pour divers scénarios.

Introduisons ces surcharges qui traitent des paramètres dans les requêtes et instructions SQL.

6.1. Chaînes avec des espaces réservés

Dans un style similaire à JDBC standard, nous pouvons utiliser des paramètres de position:

sql.execute(
    'INSERT INTO PROJECT (NAME, URL) VALUES (?, ?)',
    'tutorials', 'github.com/eugenp/tutorials')

ou nous pouvons utiliser des paramètres nommés avec une carte:

sql.execute(
    'INSERT INTO PROJECT (NAME, URL) VALUES (:name, :url)',
    [name: 'REST with Spring', url: 'github.com/eugenp/REST-With-Spring'])

Cela fonctionne pourexecute,executeUpdate,rows eteachRow. executeInsert prend également en charge les paramètres, mais sa signature est un peu différente et plus délicate.

6.2. Cordes Groovy

Nous pouvons également opter pour un style Groovier utilisant GStrings avec des espaces réservés.

Toutes les méthodes que nous avons vues ne remplacent pas les espaces réservés dans les GStrings de la manière habituelle; au contraire, ils les insèrent en tant que paramètres JDBC, garantissant ainsi que la syntaxe SQL est correctement préservée, sans avoir besoin de citer ou d'échapper quoi que ce soit et donc pas de risque d'injection.

C'est parfaitement correct, sûr et Groovy:

def name = 'REST with Spring'
def url = 'github.com/eugenp/REST-With-Spring'
sql.execute "INSERT INTO PROJECT (NAME, URL) VALUES (${name}, ${url})"

7. Transactions et connexions

Jusqu'à présent, nous avons ignoré une préoccupation très importante: les transactions.

En fait, nous n’avons pas du tout parlé de la manière dont Groovy’sSql gère les connexions.

7.1. Connexions de courte durée

Dans les exemples présentés jusqu'à présent,each and every query or statement was sent to the database using a new, dedicated connection.Sql ferme la connexion dès que l'opération se termine.

Bien sûr, si nous utilisons un pool de connexions, l'impact sur les performances peut être faible.

Pourtant,if we want to issue multiple DML statements and queries as a single, atomic operation, nous avons besoin d'une transaction.

En outre, pour qu'une transaction soit possible en premier lieu, nous avons besoin d'une connexion couvrant plusieurs instructions et requêtes.

7.2. Transactions avec une connexion en cache

Groovy SQL ne nous permet pas de créer ou d'accéder explicitement à des transactions.

À la place, nous utilisons la méthodewithTransaction avec une fermeture:

sql.withTransaction {
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('tutorials', 'github.com/eugenp/tutorials')
    """
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
    """
}

Dans la fermeture, une seule connexion à la base de données est utilisée pour toutes les requêtes et instructions.

De plus, la transaction est automatiquement validée lorsque la fermeture prend fin, sauf si elle se termine tôt en raison d'une exception.

Cependant, nous pouvons également valider ou annuler manuellement la transaction actuelle avec des méthodes de la classeSql:

sql.withTransaction {
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('tutorials', 'github.com/eugenp/tutorials')
    """
    sql.commit()
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
    """
    sql.rollback()
}

7.3. Connexions mises en cache sans transaction

Enfin, pour réutiliser une connexion à une base de données sans la sémantique de transaction décrite ci-dessus, nous utilisonscacheConnection:

sql.cacheConnection {
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('tutorials', 'github.com/eugenp/tutorials')
    """
    throw new Exception('This does not roll back')
}

8. Conclusions et lectures complémentaires

Dans cet article, nous avons examiné le module Groovy SQL et comment il améliore et simplifie JDBC avec des fermetures et des chaînes Groovy.

Nous pouvons alors en déduire que JDBC un peu vieux a l’air un peu plus moderne avec une pincée de Groovy!

Nous n'avons pas parlé de toutes les fonctionnalités de Groovy SQL; par exemple, nous avons omis lesbatch processing, les procédures stockées, les métadonnées et d’autres choses.

Pour plus d'informations, voirthe Groovy documentation.

L'implémentation de tous ces exemples et extraits de code se trouve dansthe GitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.