Tutoriel iBatis
iBatis
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"
"https://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</codea href="https://dev.mysql.com/downloads/connector/j/">téléchargeable ici</a> et à ajouter au <code class="language-plaintext">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</codecode class="language-plaintext">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