Introduction
Sous NHibernate, il y a 3 manières d’effectuer des requêtes : SQL natif, HQL, et Criteria. La 1ère méthode est à proscrire, sous peine de perdre l’abstraction avec la base, liée à la syntaxe SQL spécifique à celle-ci. Reste HQL et les Criteria, je me demandais quelle méthode utiliser.
Modèle, mapping et requêtes
Soit le modèle suivant :

pour des raisons certainement très justifiées (legacy & co), j’ai besoin d’avoir un objet pour représenter la table de jointure, et ne retenir que l’entité Association et la table de jointure entre Association et Responsable, on aura les tables suivantes : ASSOC et ASSOCRESP. Donc point de many-to-many ici, mais juste une (sous-)requête à réaliser entre ASSOC et ASSOCRESP.
Au niveau base de données, on trouve le schéma de tables suivant (généré avec la syntaxe SQLite par NHibernate) :
CREATE TABLE ASSOC ( Id integer, assoc_code TEXT NOT NULL, assoc_libelle TEXT NOT NULL, PRIMARY KEY (Id)) CREATE TABLE ASSOCRESP ( resp_id INTEGER NOT NULL, assoc_id INTEGER NOT NULL, PRIMARY KEY (resp_id, assoc_id))
et le mapping suivant :
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="true" > <class name="AssocConnector.Assoc, AssocConnector" table="ASSOC"> <id name="Id" column="Id" type="System.Int32" access="property"> <generator class="identity"></generator> </id> <property name="Code" column="assoc_code" not-null="true" type="System.String" access="property"/> <property name="Libelle" column="assoc_libelle" not-null="true" type="System.String" access="property"/> </class> </hibernate-mapping>
une clé composite pour la table de jointure :
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="true" > <class name="AssocConnector.ResponsableAssoc, AssocConnector" table="ASSOCRESP"> <composite-id> <key-property name="Responsable" column="resp_id" type="System.Int32" access="property"/> <key-property name="Assoc" column="assoc_id" type="System.Int32" access="property"/> </composite-id> </class> </hibernate-mapping>
Un test (la méthode GetAssocForAResponsable_ShouldReturn_2Assoc nous intéresse) qui utilise les 2 méthodes : HQL et Criteria : renvoie pour une responsable (id) la liste des associations dont il est responsable. Pour l’occasion (et parce qu’on est à fond), on va aussi utiliser les indicateurs (stats.) que permet la version 2 de NHibernate, pour les tests, ça pourrait servir, le code ici est à titre de démonstration des stats (stats + le Delete).
[Test] public void GetAssocForAResponsable_ShouldReturn_2Assoc() { // requête API Criteria var assocCriteria = DetachedCriteria.For<ResponsableAssoc>() .SetProjection(Projections.Distinct(Projections.Property("Assoc"))) .Add(Restrictions.Eq("Responsable", 1972)); var lassoc1 = session.CreateCriteria(typeof(Assoc), "assoc") .Add(Subqueries.PropertyIn("Id", assocCriteria)).List(); // requete HQL IQuery q = session.CreateQuery(@"FROM Assoc assoc WHERE assoc.Id IN (SELECT ra.Assoc FROM ResponsableAssoc ra WHERE ra.Responsable = :respid)"); var lassoc2 = q.SetParameter("respid", 1972).List(); _displayStats(); Assert.AreEqual(2, lassoc1.Count); Assert.AreEqual(2, lassoc2.Count); GlobalStats.Clear(); session.BeginTransaction(); session.Delete("from ResponsableAssoc ra where ra.Responsable = :respid", 1972, NHibernateUtil.Int32); session.Transaction.Commit(); _displayStats(); } [TestFixtureSetUp] public void initall() { InitalizeSessionFactory(new List<string> {"AssocImpl"}); } [SetUp] public void init() { session = CreateSession(); _create_3Assoc(); _create_3Responsables_for_1Assoc(2); _create_3Responsables_for_1Assoc(3); } [TearDown] public void end() { session.Dispose(); } private IStatistics GlobalStats { get { return session.SessionFactory.Statistics; } } private void _displayStats() { Console.WriteLine("Statistiques NHibernate"); Console.WriteLine("Insert : {0}", GlobalStats.EntityInsertCount); Console.WriteLine("Delete : {0}", GlobalStats.EntityDeleteCount); Console.WriteLine("Fetch : {0}", GlobalStats.EntityFetchCount); Console.WriteLine("Load : {0}", GlobalStats.EntityLoadCount); Console.WriteLine("Update : {0}", GlobalStats.EntityUpdateCount); } private List<Assoc> _create_3Assoc() { var assocs = new List<Assoc> { new Assoc {Code = "A01", Libelle = "Association pour la liberté du NET"}, new Assoc {Code = "A9078", Libelle = "Association des chiens errants"}, new Assoc {Code = "A4503", Libelle = "Association des geeks"} }; foreach (var asso in assocs) session.Save(asso); session.Flush(); foreach (var asso in assocs) session.Evict(asso); return assocs; } private List<ResponsableAssoc> _create_3Responsables_for_1Assoc(int associd) { var responsableAssocs = new List<ResponsableAssoc> { new ResponsableAssoc {Assoc = associd, Responsable = 1972}, new ResponsableAssoc {Assoc = associd, Responsable = 1976}, new ResponsableAssoc {Assoc = associd, Responsable = 1999} }; foreach (var resp in responsableAssocs) session.Save(resp); session.Flush(); foreach (var resp in responsableAssocs) session.Evict(resp); return responsableAssocs; }
La requête Criteria sera traduite par NHibernate par :
SELECT this_.Id AS Id0_0_, this_.assoc_code AS assoc2_0_0_, this_.assoc_libelle AS assoc3_0_0_ FROM ASSOC this_ WHERE this_.Id IN (SELECT DISTINCT this_0_.assoc_id AS y0_ FROM ASSOCRESP this_0_ WHERE this_0_.resp_id = @p0); @p0 = '1972'
La requête HQL par :
SELECT assoc0_.Id AS Id0_, assoc0_.assoc_code AS assoc2_0_, assoc0_.assoc_libelle AS assoc3_0_ FROM ASSOC assoc0_ WHERE (assoc0_.Id IN(SELECT responsabl1_.assoc_id FROM ASSOCRESP responsabl1_ WHERE (responsabl1_.resp_id=@p0 ))); @p0 = '1972'
Trace complet du test :
NHibernate: INSERT INTO ASSOC (assoc_code, assoc_libelle) VALUES (@p0, @p1); select last_insert_rowid(); @p0 = 'A01', @p1 = 'Association pour la liberté du NET' NHibernate: INSERT INTO ASSOC (assoc_code, assoc_libelle) VALUES (@p0, @p1); select last_insert_rowid(); @p0 = 'A9078', @p1 = 'Association des chiens errants' NHibernate: INSERT INTO ASSOC (assoc_code, assoc_libelle) VALUES (@p0, @p1); select last_insert_rowid(); @p0 = 'A4503', @p1 = 'Association des geeks' NHibernate: INSERT INTO ASSOCRESP (resp_id, assoc_id) VALUES (@p0, @p1); @p0 = '1972', @p1 = '2' NHibernate: INSERT INTO ASSOCRESP (resp_id, assoc_id) VALUES (@p0, @p1); @p0 = '1976', @p1 = '2' NHibernate: INSERT INTO ASSOCRESP (resp_id, assoc_id) VALUES (@p0, @p1); @p0 = '1999', @p1 = '2' NHibernate: INSERT INTO ASSOCRESP (resp_id, assoc_id) VALUES (@p0, @p1); @p0 = '1972', @p1 = '3' NHibernate: INSERT INTO ASSOCRESP (resp_id, assoc_id) VALUES (@p0, @p1); @p0 = '1976', @p1 = '3' NHibernate: INSERT INTO ASSOCRESP (resp_id, assoc_id) VALUES (@p0, @p1); @p0 = '1999', @p1 = '3' NHibernate: SELECT this_.Id as Id0_0_, this_.assoc_code as assoc2_0_0_, this_.assoc_libelle as assoc3_0_0_ FROM ASSOC this_ WHERE this_.Id in (SELECT distinct this_0_.assoc_id as y0_ FROM ASSOCRESP this_0_ WHERE this_0_.resp_id = @p0); @p0 = '1972' NHibernate: select assoc0_.Id as Id0_, assoc0_.assoc_code as assoc2_0_, assoc0_.assoc_libelle as assoc3_0_ from ASSOC assoc0_ where (assoc0_.Id IN(select responsabl1_.assoc_id from ASSOCRESP responsabl1_ where (responsabl1_.resp_id=@p0 ))); @p0 = '1972' Statistiques NHibernate Insert : 9 Delete : 0 Fetch : 0 Load : 2 Update : 0 NHibernate: select responsabl0_.resp_id as resp1_1_, responsabl0_.assoc_id as assoc2_1_ from ASSOCRESP responsabl0_ where (responsabl0_.resp_id=@p0 ); @p0 = '1972' NHibernate: DELETE FROM ASSOCRESP WHERE resp_id = @p0 AND assoc_id = @p1; @p0 = '1972', @p1 = '2' NHibernate: DELETE FROM ASSOCRESP WHERE resp_id = @p0 AND assoc_id = @p1; @p0 = '1972', @p1 = '3' Statistiques NHibernate Insert : 0 Delete : 2 Fetch : 0 Load : 2 Update : 0
Conclusion
On peut voir que les 2 méthodes génèrent environ la même requête à Epsilon près.
Mon avis sur les Criteria :
- API, donc vérifiée à la compilation au niveau de sa construction, mais négligeable, n’utilisons-nous pas le TDD pour le développement ?
,
- pour les allergiques du SQL, les Criteria paraissent être un bon compromis,
- cela reste assez verbeux, beaucoup de lignes pour pas grand chose,
sur les HQL :
- pour les fin connaisseurs de SQL, l’apprentissage s’en trouve réduit,
- comme c’est une chaîne, la (mauvaise) habitude de concaténer les arguments peut vite arriver, au lieu d’utiliser les paramètres (SetParameter), et ça, c’est mal,
- la syntaxe étant une chaîne, cela peut être pratique de mettre en place un système d’indicateurs où il y aurait les requêtes à exécuter dans un fichier par exemple (ie: requêtes statiques),
Au niveau performances, il peut y avoir une légère différence, mais pour le Web, lorsqu’il s’agit d’afficher une page, ce n’est pas un critère discriminant.
Personnellement, je penche pour HQL, bien plus proche du SQL, et NH permet d’avoir un langage d’interrogation objet, autant l’exploiter.
Ressources
On pourra essayer cet intéressant projet, plugin NHibernate pour R#.