Tutoriel iBatis

ibatisibatis_logoiBatis est un framework de persistance pour Java et .NET qui permet de mapper des objets avec des requêtes SQL ou des procédures stockées en utilisant des fichiers de description XML ou des annotations.
Jusqu’à mai 2010, le framework s’appelait iBatis et était développé par l’Apache Software Foundation, après quoi il a été déplacé vers Google Code et renommé en MyBatis. Il en résulte une évolution de l’API et de la syntaxe de fichiers de configuration qui ne gère pas de rétro compatibilité.
iBatis permet d’éliminer du code Java presque tout le code SQL, le passage (manuel) des paramètres et la récupération de chaque colonne de résultat.
Contrairement aux frameworks ORM, iBatis ne cherche pas à mapper un modèle objet sur une base de données relationnelle. Il ne fait que mapper des requêtes SQL sur des objets. Ce qui fait de iBatis un outil facile d’utilisation et aussi un très bon choix pour travailler avec une base de données existante ou dont le modèle est dénormalisé, ou simplement pour avoir un contrôle complet de l’exécution SQL.

Installation

Pour obtenir iBatis, suivez ce lien et téléchargez iBATIS_DBL-2.1.5.582.zip.
Dans l’archive, vous trouverez les fichiers ibatis-common-2.jar, ibatis-dao-2.jar, ibatis-sqlmap-2.jar que vous ajouterez au Build Path de votre projet.

Et voilà les dépendances à ajouter si vous utilisez Maven:

<dependency>
<groupId>com.ibatis</groupId>
<artifactId>ibatis2-sqlmap</artifactId>
<version>2.1.7.597</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.ibatis</groupId>
<artifactId>ibatis2-dao</artifactId>
<version>2.1.7.597</version>
<type>jar</type>
<scope>compile</scope>
</dependency>

Note: si vous n’utilisez pas Maven, suivez ce lien et téléchargez mybatis-3.0.5-bundle.zip. L’archive contient tous les JAR complémentaires nécessaires dans ce tutoriel.

Les logs sont très utiles, notamment pour visualiser ce qu’il se passe au niveau du SQL et de JDBC. Ajoutez donc au Build Path les JAR suivant: log4j-1.2.13.jar, slf4j-api-1.6.1.jar, slf4j-log4j12-1.6.1.jar.

Ou avec Maven:

<dependency>
<groupId>com.googlecode.sli4j</groupId>
<artifactId>sli4j-slf4j</artifactId>
<version>2.0</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.googlecode.sli4j</groupId>
<artifactId>sli4j-slf4j-log4j</artifactId>
<version>2.0</version>
<type>jar</type>
<scope>compile</scope>
</dependency>

Et voici le contenu du fichier de configuration des log log4j.properties à placer à la racine du CLASSPATH.

### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

log4j.logger.java.sql=debug

log4j.rootLogger=warn, stdout

Configurer le SqlMapClient

Pour fonctionner iBbatis repose sur un fichier de configuration XML. Ce fichier va contenir les informations nécessaires à établir une connexion vers la base de données ainsi que la référence vers les différents fichiers de mapping.
Il n’y a pas de contrainte de nommage pour le fichier de configuration XML. Ici nous le nommerons ibatis-config.xml. Le fichier doit être placé à la racine du CLASSPATH.
L’ensemble des paramètres de ce fichier permet de configurer la SqlMapClient.

Voilà à quoi ressemble le fichier de configuration initial (vide) pour Mybatis:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>

</sqlMapConfig>

La DTD prévoit que les balises soient déclarées dans un ordre précis.

Les propriétés

Vous pouvez utiliser un fichier de séparé (*.properties) pour déclarer un certain nombre de propriétés en dehors du fichier de configuration, en le déclarant comme suit:

<properties resource="mybatis-config.properties" />

Ces propriétés sont alors accessibles dans le reste du fichier de configuration sous la forme ${variable}.

Environnements

Ensuite il faut définir un ou plusieurs environnements de base de données, dans lesquels on détermine le mode de gestion de transaction et le DataSource.

Ensuite il faut définir un ou plusieurs environnements de base de données, dans lesquels on détermine le mode de gestion de transaction et le data-source.

<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="${driver}" />
<property name="JDBC.ConnectionURL" value="${url}" />
<property name="JDBC.Username" value="${username}" />
<property name="JDBC.Password" value="${password}" />
</dataSource>
</transactionManager>

Les valeurs des paramètres dans le fichier de propriétés:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydatabase
username=root
password=root

Nous utilisons pour l’exemple une base de données MySQL, donc le driver com.mysql.jdbc.Driver qui se trouve dans le JAR mysql-connector-java-5.1.17-bin.jar téléchargeable ici et à ajouter au buid path.

Ou avec Maven:

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.17</version>
<type>jar</type>
<scope>compile</scope>
</dependency>

DataSource

Cette partie est optionnelle, mais vous pouvez déclarer un DataSource dans votre serveur d’application (le cas échéant)
Si vous utilisez Tomcat, déclarez le DataSource dans le fichier context.xml.

<Resource name="jdbc/mydatabaseDS"
auth="Container"
type="javax.sql.DataSource"
maxActive="100"
maxIdle="30"
maxWait="10000"
username="root"
password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydatabase" />

Référencez le DataSource de type JNDI dans le fichier ibatis-config.xml.

<transactionManager type="JDBC">
<dataSource type="JNDI">
<property name="DataSource" value="java:comp/env/jdbc/mydatabaseDS" />
</dataSource>
</transactionManager>

Construction du SqlMapClient

Vous pouvez encuite utiliser cette classe pour créer
public class IbatisUtil {

private static final Logger logger = Logger.getLogger(IbatisUtil.class);
private static final String resource = "ibatis-config.xml";
private static SqlMapClient sqlMapClient;

static{
Reader reader = null;
try{
reader = Resources.getResourceAsReader(resource);
sqlMapClient = SqlMapClientBuilder.buildSqlMapClient(reader);
} catch (IOException e) {
logger.error(e);
}finally{
IOUtils.closeQuietly(reader);
}
}

public static SqlMapClient getSqlMapClient() {
return sqlMapClient;
}
}

Et récupérez une session ouverte de la façon suivante pour l’utiliser:

SqlMapClient sqlMap = IbatisUtil.getSqlMapClient();

Gestion d’une transaction

Pour commencer une transaction, appelez sqlMap.startTransaction().
Pour enregistrer des modification faites en base par cette transaction, appelez sqlMap.commitTransaction().
Toute transaction commencée doit impérativement être terminée: appelez sqlMap.endTransation() dans un bloc finally.

try{
sqlMap.startTransation();

// do something in transation

sqlMap.commitTransaction();
}catch(Exception e){
// handle exception
}finally{
sqlMap.endTransaction();
}

Bien que l’utilisation des transactions explicites soit fortement préférable, vous pouvez exploiter le mécanisme de transation automatique, généralement pour des opérations de lecteure seule.
Chaque opération exécutée en dehors d’un bloc transactionnel (explicite) initiera et validera (auto-commit) sa propre transacation.

Les fichiers de mapping

Enfin le fichier ibatis-config.xml doit faire référence aux fichiers de mapping de l’application.

<sqlMap resource="sqlmap/Book_SqlMap.xml" />
<sqlMap resource="sqlmap/User_SqlMap.xml" />

Un fichier de mapping contient un objet sqlMap XML, qui lui-même contient la définition de requêtes et de mapping objet => requête et requête => objet.
Note: pour les fonctions en dehors du SQL standard, vous pouvez (et devez) utiliser la syntaxe spécifique à la base de donnée sous-jacente.

<sqlMap namespace="book">

L’attribut namespace est très important, nous reviendrons plus loin sur son utilité.
Cet attribut sert de préfix pour accéder aux différents éléments déclarés dans les sql maps.
Ibatis référence les éléments en les préfixant par le namespace si l’attribut useStatementNamespaces de la section settings du fichier ibatis-config.xml est déclaré avec la valeur true. Par defaut, l’attribut vaut false.
Ce préfix peut être utile dans le cas où des éléments de même type utilisent le même identifiant dans des sql maps différents.

Lecture d’un objet en base

Requête simple

Nous avons ici la définition d’une requête SELECT avec son identifiant:

<select id="selectAllBooks" resultMap="bookResultMap">
SELECT *
FROM book b
</select>

L’attribut resultMap du select référence un objet resultMap dans lequel il faut déclarer pour chaque colonne de résultat de la requête, vers quel attribut de l’objet la valeur sera mappée.
Le tag id sert d’identifiant pour les résultats et pourra être utilisé pour les regroupements, point qui sera développé plus loin.
On remarque l’attribut type du resultMap. La valeur correspond au chemin complet de la classe de destination.

<resultMap type="com.soat.beans.Book" id="bookResultMap">
<result property="id" column="id_book" />
<result property="isbn" column="isbn" />
<result property="title" column="title" />
<result property="author" column="author" />
<result property="imageName" column="image_name" />
<result property="shortDescription" column="short_description" />
<result property="longDescription" column="long_description" />
</resultMap>

Mais on peut utiliser un nom plus court en déclarant un alias dans la section typeAliases de mybatis-config.xml.

<typeAlias alias="Book" type="com.soat.beans.Book" />

Ce qui permet alors d’écrire ensuite :

<resultMap type="Book" id="bookResultMap">

On peut encore raccourcir en s’affranchissant de déclarer un resultMap. En effet, si le nom de colonnes de résultat de la requête correspondent exactement au nom de attribut de la classe de destination, il suffit de déclarer un resultClass au lieu d’un resultMap, avec pour valeur le nom de la classe cible (ou son alias). On adapte les noms de colonnes en utilisant les alias SQL, avec le mot clé AS.

<select id="selectAllBooks" resultClass="Book">
SELECT
b.id_book AS id,
b.isbn,
b.title,
b.author,
b.short_description AS shortDescription,
b.long_description AS longDescription,
b.image_name AS imageName
FROM book b
</select>

Requête paramétrée

Pour passer des paramètres à une requête, il faut déclarer un attribut parameterClass.
La valeur peut être le nom d’une classe ou son alias.
Des alias sont prédéfinis pour les types courants tels que string, int, list, map.

<select id="selectBookById" parameterClass="int" resultType="Book">
SELECT
b.id_book AS id,
b.isbn,
b.title,
b.author,
b.short_description AS shortDescription,
b.long_description AS longDescription,
b.image_name AS imageName
FROM book b
WHERE b.id_book = #id#
</select>

On accède à la variable paramètre par la syntaxe #nom_du_param#.
On notera bien que tout comme en JDBC où le paramètre est symbolisé par ? il n’y a nul besoin de gérer le type de paramètre en écrivant le SQL (e.g.: les quotes pour les chaines de caractères).
Dans le cas de type simple (numérique, chaine de caractère, date), le nom du paramètre est arbitraire, étant donné qu’il n’y a qu’une seule valeur.

Dans le cas d’une Map, les noms des paramètres sont les clés de la Map.

<select id="selectBookById" parameterClass="map" resultClass="Book">
SELECT
b.*
FROM book b
WHERE b.title = #searched_title#
AND b.author = #searched_author#
</select>

On suppose ici que la Map passée en paramètre comporte des entrées pour les clés searched_title et searched_author.

Dans le cas d’un type personnalisé, notamment les types métier, les noms des paramètres sont les noms de propriétés de l’objet ; ce qui implique bien sûr que la classe en question déclare des getters pour ces propriétés.

Côté Java

Pour exécuter le SQL, il faut en premier lieu récupérer l’objet SqlMapClient évoqué plus tôt.
Ensuite, plusieurs méthodes sont proposées.
Le premier argument est toujours l’identifiant de la requête déclarée dans le fichier de mapping ; puis vient, si nécessaire l’objet paramètre.

Si la requête ne doit renvoyer qu’un seul résultat, comme une recherche sur une colonne ayant une contrainte d’unicité, comme la clé primaire:

Book book = (Book) sqlMap.queryForObject("selectBookById", 15);

Si la requête renvoie plusieurs résultats dans une liste.

List<Book> books = sqlMap.quetyForList("selectAllBooks");

Au lieu d’une liste on peut récupérer les résultats sous la forme d’une Map (clé / valeur)

Map<Integer,Book> books = sqlMap.queryForMap("selectAllBooks", null, "id");

La première méthode renvoie une Map clé => objet. Avec pour paramètre supplémentaire le nom d’une propiété de l’objet, on obtient une Map clé => propriété (donc pas l’objet en entier).

Map<Integer,String> books = sqlMap.queryForMap("selectAllBooks", null, "id", "name");

On peut limiter l’étendue des résultats avec les paramètres offset, et limit, de type entier.
Ici la liste retournée ne contiendra que 15 objets après le 10e, sous réserve d’un nombre suffisant de résultats.

List<Book> books = sqlMap.queryForList("selectAllBooks", null, 10, 15);

Les précédentes méthodes permettent de récupérer une collection d’objets qui sera utilisé ailleurs dans l’application. Mais il est possible de procéder au traitement de ces objets directement en utilisant un objet de type RowHandler passé en paramètre de de la méthode queryWithRowHandler.

final RowHandler rowHandler = new RowHandler() {
@Override
public void handleRow(Object valueObject) {
Book book = (Book) valueObject;
// custom code
book.getAuthor();
}
};

sqlMap.queryWithRowHandler("selectAllBooks ", null, rowHandler);

Remarque importante

Si vous utilisez dans vos requêtes les opérateurs de comparaison inférieur et supérieur, ceux-ci ne doivent pas être interprété comme étant du XML. Pour parer à cela, ils doivent être placés dans un tag <![CDATA[ ]]>, comme ceci:

WHERE comparable <![CDATA[ > ]]> #value#

Insertion d’un objet en base

Insertion simple

On constate bien que l’on accède directement aux propriétés de l’objet pour passer les paramètres à la requête.

<insert id="insertBook" parameterClass="Book" >
INSERT INTO book(
id_book, isbn, title, author,
short_description, long_description, image_name
)VALUES(
#id#, #isbn#, #title#, #author#,
#shortDescription#, #longDescription#, #imageName#
)
</insert>

Gestion de la clé primaire générée

Il est souvent préférable de laisser à la base de données le soin de gérer et générer les clés primaires pour l’insertion de nouvelles données. Utilisez l’attribut keyProperty avec comme valeur le nom de la propriété de l’objet qui contient cet identifiant. Après l’insertion, cette propriété de l’objet est mise à jour avec la valeur de cette clé.

<insert id="insertBook" parameterClass="Book" >
INSERT INTO book(
isbn, title, author,
short_description, long_description, image_name
)VALUES(
#isbn#, #title#, #author#,
#shortDescription#, #longDescription#, #imageName#
)

<selectKey resultClass="int"  keyProperty="id">
SELECT LAST_INSERT_ID() AS value
</selectKey>
</insert>

Attention, ici, la clé est récupérée par appelle de la fonction SELECT LAST_INSERT_ID(), qui est spécifique à MySQL. Pour les autres types de bases de données, l’opération s’effectue différemment.

Insertions multiples en une seule requête

Dans le monde de bases de données, il est souvent plus performant d’exécuter une seule requête complexe qu’une multitude de requêtes simples.
Ici on cherche à insérer une collection entière en une seule requête plutôt qu’une requête par élément de la collection.

<insert id="insertBookCategories" parameterClass="Book">
INSERT INTO category_book(
id_book,
id_category
)VALUES
<iterate property="categories" open="" close="" conjunction=",">
(#id#, #categories[].id#)
</iterate>
</insert>

Nous reviendrons sur l’utilisation du tag iterate dans le paragraphe sur le SQL dynamique.
Le SQL résultant doit ressembler à ceci:

INSERT INTO category_book(
id_book,
id_category
)VALUES (?, ?), (?, ?), (?, ?), (?, ?)

Côté Java

La syntaxe est simple et explicite:

sqlMap.insert("insertBook", book);

La méthode retourne le nombre de lignes insérées en base.

Mise à jour d’un objet en base

<update id="updateBook" parameterClass="Book">
UPDATE book SET
isbn = #isbn#,
title = #title#,
author = #author#,
short_description = #shortDescription#,
long_description = #longDescription#,
image_name = #imageName#
WHERE id_book = #id#
</update>

Côté Java

La syntaxe est simple et explicite:

sqlMap.update("updateBook", book);

La méthode retourne le nombre de lignes mises à jour en base.

Suppression d’un objet en base

<delete id="deleteBook" parameterClass="Book">
DELETE FROM book
WHERE id_book = #id#
</delete>

Côté Java

La syntaxe est simple et explicite:

sqlMap.delete("deleteBook", book);

La méthode retourne le nombre de lignes supprimées en base.

Appel d’une procédure stockée

Une procédure s’appelle comme une requête UPDATE.

Les paramètres doivent spécifier en plus de leur nom, le type JDBC et le mode (IN, OUT, INOUT) et le type JDBC, précisément dans cet ordre et sans espaces car la syntaxe est très rigide.

<procedure id="helloProcedure" parameterClass="Param" >
{ CALL helloProcedure(
#name,jdbcType=VARCHAR,mode=IN#,
#message,jdbcType=VARCHAR,mode=OUT#
)
}
</procedure>

Côté Java

Pour ce type d’appel, le paramètre doit être un objet ou une Map:

sqlMap.update("helloProcedure", param);
String res = param.getMessage();

Appel d’une fonction stockée

Une fonction peut être appelée à travers une requête SELECT qui renverra une seule valeur.

<statement id="helloFunction" parameterClass="map" resultClass="string">
select helloFunction(#name#) as message
</statement>

Une fonction peut être aussi appelée avec la syntaxe d’appel d’une procédure.

<procedure id="helloFunctionProc" parameterClass="Param" >
{ #message,jdbcType=VARCHAR,mode=OUT# = CALL helloFunction(
#name,jdbcType=VARCHAR,mode=IN#
) }
</procedure>

Côté Java

L’appel style SELECT:

String res = (String) session.selectOne("helloFunction", params);

L’appel style procédure:

Param param = new Param();
param.setName("John");
session.update("helloFunctionProc", param);
String res = param.getMessage();

Fragments SQL

Certaines requêtes se ressemblent souvent beaucoup, il alors possible d’extraire les fragments redondants pour une réutilisation dans plusieurs requêtes.
Pour cela, il suffit d’écrire le code du fragment dans un tag sql, référencé par l’attribut id.

<sql id="selectBook">
SELECT
b.id_book AS id,
b.isbn,
b.title,
b.short_description AS shortDescription
FROM book b
</sql>

Et de l’inclure dans les requêtes avec le tag include et l’attribut refid.

<select id="selectAllBooks" resultType="Book">
<include refid="selectBook"/>
</select>

Le SQL dynamique

iBatis fournit une solution relativement élégante pour gérer les requêtes dont les paramètres, colonnes et clauses doivent être inclus ou non.

Opérateur conditionnel binaire

Ce sont des opérateurs pour comparer une propriété avec une valeur ou une autre propriété. Les noms sont relativement explicites: isEqual, isNotEqual,
isGreaterThan, isGreaterEqual, isLessThan, isLessEqual.
Exemple:

<isLessEqual prepend="AND" property="age" compareValue="18">
adult = 'false'
</isLessEqual>

Opérateur conditionnel unaire

Ces opérateurs servent à évaluer une condition sur une propriété.
isPropertyAvailable et isNotPropertyAvailable pour vérifier si le paramètre contient ou non la propriété en question.
isNull et isNotNull pour évaluer si la propriété est nulle ou non.
isEmpty, isNotEmpty pour évaluer si la propriété est vide (nulle ou de taille 0) ou non, cela s’applique aux chaines de caractères et aux collections.

<isNotEmpty prepend="AND" property="name">
name = #name#
</isNotEmpty>

Autres opérateurs

isParameterPresent et isNotParameterPresent permettent de vérifier si l’unique paramètre passé côté Java est nul ou non.

Le tag dynamic englobe les opérateurs décrit précédemment. Il permet surtout la gestion du prepend dont la valeur sera ajoutée ou non de façon à rendre du code SQL valide. Dans cet exemple, les WHERE et AND seront présents ou non suivant le résultat de l’évaluation des conditions.

<dynamic prepend="WHERE">
<isGreaterThan prepend="AND" property="id" compareValue="0">
id_book = #id#
</isGreaterThan>
<isNotNull prepend="AND" property="name">
name = #name#
</isNotNull>
</dynamic>

Itération

Le tag iterate permet d’itérer sur une variable de type collection ou tableau.
Un des cas d’utilisation typique et la gestion des clauses IN.

username IN
<iterate property="userNameList" open="(" close=")" conjunction=", ">
#userNameList[]#
</iterate>

L’élément courant de la collection itérée s’accède en ajoutant [] après le nom de la variable.

Mapping avancé

Considérons la classe ayant les attributs suivants:

public class Book {

private Integer id;
private String name;
private Author author;
private List<Category> categories;

}

Nous avons vu précédemment le mapping des propriétés de type scalaire (Integer, String).

N+1 select

Pour charger les propriétés author et categories, nous pouvons nous retrouver dans le cas de ce qu’on appelle le N+1 select.
D’abord 1 SELECT pour obtenir l’objet Book: le +1, ensuite 1 SELECT pour renseigner la propriété.
Mais si l’on récupère N objets Book pour la première requête, alors il y aura N SELECT pour renseigner la propriété: un pour chaque objet Book.
D’où N+1 SELECT.
La plupart du temps, pour obtenir finalement le même résultat, il est préférable en terme de performances, d’exécuter une seule requête complexe plutôt qu’une multitude de requêtes simples.

Mapping d’une association

resultMap imbriqué

Une réponse au problème du N+1 SELECT consiste à écrire une seule requête, plus complexe, en l’occurrence avec des jointures.

<select id="selectBookAndAuthorGreedy" resultMap="greedyBook">
SELECT
b.id_book,
b.isbn,
b.title,
b.short_description,
a.id_author,
a.name_author
FROM book b
LEFT OUTER JOIN author a ON b.id_author = a.id_author
</select>

Ensuite tout se passe au niveau du resultMap, qui jusque’ici ne gérait que des propriétés scalaires, va ici gérer une association.
Notez ici l’utilisation de l’attribut extends qui permet de réutiliser un resultMap en exploitant le concept d’héritage.

<resultMap type="Book" id="greedyBook" extends="bookResultMap">
<result property="author.id" column="id_author" />
<result property="author.name" column="name_author" />
</resultMap>

L’association mappe la propriété author. Chaque propriété de author doit être déclarée. La possibilité d’utiliser un resultMap a été introduite dans les versions suivantes.

select imbriqué

Pour parvenir au même résultat, il existe une autre solution: utiliser un select imbriqué dans le resultMap.
On revient alors à une requête plus simple.

<select id="selectBookAndAuthorLazy" resultMap="lazyBook">
SELECT
b.id_book,
b.isbn,
b.title,
b.short_description,
b.id_author
FROM book b
</select>

Mais le resultMap évolue.

<resultMap class="Book" id="lazyBook" extends="bookResultMap">
<result property="author" column="id_author" javaType="Author" select="book.selectAuthorById" />
</resultMap>

Et une autre requête est exécutée pour chaque résultat de la première:

<select id="selectAuthorById" parameterClass="int" resultClass="Author">
SELECT
a.id_author AS id,
a.name_author AS name
FROM author a
WHERE a.id_author = #id#
</select>

Imbriquer un select revient ici à la situation du N+1 SELECT, mais ce mode d’utilisation des resultMap permet d’exploiter le lazy loading (voir plus bas).

Mapping d’une collection

resultMap imbriqué

Pour mapper une collection, on peut également se base sur une requête à jointures pour éviter le N+1 SELECT.

<select id="selectBookAndCategoryGreedy" resultMap="greedyBook">
SELECT
b.id_book,
b.isbn,
b.title,
b.short_description,
c.id_category,
c.name_category
FROM book b
LEFT OUTER JOIN category_book cb ON b.id_book = cb.id_book
LEFT OUTER JOIN category c ON cb.id_category = c.id_category
</select>

Ensuite tout se passe au niveau du resultMap, qui va ici gérer une collection.
Les jointures vont multiplier les lignes de résultat: il y aura autant de résultats que d’éléments dans la collection mappée. Le regroupement se fait grâce à l’attribut groupBy du resultMap, qui prend pour valeur le nom de la propriété sur laquelle on souhaite regrouper, typiquement l’identifiant de l’objet conteneur.

<resultMap type="Book" id="greedyBook" extends="bookResultMap" groupBy="id">
<result property="categories" ofType="Category" resultMap="book.categoryResultMap" />
</resultMap>

La collection mappe la propriété categories qui est une collection d’objets de type Category en utilisant le resultMap categoryResultMap.

<resultMap class="Category" id="categoryResultMap" >
<result property="id" column="id_category" />
<result property="name" column="name_category" />
</resultMap>

select imbriqué

Pour parvenir au même résultat, il existe une autre solution: utiliser un select imbriqué dans le resultMap.
On revient alors à une requête plus simple.

<select id="selectBookAndCategoryLazy" resultMap="lazyBook">
SELECT
b.id_book,
b.isbn,
b.title,
b.short_description,
b.id_author
FROM book b
</select>

Mais le resultMap évolue.

<resultMap class="Book" id="lazyBook" extends="bookResultMap">
<result property="categories" column="id_book" ofType="Category" select="book.selectCategoryByBookId" />
</resultMap>

Et une autre requête est exécutée pour chaque résultat de la première:

<select id="selectCategoryByBookId" parameterType="int" resultClass="Category">
SELECT
c.id_category AS id,
c.name_category AS name
FROM category c JOIN category_book cb ON c.id_category = cb.id_category
WHERE cb.id_book = #{id}
</select>

Attention: si vous avez activé l’utilisation du namespace, vous devez utiliser le préfix quand vous imbriquez un resultMap ou un select dans un autre resultMap.
Pour l’exemple, le namespace vaut book, ce qui donne: resultMap="book.categoryResultMap" et select="book.selectCategoryByBookId".

Cas d’une clé composite

Dans le cas d’une clé composite, pour l’attribut column de l’association ou de la collection, écrivez:

column="{p1=id_author, p2=other_id}"

Dans ce cas, le select imbriqué aura accès à un objet paramètre ayant pour clés p1 et p2 et les valeurs associées. Pas besoin de déclarer un parameterClass dans ce cas, le paramètre étant passé dynamiquement.

<select id="selectAuthorBy2Ids" resultClass="Author">
SELECT
a.id_author AS id,
a.name_author AS name
FROM author a
WHERE a.id_author = #p1#
AND a.other_id = #p2#
</select>

Imbriquer un select revient ici à la situation du N+1 SELECT, mais ce mode d’utilisation des resultMap permet d’exploiter le lazy loading (voir plus bas).

Bien sûr tous ces tags peuvent cohabiter dans la même resultMap et mapper ainsi un résultat très complexe.

Lazy loading

Le Lazy loading (ou chargement paresseux) est un modèle de conception pour retarder l’initialisation d’un objet jusqu’à ce qu’on en ait besoin. Cela peut contribuer à l’efficacité d’un programme si c’est utilisé correctement et de manière appropriée. Pour le concept inverse, on utilise le terme Eager Loading (chargement avide).

Pour exploiter les fonctionnalités de Lazy loading avec iBatis, ajoutez les JAR cglib.jar et asm.jar au build path.

Ou avec Maven:

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
<type>jar</type>
<scope>compile</scope>
</dependency>

La fonctionnalité n’est active que si la librairie est trouvée à l’exécution.
La configuration se fait dans ibatis-config.xml:

<settings
cacheModelsEnabled="true"
enhancementEnabled="true"
lazyLoadingEnabled="true" />

Pour aller plus loin

Cet article avait pour objectif d’exposer les principaux cas d’utilisation. Néanmoins, vous pouvez consulter la documentation pour approfondir des sujets plus complexes comme les différents types de gestion transactionnelle ou le mécanisme de cache.
De plus, iBatis s’intègre très bien avec d’autres outils, tels que Spring.

Vous pouvez trouver les sources sur GitHub : https://github.com/JAVASOAT/IBatisTutorial1

Nombre de vue : 377

AJOUTER UN COMMENTAIRE