Accueil > Frameworks > iBATIS : l’autre framework de persistance

iBATIS : l’autre framework de persistance

Introduction

De quoi faire frémir un Angry Bird

Si je vous dis “framework de persistance”, vous allez immédiatement penser Hibernate ou TopLink, car ce sont je pense deux des ORM les plus répandus dans notre cher monde J2EE. Cependant il en existe un autre, moins connu, qui propose des fonctionnalités parfois intéressantes : iBATIS. Par chance, j’ai travaillé sur un projet qui utilise ce framework, je vais donc vous le présenter dans cet article.

Pour la petite histoire, iBATIS était hébergé par l’Apache Software Foundation jusqu’en mai 2010, date à partir de laquelle le framework a été refactoré et amélioré pour obtenir une version 3 renommé en MyBatis (notez le subtil et incompréhensible changement de casse dans le nom) :

1
orm:~ apache$ mv /ASF/iBATIS /googlecode/MyBatis

Dans la suite de ce document, j’utiliserai indifféremment les noms iBATIS ou MyBatis, selon mon humeur et ma volonté à laisser mon doigt enfoncer la touche caps lock. N’en soyez donc pas étonné :).

Principes généraux

Je vais commencer par honteusement recopier la définition officielle tirée du manuel de MyBatis 3 :

MyBatis is a first class persistence framework with support for custom SQL, stored procedures and advanced mappings. MyBatis eliminates almost all of the JDBC code and manual setting of parameters and retrieval of results. MyBatis can use simple XML or Annotations for configuration and map primitives, Map interfaces and Java POJOs (Plain Old Java Objects) to database records.

Si vous êtes un pro de l’anglais, vous aurez compris (ou pas) que MyBatis est un mapper objet-relationnel, c’est-à-dire qu’il va être en mesure de convertir un enregistrement en base depuis ou vers un objet Java (un POJO en l’occurrence). À la différence d’Hibernate, MyBatis ne connait pas vraiment la structure des tables en base. Il se contente de mapper des objets Java depuis le résultat ou vers des paramètres de requêtes. Ces requêtes, appelées mapped statements, doivent impérativement être écrites en SQL natif (enfin presque, il y a des dynamic clauses que l’on verra par la suite). Les paramètres et les résultats de ces requêtes peuvent être décrits en XML ou via des annotations, afin de faire l’association avec les objets Java.

Fonctionnement général de MyBatis

Point de HQL donc, il va falloir ressortir vos vieux cours de SQL, qui ont pris 3cm de poussière depuis tout ce temps. Les avantages de ce mode de fonctionnement sont par exemple :

  • une simplicité d’utilisation évitant de se farcir des ResultSets et autres PreparedStatements de JDBC ;
  • une relative maîtrise des requêtes : en écrivant directement du SQL, on est sûr de récupérer uniquement ce que l’on souhaite (et pas toute une grappe d’objets par exemple), avec des possibilités d’optimisations spécifiques au SGBD utilisé (jointures, fonctions, procédures stockées, etc.) ;
  • certains parlent de meilleures performances, du fait que les requêtes soient en SQL natif (pas de conversion nécessaire) et facilement optimisables. Ce point est à prendre avec des pincettes.

Du côté des inconvénients, parce qu’il y en a forcément aussi :

  • on écrit du SQL, et pas tout le monde n’aime ça :) (d’un autre côté cela facilite l’écriture des requêtes par des DBA, et du reste du code par des développeurs) ;
  • puisqu’on écrit du SQL, on peut être tenté de trop utiliser de fonctionnalités propres à un SGBD, ce qui peut poser problème si jamais on en change un jour (mais avez-vous souvent travaillé sur un projet où c’est arrivé et où ça a posé problème ?) ;
  • il faut se taper tous les mappings XML à la main, encore que cet inconvénient puisse être minimisé grâce aux implicit mappings.

Les fichiers de mapping

Pour ceux qui n’aiment pas le XML, je vous conseille de quitter ce paragraphe. Voire même ce framework :). Mais bon, puisque vous êtes là, autant en profiter pour lire jusqu’au bout ?

Les fichiers de mapping MyBatis (aussi appelés SQL Maps ou Mapped Statements) sont grossièrement divisés en deux parties :

  • des déclarations de mappings entre une colonne d’une table et une propriété d’un POJO
  • des déclarations de requêtes SQL natives

Prenons un exemple concret d’un modèle contenant deux entités, User et Address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User {

    private Long id;
    private String name;
    private Date birthDate;
    private Address address;

    // getters & setters
}

public class Address {

    private Long id;
    private String street;
    private String city;
    private String postalCode;
    private String country;

    // getters & setters
}

Le mapping correspondant pourrait ressembler à ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.excilys.blog.mybatis.user">

    <resultMap id="User" type="com.excilys.blog.mybatis.model.User">
        <id column="ID" property="id" javaType="long" jdbcType="NUMERIC"/>
        <result column="NAME" property="name"/> <!-- Laissons MyBatis deviner le reste ;) -->
        <result column="BIRTH_DATE" property="birthDate"/>
        <association property="address" column="ADDRESS_ID" select="findAddressById"/>
    </resultMap>

    <resultMap id="Address" type="com.excilys.blog.mybatis.model.Address">
        <id column="ID" property="id"/>
        <result column="STREET" property="street"/>
        <result column="CITY" property="city"/>
        <result column="POSTAL_CODE" property="postalCode"/>
        <result column="COUNTRY" property="country"/>
    </resultMap>

    <select id="findAllUsers" resultMap="User">
        select * from USER
        order by ID asc
    </select>

    <!-- On peut également utiliser un resultType, auquel cas MyBatis suppose que
        les noms de champs et colonnes sont identiques -->
    <select id="findAddressById" parameterType="long" resultType="com.excilys.blog.mybatis.model.Address">
        <!-- Les paramètres sont entre #{} -->
        select * from ADDRESS where ID = #{id}
    </select>

</mapper>

Les mapping sont assez simples à comprendre. Les <resultMap> permettent de binder le résultat d’une requête sur un ou plusieurs objets. Viennent ensuite les requêtes. Ici je n’ai présenté que des <select>, mais de la même manière il est possible de faire des <update>, des <insert> ou des <delete>. Les paramètres de la requête sont des types prédéfinis (exemple parameterType="long" ou parameterType="hashmap") ou des objets quelconques (exemple parameterType="com.excilys.blog.mybatis.model.User"). Le résultat des requêtes est soit une map explicite, soit un objet quelconque (dans ce cas les noms de colonnes et de propriétés doivent correspondre).

SqlSessionFactory et SqlSession

À la manière des SessionFactory et Session d’Hibernate, MyBatis propose des objets SqlSessionFactory et SqlSession. La SqlSessionFactory se configure à partir d’un fichier XML :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="org.hsqldb.jdbcDriver"/>
                <property name="url" value="jdbc:hsqldb:mem:exc_mybatis_demo"/>
                <property name="username" value="sa"/>
                <property name="password" value=""/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mappings/UserManagement.xml"/>
    </mappers>
</configuration>

Ici nous définissons une datasource liée à un environnement de développement (qui utilise une base HSQLDB en mémoire). Vient ensuite ensuite l’inclusion de tous les fichiers de mapping. Côté Java, il est préférable de stocker une seule instance de la session factory à la HibernateUtil :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum MyBatisSessionFactory {

    INSTANCE;
   
    private SqlSessionFactory sessionFactory;

    private MyBatisSessionFactory() {
        Reader reader = null;
        try {
            reader = Resources.getResourceAsReader("mybatis-config.xml");
        } catch (IOException e) {
            throw new RuntimeException("Cannot initialize session factory", e);
        }
        sessionFactory = new SqlSessionFactoryBuilder().build(reader);
    }

    public SqlSession getNewSession() {
        return sessionFactory.openSession();
    }
}

Pour exécuter les requêtes, il suffit ensuite d’utiliser les méthodes select*() et leurs amis sur la session :

1
2
3
4
5
6
SqlSession session = MyBatisSessionFactory.INSTANCE.getNewSession();

List<User> users = session.selectList("com.excilys.blog.mybatis.user.findAllUsers");
Address address = (Address) session.selectOne("com.excilys.blog.mybatis.user.findAddressById", 1L);

// Et aussi insert(), update(), delete() etc.

Massive dynamic queries

Comme je l’ai brièvement énoncé tout à l’heure, les requêtes ne sont pas forcément 100% SQL d’origine contrôlée. On peut mitonner une petite sauce dynamique et obtenir une fonctionnalité similaire à l’API Criteria d’Hibernate (“J’en ai bouffé des Criteria, à l’époque, et j’en ai eu bien assez comme ça” m’a-t-on dit récemment :p). Blagues gastronomiques à part, voyons tout de suite un exemple d’une requête sous stéroïdes :

1
2
3
4
5
6
7
8
9
10
<select id="findUsersByCity" parameterType="string" resultMap="User">
  SELECT * FROM USER u
  <if test="value != null">
    JOIN ADDRESS a ON a.ID = u.ADDRESS_ID
    WHERE a.city = #{value}
  </if>
  <if test="value == null">
    WHERE ADDRESS_ID IS NULL
  </if>
</select>

On cherche la liste des utilisateurs demeurant dans une ville particulière. Comme nous savons également que certains extra-terrestres lisent ce blog, nous permettons aussi d’avoir une ville nulle, auquel cas ADDRESS_ID doit être NULL. Super, nous avons réussi à fusionner ces deux cas de figure dans la même requête, grâce à deux balises <if> dans le select. Selon la valeur du paramètre city fourni à l’exécution, on fera soit une jointure et une restriction, soit une restriction seule.

De la même manière, on peut par exemple boucler sur un paramètre de type collection ou tableau :

1
2
3
4
5
6
7
8
9
<select id="findUsersById" parameterType="list" resultMap="User">
  SELECT * FROM USER u
  <if test="ids != null">
    WHERE ID IN
    <foreach item="id" collection="ids" open="(" separator="," close=")">
        #{id}
    </foreach>
  </if>
</select>

Sous vos yeux ébahis, on vient de construire dynamiquement un WHERE... IN (x, y, z) sans même sortir un StringBuilder ! (on me signale dans l’oreillette qu’effectivement les StringBuilder c’est mal et que JPA permet de binder une collection sur une clause IN :))
À noter que iBATIS 2 proposait de nombreuses balises (isNull, isNotNull, isGreaterThan, iterate etc) qui ont été refactorées et condensées en des balises plus claires et moins nombreuses.

MyBatis Generator

MyBatis Generator (MBG pour les intimes) est un générateur de code capable d’introspecter une base de données existante afin de produire les entités (sous formes de POJO) et les DAO + mapped statements permettant de faire du CRUD. On voit l’intérêt immédiat de cette fonctionnalité lorsque l’on démarre (ou qu’on migre) une application disposant déjà d’une base de données, ou que celle-ci est conçue en premier par des DBA. Si l’on n’a pas d’attentes particulières quant au contenu de ce code “stupide” et pas forcément intéressant à écrire, ça fait gagner pas mal de temps. Les mapped statements écrits à la main peuvent être incorporés directement dans les XML générés, et le code Java peut être customisé par héritage si besoin.

Je ne l’ai pas précisé jusqu’ici (parce que je garde un peu de suspense pour la fin), mais MyBatis (et iBATIS) ne sont pas cantonnés à Java. Il existe une version .NET officiellement supportée par l’équipe de dév, ainsi qu’une version Scala en cours de préparation. Cependant, le générateur ne semble supporter que du code Java.

Et le reste ?

En plus du core framework et du générateur de code mentionnés auparavant, MyBatis propose plusieurs extensions assez intéressantes et utiles. Citons entre autres :

  • Une gestion du cache avec OSCache, Hazelcast ou EhCache, permettant de mettre en cache le résultat de certains mapped statements qui ne changent a priori que peu souvent.
  • Une integration avec Spring, permettant de gérer sa SqlSessionFactory de la même manière que pour Hibernate. Une classe SqlSessionDaoSupport est également présente pour faciliter le développement des DAO. La gestion des transactions est aussi au rendez-vous.
  • Un support de Velocity pour écrire des requêtes dynamiques, si vous en avez marre du XML :D.

Enfin, si vous aimez le lazy loading, sachez qu’il est possible d’en faire aussi bien dans iBATIS que dans MyBatis, moyennant une dépendance supplémentaire vers cglib.

Conclusion

Alors, pourquoi utiliser MyBatis (ou iBATIS) plutôt qu’Hibernate ? C’est un peu une question de goûts. Il est tout à fait possible d’utiliser Hibernate de la même manière que MyBatis, avec des requêtes en SQL natif et des résultats mis dans une Map (Transformers.ALIAS_TO_MAP est votre ami), mais c’est un peu dommage. Je ne m’avancerai pas plus dans ce débat trollesque, mais MyBatis est incontestablement plus sympa à utiliser que du JDBC pur, tout en laissant la possibilité de tout contrôler grâce aux requêtes à la mano et aux mappings explicites.

Le Dynamic SQL est une fonctionnalité très intéressante, surtout si l’on préfère éviter l’utilisation de l’API Criteria. L’intégration avec Spring ou Guice sont deux autres atouts en faveur de l’intégration de MyBatis dans un projet nouveau ou existant.

Le choix d’un framework de persistance dépend avant tout du besoin de votre projet. J’espère qu’avec cet article vous aurez un peu plus de billes le jour où vous devrez faire ou participer à ce choix, voire que vous choisirez de tester MyBatis ;)

Pour aller plus loin

Share
  1. 21/06/2012 à 09:09 | #1

    Bonjour
    Excellent post !
    Utiliser iBatis n’est pas qu’une question de gout.
    Je trouve de plus en plus de personnes qui j’exagère à peine savent faire de l’hibernate mais pas de base de données.
    Donc on peut se retrouver avec des applications loadant toute la base à cause des relations ou faisant 1000 requêtes individuelles…

    Avec iBatis on est confronté directement aux SQL alors si on ne connait que peu de SQL :
    – on demandera plus facilement à l’expert du coin au moment de les faire
    – les requêtes sont “isolées” du reste et à postériori reviewables
    On maitrise mieux les requêtes faites et le join coute un peu

    Par contre exit Hibernate Envers, Hibernate Search, …
    donc certaines évolutions futures peuvent faire mal

  2. Cyril BROUILLARD
    28/06/2012 à 16:42 | #2

    Pour le coup du “changement de SGBD” oui ça m’est déjà arrivé (et du coup je pense à plein d’autres) dans le cas très spécifique, je te l’accorde, ou le développeur ne souhaite pas installer le même SGBD sur sa machine que celui tournant en PROD. Une phrase un peu longue pour dire que je ne voulais pas installer Sybase (les goûts et les couleurs hein) alors qu’un PostgreSQL si vite installé et utilisable plus rapidement me permettait de travailler bien plus efficacement …

    Mes 2 cents cela dit :)

  3. 09/07/2013 à 10:54 | #3

    C’est intéressant de lire des expériences avec des outils modernes non-ORM. A mon avis, MyBatis a eu pas mal de succès principalement parce que le code SQL peut facilement être séparé physiquement du code Java. Parfois cela est un avantage, parfois on préfère l’approche inline SQL comme le permet jOOQ (http://www.jooq.org). De toute manière, il est important de savoir choisir quand on a besoin de vrai SQL et quand l’abstraction ORM est meilleure…

  1. 18/07/2014 à 10:26 | #1
  2. 02/06/2016 à 03:28 | #2
  3. 09/08/2016 à 20:01 | #3
  4. 09/08/2016 à 22:36 | #4
  5. 29/09/2016 à 15:22 | #5
  6. 30/09/2016 à 22:45 | #6
  7. 01/10/2016 à 02:39 | #7
  8. 29/03/2017 à 10:45 | #8