Introduction
Fonzie reprend les idées proposées par Grails (ou plus précisément GORM, ou encore plus précisément le Domain Class Querying de GORM) pour faciliter la manipulation des données persistantes (comprendre : les objets stockés en base via Hibernate). Fonzie est conçu pour tous ceux qui ne peuvent (ou ne veulent) pas utiliser Groovy.
GORM, c'est quoi donc ?
Le "Grails Object Relational Mapping" est basé sur Hibernate, donc rien de délirant, mais lui apporte une grande facilité d'utilisation et surtout est très naturel à manipuler. Pour faire simple, il s'agit d'avoir des méthodes "virtuelles" sur les objets métier qui permettent de commander leur stockage en base.
explication par l'exemple - à partir d'une entité JPA User :
@Entity
public class User {
@Id private long id;
private String name;
}
Dans le code métier, on voudrait pourvoir faire ce genre de chose, "à la Grails" :
User user = User.findByName( "bibi" );
user.lock( READ );
user.remove();
/// etc...
Comment faire ça sans les précieux mécanismes dynamiques de Groovy ?
Fonzie = GORM du pauvre 100% Java pur premium
Déclarer plutôt que coder
L'idée de Fonzie est de déclarer les méthodes de persistance dans les entités JPA, mais juste de les déclarer. Je suis un gros faignant et j'ai horreur de coder un truc qui semble évident rien qu'en lisant le nom de la méthode.
Ces méthodes peuvent typiquement être déclarées native
puisqu'elles ne sont pas destinées à être codées.
``` @Entity public class User { @Id private long id; private String name;
public static native User findByName( String name );
public native void remove();
} ```
La "pollution" du code de l'entité reste assez raisonnable. Un aspect AspectJ de Fonzie va alors instrumenter le code métier qui utilise ces méthodes pour remplacer l'appel native
qui n'est évidement pas implémenté par du code natif. Fonzie analyse dynamiquement le nom de la méthode et ses paramètres pour construire la requête JPA "qui va bien" et utilise l'entityManager de l'application - configuré via Spring.
native ?
Le mot clé native
signifie en principe que le corps de la méthode est codé en langage C dans une librairie dynamique (DLL/so). Heureusement pour nous, Java effectue le linkage des méthodes de manière dynamique, aussi il ne se rendra compte de la supercherie que si cette méthode est effectivement invoquée.
Lorsque Fonzie est utilisé sur le projet, tout les appels à ces méthodes sont remplacés par du code Fonzie, aussi cela n'arrivera jamais. Si vous utilisez vos entités JPA sans Fonzie et que vous invoquez quand même une de ces méthodes, vous aurez droit à une java.lang.LinkageError
- mais si vous utilisez ces entités sans Fonzie c'est probablement que vous êtes en dehors de votre application JPA, donc pas d'Hibernate non plus !
Installation
Déclarer la dépendance Maven qui va bien
<dependency>
<groupId>fr.loof.fonzie</groupId>
<artifactId>fonzie</artifactId>
<version>0.0.1</version>
</dependency>
Configurer l'apsect dans le contexte Spring :
<bean class="fr.loof.fonzie.ORMAspect" factory-method="aspectOf" lazy-init="false"/>
NB : si le contexte n'est pas configuré pour supporter les annotations JSR250, injecter dans l'aspect l'
entityManagerFactory
JPA et invoquer sa méthode init()
.
Utilisation
gestion de l'état
L'entité peut être gérée au niveau de sa persistance en 'déportant' sur la classe entité les méthodes de l'EntityManager :
@Entity
public class User {
public native void persist();
public native void refresh();
public native User merge();
public native void remove();
public native void lock( LockModeType type );
}
requêtes
Les méthodes de recherche sont déclarées statiques (et native) sur l'entité à identifier.
Toutes les méthodes find*
retournent l'entité qui déclare la méthode ou une liste si la méthode déclare retourner une Collection. Les count*
retournent le nombre d'éléments (quelle surprise).
count
, find
et findAll
seules effectuent une recherche sur la clé primaire. Des critères de sélection sont déclarées par un By.
L'analyse du By*
est réalisée selon plusieurs algorithmes.
requêtes nommées
Avant toute chose, Fonzie consulte les annotations de l'entité pour vérifier si une NamedQuery ne correspondrait pas au critère indique. Ainsi, User. findByCoolGuy()
va identifier la requête nommée coolGuy
.
@Entity
@NamedQuery( name="coolGuy", query="select u from User u where u.firstName in ('fonzie')" )
public class User {
public static native User findByCoolGuy();
}
A savoir, les namedQueries sont globales dans le contexte de l'EntityManager, il faut donc faire attention aux conflits de nom. Fonzie par contre ne considère que les NamedQueries déclarées par l'entité considérée. Pour éviter les conflits, nous vous recommandons de préfixer vos noms de requêtes, sachant que Fonzie ne considère que le dernier segment (le '.' servant de séparateur) :
@Entity
@NamedQuery( name="User.coolGuy", query="select u from User u where u.firstName in ('fonzie')" )
public class User {
public static native User findByCoolGuy();
}
requêtes déduites du nom de la méthode
Si rien de tel n'est trouvé, on passe à l'analyse du nom de la méthode comme chaîne de critères. Les attributs de l'entité sont utilisés pour construire la requête : findByName
fait une recherche sur l'attribut name.
On peut combiner les critères via le mot And
, et préciser le critère de recherche par l'un des mots clés Equals
(par défaut), NotEquals
, GreaterThan
, GreaterEquals
,
LessThan
, LessEquals
, Before
, After
, Like
, In
ou Between
.
On peut aussi ordonner le résultat avec un OrderBy
suivi des noms des propriétés utilisées pour le tri (séparées par And
) et d'un éventuel Desc
pour inverser l'ordre.
Fonzie contrôle : * que les propriétés déduites du nom de la méthode sont bien des attributs valides de l'entité * que les paramètres de la méthode concordent avec les types de ces attributs.
En cas d'incohérence, une IllegalException
est levée avec un joli message d'erreur explicatif.
La méthode doit bien évidemment déclarer les arguments associés aux critères de recherche dans le même ordre :
@Entity
public class User {
public native static User findByNameAndBirthDateBetween( String name, Date before, Date after );
}
Jointures
Fonzie supporte également les jointures et les Embeddables (non ? si !) via une syntaxe toujours aussi naturelle, de la forme :
@Entity
public class User {
public native static User findByNameAndAddressCityCodePostal( String name, String cp );
}
Dans l'exemple ci-dessus on recherche les User
par leur nom et par le codePostal de la ville de leur adresse.
(NB: le mot clé Join
utilisé dans les versions 0.3.x et précédentes de Fonzie est déprécié au profit de cette syntaxe chainée)
Il est possible de modifier la mécanique de chargement des entités liées en utilisant le mot clé Fetch
.
@Entity
public class User {
public native static User findByNameJoinFetchAddress( String name, String cp );
}
and more...
Qui n'est jamais tombé sur un problème de Cast avec Hibernate ? Quelque chose de ce genre :
Animal pet = Animal.findByName( "medor" );
if ( pet instanceof Dog ) {
((Dog) pet).promener();
}
Avec du code de ce type, on ne rentre jamais dans le bloc if
, et même si on y entrait on aurait un ClassCastException. Une sombre histoire de proxies ...
La bonne façon de faire, est de se baser sur une belle modélisation objet, pour laquelle promener()
est déclarée dans une interface Domestique
et pour laquelle le instanceof va fonctionner. Dans la vraie vie ce n'est pas toujours aussi simple :)
Fonzie propose une solution de repli, qui permet surtout de corriger rapidement ce type de code quand on tombe sur le problème - ce qui ne vous interdit pas de revoir votre code.
Animal
déclare ces deux méthodes Fonzie-fiables :
```
public class Animal {
public native boolean instanceOf( Class ); public native A as( Class ); } ```
On peut alors reprendre le code pour suivre cette nouvelle formulation, qui elle est valide :
Animal pet = myDao.findByName( "medor" );
if (pet.instanceOf( Dog.class )) {
pet.as( Dog.class ).promener();
}
Particularité importante : la méthode as()
implémentée par Fonzie retourne un proxy Hibernate valide, utilisable pour poursuivre les opérations de persistence. Elle ne se contente pas - contrairement à de nombreuses solution comparables - de dés-enrober le proxy Hibernate... ce qui peut se traduire au final par des données non sauvegardées en base :(
extension
Il est possible d'adapter l'analyse par mot clé à vos propres besoins en ajoutant des implémentation de KeyWord
au JPQLQueryParser
.
Il est aussi possible de venir greffer une version totalement personnalisée de QueryParser
.
L'analyseur de nom de méthode est configurable et peut donc être remplacé par une version maison si vous avez vos propres conventions, ou que vous voulez ajouter des mots clé.
Enfin, Fonzie utilise un cache pour conserver le résultat de l'analyse d'une méthode sous forme de requête JPQL et donc la réutiliser directement pour les appels suivants. Ceci peut être utilisé pour remplacer la requête produite par l'analyse par votre propre implémentation, dans un but d'optimisation ou pour faire des join fetch
ou autre subtilité JPA.
Pour que ça marche ...
Pour que tout ça fonctionne il faut que le code du projet soit instrumenté par aspectJ. Pour les mavenistes cela se traduit par l'ajout du plugin associé dans le POM :
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.1</version>
<configuration>
<aspectLibraries>
<aspectLibrary>
<groupId>fr.loof.fonzie</groupId>
<artifactId>fonzie</artifactId>
</aspectLibrary>
</aspectLibraries>
<source>1.5</source>
<target>1.5</target>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
Autre option : utiliser le Load-Time Weaver d'AspectJ, Fonzie déclare le fichier aop.xml
nécessaire. Ce n'est probablement pas adapté pour une application destinée à un serveur JEE, sans quoi tout le code du serveur va aussi être analysé par AspectJ ! Cela peut être utile pour les tests unitaires sous Eclipse
(vous pouvez aussi à utiliser l'AJDT - un peut trop gourmand à mon goût, sur un Eclipse qui a déjà de l'embonpoint).
Lancez donc vos tests jUnit avec l'option de JVM :
-javaagent:<repositoryMaven>/org/aspectj/aspectjweaver/1.6.4/aspectjweaver-1.6.4.jar
Comment tester ?
test "de bout en bout"
La première option est d'utiliser Fonzie en Load-Time-Weaving avec votre EntityManager
cible, via par exemple les classes de spring-test-context.
@ContextConfiguration( locations = { "/jpa-tests.xml" } )
@Transactional
public abstract class AbstractJpaTestCase
extends AbstractTransactionalJUnit4SpringContextTests
{
@PersistenceContext
protected EntityManager entityManager;
...
Ca marche, mais c'est pas vraiment léger, et ça suppose une base de données avec tout ce qu'il faut dedans pour le test :-/
mock de l'entityManager
Autre solution, remplacer l'EntityManager
par un mock. Fonzie vous en propose un super simplifié qui permet de programmer (méthode queryFor
) les données retournées par les requêtes successives. Il n'y a donc aucun contrôle sur les valeurs des paramètres passés. Par contre, le mécanisme de contrôle sur les attributs / arguments est actif et valide la structure de vos méthodes.
``` @Test public void mock() throws Exception { User user = new User(); EntityManagerMock entityManager = new EntityManagerMock(); entityManager.queryFor( user ); entityManager.replay();
ORMAspectTestSupport.setupAspect( entityManager );
assertSame( user, User.findByFirstNameAndLastName( "foo", "bar" ) );
}
```
Ce mode est le plus léger à mettre en oeuvre, car il permet de valider les méthodes Fonzie (noms d'attributs et types des paramètres valides, requête nommée identifiée...) sans nécessiter le chargement d'un contexte de persistance complet.
En contre partie, il faut passer par la compilation AspectJ. Sous Eclipse, cela signifie soit installer AJDT, soit utiliser le Load-Time-Weaving en ajoutant dans la "launch configuration" du test :
-javaagent:<<localrepository>>/org/aspectj/aspectjweaver/1.6.4/aspectjweaver-1.6.4.jar
Mais encore ?
Fonzie vs GORM
Comparé à GORM, Fonzie ne nécessite pas de changer de langage de programmation (même si Groovy est un super langage). Il vous donne une idée de ce que peut être la programmation avec un framework cool comme Grails sans vous sortir du monde Java traditionnel. Bien sur, une compilation AspectJ est nécessaire, mais peut tout à fait être intégrée dans la construction du projet de manière relativement transparente pour le développeur lambda.
Bien sur, GORM propose bien des facilités (contraintes, mapping et autres) que Fonzie ne supporte pas. Peut être que l'utilisation de Fonzie vous donnera envie de tester Grails à l'occasion ?
pourquoi ce nom ?
J'en ai marre d'écrire du code à la con, alors vendredi je me suis fait plaisir : j'ai voulu écrire du code cool. Et Fonzie, il est cool !