blog.pagesd.info

Aller au contenu | Aller au menu | Aller à la recherche

mercredi 24 février 2010

Convertir SQL en LINQ : DISTINCT, WHERE et ORDER BY

Ceci est une traduction assez libre de la série de billets rédigés par Bill Horst pour apprendre comment passer du langage SQL au langage LINQ et plus spécialement en VB LINQ. Pour ma part, j’ai adapté les explications et les exemples de codes pour cibler la syntaxe C# du langage LINQ.

Ce troisième billet de la série continue la description des clauses spécifiques et il va porter sur la façon de transformer les clauses DISTINCT, WHERE et ORDER BY en clauses LINQ.

La clause DISTINCT

Les commandes SQL SELECT peuvent contenir un mot-clé DISTINCT qui sert à supprimer tous les doublons dans les résultats renvoyés. Avec le langage LINQ, il n'existe pas de mot-clé "distinct" qui pourrait compléter la clause select ou être inséré au niveau de la requête LINQ. Pour éviter d'avoir des doublons dans les résultats obtenus, il faut employer la méthode Distinct(). Cette méthode renvoie des éléments distincts à partir de la source sur laquelle on l'applique. Les deux exemples ci-dessous ont le même effet :

SQL :

SELECT DISTINCT Region
FROM   ClientTable

LINQ :

var ListeRegions = (from Contact in ClientListe
                   select Contact.Region).Distinct();

La clause WHERE

Comme pour une requête SQL, une expression LINQ permet elle aussi de filtrer les résultats renvoyés en ajoutant une clause "where". Cette clause peut contenir n’importe quelle expression C# booléenne.

SQL :

SELECT *
FROM   ClientTable
WHERE  Region = 'PACA'

LINQ :

from Contact in ClientListe
where Contact.Region == "PACA"
select Contact

Utiliser un opérateur

Avec SQL, les clauses WHERE contiennent très souvent d’autres opérateurs comme AND par exemple. Il existe généralement un opérateur équivalent en C# qui peut être employé au niveau de la clause where de la requête LINQ pour obtenir le même genre de résultat.

SQL :

SELECT *
FROM   ClientTable
WHERE  Region = 'PACA'
AND    CodePostal = '06570'

LINQ :

from Contact in ClientListe
where Contact.Region == "PACA"
&& Contact.CodePostal == "06570"
select Contact

Même lorsqu’il n’existe pas vraiment d’équivalent en C#, il est généralement possible de reproduire n’importe quelle expression SQL sous forme d’expression C#. Par exemple, il n’existe pas de mot-clé en C# qui corresponde au mot-clé "BETWEEN" du SQL. Mais on peut facilement obtenir un résultat similaire en C#.

SQL :

SELECT *
FROM   CommandeTable
WHERE  DateCommande BETWEEN '2010-01-01' AND '2010-12-31'

LINQ :

from Colis in CommandeListe
where Colis.DateCommande >= "2010-01-01"
&& Colis.DateCommande <= "2010-12-31"
select Colis

Dans le cas de l’opérateur "IN" du SQL, il est possible d’utiliser la méthode Contains() pour arriver au même résultat :

SQL :

SELECT *
FROM   ClientTable
WHERE  Region IN ('IDF', 'PACA')

LINQ :

string[] regions = { "RA", "PACA" };
var ContactsSud = from Contact in ClientListe
                  where regions.Contains(Contact.Region)
                  select Contact

La clause ORDER BY

La clause ORDER BY du SQL peut elle aussi être représentée par une expression LINQ. La cause "orderby" du langage LINQ sert à indiquer comment les résultats doivent être classés en définissant une liste d’expressions séparées par des virgules. Il est possible d’employer n’importe quelle expression C#, sans qu’il soit nécessaire que cette expression fasse parti des éléments sélectionnés dans la requête LINQ.

SQL :

SELECT *
FROM   ClientTable
ORDER BY Telephone

LINQ :

from Contact in ClientListe
orderby Contact.Telephone
select Contact

Définir l’ordre du tri

La clause SQL ORDER BY peut aussi contenir les mots-clés ASC ou DESC pour préciser sir le tri doit de faire de façon croissante ou décroissante. Avec LINQ, il faut utiliser les mots-clés "ascending" ou "descending" pour obtenir le même résultat. Quand aucun de ces mots-clés n’est indiqué, le tri est réalisé de façon croissante par défaut.

SQL :

SELECT * FROM ClientTable
ORDER BY Telephone ASC, Nom DESC

LINQ :

from Contact in ClientListe
orderby Contact.Telephone ascending, Contact.Nom descending
select Contact

Conclusion

Avec ce nouveau billet et les deux billets précédents, il vous est possible de convertir des requêtes SQL basiques en requêtes LINQ. Dans le prochain billet, j’aborderai la façon de traduire les fonctions scalaires ou les fonctions d’agrégations du SQL en LINQ

mardi 23 février 2010

Convertir SQL en LINQ : FROM et SELECT

Ceci est une traduction assez libre de la série de billets rédigés par Bill Horst pour apprendre comment passer du langage SQL au langage LINQ et plus spécialement en VB LINQ. Pour ma part, j’ai adapté les explications et les exemples de codes pour cibler la syntaxe C# du langage LINQ.

Après un premier billet pour comparer SQL et LINQ, ce deuxième billet de la série va présenter comment gérer des clauses plus spécifiques du SQL, en commençant par deux des clauses les plus importantes : FROM et SELECT.

La clause FROM

Une commande SQL SELECT débute toujours par une clause SELECT suivie d’une clause FROM. Dans le cas de LINQ, une expression commence toujours par une clause « from » (ou « aggregate » comme nous le verrons par la suite). La clause SQL FROM de base indique la table sur laquelle va porter la requête, et de façon similaire, la clause LINQ from indique l’objet sur lequel va porter la requête (ClientListe dans notre exemple).

Cet objet peut représenter des données en mémoire, des données d’une table SQL ou des données XML. Dans les exemples à venir, j’utilise des données « en mémoire » pour que le code soit suffisamment simple. En plus de cet objet sur lequel porte la requête, la clause LINQ from contient toujours un identifiant pour la « ligne » courante (Contact dans notre exemple) qui sert d’alias pour représenter cette ligne.

Avec SQL, il suffit de faire « SELECT * » pour sélectionner toutes les colonnes de la table. En ce qui concerne LINQ, il suffit d’utiliser la clause « select alias » pour renvoyer tous les membres de l’objet requêté :

SQL :

SELECT *
FROM   ClientTable

LINQ :

from Contact in ClientListe
select Contact

Utiliser un alias dans le FROM

En SQL, il est possible de définir un alias sur une table au niveau de la partie FROM de la requête. Cela permet ensuite de faire référence aux colonnes de la table source par l’intermédiaire de cet alias. Comme nous l’avons vu dans le paragraphe précédent, l’identifiant qui est défini au niveau de la clause from en LINQ sert lui aussi essentiellement à cela :

SQL :

SELECT Contact.ClientID, Contact.Telephone
FROM   ClientTable Contact

LINQ :

from Contact in ClientListe
select new { Contact.ClientID, Contact.Telephone }

La clause SELECT

En SQL, les commandes SELECT contiennent une liste des valeurs à sélectionner parmi les informations disponibles (Nom, Telephone, Ville…). De la même façon, le langage LINQ vous permet lui aussi de sélectionner certains des membres pour renvoyer un objet anonyme constitué à partir de cette sélection.

Il n’est absolument pas nécessaire que les membres que vous définissez correspondent à l’objet que vous avez spécifié au niveau de la clause from. Il peut en effet s’agir de n’importe quelle expression C# valide (comme par exemple « 3 + 4 »). S’il n’est pas possible de déduire le nom d’un membre que vous avez sélectionné, vous devrez utiliser un alias (voir le paragraphe suivant).

SQL :

SELECT Nom, ClientID
FROM   ClientTable Contact

LINQ :

from Contact in ClientListe
select new { Contact.Nom, Contact.ClientID }

Utiliser un alias dans le SELECT

Toujours en SQL, il est possible de définir des alias pour les différents éléments que vous sélectionnez au niveau de la clause SELECT, ce qui permet ensuite d’utiliser ces alias pour faire référence à ces éléments. De la même façon, LINQ vous permet de donner un nom aux éléments que vous sélectionnez, ce qui vous permettra par la suite d’utiliser ce nom faire référence à ces éléments lorsque vous exploiterez le résultat de la requête LINQ.

SQL :

SELECT Nom NomContact, ClientID ContactID
FROM   ClientTable Contact

LINQ :

from Contact in ClientListe
select new { NomContact = Contact.Nom, ContactID = Contact.ClientID }

Dans le prochain billet, je présenterai les équivalents des clauses DISTINCT, WHERE et ORDER BY pour le langage LINQ.

lundi 22 février 2010

Convertir SQL en LINQ : Introduction

Ceci est une traduction assez libre de la série de billets rédigés par Bill Horst pour apprendre comment passer du langage SQL au langage LINQ et plus spécialement en VB LINQ. Pour ma part, j’ai adapté les explications et les exemples de codes pour cibler la syntaxe C# du langage LINQ.

Comme vous le savez sans doute déjà, les commandes LINQ permettent d’écrire des requêtes en C# en utilisant une syntaxe inspirée de SQL. Cependant, la syntaxe de LINQ ne correspond pas exactement à la syntaxe du SQL. Par conséquent, si vous avez déjà travaillé en SQL ou que vous êtes habitués à écrire des requêtes SQL, vous allez sans doute chercher à transformer vos requêtes SQL en requêtes LINQ.

Ce billet va être le premier d’une série consacrée à la façon de traduire du SQL en LINQ. Dans cette introduction, je souhaite présenter ce qui différencie LINQ de SQL puis j’aborderai les particularités de chaque syntaxe dans la suite de la série.

Pré-requis

Les exemples de code SQL que je vais utiliser seront basés sur une table ClientTable et une table CommandeTable. Pour les exemples C#, je m’appuierai sur des objets ClientListe et les objets CommandeListe, tous deux de type IEnumerable. Et j’utiliserai deux classes, Client et Commande qui sont définies de la façon suivante :

class Client
{
    public int ClientID;
    public string Nom;
    public string Telephone;
    public string Adresse;
    public string Ville;
    public string CodePostal;
    public string Region;
}

class Commande
{
    public int CommandeID;
    public int ClientID;
    public Single Cout;
    public string Telephone;
    public DateTime DateCommande;
    public string Livraison;
    public string NomArticle;
}

Syntaxe de base

LINQ gère l’équivalent des commandes SQL SELECT, mais pas les autres types de commandes SQL telles que CREATE, INSERT, UPDATE ou DELETE. On peut considérer que la syntaxe de base d’une requête SQL SELECT est constituée d’une série de « clauses » débutant par une clause SELECT :

sqlSelectClause [ sqlClause1 [ sqlClause2 [ ... ] ] ]

Il peut exister un certain nombre de différences en fonction des nombreuses versions de SQL, mais on a toujours plus ou moins la même syntaxe générale :

SELECT Nom NomContact, ClientID ContactID
FROM   ClientTable
ORDER BY Nom

La syntaxe de base pour une expression LINQ consiste également en une série de « clauses » qui débute par la clause « from » (ou éventuellement une clause « aggregate » comme nous le verrons plus tard) :

linqFromClause [ linqClause1 [ linqClause2 [ ... ] ] ]

Ce qui donne par exemple :

from Contact in ClientListe
orderby Contact.Nom
select new { NomContact = Contact.Nom, ContactID = Contact.ClientID }

J’ai employé le terme « expression LINQ » ci-dessus parce qu’à proprement parler les requêtes LINQ ne sont pas des commandes complètes. Alors qu’une requête SQL se suffit à elle-même, si on se place d’un point de vue syntaxique, une requête LINQ n’est que l’équivalent d’une expression telle que « 3 * 4 ». Cela ne constitue pas une commande complète et il faut donc « faire » quelque chose avec. Une requête LINQ peut apparaitre dans du code C# tel que celui-ci :

var ContactsTries = from Contact in ClientListe
                    orderby Contact.Nom
                    select new { NomContact = Contact.Nom, ContactID = Contact.ClientID };

Chaque clause d’une requête LINQ porte sur un objet de type IEnumerable<T> et renvoie un nouvel objet de type IEnumerable<T> (sans que le type T retourné soit nécessairement du même type que le T initial). En général, les clauses de la requête sont analogues aux clauses SQL (SELECT ou ORDER BY par exemple) et vous pouvez habituellement traduire votre requête SQL en LINQ en faisant du « clause à clause ». Dans les exemples ci-dessus, même si les clauses apparaissent dans un ordre légèrement différent, vous pouvez malgré tout voir qu’elles sont assez semblables.

Conclusion

Après cette introduction assez généraliste, j’aborderai des aspects plus spécifiques au cours des billets suivants. Mon objectif tout au long de ces petits tutoriels consacrés à la traduction du SQL en LINQ est de présenter les points suivants :

jeudi 18 février 2010

Suite des essais LINQ to NHibernate

Cette fois-ci je vais me contenter de refaire le même genre de tests que lors de mes premiers essais avec LINQ to NHibernate d'hier. Mais aujourd'hui, au lieu de partir d'un contact pour accéder au groupe auquel il est lié, je vais explorer l'autre côté de la relation entre les deux tables et partir d'un groupe puis accéder aux contacts qui lui sont rattachés.

Essai numéro 1

Je commence doucement avec une requête LINQ toute simple :

var linq1 = (from g in _session.Linq<Group>()
             where g.Id == id
             select g).FirstOrDefault();

Sans trop de surprise (maintenant que j'ai quelques essais au compteur), NHibernate génère la requête SQL suivante :

SELECT top 1
       this_.Id as Id0_0_, 
       this_.Name as Name0_0_ 
FROM   Groups this_ 
WHERE  this_.Id = @p0; // @p0 = 1

Puis j'essaie d'accéder à la propriété Contacts de l'objet Group que vient de renvoyer la première requête LINQ :

int count = linq1.Contacts.Count();

NHibernate génère alors une seconde requête SQL afin de charger la liste des contacts associés au groupe :

SELECT contacts0_.groupId as groupId1_, 
       contacts0_.Id as Id1_, 
       contacts0_.Id as Id1_0_, 
       contacts0_.FirstName as FirstName1_0_, 
       contacts0_.LastName as LastName1_0_, 
       contacts0_.Phone as Phone1_0_, 
       contacts0_.Email as Email1_0_, 
       contacts0_.groupId as groupId1_0_ 
FROM   Contacts contacts0_ 
WHERE  contacts0_.groupId=@p0; // @p0 = 1

Jusqu'ici, c'est tout pareil par rapport aux résultats que que j'avais obtenus au cours de mon premier essai dans l'autre sens.

Essai numéro 2

Comme lors de mes premiers tests, ce deuxième essai va consister à utiliser la méthode Extend("Entité") pour vérifier que cela fonctionne à peu près de la même façon que pour aller du contact vers le groupe. Ce qui me donne donc la requête LINQ suivante :

var linq2 = (from g in _session.Linq<Group>().Expand("Contacts")
             where g.Id == id
             select g).FirstOrDefault();

Et comme je pouvais presque m'y attendre, cela a eu pour effet de faire apparaitre une jointure avec la table Contacts dans la requête SQL qui est générée par NHibernate :

SELECT top 1 
       this_.Id as Id0_1_, 
       this_.Name as Name0_1_, 
       contacts2_.groupId as groupId3_, 
       contacts2_.Id as Id3_, 
       contacts2_.Id as Id1_0_, 
       contacts2_.FirstName as FirstName1_0_, 
       contacts2_.LastName as LastName1_0_, 
       contacts2_.Phone as Phone1_0_, 
       contacts2_.Email as Email1_0_, 
       contacts2_.groupId as groupId1_0_ 
FROM   Groups this_ 
       left outer join Contacts contacts2_ on this_.Id=contacts2_.groupId
WHERE  this_.Id = @p0; // @p0 = 1

Et comme hier, le fait d'accéder à la liste des contacts de l'objet Group renvoyé n'a pas d'impact et NHibernate ne génère pas une nouvelle requête SQL :

int count2 = linq2.Contacts.Count();

Jusqu'ici, tout va toujours bien puisque NHibernate se comporte toujours rigoureusement de la même façon dans les deux sens de la relation.

Interlude

Pour rester le plus synchro possible avec mon billet précédent, voici le fichier de mapping NHibernate que j'ai défini pour les groupes :

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="ContactManager.Models" assembly="ContactManager">

  <class name="Group" table="Groups" dynamic-update="true">

    <cache usage="read-write" />

    <id name="Id" column="Id" type="integer">
      <generator class="identity" />
    </id>
    <property name="Name" type="string" />
    <bag
        name="Contacts"
        inverse="true"
        lazy="true">
      <key column="groupId" />
      <one-to-many class="Contact" />
    </bag>

  </class>

 </hibernate-mapping>

Comme dans le fichier Contact.hbm.xml d'hier, ce fichier de mapping spécifie également la relation qui existe entre les tables Groups et Contacts. Par rapport à la simple balise <many-to-one ... /> d'hier, il faut utiliser un élément <bag ... /> pour représenter le fait qu'à un groupe sont liés plusieurs contacts.

Essai numéro 3

Dans le fichier de mapping Group.hbm.xml, le fait que je fasse du lazy-loading est pour l'instant défini de façon explicite grâce à l'attribut lazy="false". Par conséquent, si je souhaite maintenant ne plus faire de lazy-loading, il est donc nécessaire que je modifie le mapping NHibernate de la façon suivante :

...
    <bag
        name="Contacts"
        inverse="true"
        lazy="false">
      <key column="groupId" />
      <one-to-many class="Contact" />
    </bag>
...

Et désormais, si je reprend la même requête LINQ qu'au début de ce billet :

var linq3 = (from g in _session.Linq<Group>()
             where g.Id == id
             select g).FirstOrDefault();

Lorsque je l'exécute, je peux voir que NHibernate a cette fois-ci généré 2 requêtes SQL différentes. La première sert pour charger les informations du groupe et la seconde permet de charger les données correspondantes aux contacts qui sont rattachés au groupe :

SELECT top 1 
       this_.Id as Id0_0_, 
       this_.Name as Name0_0_ 
FROM   Groups this_ 
WHERE  this_.Id = @p0; // @p0 = 1

SELECT contacts0_.groupId as groupId1_, 
       contacts0_.Id as Id1_, 
       contacts0_.Id as Id1_0_, 
       contacts0_.FirstName as FirstName1_0_, 
       contacts0_.LastName as LastName1_0_, 
       contacts0_.Phone as Phone1_0_, 
       contacts0_.Email as Email1_0_, 
       contacts0_.groupId as groupId1_0_ 
FROM   Contacts contacts0_ 
WHERE  contacts0_.groupId=@p0; // @p0 = 1

Deuxième bilan

Ce qui est vraiment bien, c'est que ça fonctionne exactement de la même façon dans les deux sens ! Et donc normalement, une fois que je serai un peu mieux rodé à l'utilisation de NHibernate et de LINQ to NHibernate, je devrais grosso-modo réussir à prévoir ce qui va se passer sans trop de problème.

Par contre, j'ai encore pas mal de difficultés en ce qui concerne le mapping. Pour l'instant je fais surtout du copié / collé de différents trucs trouvés sur internet (dont le tutoriel Utiliser ASP.NET MVC et NHibernate) => je ne sais pas trop (et je me demande) s'il est nécessaire / obligatoire / conseillé de définir la relation dans les deux fichiers de mapping ou si on peut se contenter de faire ça d'un seul côté (c'est à dire dans un seul fichier de mapping). C'est d'ailleurs peut-être à ça que sert l'attribut inverse="true" ? => Encore pas mal trucs à creuser pour plus tard...

mercredi 17 février 2010

Premiers essais avec LINQ to NHibernate

Etant donné que je débute avec NHibernate et LINQ to NHibernate, j'ai fait quelques essais pour comprendre ce qui se passe côté SQL quand je fais telle ou telle requête LINQ.

Essai numéro 1

Quand je fais la requête LINQ suivante :

var linq1 = (from c in _session.Linq<Contact>()
            where c.Id == id
            select c).FirstOrDefault();

NHibernate génère la requête SQL ci-dessous :

SELECT top 1
       this_.Id as Id1_0_, 
       this_.FirstName as FirstName1_0_, 
       this_.LastName as LastName1_0_, 
       this_.Phone as Phone1_0_, 
       this_.Email as Email1_0_, 
       this_.groupId as groupId1_0_ 
FROM   Contacts this_ 
WHERE  this_.Id = @p0; // @p0 = 13

Ensuite, lorsque j'accède à une propriété liée de l'objet Contact qui a été retourné par la requête LINQ :

string NomGroupe = linq1.Group.Name;

Alors NHibernate va générer une seconde requête SQL pour charger l'objet Group associé au contact :

SELECT group0_.Id as Id0_0_, 
       group0_.Name as Name0_0_ 
FROM   Groups group0_ 
WHERE  group0_.Id=@p0; // @p0 = 1

Essai numéro 2

Après quelques recherches, j'ai trouvé qu'il était possible de reproduire la méthode Include("Entité") de ADO.NET Entity Framework en utilisant la méthode Expand(). Cela me permet donc d'écrire ma requête LINQ de la façon suivante :

var linq2 = (from c in _session.Linq<Contact>().Expand("Group")
            where c.Id == id
            select c).FirstOrDefault();

Et dans ce cas, NHibernate insère automatiquement une jointure avec la table Groups dans la requête SQL qu'il génère :

SELECT top 1 
       this_.Id as Id1_1_, 
       this_.FirstName as FirstName1_1_, 
       this_.LastName as LastName1_1_, 
       this_.Phone as Phone1_1_, 
       this_.Email as Email1_1_, 
       this_.groupId as groupId1_1_, 
       group2_.Id as Id0_0_, 
       group2_.Name as Name0_0_ 
FROM   Contacts this_ 
       inner join Groups group2_ on this_.groupId=group2_.Id
WHERE  this_.Id = @p0; // @p0 = 13

Comme je suis d'un naturel méfiant, je suis allé jusqu'à vérifier ce qui se passait quand j'accédais à la propriété Group de l'objet Contact :

string NomGroupe2 = linq2.Group.Name;

Et fort heureusement il ne se passe rien et NHibernate ne génère pas de requête SQL supplémentaire.

Interlude

OK. Mais NHibernate ça ne peut pas être aussi simple que ça et il y des tas de trucs à prendre en compte. Jusqu'à maintenant, j'ai fait mes deux essais avec le fichier de mapping suivant pour les contacts :

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="ContactManager.Models" assembly="ContactManager">

  <class name="Contact" table="Contacts" dynamic-update="true">

    <cache usage="read-write" />

    <id name="Id" column="Id" type="integer">
      <generator class="identity" />
    </id>
    <property name="FirstName" type="string" />
    <property name="LastName" type="string" />
    <property name="Phone" type="string" />
    <property name="Email" type="string" />
    <many-to-one name="Group" column="groupId" not-null="true" />

  </class>

</hibernate-mapping>

Dans ce fichier, il y a une ligne <many-to-one name="Group" column="groupId" not-null="true" /> qui sert à définir la relation entre les tables Contacts et Groups :

  • 1 contact appartient à 1 groupe
  • 1 groupe peut contenir 0 à N contact(s)

Par défaut, cette relation "fonctionne" en mode lazy-loading, ce qui signifie que le groupe ne sera chargé que lorsqu'on en aura réellement besoin. Et au vu de mes deux premiers essais, je ne peux qu'être satisfait puisque c'est exactement comme cela que ça s'est passé.

Essai numéro 3

Et maintenant, supposons que je ne veuille plus faire de lazy-loading ? Pour commencer, je dois ajouter lazy="false" à mon fichier de mapping :

...
    <many-to-one name="Group" column="groupId" not-null="true" lazy="false" />
...

Et ce coup-ci, quand je refais la même requête LINQ que lors de mon premier essai :

var linq3 = (from c in _session.Linq<Contact>()
            where c.Id == id
            select c).FirstOrDefault();

Je peux constater que NHibernate a généré directement les 2 requêtes SQL suivantes :

SELECT top 1
       this_.Id as Id1_0_, 
       this_.FirstName as FirstName1_0_, 
       this_.LastName as LastName1_0_, 
       this_.Phone as Phone1_0_, 
       this_.Email as Email1_0_, 
       this_.groupId as groupId1_0_ 
FROM   Contacts this_ 
WHERE  this_.Id = @p0; // @p0 = 13

SELECT group0_.Id as Id0_0_, 
       group0_.Name as Name0_0_ 
FROM   Groups group0_ 
WHERE  group0_.Id=@p0; // @p0 = 1

Par acquit de conscience j'ai re-vérifié mon premier essai sans lazy="false". Pas de problème : NHibernate ne génère bien que la 1° requête SQL si je me contente de faire la requête LINQ et que je ne cherche pas à accéder à l'objet Group.

Premier bilan

Au moins, ça commence à être un peu plus clair pour ce côté de la relation. Si tout va bien, je ferais plus tard quelques essais supplémentaires pour étudier comment ça se passe dans le cas où je pars d'un groupe et que j'accède ensuite à ses contacts.

J'ai pas trainé : la suite de mes essais avec LINQ to NHibernate !