Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

DAO et ORM sont-ils compatibles ?

Les architectures modernes sont typiquement découpées en couches représentant des services concentriques de plus en plus haut niveau :

  • Accès aux données (couche de persistance ou DAO, l'objet de ce billet),
  • Traitements métiers,
  • Présentation à l'utilisateur ou exposition à des systèmes tiers.

Les bonnes pratiques imposent que chacune de ces couches soit représentée par une interface exprimant le service qu'elle rend et masquant la façon dont elle est implémentée. Leur respect permet d'obtenir des architectures modulaires et faiblement couplées, facilitant le test et la maintenance des applications.

Pourtant, les ORMs remettent en cause ce schéma.

La couche de persistance

Le service demandé à une couche de persistance est parfaitement exprimé par l'acronyme CRUD (Create, Retrieve, Update, Delete), c'est-à-dire que l'on souhaite pouvoir sauvegarder, récupérer, mettre à jour et supprimer les données que l'application manipule.

L'interface typique d'une couche DAO ressemble donc à :

  1. public interface ClientDao
  2. {
  3. public Client save(Client client);
  4. public Client update(Client client);
  5. public void delete(Client client);
  6. public Client getClient(ClientId id);
  7. public Collection<Client> getAllClients();
  8. }

Constatez que cette interface est suffisamment générique pour ne laisser transparaître aucun détail technique, et peut ensuite être implémentée facilement de différentes façons : écriture dans un fichier plat ou XML, insertion en base de données, sauvegarde sur un système distant, sauvegarde en mémoire...

DAO et bases de données : le problème Hibernate

Les bases de données suivent depuis longtemps le modèle CRUD ; les instructions SQL INSERT, SELECT, UPDATE, DELETE en sont une transposition directe. Il est donc très simple d'implémenter une couche DAO via de simples appels JDBC basiques.

Tout était bien dans le meilleur des mondes, jusqu'à l'apparition des frameworks de mapping objet-relationnel (ORM), Hibernate en tête. Sous couvert de simplifier la vie du développeur (ce qui reste à démontrer), ils apportent une sémantique tout à fait différente et largement incompatible avec le système CRUD. En cause, la notion de cycle de vie des objets manipulés (transient / attaché / détaché), qui doit être géré manuellement par le développeur.

S'il apporte des fonctionnalités puissantes (mise à jour automatique, gestion de grappes d'objets...), ce système a une forte tendance à transpirer dans les couches supérieures - souvent même jusqu'à la couche de présentation, ce qui est une hérésie totale du point de vue architectural (cf. le pattern "open session in view").

Exit donc le pattern CRUD. Exit aussi l'interface DAO générique indépendante de l'implémentation : les couches supérieures devant gérer finement le cycle de vie des objets, il est indispensable qu'elles puissent accéder aux APIs bas niveau du framework de persistance : persist, merge, delete, saveOrUpdate, lock...

DAO par ORM : isolation plutôt que découplage

Si la couche DAO n'est plus découplée des couches supérieures, est-elle encore utile ? Si son interface n'est qu'un simple miroir des APIs de l'implémentation sélectionnée, à quoi sert-elle ? Ne serait-il pas plus simple de reconnaître cet état de fait et de supprimer la couche DAO lorsqu'on utilise un framework d'ORM ?

A cette question qui mérite réflexion, je préfère apporter une réponse nuancée.
Certes, le découplage est cassé ; mais la couche DAO conserve une utilisé pratique : elle permet d'isoler une partie du code technique spécifique (requêtes HQL / SQL, Criterions, Transformers...) dans des packages bien définis, et de garder l'apparence d'une architecture en couches standard.

Conclusion

Lors de la mise en place d'une pile technique, il faut savoir jeter un regard critique sur les différentes technologies disponibles sur le marché, et déterminer si leurs forces et faiblesses sont acceptables dans le cadre du projet. En particulier, il faut résister aux sirènes du "standard" (Spring/Hibernate/Struts par exemple) et ne pas hésiter à faire ses propres choix argumentés.

Cet article avait notamment pour but de vous faire prendre conscience que les ORMs ne sont pas des outils magiques, et qu'ils imposent de fortes contraintes sur l'architecture (en plus d'une certaine complexité), pouvant mener à des difficultés de testabilité ou de maintenance.


Commentaires

1. Le lundi 27 juillet 2009, 14:14 par ehsavoie

Suite à ma remarque sur Twitter, je la reformule ici:
Je trouve dommage de ne pas utiliser une DAO generics telle qu'on l'a trouve chez Hibernate ou ailleurs :
@@
public interface Dao <T, PK extends Serializable>

     public T save(T persistedObject);
     public T update(T persistedObject);
     public void delete(T persistedObject);
     public T find(PK id);
     public Collection<T> findAll();

}
@@
t avec la DAO abstraite qui va bien on n'a presque plus que des 'finders' dans nos DAOS et ainsi on réduit considérablement le code à manipuler.

D'ailleurs en ce qui concerne les patterns JavaEE, je vous engage à suivre le projet [Java EE Patterns And Best Practices
|http://kenai.com/projects/javaee-patterns|en] géré par Adam Bien (http://www.adam-bien.com).

Emmanuel

2. Le mardi 28 juillet 2009, 09:50 par Gilles S

Bonjour,
merci pour ce billet. Je partage tout à fait l'avis sur Hibernate tellement poussé par le marché, mais que seuls 10% des développeurs comprennent vraiment et qui s'avère pour moi etre vraiment une bombe à retardement pour la pérennité des applications d'entreprise.
Je préfère pour ma part iBATIS qui respecte complètement le pattern DAO et les couches applicatives.
De plus je peux expliquer le fonctionnement d'iBATIS à un développeur en 2h avec démo de code et de config XML. Et surtout quand il y a un probleme on sait d'ou ça vient et on peut réparer en 5 minutes.
Pour finir, le générateur de code iBATOR est très bien.

En prenant en compte la poussée naturelle vers le management et l'age moyen des développeurs: 25 ans et 3 ans d'experience, je pense qu'Hibernate est "dangereux" :)

3. Le mardi 28 juillet 2009, 15:09 par Nicolas Martignole

Merci Olivier pour ton article. Je partage ton point de vue : bien utiliser Hibernate n'est pas compatible avec une architecture stricte au niveau du découpage par couche. Je m'explique : pas plus tard que ce matin j'ai vu qu'une bonne partie du code n'utilise pas correctement le chargement tardif, voire pas du tout (lazy=false, ça marche partout). J'ai aussi vu un getSessionFactory dans un DAO, sans doute pour laisser une chance à l'utilisateur de controler/rafraichir/evincer des objets... Youpi une interface de type DAO avec une dépendance sur Hibernate...

Toi et moi nous connaissons SpringFuse et Celerio. C'est aussi une solution je pense pour générer du code de qualité, robuste et propre.

4. Le mardi 28 juillet 2009, 15:38 par Olivier Croisier

Merci de ces remarques et retours.
Je trouve d'ailleurs la remarque sur la compétence et l'expérience moyenne des développeurs particulièrement judicieuse.

Suite à différentes remarques que l'on m'a transmises par mail ou de vive voix, je tiens à rappeler que ce billet ne remet pas en cause l'utilité des frameworks d'ORM, mais vise à attirer l'attention sur le fait que le choix de leur utilisation n'est pas sans conséquences.

Notamment, la persistance transitive et l'auto-update des objets modifiés sont des différences de comportement majeures entre un DAO JDBC et un DAO ORM. En remplaçant l'un par l'autre (en supposant qu'ils puissent être décrits par une interface commune sans brider leurs capacités respectives), on provoque des effets de bord importants dans le comportement de l'application.
Or, le découpage en couches permet justement, en théorie, de remplacer une implémentation par une autre de manière transparente.
On pourra toujours m'objecter qu'on peut paramétrer Hibernate pour ne pas charger les relations, ne pas surveiller les modifications sur les objets, etc. Mais ce serait un cas très particulier qui ne représente pas l'utilisation standard de ce framework, et qui briderait ses capacités.

Pour conclure, oui on peut faire le choix d'un ORM, mais il sera difficile de revenir sur cette décision ensuite, car cette technologie n'est pas directement interchangeable avec ses alternatives.

5. Le samedi 8 août 2009, 14:00 par HollyDays

Étonnant comme ces discussions me rappellent celles que le petit monde de l'objet pouvait avoir il y a 15 ou 20 ans, alors que l'industrie n'avait pas encore franchi le pas (elle en était encore au client-serveur classique), mais que de nouveaux acteurs commençaient déjà à lui proposer de nouveaux outils orientés objet.

Je me souviens notamment du slogan d'un vendeur de SGBD objet : "le même objet de la base à l'écran".

Ce slogan semble une hérésie aujourd'hui. Il date d'une époque où le n-tiers était encore méprisé par l'industrie. Pourtant, on a tendance à oublier que fondamentalement, le DAO a d'abord servi, dans les programmes objet, à encapsuler la différence de structure fondamentale entre le relationnel, plat et ensembliste, et l'objet, intrinsèquement hiérarchique. L'idée de base était d'isoler la logique relationnelle de la base de données et de la réduire au minimum de code, et de conserver le paradigme objet sur le maximum de code.

Le CRUD n'est que le reflet de la cohabitation forcée entre ces deux paradigmes contradictoires de parcours des données que sont le relationnel et l'objet : c'est le plus grand dénominateur commun entre les deux. Et l'on comprend dès lors que, si la couche de persistance est elle-même objet, le DAO perd son utilité fondamentale. Parce que si la couche de persistance permet elle-même de parcourir les objets de manière hiérarchique, il est idiot de se contenter de ces quelques opérations de base (pour des raisons assez bêtes de performance).

En fait, le slogan du vendeur de SGBD objet garde son sens tant que le modèle de données métier, celui qu'utilise l'application en mémoire, et le modèle de données persistant, celui qui est écrit en base, restent identiques ou très proches. Et c'est parfaitement logique : pourquoi faire une surcouche (en fait, une sous-couche) avec d'autres objets qui ont la même structure, et passer son temps à faire des recopies d'objets ? A part pour avoir le plaisir d'écrire un peu plus de code qu'il faudra ensuite maintenir, et avoir au final une application plus lente ?

Par contre, le slogan perd son sens lorsque la structure des données métier diffère notablement de celle des données persistantes. Cependant, dans le monde de l'industrie, c'est une situation rare.

Ce que je trouve le plus cocasse dans ces discussions, c'est, je cite Olivier : "En cause, la notion de cycle de vie des objets manipulés (transient / attaché / détaché), qui doit être géré manuellement par le développeur."

Voilà qui me fait revenir près de 15 ans en arrière, lorsque j'ai découvert qu'il existait à l'époque un SGBD objet qui gérait une persistance implicite par attachement (persistence by reachability, en anglais) : on crée un objet, il est temporaire ; on le lie à un autre objet qui est persistant (ce qui le rend "atteignable", ou "reachable" en anglais, depuis un objet persistant), et l'objet temporaire devient automatiquement persistant (ou il le deviendra au prochain commit de la transaction) ; on coupe toutes les références à cet objet provenant d'objets eux mêmes persistants, et l'objet redevient temporaire, ainsi que tous les objets que lui-même référence, dès lors qu'ils ne sont plus non atteignables via des objets toujours persistants. Point barre. Plus aucun "attach", "detach", "save", "load" : le système se charge de charger les objets lorsque cela est nécessaire, de les écrire au commit s'ils ont été modifiés, de poser les verrous là où il faut pour sécuriser les accès concurrentiels, et le développeur se contente de faire des "transaction.begin();" et des ""transaction.commit();" quand il le faut. Il ne reste plus qu'à définir au départ quelques noms publics d'objets, sortes de variables typées définies dans la base et qui serviront de racines de persistance, et le tour est joué.

Et curieusement, depuis 8 ans que je ne travaille plus dans ce milieu, à chaque fois que j'explique cela, on me regarde bizarrement, et on me rétorque qu'au contraire, il est préférable de gérer manuellement la persistance, et que le système que je décris n'a aucune utilité...

6. Le lundi 10 août 2009, 10:05 par Olivier Croisier

Je suis d'accord avec cette analyse, et je trouve également étonnant que les bases de données objet n'aient pas encore véritablement percé. Est-ce un problème de maturité (robustesse/performance/fonctionnalités), ou simplement de visibilité sur le marché ?
Je pense également que les clients sont extrêmement réticents à remettre en cause des années d'expérience sur les produits classiques (notamment Oracle) et les contrats qui les couvrent.
Peux-tu nous conseiller une base objet en particulier ?

A part ça, j'émettrais juste une réserve quant à la gestion automatique de la synchronisation objet/base : c'est une fonctionnalité pratique, mais qui demande de prendre certaines précautions. Il est par exemple fréquent de charger une liste d'objets depuis la base, puis de la manipuler avant de l'afficher à l'écran : il faut alors faire très attention à travailler sur une copie de la liste renvoyée, sous peine de voir les changement involontairement propagés en base.

7. Le mardi 11 août 2009, 16:05 par HollyDays

Les SGBD objet n'ont pas réussi à percer à l'aube des années 2000, parce que les sociétés qui les diffusaient étaient des nains économiques face à leurs véritables concurrents. Concurrents qui étaient, en clair, Oracle, IBM, Microsoft, Sybase, Informix, ... (bagatelle, n'est-ce pas ?). Et ces acteurs majeurs du marché ont fait tout ce qu'il fallait pour étouffer les petits nouveaux concurrents qui arrivaient (notamment via l'OMG, que ces sociétés trustrent largement ; or l'OMG, c'est censé être le consortium de standardisation de tout ce qui touche à l'objet ; l'entrave à l'OMG était telle que les sociétés vendant un SGBDO ont été obligées de fonder leur propre consortium dans leur coin, l'ODMG).

Oracle, notamment, a réussi à ce que l'essentiel du monde de l'informatique pense "Oracle" en pensant "base de données". Alors même que techniquement, pendant longtemps, il était loin d'être le meilleur des SGBDR (que ce soit en performance, en facilité d'administration, en richesse de modélisation), Oracle a réussi à devenir le n°1 des SGBD toutes générations confondues, puis à le rester dans la durée. J'ai même entendu en 1998 un dirigeant de DSI refuser de passer à un SGBDO ne nécessitant pratiquement aucune administration en rétorquant : "Ah non ! Mon dbadmin Oracle me coûte plus d'un million de francs par an, je veux le rentabiliser !" Apparemment, il ne lui venait pas à l'esprit qu'il aurait pu économiser plus d'un million de francs par an.

Résultat : à la fin des années 1990, les seuls véritables cas où des SGBDO étaient utilisés (pour des applications un tant soit peu importantes), étaient lorsque les SGBDR ne fonctionnaient plus. J'ai par exemple en tête la bourse de Chicago, plus grande place boursière au monde pour les matières premières, où les experts de chez Oracle ont fini par conclure que leur produit ne savait pas gérer un tel débit transactionnel avec une telle complexité de données. Les équipes de dev ont fini par se tourner vers un SGBDO, et à ma connaissance, l'appli tourne toujours de manière satisfaisante (en l'occurrence, ''Versant'', qui était leader à l'époque sur le marché américain). Preuve que côté performance, un SGBDO, ça ne répond pas trop mal.

En fait, l'erreur des vendeurs de SGBDO est sans doute d'avoir voulu se mesurer à Oracle ou IBM : même si techniquement, c'était parfaitement justifié, économiquement, c'était sans espoir. Ainsi, dans les années 2000, les seuls SGBD nouveaux qui ont réussi à sortir leur épingle du jeu sont ceux qui se sont placés hors du champ de concurrence qu'Oracle s'était fixé : l'exemple typique, c'est MySQL, qui s'est longtemps présenté comme une "petite" base de données, personnelle ou pour un petit site (d'ailleurs, pendant longtemps, MySQL n'a même pas été un SGBDR au sens strict, car son moteur MyISAM ne supporte pas les transactions ACID, qui sont pourtant un des fondements d'un véritable SGBD ; ce n'est qu'après de nombreuses années, une fois largement diffusé, que MySQL a proposé un nouveau moteur, InnoDB, qui est un véritable moteur de SGBD transactionnel). Même Microsoft s'est cassé les dents sur ce secteur : autant Access a assez bien réussi économiquement, autant SQL Server, censé être un SGBD un plus plus sérieux, n'a pas réussi à vraiment percer, malgré le rouleau compresseur marketing que peut être Microsoft.

= = = = = =

Concernant la réserve pratique que tu émets, Olivier, l'expérience montre que c'est une erreur, peut-être même à double titre.

1. Tout d'abord, par principe, il faut réduire au maximum le nombre de copies d'objet, pour des raisons de performance. Si un programme a besoin de travailler sur des objets temporaires, de toute façon, il créera ses objets temporaires, quitte à ce que ces objets temporaires référencent des objets persistants (ce qui ne les rend pas persistants, puisque les objets persistants ne les référencent pas, eux).

Ensuite, si tu veux être absolument sûr que ta transaction ne modifie aucun objet persistant, il y a plusieurs solutions. Par exemple, tu peux faire un rollback plutôt qu'un commit en fin de transaction (là, au moins, tu es tranquille : aucune modification faite au cours de la transaction ne sera écrite en base !). Tu peux aussi lire tes objets dans une transaction read-only : la moindre modification d'un objet persistant sera rejetée ; et aucun objet temporaire ne peut être rendu persistant (ou un objet persistant rendu temporaire), puisque pour cela, il faut le rendre accessible (resp. inaccessible) depuis un objet déjà persistant (via l'affectation d'une référence), autrement dit modifier un objet déjà persistant.

Cependant, par principe, toute modification d'un objet persistant faite durant une transaction ACID read-write doit être écrite en base au moment du commit : c'est un principe intangible. Tu trouverais inacceptable que, avec un SGBDR, parmi tes 100 INSERT/UPDATE/DELETE faits dans une seule transaction, seuls certains soient pris en compte, et d'autres ne le soient pas !

2. Seconde erreur de raisonnement (je ne suis pas sûr que toi, tu aies fait cette erreur, mais certains de tes lecteurs, j'en suis sûr) : vouloir travailler sur une copie de la collection suppose que ta collection d'objets tient en mémoire. Ce qui n'est pas forcément le cas. Et oui, d'une certaine manière, une collection d'objets persistants est une généralisation d'une table relationnelle ! Sauf qu'en plus, les objets de la collection peuvent être de types différents, héritage de classes oblige. Et l'image qu'on se fait habituellement d'une table relationnelle, c'est typiquement un ensemble de données susceptible de ne pas tenir intégralement en mémoire (alors que l'image qu'on se fait d'une collection, c'est celle d'un ArrayList ou d'une HashMap : bref : une petite collection mémoire).

Du coup, il peut être difficile de "dupliquer" la collection. Parce que même la charger intégralement en mémoire serait totalement contre-performant (ce que les SGBDO ne font jamais, bien évidemment, sauf si la collection est toute petite, auquel cas charger les quelques premiers éléments revient à charger tout le contenu de la collection).

= = = = = =

Côté produits, le vendeur de SGBD objet que j'évoquais dans ma précédente réaction (O2 Technology, pour ne pas le citer) était en son temps presque systématiquement confronté à son concurrent Versant. A l'époque, les autres produits ne faisaient généralement pas le poids : soit pas assez ouverts ou interopérables (trop dépendants d'une plate-forme donnée : le serveur devrait pouvoir converser à la fois avec des PC sous Windows et des machines HP/UX par exemple ; trop liés à un langage de programmation : le serveur devrait supporter Java aussi bien C++, voire d'autres langages objet ; ...), soit pas assez costauds (nombre de connexions simultanées au serveur ou débit transactionnel trop limités, bases de centaines de Go voire To impossibles, très mauvais support ou absence de support de l'évolution de la structure de la base en cours de vie, ...), etc.

Certains produits disposent d'un langage de requête type SQL orienté objet, d'autres pas (l'ODMG a normalisé un langage de requête dérivé de SQL, appelé OQL (plus facile à comprendre, d'ailleurs) que Sun a largement repris pour son EJB-QL). A mon avis, ce point du langage de requête type SQL est d'ailleurs le principal point faible de Versant, qui n'a pas de tel langage. Pour le reste, Versant est assez bon, voire vraiment bon (et c'est l'avis d'un ancien concurrent !).

Ce diagnostic me semble aujourd'hui inchangé : Versant est toujours un leader de ce marché, et la plupart des autres SGBDO apparus depuis sont de "petits" SGBD, qui me semblent tenir difficilement la comparaison. Tout simplement parce qu'un SGBDO, c'est d'abord un moniteur transactionnel objet, et qu'un moniteur transactionnel (même non objet), ça ne se développe pas en un ou deux ans avec 3 ou 4 développeurs, même super bons : c'est beaucoup plus compliqué que cela à réaliser (rappelez-vous que même Microsoft n'a pas réussi à proposer un moniteur transactionnel robuste en toute situation : on arrive assez facilement à planter feu MTS ou BizTalk). Donc pour développer un tel produit, il faut, derrière, une société privée à l'assise financière suffisante pour pouvoir investir assez d'hommes et de temps : chose que bien peu de sociétés à part les majors de l'informatique peuvent se permettre, et chose que ces majors ont, jusqu'à présent, toujours refusé de faire, préférant continuer à privilégier leur propre produit reposant sur le relationnel.

8. Le lundi 31 août 2009, 14:53 par pic

Il me semble préférable d'utiliser JPA (Java Persistance Api), quand c'est possible, plutôt que Hibernate.

9. Le lundi 19 octobre 2009, 13:00 par Antonio

la couche DAO conserve une utilisé pratique : elle permet d'isoler une partie du code technique spécifique (requêtes HQL / SQL, Criterions, Transformers...)

D'accord, mais il y a un autre cas où les DAOs ne servent à rien : les named query. Les named query doivent être définie sur l'entity (on n'a pas le choix, elles ne peuvent pas être regroupées dans une seule classe par exemple) ou dans un fichier XML (à conseiller car sur l'entity, c'est vite illisible, mais c'est l'exemple que je prendrai pour illustrer le cas). On a donc le code suivant :

@Entity
@NamedQuery(name = "findAll", query = "SELECT c FROM Customer c")
public class Customer {
...
}

Ensuite, la couche service peut directement invoquer cette requete de la manière suivante :

Query q = entityManager.createNamedQuery("findAll");
List<Customer> customers = q.getResultList();

Encapsuler ces deux lignes de code (ou même une seule en simplifiant) dans un DAO me parait être superflu.

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.