Plus d’infos sur le blog d’Emmanuel Bernard
Alors qu’au début, il n’y avait que Infinispan, on peut désormais y trouver MongoDB et Ehcache. D’autres types viendront surement. Ici, Ehcache est utilisé non pas comme un cache mais comme un datastore noSQL clé/valeur.
Pour effectuer des insertions dans Ehcache via OGM, c’est vraiment simple. Après avoir ajouter la dépendance hibernate-ogm-ehcache à votre POM, il faut ensuite faut un fichier persistence.xml.
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="ogm-ehcache" transaction-type="JTA"> <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider> <properties> <property name="hibernate.ogm.datastore.provider" value="ehcache"/> <property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.JBossStandAloneJtaPlatform"/> </properties> </persistence-unit> </persistence>
Note : Actuellement, les transactions, qui sont gérées de manière native par Ehcache, ne fonctionnent pas avec OGM.
Il faut ensuite instancier l’entityManager.
Configuration cfg = new OgmConfiguration(). setProperty("hibernate.ogm.datastore.provider", "ehcache"). addAnnotatedClass(Appli.class).addAnnotatedClass(Platform.class); TransactionManager tm = getTransactionManager(); EntityManagerFactory emf = Persistence.createEntityManagerFactory("ogm-ehcache"); try { EntityManager em = emf.createEntityManager();
Le modèle est le suivant : 2 classes, Appli et Perform.
@Entity @Indexed public class Appli { @Id @GeneratedValue(strategy = GenerationType.TABLE, generator = "appli") @TableGenerator( name = "appli", table = "sequences", pkColumnName = "key", pkColumnValue = "apply", valueColumnName = "seed" ) public Long getId() { return id; } public void setId(Long id) { this.id = id; } private Long id; public String getName() { return name; } public void setName(String name) { this.name = name; } private String name; @ManyToOne @IndexedEmbedded public Platform getPlatform() { return platform; } public void setPlatform(Platform platform) { this.platform = platform; } private Platform platform; }
@Entity @Indexed public class Platform { @Id @GeneratedValue(generator = "uuid") @GenericGenerator(name="uuid", strategy="uuid2") public String getId() { return id; } public void setId(String id) { this.id = id; } private String id; @Field public String getName() { return name; } public void setName(String name) { this.name = name; } private String name; }
On crée ensuite un objet learnAnimals de type Appli qui a pour plateforme itunes.
Platform itunes = new Platform(); itunes.setName("iTunes"); Appli learnAnimals = = new Appli(); learnAnimals.setName("J apprends les animaux" + i); learnAnimals.setPlatform(itunes);
On persiste de la meme manière qu’avec hibernate.
tm.begin(); em.persist(learnAnimals); Long leanAnimalsId = learnAnimals.getId(); em.flush(); em.close(); tm.commit();
Et voilà, l’objet learnAnimals est persisté. Mais à la différence d’une persistance classique avec Ehcache, qui fonctionne par sérialisation/déserialisation des objets, le modèle est déshydraté.
Pour une entité, elle est donc stockée dans une store ENTITIES avec le modèle clé/valeurs suivant :
Clé
Chaque clé est de type EntityKey, classe qui contient entre autre :
private final String table; private String[] columnNames; private Object[] columnValues;
On aura par exemple : {table=’Appli’,columnNames=[‘id’],columValues=[‘1’]} pour l’appli qui a pour identifiant la valeur 1.
Pour la valeur, elle est construite sous la forme d’une map.
Ainsi pour un object Appli qui contient une Plateforme, la map aura cette forme :
Clé | Valeur id | 1 name | monAppli platform_id | 65d2183a-3a73-4079-83fb-57f9072e0915
A l’insertion c’est un peu plus compliqué. On passera par des objets transitoires, des aggregats nommé TupleOperation qui contiennent le nom de la colonne, sa valeur et son type TupleOperationType, celui-ci pouvant prendre 3 valeurs, PUT, PUT_NULL et REMOVE.
On a donc en fait :
Key | Value id | {columnName='id',columnValue='1'.columnType=TupleOperationType.PUT} name | {columnName='name',columnValue='monAppli'.columnType=TupleOperationType.PUT} platform_id | {columnName='platform_id',columnValue='2886a75c-11ae-4f3d-a132-8d58010382b3'.columnType=TupleOperationType.PUT}
La valeur PUT indiquera qu’il faut faire un
map.put( action.getColumn(), action.getValue() );
C’est à dire pour le premier exemple :
map.put('id',1)
Un remove aurait entrainé une suppression de la paire clé/valeur. Un PUT_NULL fait la même chose qu’un PUT.
On a donc bien inséré en base :
Clé | Valeur id | 1 name | monAppli platform_id | 65d2183a-3a73-4079-83fb-57f9072e0915
On aura une entrée similaire dans le meme cache ENTITIES pour les objets de types Platform. Pour résumer, on a donc :
Map<EntityKey,Map<String,Object>>
La deuxième store est la store ASSOCIATION. Elle n’est utile que dans les relations plus complexes.On y reviendra par la suite. Dans ce cas, elle reste vide.
La troisième store est la store des IDENTIFIERS, qui stocke les informations relatives aux séquences, notamment toutes celles qui permettent de gérer de manière automatique les identifiants.
On définit maintenant une application comme pouvant être associée à N plateforme. Le modèle de Platform ne change pas, celui d’Appli légerement.
@Entity @Indexed public class AppliManyToMany { @Id @GeneratedValue(strategy = GenerationType.TABLE, generator = "appli") @TableGenerator( name = "appli", table = "sequences", pkColumnName = "key", pkColumnValue = "apply", valueColumnName = "seed" ) public Long getId() { return id; } public void setId(Long id) { this.id = id; } private Long id; public String getName() { return name; } public void setName(String name) { this.name = name; } private String name; @ManyToMany @IndexedEmbedded public List getPlatforms() { return platforms; } public void setPlatforms(List platforms) { this.platforms = platforms; } private List platforms=null; }
En ce qui concerne la table ENTITY, elle est un peu modifié.
La clé reste identique, par contre l’enregistrement ayant pour clé platform_id n’existe plus. On a uniquement :
Clé | Valeur id | 1 name | monAppli
L’association est désormais portée par un enregistrement dans la table ASSOCIATION.
La clé, de type AssociationKey, similaire à une EntityKey, représentant l’id 5 :
{table=’AppliManyToMany_Platform’, columnNames=[AppliManyToMany_id], columnValues=[5]}
La valeur est toujours une Map :
Clé (de type RowKey) : {table=’AppliManyToMany_Platform’, columnNames=[AppliManyToMany_id, platforms_id], columnValues=[5, 6301e8be-307f-4884-b3a1-5ec0dad7c3e5]}
Valeur (sous la forme d’une Map): {AppliManyToMany_id=5, platforms_id=2886a75c-11ae-4f3d-a132-8d58010382b3}}
On a donc une structure de la forme :
Map<AssociationKey,Map<RowKey,Map<String,Object>>>
Pour cet exemple, il n’y a qu’une seule plateforme, celle qui a l’id 6301e8be-307f-4884-b3a1-5ec0dad7c3e5, ainsi, on a donc un unique enregistrement dans la map.
Si on prends un exemple où une application est associée à deux personnes (appli avec id= 6):
On pourrait avoir comme clé principale :
table=’AppliManyToMany_Platform’, columnNames=’AppliManyToMany_id’, columnValues=’6′
Puis comme value les couples clés/valeurs suivants :
RowKey 1 :
Clé (RowKey) : {table=’AppliManyToMany_Platform’, columnNames=[AppliManyToMany_id, platforms_id], columnValues=[6, b705d241-b6dd-4a81-9fb2-2f9f732530d7]}
Valeur associée (sous forme de map): {AppliManyToMany_id=6, platforms_id=b705d241-b6dd-4a81-9fb2-2f9f732530d7}
RowKey 2 :
Clé (RowKey) : {table=’AppliManyToMany_Platform’, columnNames=[AppliManyToMany_id, platforms_id], columnValues=[6, 2886a75c-11ae-4f3d-a132-8d58010382b3]}
Valeur associée (sous forme de map): {AppliManyToMany_id=6, platforms_id=2886a75c-11ae-4f3d-a132-8d58010382b3}
On a donc bien pour l’application 6 deux plateformes, la plateforme b705d241-b6dd-4a81-9fb2-2f9f732530d7 et la 2886a75c-11ae-4f3d-a132-8d58010382b3 . Le framework se servira des identifiants dans un deuxième temps pour retrouver dans la table ENTITIES les plateformes correspondantes et les ‘ajoutera’ dans la liste des plateformes de l’application id=6. Pour avoir une idée encore plus concrète, il y a des schémas dans la documentation d’OGM.
A venir : Hibernate Search, Lucene et autres recherches avec Hibernate OGM.
]]>Hydra est une entité qui a la particularité de contenir une liste immuable de tête (heads.)
@Entity public class Hydra { private Long id; private List heads = new ArrayList(); @Id @GeneratedValue public Long getId() {...} protected void setId() {...} @OneToMany(cascade=CascadeType.ALL) public List getHeads() { return Collections.unmodifiableList(heads); } protected void setHeads(List heads) {...} } // creates and persists the hydra with 3 heads // new EntityManager and new transaction Hydra found = em.find(Hydra.class, hydra.getId());
La question est la suivante, combien d’appel sont fait en base de données lors de la deuxième transaction (créer lors de em.find).
(a) 1 select
(b) 2 selects
(c) 1+3 selects
(d) 2 selects, 1 delete, 3
inserts
(e) None of the above
Pendant la recherche, em.find entraine un unique select en base de donnée sur l’hydre.
Pendant le commit qui est effectué à la fin de la transaction, hibernate vérifie que la collection n’est pas dirty, c’est à dire que les objets devraient être recréés en comparant les références objects des listes. Un deuxième select est alors effectué sur les têtes. Dans notre cas, les références ne correspondant pas, l’ensemble de la liste est alors recréé, ce qui explique le delete et les 3 inserts.
Contrairement à ce que l’on pourrait penser dans un premier temps, la bonne réponse est donc la réponse d.
Il faut donc être bien conscient que si on a un objet qui contient une collection et qui porte la liaison, si on affecte une nouvelle liste à l’élément, la collection est recrée entièrement : un delete et n insertions d’éléments. On peut rencontrer également ce genre de problème si on utilise des outils qui suppriment les proxies hibernate sur les objets.
En régle générale, il vaut mieux travailler directement avec les collections retournées par hibernate à moins de savoir ce que l’on fait.
@Entity public class Developer { @Id @GeneratedValue private Long id; private String mainTechnology; public boolean likesMainTechnology() { return "hibernate".equalsIgnoreCase(mainTechnology); } } // creates and persists a developer that uses hibernate as mainTechnology // new EntityManager and new transaction Developer dev = em.find(Developer.class, id); boolean foundCoolStuff = false; for (String tech : new String[]{"HTML5", "Android", "Scala"}) { dev.setMainTechnology(tech); // othersAreUsingIt entraine select count(*) from Developer where mainTechnology = ? and id != ? if (othersAreUsingIt(tech, dev) && dev.likesMainTechnology()) { foundCoolStuff = true; break; } } if (!foundCoolStuff) { // still use hibernate dev.setMainTechnology("hibernate"); }
(a) 2 selects
(b) 4 selects
(c) 4 selects, 1 update
(d) 4 selects, 4 inserts
(e) None of the above
La bonne réponse est la réponse d, 4 selects et 4 inserts. En effet, hibernate doit garantir la bonne valeur des requêtes exécutées et parfois doit effectuer une flush pendant une transaction. Si on n’effectue plus l’appel à othersAreUsingIt (qui entraine un select sur la table Developer), il n’y a plus d’update.
List semantics
@Entity public class Forest { @Id @GeneratedValue private Long id; @OneToMany Collection<Tree> trees = new HashSet<Tree>(); public void plantTree(Tree tree) { trees.add(tree); } } // creates and persists a forest with 10.000 trees // new EntityManager and new transaction Tree tree = new Tree(“oak”); em.persist(tree); Forest forest = em.find(Forest.class, id); forest.plantTree(tree);
(a) 1 select, 2 inserts
(b) 2 selects, 2 inserts
(c) 2 selects, 1 delete,
10.000+2 inserts
(d) Even more
La bonne réponse est la réponse c. La combinaison de l’annotation OneToMany et d’une collection entraine un bag semantic. La collection est donc recrée.
Semantic | Java Type | Annotation | Add 1 element | Update 1 element | Remove 1 element |
Bag Semantic | java.utill.Collection java.util.List |
@ElementCollection || @OneToMany || @ManyToMany |
1 delete + n insert | 1 delete + n insert | 1 update |
Set Semantic | java.utill.Set | @ElementCollection || @OneToMany || @ManyToMany |
1 insert | 1 update | 1 delete |
List Semantic | java.util.List | (@ElementCollection || @OneToMany || @ManyToMany)&&(@OrderColumn||@IndexColumnn) |
1 insert+ m update | 1 delete + m insert | 1 update |
@OneToMany with no cascade options
La première intuition est de remplacer le Set par une List (List<Tree> trees = new ArrayList<Tree>() ). Néanmoins, cela marche exactement de la même manière.
Le seul moyen de ne pas avoir de bag semantic est d’utiliser orderColumn ou indexColumn
Il faut faire attention à choisir une collection appropriée sur la partie qui contient la liaison. Ainsi dans notre cas, Set<Tree> trees = new HashSet<Tree>() permet d’éviter toutes les insertions parasites.
Utilisation d’un set sur l’object qui ne contient pas la liaison.
@Entity public class Forest { @Id @GeneratedValue private Long id; @OneToMany (mappedBy = “forest”) Collection<Tree> trees = new HashSet<Tree>(); public void plantTree(Tree tree) { trees.add(tree); } } @Entity public class Tree { @Id @GeneratedValue private Long id; private String name; @ManyToOne Forest forest; public void setForest(Forest forest) { this.forest = forest; this.forest.plantTree(this); } } // creates and persists a forest with 10.000 trees // new EntityManager and new transaction em.remove(forest);
L’appel à em.remove entraine java.sql.BatchUpdateException : cannot delete or update a parent row : a foreign key constraint fails.
Si on garde le modèle, la seule solution est de parcourir l’ensemble des arbres de la forêt et de setter leur forêt à null.
Il est ensuite possible de supprimer la foret. Ce qui entraine 10 000 updates et 1 delete …
D’autres types de collections auraient été plus adéquats.
Il existe beaucoup d’autres anti-patterns. Pour les débusquer dans votre code, il est plus que recommander d’observer attentivement les requêtes ! Les slides sont ici : http://www.yonita.com/2011_11_16_PERFORMANCE_ANTIPATTERNS_DEVOXX.pdf
]]>C’est 5 jours de conférences, principalement orientés java à Anvers en Belgique. La réception des devoxians à lieu au Metropolis, une salle de cinéma dans la banlieue d’Anvers, à environ 20 min de la gare centrale en tramway. Après avoir récupéré nos badges, on nous fournit un sac de goodies (sac/stylo/bloc note/Tee shirt).
La première conférence à laquelle j’ai assisté sur Kanban a duré 3 heures. Kanban est une méthode agile avec un fonctionnement plus simple que Scrum, dans le sens où elle est beaucoup moins contraignante. C’est une méthode avec une forte composante visuel où un radiateur d’information constitue un point central de la méthode. Ses trois principes sont la confiance, l’esprit d’équipe et la transparence. Il n’y a pas de notions de sprint ou d’itération . Les demandes arrivent en permanence et sont traitées au fur et à mesure.
La deuxième conférence sur JSF 2 était assez pointue, organisée en 3 parties : une heure sur la vue, une heure sur le controleur, une heure sur le modèle. L’aspect orientation composant au lieu de contenu comme c’est le cas avec JSP est assez interessant. L’inconvénient de JSF 1.2 était la quantité très importantes d’XML, JSF 2.0 passe aux annotations, ce qui la rend beaucoup plus simple à manipuler. D’autres améliorations : pouvoir passer des paramètres en GET (sans commentaire), gérer des bookmarkables URLs …
Un des points qui a été souvent abordé lors de la présentation est le futur de JSF. Par exemple, le fait d’avoir une API standard pour AJAX mais également les points bloquants (certaines lourdeurs de configurations). J’ai beaucoup aimé la franchise de Peter, qui a bien montré ce que l’on peut faire et ce qui peut être améliorer.
Place ensuite à 2 petites présentations qui m’ont laissé sur ma faim. En une quarantaine de minutes, difficile de rentrer dans un sujet technique, tout au moins d’en montrer ses limites.
Une présentation scolaire mais malgrès tout intéressante d’Hades, qui permet de simplifier l’utilisation de JPa n’a cependant pas montré les cas limites d’utilisation.
De même, la présentation NoSQL n’est pas rentrée assez dans les détails pour que cela deviennent vraiment interessant.
De 19h à 22h, place à 3 BoFs : Une session très intéressante sur le NoSQL qui a abordé beaucoup de points, comme l’efficacité, les différents types d’outils, l’importance de choisir un système qui correspond à nos attentes. Pour choisir un bon type de base de données, il est conseillé de faire ressortir 2 priorités entre : Consistency, Partition Tolerance et Availability. Ainsi, Cassandra par exemple, dans le cas de serveurs distribués, va remonter la dernière information mais sans que cela ne soit garantie (choix fait sur Partition Tolerance et Availability ). L’utilisation pour les boutiques en ligne est tout indiquée (c’est le système d’Amazon). Alors que d’autres systèmes tel que HBase garantissent eux une consistance des données, ils seront plutôt utilisés dans des environnements type CMS, où ce qui est visible à l’écran doit correspondre aux dernières données.
Sur JSF2, la BoF est venu compléter la présentation de l’après midi.
La BoF d’hibernate a été assez intéressante. Une présentation assez longue de l’API Criteria (qui n’est pas très sexy au premier abord) ne m’a pas vraiment fait changé d’avis mais néanmoins, le fait que le meta-modèle soit généré automatiquement par l’IDE simplement est quand même important. Le principal intêret de cet specification est le fait que cela soit typé, néanmoins, peut être parceque je ne suis plus habituée à utiliser un ORM (JDBC pur), je trouve cela très (trop) verbeux. Cet aspect verbeux provient du fait que l’API critéria est très puissante et supporte énormément de choses. On peut l’imaginer encapsulée dans un framework plus simple pour ne retenir que les choses ‘utiles’ à son projet dans une forme verbeuse plus simple.
Et voilà le bilan de cette première journée. D’autre articles arriveront sous peu !
]]>