Tutoriel MyBatis

mybatis_logomybatisMyBatis 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é.
MyBatis 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, Mybatis 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 MyBatis 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 MyBatis, suivez ce lien et téléchargez mybatis-3.0.5-bundle.zip.
Dans l’archive, vous trouverez le fichier mybatis-3.0.5.jar que vous ajouterez au Build Path de votre projet.

Et voilà la dépendance à ajouter si vous utilisez Maven:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.0.5</version>
<type>jar</type>
<scope>compile</scope>
</dependency>

Attention: pour fonctionner, MyBatis requiert Java 5 au minimum.

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

Configuration de la SqlSession

Pour fonctionner Mybatis 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 mybatis-config.xml. Le fichier doit être placé à la racine du CLASSPATH.
L’ensemble des paramètres de ce fichier permet de configurer la SessionFactory.

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

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

</configuration>

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.

<environments default="development">

<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>

</environments>

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 mybatis-config.xml.

<environments default="integ">

<environment id="integ">
<transactionManager type="JDBC" />
<dataSource type="JNDI">
<property name="data_source" value="java:comp/env/jdbc/mydatabaseDS" />
</dataSource>
</environment>

</environments>

Le fonctionnement de l’application doit être identique.

Construction de la SqlSession

Vous pouvez ensuite utiliser cette classe pour créer

public class MybatisUtil {

private static final Logger logger = Logger.getLogger(MybatisUtil.class);
private static final String resource = "mybatis-config.xml";
private static SqlSessionFactory sqlSessionFactory;

static{
Reader reader=null;
try{
reader = Resources.getResourceAsReader(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (IOException e) {
logger.error(e);
}finally{
IOUtils.closeQuietly(reader);
}
}

public static SqlSession getSession() {
return sqlSessionFactory.openSession();
}
}

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

SqlSession session = MybatisUtil.getSession();

Gestion d’une transaction

Une transaction est initialisée par défaut à l’ouverture de la session.
Pour enregistrer des modification faites en base par cette transaction, appelez session.commit(), pour annuler explicitement ces modifications, appelez session.rollback(). Toute modification qui n’aura pas été enregistrée sera perdu à la fermeture de la session: session.close().

try{
// do something with the session

session.commit();
}catch(Exception e){
session.rollback();
}finally{
session.close();
}

Les fichiers de mapping

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

<mappers>
<mapper resource="mapper/BookMapper.xml" />
<mapper resource="mapper/UserMapper.xml" />
</mappers>

Un fichier de mapping contient un objet mapper 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.

<mapper namespace="com.soat.dao.mybatis.mapper.BookMapper">

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 mappers. Mybatis référence les éléments avec et sans ce préfix, ce qui rend généralement son utilisation facultative lors de l’appel de requêtes. Ce préfix peut être utile dans le cas où des éléments de même type utilisent le même identifiant dans des mappers 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">
<id 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.

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

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 resultType 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" 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
</select>

Requête paramétrée

Pour passer des paramètres à une requête, il faut déclarer un attribut parameterType.
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" parameterType="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" parameterType="map" resultType="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 SqlSession é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) session.selectOne("selectBookById", 15);

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

List<Book> books = session.selectList("selectAllBooks");

Au lieu d’une liste on peut récupérer les résultats sous la forme d’une Map (clé / valeur); le troisième paramètre est le nom de la propriété qui est utilisée comme clé pour la Map.

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

On peut limiter l’étendue des résultats avec un paramètre de type RowBounds (offset, limit).
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 = session.selectList("selectAllBooks", null, new RowBounds(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 ResultHandler passé en paramètre de de la méthode select.

final ResultHandler handler = new ResultHandler() {
@Override
public void handleResult(ResultContext context) {
final Book book = (Book) context.getResultObject();
// custom code ...
}
};
session.select("selectAllBooks", handler);

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é dans un tag <![CDATA[ ]]>, comme ceci:

WHERE comparable <!&#91;CDATA&#91; > &#93;&#93;> #{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" parameterType="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" parameterType="Book" keyProperty="id">
INSERT INTO book(
isbn, title, author,
short_description, long_description, image_name
)VALUES(
#{isbn}, #{title}, #{author},
#{shortDescription}, #{longDescription}, #{imageName}
)
</insert>

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" parameterType="Book">
INSERT INTO category_book(
id_book,
id_category
)VALUES
<foreach collection="categories" item="cat" open="" close="" separator=",">
(#{id}, #{cat.id})
</foreach>
</insert>

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

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

Rappel: pour ce type d’insertion, la syntaxe est spécifique à MySQL. La requête peut s’écrire différemment ou ne pas être possible pour d’autres types de bases de données.

Côté Java

La syntaxe est simple et explicite:

session.insert("insertBook", book);

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

Mise à jour d’un objet en base

La mise à jour utilise le même modèle:
<update id="updateBook" parameterType="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:

session.update("updateBook", book);

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

Suppression d’un objet en base

La suppression utilise le même modèle:

<delete id="deleteBook" parameterType="Book">
DELETE FROM book
WHERE id_book = #{id}
</delete>

Côté Java

La syntaxe est simple et explicite:

session.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.
Il faut préciser l’attribut statementType avec la valeur CALLABLE.
Les paramètres doivent spécifier en plus de leur nom, le mode (IN, OUT, INOUT) et le type JDBC.

<update id="helloProcedure" statementType="CALLABLE" parameterType="Param">
{CALL helloProcedure(
#{name, mode=IN, jdbcType=VARCHAR},
#{message, mode=OUT, jdbcType=VARCHAR}
)
}
</update>

Côté Java

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

Param param = new Param();
param.setName("John");
session.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.

<select id="helloFunction" parameterType="map" resultType="string">
SELECT helloFunction(#{name}) AS message
</select>

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

<update id="helloFunctionProc" statementType="CALLABLE" parameterType="Param">
{ #{message,jdbcType=VARCHAR,mode=OUT} = CALL helloFunction(
#{name,jdbcType=VARCHAR,mode=IN}
) }
</update>

Côté Java

L’appel style SELECT:

Param param = new Param();
param.setName("John");
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

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

Les conditions

if permet d’ajouter ou non du code SQL en fonction d’une condition.
La valeur du test doit être un booléen ou homogène à un booléen.
Vous pouvez appeler des fonctions Java sur la propriété évaluée.
Pour utiliser les opérateur de comparaison relative (inférieur, supérieur), les symbole entrant en conflit avec le XML, utilisez les codes à la place: lt, le, eq, ne, ge, gt.

<if test="value != null">
b.id_book = #{value}
</if>

Sur le même principe, choosewhenotherwise s’utilise comme un ifelse

<choose>
<when test="value == null">
name = #{value}
</when>
<otherwise>
name = ''
</otherwise>
</choose>

ou comme un switch

<choose>
<when test="value == 1">
code = 'A'
</when>
<when test="value == 2">
code = 'B'
</when>
<otherwise>
code IS NULL
</otherwise>
</choose>

Le tag where 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.

<where>
<if test="id gt 0">
id_book = #id#
</if>
<if test="name != null">
AND name = #name#
</if>
</where>

Le set pour les requêtes UPDATE fonctionne exactement de la même manière.

Itération

Le tag foreach 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
<foreach item="userName" index="index" collection="userNamelist" open="(" separator="," close=")">
#{userName}
</foreach>

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">
<association property="author" javaType="Author" resultMap="authorResultMap" />
</resultMap>

L’association mappe la propriété author de type Author en utilisant le resultMap authorResultMap.

<resultMap type="Author" id="authorResultMap" >
<id property="id" column="id_author" />
<result property="name" column="name_author" />
</resultMap>

Notez: le contenu du resultMap authorResultMap pourrait être aussi défini directement dans le tag association.

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 type="Book" id="lazyBook" extends="bookResultMap">
<association property="author" column="id_author" javaType="Author" select="selectAuthorById" />
</resultMap>

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

<select id="selectAuthorById" parameterType="int" resultType="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’élements dans la collection mappée. Le regroupement se fait grâce à l’élément id du resultMap, qui prend pour valeur le nom de la propriété sur laquel on souhaite regrouper, typiquement l’identifiant de l’objet conteneur.

<resultMap type="Book" id="greedyBook" extends="bookResultMap">
<collection property="categories" ofType="Category" resultMap="categoryResultMap" />
</resultMap>

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

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

Notez: le contenu du resultMap categoryResultMap pourrait être aussi défini directement dans le tag collection.

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 type="Book" id="lazyBook" extends="bookResultMap">
<collection property="categories" column="id_book" ofType="Category" select="selectCategoryByBookId" />
</resultMap>

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

<select id="selectCategoryByBookId" parameterType="int" resultType="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>

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 parameterType dans ce cas, le paramètre étant passé dynamiquement.

<select id="selectAuthorBy2Ids" resultType="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 vous pouvez mapper un résultat très complexe en faisant cohabiter tous ces tags dans la même resultMap et dans l’ordre que la DTD stipule.

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 MyBatis, 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 mybatis-config.xml:

<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>

Par défaut, les valeurs pour les propriétés lazyLoadingEnabled et aggressiveLazyLoading sont à true (donc actives).
Si le paramètre aggressiveLazyLoading reste activé, implique qu’à la demande de chargement de la première propriété lazy d’un objet, toute les propriétés lazy de cet objet seront chargées.
Donc, si on applique cette remarque à l’exemple utilisé plus haut: lors de l’accès à la propriété author de l’objet Book, aggressiveLazyLoading provoque aussi le chargement de la liste categories.

Utilisation des Mappers

Les Mappers sont des interfaces créées pour être liées aux requêtes mappées. On obtient une isntance de ce mapper à partir de la SqlSession.

Le nom complet de l’interface doit correspondre exactement au nom de namespace déclaré dans le fichier de mapping (ou inversement), ici: com.soat.dao.mybatis.mapper.BookMapper.
Le nom des méthodes correpond à l’id des requêtes déclarée dans BookMapper.xml.
L’unique paramètre correpond au parameterType. Le type de retour (ou le type de collection de retour) correspond au resultType.

package com.soat.dao.mybatis.mapper;

import java.util.List;
import com.soat.beans.Book;

public interface BookMapper {

public Book selectBookById(Integer id);

public List<Book> selectAllBooks();

public void insertBook(Book book);

public void updateBook(Book book);

public void deleteBook(Book book);

}

Et voilà comment on l’utilise

try{
final BookMapper mapper = session.getMapper(BookMapper.class);

final Book book = mapper.selectBookById(15);
book.setName("New book name");
mapper.update(book);

final List<Book> books = mapper.selectAllBooks();
}catch(Exception e){
// ...
}

On peut même aller plus loin: en décrivant toutes les informations présentes dans le XML sous forme d’annotations, ce qui permettait de s’abstraire de ce fichier XML.
L’utilisation d’annotations à la place du XML est adaptée pour les cas simples. Dans les cas plus complexes, cela risque d’alourdir la lecture, ce qui rendrait plus difficile la compréhension et la maintenance.

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 transactionnel ou le mécanisme de cache.
De plus, MyBatis s’intègre très bien avec d’autres outils, tels que Spring.

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

Nombre de vue : 1373

COMMENTAIRES 6 commentaires

  1. Bruno DOOLAEGHE dit :

    Très intéressant Erwan… Une approche un peu plus “SQL friendly” qu’avec Hibernate

    Suite à cette lecture, j’ai quelques questions qui me viennent en vrac à l’esprit :

    Q1) Le fait d’externaliser les requêtes SQL dans des fichiers de mapping XML permet de sortir le SQL du code java, une bonne chose de faite… MAIS on mélange malgré tout de ce fait toujours 2 langages (SQL + XML), ce qui pose encore quelques problèmes (e.g. devoir “esacaper” les caractères spéciaux avec des CDATA). Y a-t-il un moyen de sortir les req SQL dans des fichiers dédiés à part, pour ne plus avoir ce genre de problème ? Peut-être en utilisant les fichiers properties mentionnés au début de ton article ? Note que cette remarque n’a de sens que sur les req SQL “statiques”… la partie dynamique étant définie… en XML !

    Q2) Y a-t-il un moyen de valider (A la compil – j’y crois pas trop – ou au démarrage de mybatis) que les requêtes SQL (statiques et dynamiques) embarquées dans notre appli seront syntaxiquement correctes ? (Rien de plus énervant que de devoir patcher en prod pour une virgule oubliée dans uen requête SQL…)

    Q3) Y a-t-il un mécanisme de “fetching” comme en JDBC, permettant de gérer les gros résultats de requête ? i.e. si un select retourne 1000000 de lignes, on risque d’exploser en OutOfMemory en essayant de mapper ça entièrement dans la List java… Y aurait-il un iterator intelligent sur la List, qui chargerait les items de la liste par paquets depuiq la base ?

  2. Erwan Letessier dit :

    Merci pour tes questions.

    R1) Pour autant que je sache, les fichiers .properties ne sont pas utilisés pour écrire du SQL; ou du moins pas directement. Il doit être possible de détourner un peu leur usage pour gérer ce problème d’échappement, en injectant ces propriétés dans le SQL, comme elles le sont dans le fichier de configuration: ${myProperty}.
    Il est également possible de passer la valeur en paramètre, en y accédant avec la même syntaxe: ${myParam}, ainsi la valeur n’est pas échappée, contrairement à la syntaxe #{myParam}.
    La documentation donne comme exemple: ORDER BY ${columnName}
    pour notre cas, e.g: gérer les opérateurs de comparaison, cela donnerait: WHERE my_column ${compareOperator} #{compareValue}.
    Attention toutefois !! à l’origine de ces valeurs non échappées, qui ne doit pas être l’utilisateur (injection SQL)!

    R2) MyBatis ne vérifie que le XML: syntaxe et références entre objets. On pourrait donc écrire presque n’importe quoi entre les balises. D’ailleurs, le dialecte n’est pas connu, on ne peut savoir qu’à l’exécution. Il faut donc utiliser la bonne vieille méthode: copier coller dans un éditeur de son outils de gestion de base préféré (Toad ou autre …).

    R3) Le ResultHandler que j’évoque à la fin du paragraphe sur le SELECT, il permet de traiter chaque résultat au fur et à mesure que le fetch avance, donc pas besoin de récupérer toute la liste pour itérer et traiter ensuite. Mais ça se complique un peu lors de l’utilisation des fonctionnalités un peu plus avancées comme le mapping d’association, le lazy loading.
    Donc, là, c’était pour la traitement 1 par 1, pour le chargement par paquet, si la fonction existe en JDBC, j’ose espérer qu’il existe un équivalent.

  3. Becquart Guillaume dit :

    Bonjour et merci pour ce tuto Erwan. Je débute avec Mybatis, ton tuto m’a sacrément bien aidé !

    J’ai une petite question concernant le “SQL Dynamique”, il me semble qu’avec Ibatis nous pouvions faire quelque chose du style :
    Select toto from NomDeLaTable [NomDeLaTable étant variable] en utilisant le “remapResults” .. qu’en est-il du coté de MyBatis ?

    Encore merci pour ton aide ! 🙂

  4. Erwan Letessier dit :

    Il semble que contrairement à iBatis, Mybatis ne mette plus le mapping en cache (http://mybatis-user.963551.n3.nabble.com/What-happened-to-remapResults-td3393950.html), d’où la disparition du remapResults qui n’est plus nécessaire.
    Bon courage.

  5. Becquart Guillaume dit :

    Très bien, merci à vous, encore une fois !

  6. Cédric D. dit :

    Enfin un tutoriel qui indique une façon de faire différente! Il y en a à foison avec spring mais j’ai mis un moment à arriver à trouver ce tutoriel.

    Je ne peux pas ajouter spring à mon appli et j’étais en iBatis, j’ai eu du mal à trouver un exemple reprenant la façon de faire ibatis avec les requêtes dans les fichiers xml. Et dans le cadre d’une migration c’est fortement appréciable.

    Merci pour tout bonne continuation.

AJOUTER UN COMMENTAIRE