L'une des tâches les plus fastidieuses pour les développeurs Java réside souvent dans l'intégration de requêtes SQL directement au sein du code applicatif. Cette difficulté est exacerbée lorsque ces requêtes nécessitent une construction dynamique, un scénario où les fichiers externes ou les procédures stockées ne suffisent pas toujours. Bien que MyBatis offre des capacités robustes de génération SQL dynamique via ses mappings XML, il existe des situations où la construction de requêtes directement en Java est inévitable. Dans ces cas, l'API SQL de MyBatis se révèle être un outil précieux, simplifiant la gestion des concaténations, des guillemets, des retours à la ligne, du formatage et surtout, de la logique complexe des clauses conditionnelles (WHERE, AND, OR).
La génération manuelle de SQL dynamique en Java peut rapidement devenir un cauchemar de maintenance, comme l'illustre l'exemple suivant :
String rawSql = "SELECT U.USER_ID, U.LOGIN_NAME, U.EMAIL, U.FIRST_NAME, " +
"U.LAST_NAME, U.CREATED_ON, U.LAST_MODIFIED " +
"FROM APP_USER U, USER_ROLE UR " +
"INNER JOIN DEPARTMENT D ON D.DEPARTMENT_ID = U.DEPARTMENT_ID " +
"INNER JOIN COMPANY C ON D.COMPANY_ID = C.COMPANY_ID " +
"WHERE (U.USER_ID = UR.USER_ID AND U.FIRST_NAME LIKE ?) " +
"OR (U.LAST_NAME LIKE ?) " +
"GROUP BY U.USER_ID " +
"HAVING (U.LAST_NAME LIKE ?) " +
"OR (U.FIRST_NAME LIKE ?) " +
"ORDER BY U.USER_ID, U.FIRST_NAME";
Simplification avec la Classe SQL de MyBatis
MyBatis 3 introduit la classe utilitaire org.apache.ibatis.jdbc.SQL pour relever ce défi. Grâce à cette classe, la création de requêtes SQL dynamiques est grandement simplifiée. Il suffit d'instancier la classe et d'appeler ses méthodes pour construire la requête étape par étape. Reprenons l'exemple précédent avec l'API SQL :
private String buildUserDetailedQuery() {
return new SQL() {{
SELECT("U.USER_ID, U.LOGIN_NAME, U.EMAIL, U.FIRST_NAME");
SELECT("U.LAST_NAME, U.CREATED_ON, U.LAST_MODIFIED");
FROM("APP_USER U");
FROM("USER_ROLE UR");
INNER_JOIN("DEPARTMENT D ON D.DEPARTMENT_ID = U.DEPARTMENT_ID");
INNER_JOIN("COMPANY C ON D.COMPANY_ID = C.COMPANY_ID");
WHERE("U.USER_ID = UR.USER_ID");
WHERE("U.FIRST_NAME LIKE ?");
OR();
WHERE("U.LAST_NAME LIKE ?");
GROUP_BY("U.USER_ID");
HAVING("U.LAST_NAME LIKE ?");
OR();
HAVING("U.FIRST_NAME LIKE ?");
ORDER_BY("U.USER_ID");
ORDER_BY("U.FIRST_NAME");
}}.toString();
}
L'avantage principal de cette approche est l'élimination des soucis liés aux opérateurs logiques tels que AND ou OR dans les clauses WHERE et HAVING. La classe SQL gère automatiquement l'insertion appropriée de WHERE initial, puis des AND successifs, et les OR lorsque spécifiés, tout en prenant en charge les concaténations de chaînes.
Exemples d'utilisation de la classe SQL
Voici d'autres cas d'utilisation pour diverses opérations SQL :
// Style classe anonyme avec bloc d'initialisation
public String removeUserRecord() {
return new SQL() {{
DELETE_FROM("APP_USER");
WHERE("USER_ID = #{id}");
}}.toString();
}
// Style constructeur fluent
public String registerNewUser() {
String sql = new SQL()
.INSERT_INTO("APP_USER")
.VALUES("USER_ID, FIRST_NAME", "#{userId}, #{givenName}")
.VALUES("LAST_NAME", "#{familyName}")
.toString();
return sql;
}
// Conditions dynamiques (les paramètres doivent être `final` pour être accessibles dans la classe anonyme)
public String searchUsersByCriteria(final String identifier, final String firstNamePattern, final String lastNamePattern) {
return new SQL() {{
SELECT("U.USER_ID, U.LOGIN_NAME, U.FIRST_NAME, U.LAST_NAME");
FROM("APP_USER U");
if (identifier != null) {
WHERE("U.USER_ID LIKE #{identifier}");
}
if (firstNamePattern != null) {
WHERE("U.FIRST_NAME LIKE #{firstNamePattern}");
}
if (lastNamePattern != null) {
WHERE("U.LAST_NAME LIKE #{lastNamePattern}");
}
ORDER_BY("U.LAST_NAME");
}}.toString();
}
public String modifyUserFirstName() {
return new SQL() {{
UPDATE("APP_USER");
SET("FIRST_NAME = #{givenName}");
WHERE("USER_ID = #{userId}");
}}.toString();
}
Méthodes de la Classe SQL
Le tableau suivant répertorie les principales méthodes disponibles dans la classe SQL :
| Méthode | Description |
|---|---|
SELECT(String), SELECT(String...) |
Initialise ou ajoute des colonnes à la clause SELECT. Peut être appelée plusieurs fois pour ajouter des éléments. Les arguments sont généralement des noms de colonnes ou des alias, séparés par des virgules. |
SELECT_DISTINCT(String), SELECT_DISTINCT(String...) |
Similaire à SELECT, mais ajoute le mot-clé DISTINCT à la requête générée. |
FROM(String), FROM(String...) |
Initialise ou ajoute des tables à la clause FROM. Peut être appelée plusieurs fois. Les arguments sont généralement des noms de tables ou des alias. |
JOIN(String), JOIN(String...) INNER_JOIN(String), INNER_JOIN(String...) LEFT_OUTER_JOIN(String), LEFT_OUTER_JOIN(String...) RIGHT_OUTER_JOIN(String), RIGHT_OUTER_JOIN(String...) |
Ajoute une clause JOIN du type spécifié. Les arguments peuvent inclure les conditions de jointure (par exemple, "TABLE_A TA ON TA.ID = TB.A_ID"). |
WHERE(String), WHERE(String...) |
Ajoute une condition à la clause WHERE. Les appels multiples sont automatiquement combinés avec AND. Utilisez OR() pour une séparation explicite par OR. |
OR() |
Sépare les conditions actuelles de la clause WHERE ou HAVING avec l'opérateur OR. |
AND() |
Sépare les conditions actuelles avec AND. Moins courant car WHERE et HAVING utilisent AND par défaut. |
GROUP_BY(String), GROUP_BY(String...) |
Ajoutte des colonnes à la clause GROUP BY, séparées par des virgules. |
HAVING(String), HAVING(String...) |
Ajoute une condition à la clause HAVING. Les appels multiples sont automatiquement combinés avec AND. Utilisez OR() pour une séparation explicite par OR. |
ORDER_BY(String), ORDER_BY(String...) |
Ajoute des colonnes à la clause ORDER BY, séparées par des virgules. |
LIMIT(String), LIMIT(int) |
Ajoute une clause LIMIT. Applicable uniquement avec SELECT(), UPDATE(), DELETE(). S'utilise souvent avec OFFSET() pour SELECT. (Introduit en 3.5.2) |
OFFSET(String), OFFSET(long) |
Ajoute une clause OFFSET. Applicable uniquement avec SELECT(). S'utilise souvent avec LIMIT(). (Introduit en 3.5.2) |
OFFSET_ROWS(String), OFFSET_ROWS(long) |
Ajoute une clauce OFFSET n ROWS. Applicable uniquement avec SELECT(). S'utilise souvent avec FETCH_FIRST_ROWS_ONLY(). (Introduit en 3.5.2) |
FETCH_FIRST_ROWS_ONLY(String), FETCH_FIRST_ROWS_ONLY(int) |
Ajoute une clause FETCH FIRST n ROWS ONLY. Applicable uniquement avec SELECT(). S'utilise souvent avec OFFSET_ROWS(). (Introduit en 3.5.2) |
DELETE_FROM(String) |
Commence une instruction DELETE, spécifiant la table. Généralement suivie d'une clause WHERE. |
INSERT_INTO(String) |
Commence une instruction INSERT, spécifiant la table. Doit être suivie d'appels à VALUES(), ou INTO_COLUMNS() et INTO_VALUES(). |
SET(String), SET(String...) |
Ajoute une ou plusieurs clauses SET à une instruction UPDATE. |
UPDATE(String) |
Commence une instruction UPDATE, spécifiant la table. Doit être suivie d'appels à SET() et généralement à WHERE(). |
VALUES(String, String) |
Ajoute une paire de colonnes/valeurs à une instruction INSERT. |
INTO_COLUMNS(String...) |
Spécifie les colonnes pour une instruction INSERT. À utiliser avec INTO_VALUES(). |
INTO_VALUES(String...) |
Spécifie les valeurs pour une instruction INSERT. À utiliser avec INTO_COLUMNS(). |
ADD_ROW() |
Ajoute une nouvelle ligne de données pour les insertions multiples (batch). (Introduit en 3.5.2) |
Il est important de noter que la classe SQL insère les clauses LIMIT, OFFSET, OFFSET n ROWS et FETCH FIRST n ROWS ONLY telles quelles. Aucune transformation n'est effectuée pour les bases de données qui ne supportent pas ces syntaxes. L'utilisateur doit donc s'assurer de la compatibilité avec la base de données cible pour éviter les erreurs d'exécution.
Utilisation des arguments de longueur variable (Varargs) - Depuis MyBatis 3.4.2
À partir de la version 3.4.2, il est possible d'utiliser des arguments de longueur varible pour simplifier l'écriture :
public String queryDetailedUserSql() {
return new SQL()
.SELECT("U.USER_ID", "UR.ROLE_NAME", "U.EMAIL", "U.FIRST_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
.FROM("APP_USER U", "USER_ROLE UR")
.INNER_JOIN("DEPARTMENT D ON D.DEPARTMENT_ID = U.DEPARTMENT_ID", "COMPANY C ON D.COMPANY_ID = C.COMPANY_ID")
.WHERE("U.USER_ID = UR.USER_ID", "U.FIRST_NAME LIKE #{name}")
.ORDER_BY("U.USER_ID", "U.FIRST_NAME")
.toString();
}
public String insertUserMinimalSql() {
return new SQL()
.INSERT_INTO("APP_USER")
.INTO_COLUMNS("USER_ID", "FIRST_NAME")
.INTO_VALUES("#{id}", "#{givenName}")
.toString();
}
public String updateUserContactSql() {
return new SQL()
.UPDATE("APP_USER")
.SET("EMAIL = #{userEmail}", "PHONE_NUMBER = #{userPhone}")
.WHERE("USER_ID = #{id}")
.toString();
}
Construction d'instructions d'insertion en lot (Batch Inserts) - Depuis MyBatis 3.5.2
Depuis la version 3.5.2, vous pouvez construire des insertions en lot :
public String batchInsertUsersSql() {
// INSERT INTO APP_USER (USER_ID, FIRST_NAME)
// VALUES (#{userA.id}, #{userA.givenName}) , (#{userB.id}, #{userB.givenName})
return new SQL()
.INSERT_INTO("APP_USER")
.INTO_COLUMNS("USER_ID", "FIRST_NAME")
.INTO_VALUES("#{userA.id}", "#{userA.givenName}")
.ADD_ROW()
.INTO_VALUES("#{userB.id}", "#{userB.givenName}")
.toString();
}
Requêtes SELECT avec pagination (LIMIT/OFFSET) - Depuis MyBatis 3.5.2
Depuis la version 3.5.2, la construction de requêtes SELECT avec des clauses de pagination est prise en charge :
public String fetchUsersWithPagination() {
// SELECT user_id, first_name FROM APP_USER
// LIMIT #{maxRows} OFFSET #{startIndex}
return new SQL()
.SELECT("user_id", "first_name")
.FROM("APP_USER")
.LIMIT("#{maxRows}")
.OFFSET("#{startIndex}")
.toString();
}
public String fetchUsersWithRowOffset() {
// SELECT user_id, first_name FROM APP_USER
// OFFSET #{startIndex} ROWS FETCH FIRST #{maxRows} ROWS ONLY
return new SQL()
.SELECT("user_id", "first_name")
.FROM("APP_USER")
.OFFSET_ROWS("#{startIndex}")
.FETCH_FIRST_ROWS_ONLY("#{maxRows}")
.toString();
}
SqlBuilder et SelectBuilder (Obsolètes)
Avant la version 3.2, l'implémentation de la génération SQL dynamique était différente, utilisant des variables ThreadLocal pour masquer certaines contraintes du DSL Java. Avec l'évolution des frameworks de construction SQL et la familiarisation des développeurs avec le pattern "builder" et les classes anonymes, les classes SelectBuilder et SqlBuilder basées sur cette approche ont été dépréciées.
Les méthodes suivantes sont spécifiques aux classes obsolètes SqlBuilder et SelectBuilder :
| Méthode | Description |
|---|---|
BEGIN() / RESET() |
Ces méthodes réinitialisent l'état ThreadLocal de SelectBuilder et préparent la construction d'une nouvelle instruction. BEGIN() est l'option la plus lisible pour démarrer une nouvelle requête. RESET() est utile pour réinitialiser l'état en cours de construction si la logique du programme exige un changement complet. |
SQL() |
Cette méthode renvoie la chaîne SQL générée et réinitialise l'état de SelectBuilder (équivalent à appeler BEGIN() ou RESET()). Elle ne doit donc être appelée qu'une seule fois par séquence de construction. |
SelectBuilder et SqlBuilder n'étaient pas magiques, mais leur fonctionnement reposait sur l'importation statique de leurs méthodes et l'utilisation de variables ThreadLocal pour offrir une syntaxe concise. Pour les utiliser, il fallait importer statiquement les méthodes de l'une des classes (mais pas les deux simultanément) :
import static org.apache.ibatis.jdbc.SelectBuilder.*;
import static org.apache.ibatis.jdbc.SqlBuilder.*;
Puis, les requêtes étaient construites comme suit :
/* Déprécié */
public String fetchProductNamesSql() {
BEGIN(); // Réinitialise l'état ThreadLocal
SELECT("PRODUCT_NAME");
FROM("PRODUCTS");
return SQL();
}
/* Déprécié */
private String fetchDetailedUserLegacySql() {
BEGIN(); // Réinitialise l'état ThreadLocal
SELECT("U.ID, U.LOGIN, U.EMAIL, U.NOM_COMPLET");
SELECT("U.DATE_CREATION, U.DATE_MODIFICATION");
FROM("UTILISATEUR U");
FROM("COMPTE C");
INNER_JOIN("SERVICE S ON S.ID = U.SERVICE_ID");
INNER_JOIN("ENTREPRISE E ON S.ENTREPRISE_ID = E.ID");
WHERE("U.ID = C.UTILISATEUR_ID");
WHERE("U.NOM like ?");
OR();
WHERE("U.PRENOM like ?");
GROUP_BY("U.ID");
HAVING("U.PRENOM like ?");
OR();
HAVING("U.NOM like ?");
ORDER_BY("U.ID");
ORDER_BY("U.NOM_COMPLET");
return SQL();
}