etude exploratoire de linq - code.ulb.ac.becode.ulb.ac.be/dbfiles/sch2010mastersthesis.pdf ·...

70
Année académique 2009 - 2010 Faculté des Sciences appliquées Etude exploratoire de Linq Directeur de mémoire : Mémoire présenté par Dr. Ir. Hugues Bersini Charles Schoonenbergh En vue de l’obtention du diplôme de Master en ingénieur civil informaticien, spécialité ingénierie informatique

Upload: phamkhue

Post on 30-Sep-2018

225 views

Category:

Documents


0 download

TRANSCRIPT

Année académique 2009 - 2010

Faculté des Sciences appliquées

Etude exploratoire de Linq

Directeur de mémoire : Mémoire présenté par Dr. Ir. Hugues Bersini Charles Schoonenbergh

En vue de l’obtention du diplôme de Master en ingénieur civil informaticien, spécialité ingénierie informatique

Abstract Linq est une technologie apparue avec C# 3 et VB 2008 et a été présenté comme un gigantesque bon

en avant dans le domaine de la gestion des données. La présente étude se propose d’explorer Linq

depuis ses fondements jusqu’à l’analyse de plusieurs mises en situation. L’étude débutera par

l’analyse des enrichissements syntaxiques apportés avec C# 3 pour ensuite définir le formalisme de

requêtes utilisé par Linq. Linq se décline en une série d’implémentations, celles-ci seront examinées

en détails en commençant par Linq to Object. Ensuite viendra le tour de Linq to Sql. Cette

implémentation étant source de beaucoup de controverse, une attention toute particulière y sera

apportée. Plusieurs tests de mise en œuvre seront appliqués, notamment en ce qui concerne les

performances relatives à un code équivalent rédigé avec la couche ADO.NET classique. D’autres

implémentations relationnelles seront étudiées comme Linq to DataSet, Linq to Entities exploitant la

toute nouvelle technologie Entity Framework et une brève analyse de l’implémentation Db Linq sera

faite. L’implémentation Linq to Xml sera ensuite étudiée, présentant les nouveautés liées à la gestion

du contenu Xml. Un bilan des nouveautés proposées par Linq sera ensuite établi et l’étude se

terminera par l’analyse des alternatives à Linq, que ce soit pour la plateforme .NET ou pour d’autres

langages orientés objet, ainsi que Java, PHP ou Python.

Table des matières

Conventions d’écriture 1

Introduction 2

1. Vue d’ensemble 3

2. Mécanismes sous-jacents 4

2.1 Quelques rappels 4

2.1.1 Généricité 4

2.1.2 Délégués 4

2.1.3 Méthodes anonymes 5

2.2 Enrichissements syntaxiques 6

2.2.1 Inférence sur le typage 6

2.2.2 Expressions lambda 7

2.2.3 Expressions arborescentes 7

2.2.4 Extensions de méthode 11

2.2.5 Formalisme d’une requête 11

3. Implémentation Objets 13

3.1 Champ d’application 13

3.2 Quelques opérateurs à la loupe 14

3.2.1 Projection groupée 14

3.2.2 Inversion et indexes 14

3.2.3 Groupement 14

3.2.4 Quantificateurs 15

3.2.5 Conversion en énumérable 15

3.3 Exemple illustratif 16

4. Implémentations relationnelles 18

4.1 Entités en mémoire 18

4.2 Linq to SQL 18

4.2.1 Mapping 19

4.2.1.1 Syntaxe au niveau du code 19

4.2.1.2 Principe et fonctionnement 19

4.2.1.3 Gestion par le DataContext 21

4.2.2 Opérations fondamentales 22

4.2.3 Cas pratiques 23

4.2.4 Erreurs et difficultés 26

4.2.5 Performances 29

4.2.5.1 Test d’insertion 29

4.2.5.2 Test en situation concurrentielle 31

4.2.6 Conclusion 32

4.3 Linq to DataSet 33

4.4 Linq to Entities 35

4.4.1 Concept fondateur 35

4.4.2 Fonctionnement 36

4.4.3 Mapping 37

4.4.4 Rôlede Linq 38

4.5 Db Linq 38

5. Implémentation XML 39

6. En pratique 41

6.1 Ressenti général 41

6.2 Points forts, points faibles 41

6.3 Quelles alternatives ? 43

6.3.1 Java 43

6.3.1.1 JDO 43

6.3.1.2 Hibernate 44

6.3.2 .NET 45

6.3.2.1 NHibernate 45

6.3.2.2 Entity Framework 45

6.3.3 Autres langages 46

6.3.3.1 Persistance personnalisée 46

6.3.3.2 Bases de données objets 47

6.4 Evolutions de Linq 48

6.4.1 Version 4 48

6.4.2 Ce qui reste à améliorer 48

6.4.3 Linq et Mono 49

Conclusion 50

Bibliographie 51

Annexe 1 : Code de l’exemple Linq to object 52

Annexe 2 : Code de test de performance Linq to Sql 55

Annexe 3 : Utilisation d’une base de données MySql avec Linq to DataSet 65

Conventions d’écriture Afin de rendre plus aisée la lecture de ce document, des conventions d’écriture ont été adoptées.

Les termes faisant référence à une classe, un assemblage ou une méthode seront écrits en italique et

dans le respect de la casse originale, comme ceci : Methode définie pour telle Classe.

Les termes faisant références à des procédures ou à des techniques bien connues seront écrit avec

une majuscule pour mieux ressortir, comme ceci :

La clause Select du langage Sql.

Les extraits de code seront présentés dans une police spéciale pour bien ressortir du texte principal.

En outre, un alinéa supplémentaire sera observé, comme ceci :

Ceci est un extrait de code;

Les références à des sites internet se feront comme des références classiques lorsque le site contient

de l’information dont le présent écrit est inspiré. Lorsque du contenu est cité explicitement, une note

de bas de page présentera l’adresse web exacte.

Introduction Depuis l’apparition des langages orientés objet, le problème de la persistance des données a toujours

été crucial. Un programme incapable de retenir ce qui s’est passé lors de sa précédente exécution ne

présente qu’un intérêt limité. Malheureusement aucun langage orienté objet n’a de prise directe sur

cette capacité de persistance. Des systèmes de stockage se sont développés, certains très antérieurs

aux langages orientés objet eux-mêmes. C’est le cas des bases de données relationnelles qui stockent

l’information sous forme de tables ayant des relations entre elles. Ce mode de stockage c’est petit à

petit imposé comme acteur principal lorsqu’il s’agit d’enregistrer des informations depuis un

programme. L’ennui est que les langages de programmation actuels ne s’interfacent pas

parfaitement avec ce type de persistance. Depuis longtemps déjà, des solutions sont mises en œuvre

pour palier à ce problème.

Réaliser un interfaçage n’est pas tout, il reste le problème de la dépendance au niveau du code. Si

changer le système d’enregistrement des données implique de réécrire la moitié de l’application qui

les utilise, la situation est gênante. L’abstraction envers le stockage a ses limites, lui aussi. Les bases

de données orientées objet et plus tard leurs homologues basées sur Xml sont venues s’ajouter dans

le paysage moderne de la persistance. Ces gestionnaires de données n’utilisent cependant pas le

même langage et passer de l’un à l’autre sans rien changer est tout simplement irréalisable. Nous

avons donc actuellement affaire à des applications mêlant plusieurs langages de programmation,

généralement de l’orienté objet couplé à un langage de requêtes propre à la source de données.

C’est là que la tâche du développeur se complique. Il faut non seulement maîtriser plusieurs langages

mais en plus pouvoir interfacer les deux de la façon la plus abstraite possible car un changement de

politique de stockage est toujours possible.

Linq a été introduit avec C# 3 et VB 2008. Derrière cet acronyme, Language INtegrated Query, se

cache presque tout un paradigme. Une nouvelle manière d’appréhender les données, spécialement

celles qui sont persistantes. La présente étude se propose d’explorer les différentes possibilités qui

caractérisent Linq tout en gardant un œil critique sur ce que Microsoft annonce comme étant LA

révolution dans le monde de la programmation. Nous tenterons dans un premier temps de mettre en

lumière les solutions techniques qui ont été mises en œuvre avec Linq. La question du champ

d’application sera ensuite abordée, passant en revue les différentes implémentations de Linq : ce

qu’il peut faire ou ne pas faire et quels types de données il peut cibler. Nous nous intéresserons tout

particulièrement au cas des données relationnelles qui restent, comme déjà précisé, un acteur

incontournable dans le domaine de la gestion des données non volatiles. Après ces différentes

études de cas, nous tenterons de faire le point sur les forces et faiblesses de Linq avant d’envisager

les alternatives à son utilisation. Viendra enfin un examen de son devenir et de ses éventuelles

évolutions avant de conclure notre étude.

Chapitre 1 : Vue d’ensemble Dans cette section nous allons donner un avant-goût de Linq et préciser la méthode générale qui sera

suivie durant cette étude. Tout au long de ce rapport nous utiliserons le terme de Linq pour désigner

l’ensemble des fonctionnalités accessibles grâce à ses implémentations. Linq n’est pas réellement un

tout unifié mais davantage un regroupement de plusieurs API partageant une syntaxe commune. Il

est bon de garder cela en tête pour éviter toute confusion. Ce chapitre est inspiré de [1].

Comme son acronyme l’indique, Linq offre la possibilité de faire des requêtes intégrées au langage,

mais que devons-nous comprendre par là ? En réalité, le terme requête sera expliqué dans le

prochain chapitre et lorsque nous parlons de langage il s’agira de C# et VB 2008. Ces deux langages

de programmation font partie de la plateforme .NET (prononcé dotNet, à l’Anglaise), parfois aussi

appelé framework .NET. Comme abordé dans l’introduction, un problème majeur actuellement est

de devoir mélanger des langages appartenant à des paradigmes différents : en général un pour la

programmation orientée objet et l’autre pour la gestion des données stockées. L’ensemble des

difficultés engendrées par la mixité des langages est souvent appelé « impedance mismatch » dans la

littérature internationale. Ce problème d’impédance entre langages résulte souvent en une baisse de

productivité pour le développeur et une panoplie d’erreurs difficiles à débusquer car n’apparaissant

potentiellement que très tard, à l’exécution. Linq est présenté comme le remède à tous ces soucis,

offrant un seul langage où tout est vérifié à la compilation. Reste à voir quel fossé existe entre ce

genre d’annonces publicitaires et la réalité de la conception d’applications.

Cette étude a pour but d’explorer Linq et ses fonctionnalités, c'est-à-dire d’examiner de quelle façon

Linq se propose de faire la jonction entre le monde des objets et celui de leur persistance. Nous ne

prétendons pas porter de jugement sur le choix de telle ou telle manière de réaliser le stockage des

données. Il n’est pas non plus de notre ressort d’établir que tel langage de programmation est mieux

qu’un tel autre. Nous tâcherons également de rester aussi objectifs que possible dans nos

considérations, il ne s’agit pas de vanter Microsoft ou même Linq mais d’étudier ce dernier. Nous

essayerons de partir de sources documentées aussi officielles que possible pour ensuite mettre en

œuvre des cas concrets. Diverses autres sources seront cependant mentionnées mais leur caractère

non officiel sera précisé.

Chapitre 2 : Mécanismes sous-jacents

2.1 Quelques rappels Avant de se lancer dans l’analyse des nouveautés syntaxiques, rappelons certains aspects

intéressants déjà présents dans le deuxième framework de .NET. Ces fonctionnalités sont à la base

de Linq et constituaient déjà des avancées majeures dans le domaine de l’abstraction en général. Il

s’agit des classes génériques, des méthodes anonymes ainsi que des délégués. Nous allons les passer

rapidement en revue en montrant leur intérêt du point de vue de Linq ainsi que leur rôle. Les

améliorations venues se greffer par-dessus ces mécanismes seront détaillées dans la prochaine

section. L’ensemble de cette section est principalement inspiré de *2+ pour le contenu et de *1+ pour

sa structure.

2.1.1 Généricité

La généricité, en tant que telle, a été introduite dans le langage C++ par le biais des classes

templates. Le principe, rappelons-le brièvement, est de pouvoir considérer l’exécution d’un code

sans avoir à spécifier quel type d’objets il manipulera. C#, dans sa version 2, permettait l’usage et la

définition de classes, de structures, d’interfaces et de fonctions génériques. En plus d’apporter une

plus grande sûreté dans la programmation1, la généricité rend également un code plus réutilisable

lorsqu’elle est utilisée. De ce fait, l’usage des génériques offre un niveau d’abstraction facile

d’emploi. Une liste peut être définie de manière générique, car les principes d’insertion, de

recherche et de suppression ne sont aucunement liés au type de contenu. A l’utilisation, la liste sera

précisée comme étant une liste d’entiers, de strings ou de n’importe quel type d’objets dont il sera

question à ce moment-là. La définition de la liste représente bien une abstraction par rapport à son

contenu et néanmoins, le compilateur remarquera les opérations illicites au niveau typage

(impossible d’ajouter un string à une liste d’entiers par exemple). La généricité est importante pour

envisager Linq, les manipulations de données (telles qu’une insertion, une suppression ou une

recherche, tout comme pour la liste) n’étant pas liées directement au contenu. Comme nous le

verrons plus loin, Linq utilise une interface générique pour manipuler des données.

2.1.2 Délégués

Les délégués sont des objets (de classe Delegate ou d’une de ses sous-classes) qui peuvent appeler

une ou plusieurs fonctions, à la manière des pointeurs de fonctions en C++. Le principe n’est pas

nouveau, son origine remonte au C++ comme nous venons de le préciser. Ce qui est nouveau, en

revanche, ce sont les facilités de syntaxes ainsi que la possibilité d’ajouter un nombre variable de

fonctions à un délégué. Un exemple illustratif inspiré de [2] mettra en lumière ces aspects à la page

suivante.

1 La généricité dans les collections permet d’encore accroître le typage et donc la vérification à la compilation.

class Exemple

{

static void f1() { Console.WriteLine("fct1"); }

static void f2() { Console.WriteLine("fct2"); }

delegate void T(); //declaration du delegue

static void Main(string[] args)

{

T del = new T(f1);

del += new T(f2);

del(); //execute les deux fonctions pointees

}

}

En lisant l’exemple ci-dessus, la première chose qui va nous intéresser est la déclaration du délégué.

Ceci correspond bien à une déclaration de type (ici un type nommé T) dont nous avons le choix de

son nom (T) et le mot clé delegate précise qu’il s’agit d’un type délégué. Il est possible d’ajouter au

délégué une ou plusieurs références à des fonctions à la condition que celles-ci respectent la

signature du délégué (ici aucun paramètre et void comme retour). C’est ce dont il s’agit dans les deux

premières lignes de la méthode Main. Lorsqu’un délégué fait référence à plusieurs fonctions, il les

exécute toutes dans l’ordre avec lequel elles ont été assignées au délégué. Ainsi, le code de

l’exemple plus haut produirait en sortie le résultat suivant :

Le principe des délégués est à la base des expressions lambda fortement utilisées par Linq. Comme

nous le verrons ces expressions sont spécialement utilisées dans la résolution des clauses de

requêtes (select, where …).

2.1.3 Méthodes anonymes

Les méthodes anonymes ont été introduites avec la deuxième version de .NET. Il s’agit de suites

d’instructions jouant le rôle d’une méthode, sans pour autant définir explicitement de fonction. Cela

s’utilise en conjonction avec les délégués, de la manière suivante :

del += delegate { string text = "Il est " + DateTime.Now.ToString();

Console.WriteLine(text); };

En effet aucun nom de méthode n’a été spécifié. La seule contrainte d’utilisation est qu’il est

impossible de faire marche arrière, puisque retirer la référence à cette méthode dans le délégué

implique de pouvoir nommer cette référence. En poussant le raisonnement plus loin, pourquoi ne

pas utiliser une méthode anonyme en paramètre d’une autre méthode ? La lisibilité et la

compréhensibilité du code s’en trouvent améliorées. Linq utilise les méthodes anonymes dans ce

but, améliorer la lisibilité des requêtes. Vouloir obtenir « tous les étudiants tels que leur moyenne est

supérieure ou égale à 10 et les grouper par section » s’écrirait en pseudo code :

etudiants.Where(moyenne >= 10).OrderBy(section).Select(etudiant);

Figure 1 : appels de délégués

Etudiants désignant une collection de données, l’appel à la méthode Where devrait prendre en

paramètre une ou plusieurs conditions à tester. Comment effectuer ces tests sans savoir au préalable

leur nombre et leur nature ? Le recours à une méthode anonyme va permettre de manipuler une ou

plusieurs conditions complexes2 et d’extraire ainsi les objets répondant à la condition pour passer

cette nouvelle collection en paramètre de l’appel suivant et ainsi de suite. Linq fait un usage extensif

des méthodes anonymes en conjonction avec une syntaxe simplifiée (ce point sera développé lors de

la présentation des expressions lambda) pour exprimer les clauses d’une requête, comme le dernier

exemple l’a suggéré.

2.2 Enrichissements syntaxiques Comme nous venons de le voir, certains aspects du langage C# dans sa version 2 introduisent des

fonctionnalités qui nous seront utiles pour mieux cerner les fondements de requêtes sur des

données abstraites. Nous allons maintenant aborder de nouveaux éléments syntaxiques, pour la

plupart basés sur les mécanismes que nous venons de passer en revue. Ces nouvelles fonctionnalités

vont un cran plus loin et mettent en place les bases d’un langage de requêtes. Nous allons y voir

comment le compilateur peut désormais inférer le typage d’un objet lorsque celui-ci n’est pas

spécifié, comment mettre en place les méthodes d’interrogations proprement dites et enfin nous

allons découvrir une nouvelle façon d’écrire les méthodes anonymes au moyen des expressions

lambda. Avec tous ces pré requis en place, nous allons pouvoir examiner la syntaxe globale d’une

requête au sens de Linq en y montrant le rôle de chacun des mécanismes vus jusque là. Cette section

repose essentiellement sur des informations tirées de [4] et arrangées selon une structure inspirée

de [1].

2.2.1 Inférence sur le typage

L’inférence sur le typage permet au compilateur de déduire le type de variables sans que le

développeur ait besoin de le lui signaler explicitement. Comme cette étape se passe à la compilation,

les risques d’erreur restent limités tout en autorisant une syntaxe plus libre. Le mot-clé var peut être

utilisé en lieu et place du type d’une variable et indique au compilateur que son type devra être

inféré à la compilation. A noter toutefois que l’inférence sur le typage est une opération locale et ne

peut donc être appliquée au champ d’une classe ni dans le cas d’un paramètre.

Il est important de parler, à ce stade, d’une autre nouveauté introduite par la troisième version du

framework .NET, le typage anonyme. Le typage anonyme permet de créer une variable sans en

spécifier le type ni l’appel explicite au constructeur (cette dernière particularité constitue elle aussi

une nouveauté et est disponible pour tous les types). Ainsi il est parfaitement licite d’écrire le code

suivant pour une classe Personne ayant comme attributs publics les champs Nom (de type string) et

Age (de type int) :

Personne p = new Personne { Nom = "Tartempion", Age = 40 };

var monTypeAnonyme = new { p.Nom, p.Age };

Linq étant présenté comme une couche d’abstraction dans l’accès aux données, l’intérêt d’intégrer

une telle syntaxe est évident. Un changement dans le type du champ Age (passage de integer en

short par exemple) laissera intacte la présentation des résultats de requêtes ou les modifications de

valeurs impliquant ce champ.

2 Complexe est utilisé ici pour signifier qu’on dépasse le cadre d’un simple opérateur conditionnel tel que ==.

2.2.2 Expressions lambda

Cette sous-section rassemble également des informations provenant de [1] et de [6]. Le type

delegate introduit avec la version 2 du framework .NET représente un moyen de faire pointer du

code vers une fonction définie ailleurs. L’expression lambda simplifie cette pratique et permet plus

de souplesse à l’utilisation. Ceci s’avère particulièrement utile pour l’écriture de méthodes

anonymes, comme le montre l’exemple suivant (trouvé sur [6]) :

//methode anonyme recuperant les entiers positifs, ecrite avec la

//syntaxe 2.0

List<int> list = new List<int>(new int[] { -1, 2, -5, 45, 5 });

List<int> positiveNumbers = list.FindAll(delegate(int i) { return i >

0; });

//meme code ecrit avec une expression lambda

List<int> list = new List<int>(new int[] { -1, 2, -5, 45, 5 });

var positiveNumbers = list.FindAll((int i) => i > 0);

La syntaxe d’une expression lambda est toute simple. Elle se compose d’une liste de paramètres (un

seul, dans l’exemple ci-dessus, l’entier i), du symbole « => » intuitivement traduit par « tel que » et

d’une expression qui constitue en fait la valeur retournée par l’expression lambda. Cette formulation

est bien plus lisible et se prononce « tous les entiers i tels que la valeur de i est strictement

supérieure à 0 ». Un autre avantage est que la valeur retournée par l’expression lambda est typée

implicitement, comme l’indique le mot-clé var dans l’exemple précédent.

En plus d’une simplicité d’écriture pour les méthodes anonymes, les expressions lambda permettent

de traiter des expressions proches des clauses Sql. Ainsi une clause Where dans une requête Sql peut

se voir comme une condition de filtrage du résultat. Pour exprimer ceci en termes d’expressions

lambda, nous appelons notre condition de filtrage définie par une expression lambda sur le résultat,

de cette manière résultat.Filtrage(conditions). Le résultat de ce filtrage peut être lui-même soumis à

d’autres opérations de ce type, présentant l’avantage de pouvoir lire les opérations séquentiellement

de gauche à droite : résultat.Filtre1( condition1.1, condition1.2).Filtre2( condition2.1, … condition2.n)

et ainsi de suite. C’est ainsi que Linq en fait usage, pour passer à chaque opérateur de clause le

résultat créé par l’appel des clauses précédentes : EnsembleFrom.Where(clause where).Select(clause

select).OrderBy(critère de tri). Nous ne parlons ici que des clauses au sens relationnel du terme, le Sql

étant devenu en quelque sorte le standard pour interroger des données, nous nous contentons

d’utiliser ici l’analogie. Nous verrons plus loin que Linq a adopté une syntaxe qui en est très proche.

2.2.3 Expressions arborescentes

Agir sur une source de données implique de pouvoir naviguer, disons, parmi les nœuds de sa

structure. La notion de relation entre des données stockées caractérise ceci au niveau conceptuel,

mais en pratique, qu’en est-il ? Pour des collections d’objets en mémoire, des itérateurs ou des

méthodes de saut à un autre élément sont généralement fournis par la collection elle-même. Pour

une structure relationnelle, les relations sont réalisées par des jointures entre plusieurs tables. Aucun

mécanisme orienté objet ne permet d’appeler une fonction qui sautera directement vers l’élément

désiré. Le problème est le même lorsqu’on manipule des données en Xml bien que, dans ce cas, la

syntaxe des requêtes soit plus proche du paradigme objet. Pour résoudre ce problème, les

expressions arborescentes (expression tree, en Anglais) ont vu le jour. L’intérêt n’est pas tant de

manipuler en soi la source de données mais plutôt de pouvoir naviguer dans un résultat et surtout

dans une requête. Comprendre la notion de jointure est indispensable pour écrire une requête qui y

fait appel et l’idée est ici que nous dotons notre programme orient objet d’un outil capable de

comprendre comment réaliser la navigation. Nous allons tenter de brièvement résumer son

fonctionnement.

Le concept des expressions arborescentes est dérivé de celui des expressions lambda. Premièrement,

une expression arborescente est une expression lambda dont le code va être maintenu en mémoire.

Deuxièmement, cette expression peut être parcourue lors de l’exécution. Troisièmement, elle devra

être compilée pour pouvoir être exécutée dans le langage propre à la source de données. Tentons

maintenant de faire le lien entre ces trois propriétés. Le code sera maintenu en mémoire pour

pouvoir être analysé pas à pas et modifié le cas échéant. L’expression devra être compilée, donc son

impact sur la source de données sera construit au moment de l’exécution. Il faut comprendre ici le

terme « compilé » comme signifiant « transformé dans le langage propre à la source de données et

optimisé pour plusieurs exécutions successives »3. La compilation classique, avec validation

syntaxique, a bien entendu toujours lieu. Par exemple, si la requête implique une lecture dans une

base de données relationnelle, la création de la requête Sql se fera à la volée. Une requête peut être

jugée comme étant syntaxiquement correcte sans avoir besoin de connaître la nature de la source de

données. Pour pouvoir être compilée, la requête doit disposer de son code en mémoire. Ce code sera

parcouru de manière à créer des instructions pour la source de données. Ces instructions se

composent de blocs logiques à la manière d’une équation mathématique. La longueur de ces

instructions n’est pas fixée et l’expression qui en représente une possède une structure en arbre,

d’où le terme d’expressions arborescentes. En résumé, cela revient à dire qu’une expression

arborescente représente un traitement générique (les paramètres sont fournis à l’exécution) et que

le code de cette expression est compilé à la volée, durant l’exécution du programme. L’intérêt est de

pouvoir construire des requêtes dont les paramètres seront fournis à l’exécution et de choisir quand

exécuter cette requête dans une optique d’optimisation. Cet aspect générique va permettre au

développeur d’écrire une requête Linq paramétrique et de choisir à quel moment et vers quelle cible

il décidera de l’exécuter. Cette requête pourra ainsi être traduite en une expression Sql si la source

de données est relationnelle ou au contraire en une requête XQuery à destination d’un fichier Xml.

Jusqu’ici rien n’a été cité pour justifier l’utilisation d’autre chose qu’une expression lambda adaptée.

En réalité, les possibilités offertes par les expressions lambda sont limitées lorsqu’un certain nombre

d’appels, récursifs ou non, sont nécessaires en leur sein. Considérons le code suivant (le code et

l’explication y afférante sont tirés de *1+):

fac => x => == 0 ? 1 : x * fac(x-1)

Ce code ne compilera pas tel quel, il est impossible d’appeler une expression lambda depuis sa

définition, ce que tente pourtant de faire l’appel « fac(x-1) ». Bien qu’il existe des techniques pour

palier à ce problème, nous n’allons pas les traiter ici. Le but était seulement de montrer avec un

exemple simple que les expressions lambda ne permettent pas de tout faire. Rappelons que les

expressions lambda sont utilisées par Linq pour représenter les clauses d’une requête. Mais certaines

clauses peuvent être arbitrairement complexes. Pensons par exemple à la possibilité de réaliser une

requête imbriquée dans une clause Where en Sql. Une simple expression lambda ne peut en elle-

même représenter une clause réalisant une requête imbriquée, cela l’obligerait à s’appeler elle-

3 Le terme compilé est traduit de l’Anglais « compiled » tel que mentionné dans *1+. L’explication fournie est en

revanche personnelle.

même durant sa définition. Les expressions arborescentes permettent de résoudre ce problème en

constituant une super expression composée de nœuds selon une structure en arbre. La construction

finale de l’expression se fait en commençant par les feuilles de l’arbre, en remontant ensuite.

L’ouvrage *1+ propose l’explication la plus claire qui a pu être trouvée sur la cuisine interne des

expressions arborescentes. Cette explication se base sur un exemple qui confronte une expression

lambda avec une expression arborescente, le code présenté ci-dessous est tiré de cet exemple. Nous

allons tenter de résumer cette explication afin de bien percevoir ce qu’est une expression

arborescente. Commençons donc par un code qui devrait permettre d’afficher le type d’une

expression lambda et celui d’une expression arborescente (certains extraits ont été traduits en

dehors de quoi le code est identique à celui proposé dans l’ouvrage) :

static void exemple()

{

Func<int, int> lambdaInc = (x) => x + 1;

Expression<Func<int, int>> exprInc;

exprInc = (x) => x + 1;

Console.WriteLine("avec expression lambda : {0}",

lambdaInc.ToString());

Console.WriteLine("avec expression arborescente : {0}",

exprInc.ToString());

}

Bien que la déclaration soit écrite de manière différente, le résultat est préssenti comme devant être

le même. Parlons maintenant du résultat non pas de ces deux expressions mais de l’affichage dans la

console. Le résultat produit par ce code prend cette forme :

Notre expression lambda est en fait une référence vers un type créé par le compilateur, le type « `2 »

prenant deux paramètres entiers. Notre expression arborescente quant à elle semble contenir la

structure de l’expression sous forme plus lisible. Rappelons-nous qu’une expression lambda est en

réalité une abréviation pour l’écriture d’un délégué. Le compilateur crée donc un objet délégué d’un

nouveau type, puisqu’une expression lambda fait usage du typage anonyme. Le type créé pour notre

expression arborescente n’est pas un délégué, comme nous pouvons le voir. Pour découvrir

comment est interprétée cette expression, [1] nous fournit le schéma page suivante ainsi que le code

correspondant à ce schéma.

Figure 2 : types d'expressions

Pour réaliser une structure similaire, voici le code proposé :

static void equivalent()

{

Expression<Func<int, int>> exprInc;

ConstantExpression constante = Expression.Constant(1,

typeof(int));

ParameterExpression parametre =

Expression.Parameter(typeof(int), "x");

BinaryExpression addition = Expression.Add(parametre,

constante);

exprInc = Expression.Lambda<Func<int, int>>(addition, new

ParameterExpression[] { parametre });

Console.WriteLine("ex. arb. avec syntaxe equivalente :");

Console.WriteLine("{0}", exprInc.ToString());

}

Et ce code produit le résultat que voici :

Notons que les quelques lignes qui apparaissent entre la déclaration de l’expression et l’affichage en

console, tout ce groupe représente l’équivalent de ce qui avait écrit la première fois comme

« exprInc = (x) => x + 1 ». Nous avons bien généré l’arbre en partant des feuilles, nœud par nœud.

Sans compliquer trop l’explication, signalons juste que chaque nœud est matérialisé par un objet

nœud de type générique, permettant d’assurer la navigation au sein de l’expression.

Figure 3 : schéma d'une expression arborescente

Figure 4 : type d'une expression arborescente

2.2.4 Extensions de méthode

« Extension de méthode » est le terme utilisé pour ajouter une méthode à une classe donnée sans

manipuler le code de cette classe. Cela reviendrait à créer une classe héritant de la classe de base en

spécifiant la ou les méthodes que l’on souhaite y ajouter. Plus qu’une facilité syntaxique, l’extension

de méthode permet d’éviter les pièges tendus par l’héritage et n’introduit pas de biais conceptuel

dans la programmation. Une extension de méthode ne peut utiliser que les champs publics de la

classe dont elle est l’extension. Elle doit elle-même être déclarée comme publique et statique. Son

premier paramètre doit être précédé du mot-clé this et le type de ce paramètre définit le type dont

la méthode est une extension. Le code suivant (tiré de [11]) montre une extension de méthode ayant

pour effet de représenter un nombre décimal sous forme de chaîne de caractères au format standard

US :

//Exemple fourni par Linq in Action

public static string FormattedUS( this decimal d) {

return String.Format( formatUS, "{0:#;0.00}", d);

}

L’utilisation des extensions de méthodes donnera peut-être l’impression de faire double emploi avec

les méthodes virtuelles mais il faut garder à l’esprit qu’une méthode virtuelle est résolue à

l’exécution alors qu’une extension de méthode doit être résolue dès la compilation. Précisons enfin

que le type étendu peut être générique, ce qui offre la possibilité d’étendre un ensemble de types

simultanément.

Typiquement, les extensions de méthodes sont utilisées lors de l’appel de clauses dans les requêtes

comme les clauses Where ou Select. Le framework .NET dans sa version 3 propose une interface

générique IEnumerable<T> pour représenter toute donnée susceptible de faire l’objet d’une requête

ou d’une manipulation propre aux données. Des extensions de méthodes sont définies pour chaque

clause avec cette interface (ou l’un de ses descendants) comme type étendu.

2.2.5 Formalisme de requêtes

Cette sous-section s’inspire très fortement de *1+ et *11+. Les expressions de types requêtes ont été

introduites avec la troisième version du framework .NET et définissent la forme générale d’une

requête lorsqu’elle est écrite en C# ou en Visual Basic 2008. Une requête apparaît ici au sens large

puisque la syntaxe sera la même que ce soit une requête vers une base de données relationnelle,

vers une liste d’objets en mémoire ou encore vers un fichier XML. Une requête correctement

formulée doit commencer par une clause From et se terminer par une clause Select ou une clause

group. La clause From indique quels types d’objets vont être interrogés et ceux-ci doivent

implémenter l’interface IEnumerable<T> représentant une collection de données générique

susceptible de faire l’objet d’une requête. Chaque clause de l’expression correspond à un ou

plusieurs appels d’extensions de méthodes pour l’interface citée précédemment. Voici un exemple

de requête typique :

var query = from e in etudiants

where e.moyenne >= 10

select e;

Le compilateur interprétera l’expression ci-dessus de la manière suivante :

var query = etudiants.Where(e => e.moyenne >= 10).Select(e => e);

A noter que etudiants n’a pas été particularisé et pourrait être aussi bien une classe représentant

une table de données relationnelle qu’une liste ordonnée d’objets en mémoire. Dans les deux cas, le

code écrit reste correct.

Les expressions de type requête sont traduites par le compilateur. La validité d’une requête et la

vérification des types sont établis lors de la phase de compilation du code. Les fautes de frappe et les

erreurs syntaxiques seront donc détectées par le compilateur, ce qui représente un avantage certain

par rapport aux accès Sql ou Xml qui ne révèleront leurs erreurs qu’à l’exécution.

Ceci permet déjà d’avoir une vue d’ensemble de Linq au travers des enrichissements syntaxiques

apportés par le framework version 3. Les expressions de type requête sont des interrogations

génériques qui appellent une succession de méthodes, chacune utilisant la réponse de la précédente

comme paramètre. Les méthodes d’extensions simplifient l’écriture de ces appels, les rendant plus

lisibles et plus faciles à implémenter. Les méthodes anonymes permettent la transmission des

paramètres d’un appel à l’autre. Les expressions lambda permettent de définir la logique de la

plupart des opérations effectuées par ces méthodes. Les types anonymes permettent de gérer en

mémoire les résultats de requêtes et l’inférence sur le typage est ce qui permet au compilateur de

faire le lien entre toutes ces fonctionnalités et de les valider à la compilation. Avant de rentrer dans

les détails de comment Linq est implémenté, deux remarques sont encore à souligner.

Premièrement, au vu des nouveautés syntaxiques du langage, Linq apparaît d’ores et déjà comme

une partie intégrante du langage et non comme un outil ORM4 à greffer sur une architecture

existante. Cela implique qu’utiliser Linq ne nécessite que le fait de programmer avec une version

trois (ou supérieure) du framework .NET. L’autre point à soulever est que Linq n’est défini jusqu’ici

qu’en termes d’interface générique (IEnumerable<T> pour rappel) et de mécanismes de langage. Ceci

implique deux choses : Linq est défini en plusieurs implémentations de base et il est possible

d’ajouter une implémentation de Linq pour s’adapter à un besoin précis.

Ceci termine notre tour d’horizon des changements généraux d’ordre purement syntaxique proposés

avec la troisième version du framework .NET. Nous avons introduit les principaux mécanismes qui

constituent Linq ainsi que leurs interactions, ayant souligné également que Linq se compose de

plusieurs implémentations. Nous allons de ce pas examiner ces différentes implémentations en

détails.

4 ORM signifie Object Relational Mapper. Il s’agit le plus souvent d’une désignation pour un utilitaire assurant

une certaine automatisation dans la réalisation du mapping objet relationnel.

Chapitre 3 : Implémentation Objets Nous avons vu jusqu’ici quelle est la manière de formuler une requête syntaxiquement valide,

requête au sens de Linq. Rien n’a été spécifié en ce qui concerne la nature de la source de données,

ce qui implique que tout ce qui a été vu jusqu’ici sera toujours valable tant que nous parlons de Linq.

Mais Linq en lui-même n’est qu’un tas de concepts mis bout à bout. Si nous faisons le point de ce qui

a déjà été vu, nous avons un formalisme pour écrire des requêtes avec l’explication générale de

comment cette requête pourra être traduite vers une source de données, quelle qu’elle soit. C’est

précisément cette opération de traduction qui va nécessiter de connaître cette source plus en

détails. Pour répondre à cette attente, Linq est présenté avec une syntaxe unifiée, faisant abstraction

(répétons-le une fois encore) de la nature du stockage des données. Et pourtant, ce que nous

appelons Linq depuis le début est en réalité un ensemble d’implémentations partageant une même

syntaxe extérieure. Chaque implémentation est définie par un nom qui correspond au type de

données qu’elle cible. Ainsi nous parlerons de Linq to Sql, Linq to DataSet et ainsi de suite. Nous

commencerons notre étude par l’implémentation objet, Linq to Objects. Cette implémentation sera

rapidement parcourue, examinant quelques aspects intéressants. Parmi ces aspects, nous nous

intéresserons à certains opérateurs généraux (donc valables pour toutes les implémentations) et à

l’interface IEnumerable<T> que doit implémenter toute donnée désirant interagir avec Linq. Ce

chapitre dans son ensemble est inspiré de [11] et de [1] pour la description des opérateurs.

3.1 Champ d’application Linq to Objects permet d’interagir avec toute collection qui implémente IEnumerable<T>. Les

collections standards intégrées au framework implémentent toutes cette interface, ce qui permet de

ne pas se poser la question dès lors qu’on utilise une structure de base. Pour ce qui est des

collections définies par l’utilisateur (le programmeur), le plus simple est de créer une collection par

héritage d’une structure standard. Cela peut s’avérer être une erreur conceptuelle dans certains cas,

et l’utilisateur devra réaliser lui-même l’implémentation de cette interface. Ceci s’applique à toutes

les implémentations Linq.

Regardons d’un peu plus près cette fameuse interface. Elle est définie dans l’espace de noms

System.Collections.Generic et la documentation5 [4] montre que la seule méthode qui est renseignée

est GetEnumerator qui doit retourner un objet capable de parcourir les éléments de la collection un à

un. La documentation officielle fait également état d’un ensemble d’extensions de méthode qui

représentent en fait l’ensemble des opérateurs disponibles. Implémenter IEnumerable<T> signifie

posséder les extensions de méthode nécessaires à Linq pour assurer le bon déroulement de requêtes

écrites avec la syntaxe vue précédemment. Nous allons maintenant nous intéresser à certains

opérateurs, ne pouvant tous les détailler par manque de place (il y en a plus d’une centaine en

comptant toutes les surcharges !).

5 La référence exacte est : http://msdn.microsoft.com/en-us/library/9eekhta0.aspx

3.2 Quelques opérateurs à la loupe Les opérateurs représentent ce qu’il est possible de faire en utilisant Linq sur un ensemble de

données. Nous pouvons aisément nous imaginer qu’il est possible de réaliser une projection à la

manière d’un Select en Sql ou même un filtrage avec une clause Where puisque ces opérations sont

définies explicitement dans la syntaxe. Certaines fonctionnalités légèrement plus subtiles se sont

néanmoins glissées dans Linq et nous allons tâcher d’en rassembler quelques unes. Nous parlerons

ainsi de l’opérateur de projection groupée, de l’opérateur d’inversion, de l’opérateur de

groupement, des quantificateurs et de l’opérateur de conversion en énumérable.

3.2.1 Projection groupée

L’opérateur de projection groupée est appelé officiellement SelectMany. Son comportement est

globalement similaire à celui de l’opérateur Select, mais il a pour effet d’aplatir le résultat obtenu.

Ainsi, une requête demandant l’ensemble des cours suivis par les étudiants de dernière année

recevrait une réponse sous la forme d’une collection de cours et non comme une collection de

collections de cours. Lorsque seules les valeurs distinctes doivent être reprises, il suffit d’appliquer au

résultat l’opérateur Distinct. Il est intéressant de noter que l’aplatissement du résultat est disponible

mais non imposé au programmeur. S’il désire travailler sur les collections, un Select suffira. S’il désire

travailler sur les éléments eux-mêmes, un SelectMany constituera un raccourci appréciable.

3.2.2 Inversion et indexes

Les opérateurs de tri classiques que sont OrderBy et ThenBy permettent de trier les éléments d’un

résultat pour peu que ceux-ci implémentent l’interface IComparable<T>. Mais en certaines

circonstances, il peut être intéressant d’avoir les derniers éléments de la liste. Pour ce faire, Linq

propose un opérateur d’inversion appelé Reverse qui retourne l’ordre des éléments au sein d’une

collection. Rien d’extraordinaire jusqu’ici mais il est nécessaire d’introduire un autre petit concept,

celui des opérateurs à index. Il faut savoir que plusieurs opérateurs acceptent un argument

supplémentaire de type entier et qui permet de ne considérer le résultat qu’après en avoir exclu un

nombre d’éléments correspondant à la valeur passée en argument. Avec cette information,

l’opérateur d’inversion permet d’inverser un résultat dont les x premières entrées ont été retirées ou

d’ignorer les x dernières entrées d’un résultat. Bien que cela puisse faire l’effet d’un simple gadget, il

est intéressant de noter cette combinaison d’opérateurs qui permet de réaliser une sélection plus

fine du résultat.

3.2.3 Groupement

L’opérateur de groupement, appelé GroupBy, réalise le groupement des résultats selon une clé

spécifiée. Une clause de groupement rend facultative la clause de projection (de sélection) car le

groupement renvoie le résultat sous forme d’une collection de collections, chaque collection étant

accessible par la valeur de sa clé associée. Le critère de groupement est défini par une expression

lambda ce qui rend le groupement très lisible. Notons que le critère de groupement n’est pas tenu de

faire partie de la clause de projection, comme c’est le cas en Sql. L’opérateur de groupement

possède plusieurs surcharges, permettant des réglages supplémentaires. Parmi ces surcharges, citons

la possibilité de préciser un sélecteur d’éléments ainsi qu’un sélecteur de résultat. Un exemple

d’utilisation se trouve à la page suivante.

var r = etudiants.GroupBy(

e => e.facultes,

e => new { e.nom, e.matricule },

(key, elements) => new {

Cle = key,

Nbr = elements.Count()} );

Le premier paramètre est le critère de groupement, le second est le sélecteur d’éléments qui va

jouer le rôle d’opérateur de projection embarqué. C’est ce paramètre qui va définir quelles valeurs

seront gardées lors de l’affichage du résultat. Le troisième paramètre est le sélecteur de résultat qui

permet de réaliser une projection non plus sur les éléments mais sur les groupes, ce qui est

particulièrement intéressant lorsqu’on souhaite obtenir des informations d’agrégation sur les

résultats sans le détail de ceux-ci. Le résultat produit par le code ci-dessus fournira des informations

sur nombre d’étudiants inscrits dans chaque faculté. Un groupement similaire mais sans le sélecteur

de résultat donnerait le nom et le matricule de chaque étudiant, ceux-ci étant groupés par faculté.

3.2.4 Quantificateurs

Les quantificateurs sont bien pratiques lorsqu’il s’agit de faire des manipulations complexes sur les

données. Les fameux « Pour tout » et « Il existe » sont d’ailleurs une légère entrave en Sql puisque

les quantificateurs universels n’y sont pas définis [12]. Linq fournit trois opérateurs de quantifications

Any, All et Contains. Le quantificateur Any retourne la valeur true si au moins un élément de la

collection sur laquelle il est appelé répond favorablement à l’expression lambda qui lui est passée en

paramètre. Si aucune expression ne lui est passée, il répond simplement à la question « Y a-t-il un

élément dans la collection ? ». Le quantificateur All permet de vérifier que l’ensemble des éléments

d’une collection vérifient un prédicat ayant la forme d’une expression lambda. En réalité cet

opérateur tente de vérifier qu’aucun élément de la collection ne vérifie pas le prédicat, ce qui signifie

qu’il retournera toujours true sur une collection vide. Le quantificateur Contains tente de vérifier si

une collection contient un élément spécifique. En termes objets, n’oublions pas que deux éléments

seront jugés identiques si et seulement si leurs références pointent toutes deux vers le même objet.

Il est néanmoins possible de fournir un comparateur personnalisé pour déterminer l’égalité de deux

objets sur base d’autres critères que leurs simples références.

3.2.5 Conversion en énumérable

Il s’agit ici d’un opérateur prenant en paramètre une séquence d’objets et cet opérateur retournera

des valeurs identiques mais sous forme d’objets énumérables, c’est-à-dire implémentant l’interface

IEnumerable<T>. Comment s’y prend-il ? Très simplement, en utilisant un énumérable générique

dans lequel il encapsule les données de chaque élément. Cet énumérable générique est en fait une

collection assez pauvre puisque son seul but va être de fournir accès aux extensions de méthodes qui

caractérisent les énumérables. Toute autre information sera superflue puisque supposée déjà être

présente dans la collection à convertir. Notons qu’il existe d’autres opérateurs de conversion tels que

ToList, ToArray et ToDictionnary qui peuvent être utiles pour convertir un résultat dans un format

plus classique. L’opération de conversion en énumérable est fortement utilisé avec l’implémentation

Linq to DataSet, ceci sera développé dans la section consacrée à cette implémentation.

3.3 Exemple illustratif Considérons un graphe d’objets représenté par une liste de listes. A titre d’exemple, imaginons que

les nœuds de ce graphe soient des villes et que chaque ville soit reliée par une route à une ou

plusieurs autres villes. Pour corser un peu la situation, nous allons doter nos villes d’informations

supplémentaires, un nombre d’habitants, un nom et une liste de toutes les communes qui

dépendent de celle-ci. Il faut bien sûr que chaque ville ait à sa disposition la liste des villes auxquelles

elle est connectée, cela constituera les branches de notre réseau. Le code comprenant les définitions

nécessaires à cet exemple se trouve en annexe (Annexe 1 : Code de l’exemple Linq to object).

Commençons par écrire une requête adressée au réseau et permettant de retrouver toutes les villes

comptant au plus 10000 habitants. Ceci s’écrit comme suit :

var petitesVilles = from v in reseauRoutier

where v.Hab <= 10000

orderby v.Hab

select new { Ville = v.Nom };

foreach (var ville in petitesVilles.Reverse())

{

Console.WriteLine(ville);

}

Ce code, en plus d’être intuitif, permet de réaliser aisément la récupération du résultat. Ceci n’est

cependant pas très dur à récupérer en utilisant une syntaxe classique. Imaginons maintenant vouloir

connaître l’ensemble des villes qui comprennent au moins deux communes et qui sont connectées à

une ville de moins de 10000 habitants. Avec une écriture classique, cela implique de parcourir la liste

des villes du réseau. Pour chacune de ces villes, il faudra récupérer l’ensemble des communes et les

dénombrer. Pour celles qui possèdent au moins deux communes, il faudra parcourir la liste des villes

avec lesquelles une connexion existe. Si les villes à l’autre bout de cette connexion ont un nombre

d’habitant inférieur ou égal à la borne souhaitée, nous devons ajouter la ville initiale à l’ensemble qui

sera le résultat. C’est déjà relativement complexe à écrire en syntaxe classique, mais si nous désirons

maintenant montrer le nom des villes à faible population qui figurent dans les connexions, c’est

encore pire. Il faudra créer des associations voire même recréer un graphe d’objets et le retourner

comme résultat. Ecrivons cela avec Linq :

var requete = from v in reseauRoutier

from c in v.liaisons

where v.communes.Count() >= 2

&& c.Hab <= 10000

select new {Ville = v.Nom, Connexion = c.Nom,

Habitants=c.Hab};

foreach (var v in requete)

{

Console.WriteLine(v);

}

Ceci produit le résultat suivant qui correspond bien à ce que nous attendions :

Les deux clauses From représentent en réalité l’équivalent d’une jointure. Les relations sont

accessibles de manière simple, il suffit d’avoir un accès à la propriété correspondante. Ceci permet

de justifier l’emploi de Linq même dans un contexte déjà « tout objet ». Linq représente un moyen à

la fois simple et puissant d’exprimer des requêtes. Nous avons vu en détails plusieurs opérateurs

ainsi qu’un exemple pratique mettant en scène l’implémentation objet de Linq. Nous allons

maintenant nous pencher sur les sources de données relationnelles.

Figure 5 : exemple Linq to object

Chapitre 4 : Implémentations relationnelles La problématique de la persistance des objets remonte probablement à la popularisation des

premiers langages orientés objets [3]. Les applications utilisent des objets en mémoire pour décrire

le monde et ceux-ci disparaissent dès la fin de l’application. Pour les enregistrer, le recours à des

bases de données relationnelles reste une option plus que répandue [3]. Or le monde objet et celui

du relationnel ne s’interfacent pas parfaitement. Tout ceci n’est pas nouveau, bien sûr, mais pourtant

de nouvelles solutions à ce problème continuent de voir le jour à un rythme régulier. Linq se

présente comme l’une de ces solutions, voulant offrir une transition plus souple entre ces deux

mondes. Nous allons voir comment Linq a été pensé pour interagir avec les données relationnelles.

Nous commencerons par présenter quelques concepts à la base de la correspondance objet-

relationnel telle que réalisée par Linq. Nous détaillerons ensuite les différentes implémentations

relationnelles de Linq, en précisant leur moyen d’arriver à une correspondance entre objets et tables

relationnelles. Nous tâcherons également de soulever les avantages et limitations de chaque

implémentation. Après ces analyses, nous terminerons par un bilan global des possibilités offertes

par Linq pour manipuler des bases de données. Ce chapitre est inspiré de *3+ pour l’introduction.

Chaque section ayant été le fruit de recherches particulières, les références y seront détaillées au cas

par cas.

4.1 Entités en mémoire Cette section est tiré de [1] et [11]. Interroger une base de données suppose d’avoir préalablement

établi un canal de communication ainsi qu’un langage commun. Linq ne permet pas d’écrire des

requêtes pour une base de données mais plutôt d’écrire des requêtes qui seront soumises aux objets

réalisant le mapping de la base de données. Il s’agit d’un mécanisme de cache où des objets

représentent l’état des données dans la base. Nous appellerons ces objets des entités et les

définitions de leur classe seront appelées des classes entités (ceci correspond à la nomenclature

utilisée dans l’ouvrage *1+). Cette notion est centrale et doit toujours être gardée à l’esprit : nous ne

pourrons jamais parler directement à la base de données. Chaque implémentation proposée par Linq

repose sur le concept des entités, bien que les solutions varient de l’une à l’autre, comme nous allons

le voir.

4.2 Linq to Sql La première implémentation que nous allons étudier porte le nom officiel de « Linq to Sql » et nous

pourrions intuitivement penser qu’il s’agit de l’implémentation avec un grand « i » au regard des

bases de données relationnelles. Nous verrons que ce n’est pas le cas. Cette section contient

énormément d’informations tirées de *11+ pour les concepts théoriques, de *1+ et de *4+ pour

combler les vides laissés par [11]. Nous allons voir comment cette implémentation réalise le mapping

objet relationnel au niveau du code et quels sont les outils qui permettront de nous aider dans cette

tâche. Nous verrons quelles classes sont impliquées dans le mapping et quel est leur rôle, avec un cas

concret. Nous aborderons ensuite les possibilités de manipulation des données et les contraintes

associées. Nous terminerons l’étude de cette implémentation par un bref passage en revue des

pièges à éviter lors de l’usage de Linq to Sql.

4.2.1 Mapping

4.2.1.1Syntaxe au niveau du code

Commençons par une petite précision. Les cibles de Linq to Sql doivent implémenter l’interface

IQueryable<T> qui est une descendante de IEnumerable<T>. L’explication est simplement que

certaines possibilités disparaissent avec l’apparition des bases de données, comme par exemple la

notion d’ordre dans une table. Les opérateurs de sélection d’éléments permettant de choisir un

élément du résultat sur base d’un index n’ont plus de raison d’être. Des notions de synchronisations

entrent également en jeu, puisqu’il va falloir surveiller que nos entités n’aient pas fait l’objet d’une

modification dans la base de données. Pour asseoir ce changement de comportement (qui pourrait

entraîner des erreurs), c’est une autre interface qui définit les extensions de méthodes. Cette

précision faite, nous pouvons envisager la question du mapping.

L’assemblage System.Data.Linq permet d’utiliser les classes entités dans le code. Ici, l’esprit général

des entités est de concevoir une classe entité comme la représentation d’une table dans la base de

données. Cette représentation doit être explicitement renseignée dans la définition d’une classe, au

moyen du mot clé « Table » suivi par l’expression « (Name= « NomDeLaTable ») », le tout, entre

crochets, placé juste avant l’identificateur de la classe. Chaque propriété de la classe représentant un

attribut de la table doit être précédée par le mot clé « Column » entre crochets également. Lorsque

le nom de la propriété est différent de celui de l’attribut, une expression « Name » similaire à celle de

la table doit être utilisée. Voici l’exemple d’une classe entité (cet exemple s’appuie sur *11+ et *1+) :

[Table(Name="Clients")]

public partial class Class1

{

[Column] private string IDClient;

[Column(Name="Nom")]private string _champ2;

}

Cette classe est une correspondance de la table Clients de la base de données, table qui possède

dans ses attributs au moins un champ « IDClient » et un champ « Nom ». Une classe entité n’est pas

tenue d’effectuer le mapping de chaque attribut de la table. Elle peut également contenir des

attributs ou propriétés qui lui sont propres et qui ne participent pas au mapping. Il s’agit ici de

considérations syntaxiques mais comment le mapping est-il réellement effectué ? Nous n’avons

spécifié aucune information qui pourrait permettre d’identifier la base de données, nous avons

seulement fait « l’autre moitié » du mapping. En effet, nous avons renseigné qu’une classe Class1 est

une classe entité et que certaines de ses propriétés sont en correspondance directe avec des

attributs relationnels. Jetons maintenant un coup d’œil sur l’autre facette de ce mapping.

4.2.1.2 Principes et fonctionnement

La correspondance est réalisée au moyen de fichiers spécifiques pouvant être de deux formats : le

format Dbml (pour DataBase Markup Language) et le format Xml. Commençons par parler du format

Dbml qui est considéré comme principal pour Linq to Sql. Le format Dbml est un langage à balises

tout comme le Xml dont il est dérivé. Une définition de ce schéma est disponible avec Visual Studio

2008, cette définition servant de validateur intégré. Dans un fichier Dbml représentant un mapping

valide, les premières lignes fournissent tout un tas de renseignements généraux comme la chaîne de

connexion à la base de données et le nom de la classe utilisée comme DataContext. Nous reparlerons

plus amplement du DataContext dans une section ultérieure, considérons pour l’instant cette classe

comme gestionnaire des objets entités. Viennent ensuite les définitions des tables relationnelles et

les classes entités qui leur correspondent. Les procédures stockées et les fonctions utilisateurs de la

base de données sont également renseignées à cet endroit. Pour chaque table, les attributs et leurs

propriétés Sql sont renseignés. En plus des propriétés purement relationnelles, des informations de

synchronisation sont fournies, comme IsDbGenerated dans l’exemple ci-dessous. Ceci touche en

particulier à la synchronisation qui sera abordée lorsque nous aborderons les cas pratiques de

mapping. Voici un extrait d’un fichier Dbml6 :

<?xml version="1.0" encoding="utf-8"?>

<Database Name="LinqDB" Class="MappingDataContext"

xmlns="http://schemas.microsoft.com/linqtosql/dbml/2007">

<Connection Mode="AppSettings" ConnectionString="Data

Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\LinqDB.mdf;Integrated

Security=True;Connect Timeout=30;User Instance=True"

SettingsObjectName="LinqSelectTest.Properties.Settings"

SettingsPropertyName="LinqDBConnectionString1"

Provider="System.Data.SqlClient" />

<Table Name="dbo.Customers" Member="Customers">

<Type Name="Customers">

<Column Name="ID" Type="System.Int32" DbType="Int NOT NULL IDENTITY"

IsPrimaryKey="true" IsDbGenerated="true" CanBeNull="false" />

<Column Name="Name" Type="System.String" DbType="VarChar(50) NOT

NULL" CanBeNull="false" />

<Column Name="Address_city" Type="System.String" DbType="VarChar(50)

NOT NULL" CanBeNull="false" />

<Column Name="Address_zip" Type="System.String" DbType="VarChar(8)

NOT NULL" CanBeNull="false" />

<Column Name="Address_street" Type="System.String"

DbType="VarChar(50)" CanBeNull="true" />

<Column Name="Address_number" Type="System.Int32" DbType="Int"

CanBeNull="true" />

<Column Name="Email" Type="System.String" DbType="VarChar(50)"

CanBeNull="true" />

<Association Name="Customers_Orders" Member="Orders"

OtherKey="Customer" Type="Orders" />

<Association Name="Customers_ForeignCustomers"

Member="ForeignCustomers" OtherKey="ID" Type="ForeignCustomers"

Cardinality="One" />

</Type>

</Table>

</Database>

Lorsque le DataContext approprié est instancié, le fichier Dbml fournit toutes les informations

nécessaires à la correspondance entre les mondes objet et relationnel. Le lien entre ces deux fichiers

est très simple : le DataContext possède des références vers les classes entités, elles-mêmes validées

par le Dbml du même nom que le DataContext7 . Il faut savoir que le format Dbml est le format le

plus riche en termes d’informations incluses dans le mapping mais il n’est pas le seul disponible. Il a

été prévu qu’un fichier Xml puisse servir de source externe de mapping, le terme « externe » est le

terme officiel utilisé par Microsoft (dans la documentation) et désigne le fait que en interne ce sera

toujours un fichier Dbml qui sera manipulé même s’il pourra être créé depuis une source en Xml. Les

informations fournies par un tel fichier Xml sont sensiblement moins fines que celles d’un fichier

Dbml. En effet, les informations générales sur les tables et les entités sont présentes dans les deux

6 L’alignement des balises laisse ici quelque peu à désirer, ceci étant du au manque de place.

7 nomDataContext hérite de DataContexte et nom.dbml est le fichier de mapping.

cas, mais certaines informations concernant la synchronisation des entités en mémoire ne peuvent

pas être exprimées. Nous n’allons pas approfondir ces détails dans le cadre de cette étude, signalons

simplement que ces deux formes de mappings sont équivalentes, l’une étant moins expressive que

l’autre (des réglages supplémentaires seront à la charge du programmeur s’il décide d’utiliser cette

méthode).

4.2.1.3 Gestion par le DataContext

A ce stade, nous sommes en droit de poser la question suivante « Quand et Comment seront

instantiées ces classes entités ? ». L’idée est que ces objets ne seront créés que lorsqu’ils seront

nécessaires afin de ne pas encombrer la mémoire et une classe en particulier sera responsable de

leur gestion. Cette classe sera en quelque sorte le « chef d’orchestre » du mapping, il s’agit de la

classe DataContext.

Un objet de classe DataContext représente le lien entre la base de données et les classes entités qui

lui sont assignées. Un DataContext contient une ou plusieurs classes entités et est responsable de la

génération du code Sql qui sera envoyé à la base de données. C’est toujours le DataContext qui va

maintenir les entités, au besoin les créer et les mettre à jour. En pratique, la classe DataContext est le

plus souvent dérivée et c’est un de ses descendants qui est utilisé [1], la raison en deviendra

apparente lorsque nous envisagerons notre cas pratique. Quand est-ce que les objets entités seront

instanciés ? Lorsque le DataContext reçoit une requête, s’il n’a pas les entités nécessaires à la

fabrication de la réponse, il va soumettre la requête à la base de données en créant les entités

correspondantes. Nous devons attirer l’attention sur le terme « reçoit » qui est laissé expréssément

flou. Linq to Sql utilise une méthode d’exécution différée (deferred loading en Anglais) pour ses

requêtes. Cela signifie que lorsqu’une requête est soumise au DataContext, celui-ci ne va pas

nécessairement la transmettre directement à la base de données8. Par contre, une provision

d’entités sera créée pour pouvoir accélérer leur chargement le moment venu. Bien que cela puisse

apparaître comme un mécanisme tordu, cela prend son sens dans un contexte où les accès à la base

de données sont nombreux et éventuellement concurrentiels. Une politique d’optimisation complexe

est déployée par Linq mais nous en reparlerons plus tard. Nous avons vu jusqu’ici le rôle d’un

DataContext vis-à-vis des entités, tant celles qui se trouvent en mémoire que celles qui séjournent

dans les tables relationnelles. Nous verrons plus loin comment la théorie du mapping est mise en

pratique et quel y est le rôle du DataContext.

8 Il s’agit là d’une interprétation de diverses sources contradictoires. *11+ précise que chaque appel au

DataContexte entraîne une réaction vers la base de données, sans préciser sa nature. [4] reste très flou. [1] dit clairement que pour une lecture, l’interaction n’est pas initiée par le programmeur.

4.2.2 Opérations fondamentales

Nous allons explorer dans cette sous-section les procédures dites CUD (Create Update Delete) et

comment elles sont traduites vers les habituelles instructions Sql INSERT, UPDATE et DELETE. Nous

examinerons en priorité les mécanismes sous-jacents et comment ceux-ci font pour surveiller les

éventuels changements dans les entités. Chacune des opérations sera ensuite analysée pour avoir

une idée globale de la gestion des entités vis-à-vis du support relationnel en arrière-plan. De

nombreuses informations apparaissant ici sont directement tirées de [4], des sources non officielles

[13] ont contribué à éclaircir les points obscurs.

Pour pouvoir considérer des entités et leur évolution, il faut pouvoir agir sur ces entités et disposer

d’un système capable de gérer ces modifications. Nous allons commencer par nous intéresser à ce

dernier. Pour Linq to Sql, ce système porte le nom de « change tracking service » que nous

appellerons le service sentinelle bien que ceci ne constitue en aucun cas une appellation officielle.

Cette sentinelle est chargée de conserver les états originaux des entités de manière à pouvoir

détecter les changements. L’autre fonction de cette sentinelle est de pouvoir recréer les

modifications observées en termes d’instructions Sql, pour éviter de systématiquement inclure les

valeurs de tous les attributs d’une entité [4]. Le service sentinelle a recours à un système de

marquage pour identifier les entités modifiées. Les marques désignent l’état courant de l’entité,

l’état par défaut étant « non modifiée ». Des états comme « à insérer », « à supprimer » ou « à

mettre à jour » permettent de rapidement savoir quelle modification a été apportée à l’entité. Le

service sentinelle est accessible depuis le DataContext via l’appel de la méthode GetChangeSet [1]. La

sentinelle est active en permanence et collecte le moindre changement survenu sur une entité.

Lorsque le DataContext reçoit l’instruction d’exécuter les modifications faites jusqu’alors, le service

sentinelle vérifie l’état de toutes les entités en mémoire et créer le code Sql nécessaire pour obtenir

les mêmes modifications dans la base de données.

Les ajouts et suppressions d’entités se font de manière relativement similaire, nous allons donc les

examiner ensemble. Ajouter une entité se fait de la même manière qu’un ajout de n’importe quel

type d’objets, par un appel au constructeur. La suppression d’une entité implique de l’avoir

préalablement isolée, autrement dit d’avoir en main l’objet entité en question. Pour l’une ou l’autre

de ces opérations, il suffit désormais d’appliquer les changements au DataContext en marquant

l’entité à modifier. Marquer une entité « à insérer » se fait en appelant InsertOnSubmit sur le

DataContext en passant en paramètre une référence vers l’entité à marquer. Pour réaliser la même

chose lors d’une suppression, il faut utiliser DeleteOnSubmit, toujours en passant la référence

adéquate en paramètre. A noter qu’une version a été développée pour chacune de ces méthodes de

manière à opérer en une fois l’opération sur une collection d’entités. Ces versions groupées sont

appelées InsertAllOnSubmit et DeleteAllOnSubmit respectivement. Ces variantes groupées ne sont là

que dans le but simplifier l’écriture, aucune forme d’optimisation par groupement n’a lieu9.

La mise à jour d’une entité est légèrement différente. Elle nécessite d’avoir isolé l’entité et d’avoir

changé la valeur d’au moins une de ses propriétés. Linq to Sql n’autorise pas la modification de la

propriété renseignée comme étant la clé primaire. Ceci pourrait être intéressant lorsqu’on souhaite

remplacer une entité par une autre en prenant en compte la participation à des relations entre

entités. Une mise à jour de ce type se fait par insertion de la nouvelle valeur et remplacement de

9 Ceci a été vérifié en rebasculant le log d’écriture Sql vers Console.out : y défile une longue suite de requêtes

Sql indépendantes.

l’ancienne valeur dans chaque relation à laquelle elle participe. Notons également que les mises à

jour sont les seules opérations pouvant être invisibles au programmeur inattentif. Dans le cadre

d’une relation entre entités, s’il l’un des participants est supprimé l’autre subira une mise à jour pour

refléter que cette relation n’existe plus [4] [11]. Cette mise à jour n’aura éventuellement pas été

spécifiée explicitement. De même, lorsqu’une association est créée en passant à une entité une

référence vers une autre entité, l’entité reliée (mais non modifiée explicitement) sera mise à jour.

Ajouter ou modifier une entité du DataContext n’est en soi que la moitié du travail. Les entités en

mémoire ne sont plus synchronisées par rapport à la base de données et les modifications désirées

n’y seront pas encore effectives [1]. Pour notifier le DataContext qu’une mise à jour est souhaitée, il

faut le faire explicitement par le biais de la méthode SubmitChanges. Cette opération va prendre en

charge toutes les modifications apportées aux entités, qu’il s’agisse de créations, de mises à jour ou

de suppressions. C’est lors de l’appel de cette méthode que le service sentinelle va répertorier toutes

les modifications apportées aux entités. Des instructions Sql vont ensuite être générées pour amener

la base de données correspondant aux entités en mémoire. Ceci a plusieurs implications en termes

de performances et d’ordonnancement des opérations. Premièrement, chaque appel de la méthode

SubmitChanges peut potentiellement donner lieu à des opérations inattendues à cet instant précis

[4]. En effet, la sentinelle enregistre toutes les modifications et il est tout à fait possible qu’une autre

partie du programme ait créé des modifications entre une insertion et sa soumission quelques lignes

plus loin. Dans un contexte multithreads ou dans celui d’une programmation par événements,

l’enchaînement exact de chaque instruction est au mieux dur à prévoir. Les mises à jour implicites

sont également à ranger dans cette catégorie d’opérations fantômes. Deuxième remarque à

soulever, le service sentinelle passe en revue chaque entité en mémoire à chaque appel de cette

fameuse méthode. La seule opération effectuée sur une entité non modifiée consiste à tester si son

marquage est toujours égal à « non modifiée ». Pour un ensemble conséquent d’entités, ceci peut

créer un délai non désiré dont il faut rester conscient. Il est, par exemple, conseillé d’éviter d’appeler

SubmitChanges dans une boucle destinée à créer un ensemble d’objets.

4.2.3 Cas pratiques

Il nous reste maintenant à découvrir les techniques pratiques pour construire un mapping et son

DataContext associé. Nous allons ensuite soumettre quelques requêtes et observer comment nous

parviennent les résultats. Nous envisagerons d’abord un cas très simple à vocation illustrative avant

de considérer des scénarios plus complexes.

Supposons que nous ayons une base de données Sql Server 2008 pleinement fonctionnelle à notre

disposition et que notre application ne souhaite pas prendre en charge les accès concurrentiels pour

l’instant. Nous allons réaliser tout cela avec Visual Studio 2008, étape par étape.

En ayant créé un nouveau projet C#, nous allons ouvrir l’explorateur de serveurs pour y localiser

notre base de données. Lorsque nous l’avons trouvée, nous pouvons désormais voir les tables qu’elle

contient, ses procédures stockées, ses fonctions utilisateurs, les associations entre les tables et de

nombreuses autres informations dont nous ne ferons pas usage dans ce simple petit test. Ajoutons

une classe à notre projet dont le type sera « Linq to Sql class ». Les classes Linq to Sql arborent

l’extension Dbml, aussi nous pouvons en conclure qu’il s’agira de notre fichier de mapping.

Choisissons lui un nom de circonstance (par exemple « MonMapping ») et validons notre choix afin

de créer ce fameux fichier de mapping. Rappelons que la structure d’un tel fichier, vue à la section

précédente, est assez complexe et semble assez rébarbative à établir. Aller vérifier chacune des

nombreuses propriétés de chaque attribut d’une table, pour toutes les tables impliquées dans le

mapping, cela semble totalement rebutant. Et bien, réjouissons-nous, cela n’a pas à être fait. En

effet, à la création de notre classe à l’extension Dbml, Visual Studio nous présente un éditeur

graphique où nous pouvons, à notre convenance, créer des entités depuis la base de données ou de

toutes pièces pour les ajouter dans le futur. Créer une classe entité depuis la base de données, cela

signifie faire du « glisser-déplacer » depuis l’explorateur de serveurs vers l’espace d’édition

graphique. Toutes les métadonnées sont automatiquement extraites depuis les tables concernées,

mettant directement à jour le fichier Dbml. Ajoutons deux tables à celui-ci. Si nous consultons le

fichier Dbml, nous pouvons y trouver désormais la définition d’une classe nommée

MonMappingDataContext héritant de DataContext et des deux classes entités que nous avons créées

par « glisser-déplacer ». La génération des informations de mapping ainsi que celle de notre classe

héritant de DataContext. Voici pourquoi nous avions dit plus haut qu’en pratique c’est une classe

dérivée qui est utilisée. Cela nous permet également de définir plusieurs contextes et mappings

différents si tel est notre bon vouloir. De retour à notre exemple, nous pouvons à présent écrire des

requêtes pour nos nouvelles classes entités, le compilateur aura à sa disposition toutes les

informations nécessaires pour juger de la validité syntaxique de cette requête. C’est ce que nous

allons faire.

MonMappingDataContext db = new MonMappingDataContext();

var query = from c in db.demo_customer

//clause where eventuelle

select c.customer_name;

Console.WriteLine("Voici la table demo_customer :");

foreach (var q in query)

{

Console.WriteLine(q);

}

En exécutant ce code sur le mapping considéré précédemment (nommé MonMapping), nous

retrouvons notre DataContext ainsi que la structure des expressions de requêtes. Notons la simplicité

d’utilisation : le mapping a été créé de manière presqu’entièrement automatique, une ligne pour

instancier notre DataContext, deux lignes pour notre requête et l’affaire est faite. Nous avons par

ailleurs la sécurité de la validation par le compilateur, aucun code « unchecked » n’est utilisé.

Procédons maintenant à une insertion et à une suppression. Le code correspondant à ces opérations

est le suivant :

Console.WriteLine("Ajoutons un nouveau client avec Linq");

demo_customer dc = new demo_customer();

dc.customer_name = "nouveau client";

db.demo_customer.InsertOnSubmit(dc);

db.SubmitChanges();

Console.ReadLine();

Console.WriteLine("Voici la nouvelle table demo_customer :");

foreach (var q2 in query)

{

Console.WriteLine(q2);

}

Console.WriteLine("Supprimons le nouvel ajout");

db.demo_customer.DeleteOnSubmit(dc);

db.SubmitChanges();

Console.ReadLine();

Console.WriteLine("Regardons a nouveau la table demo_customer :");

var query2 = from c in db.demo_customer

select new { c.customer_id, c.customer_name };

foreach (var q3 in query2)

{

Console.WriteLine(q3);

}

Console.ReadLine();

Et le résultat de l’ensemble de ce code est le suivant :

Tout cela paraît bien simple et automatisation rime souvent avec perte de contrôle sur les

mécanismes sous-jacents. En effet, revenons à nos hypothèses de départ, à savoir que nous ayons

accès à une base de données Sql Server 2008 pleinement opérationnelle et que nous ne nous

préoccupions pas des accès concurrentiels. Dans la pratique, il est tout à fait possible que nous ayons

affaire à une base de données dont la structure soit connue mais sans l’avoir à disposition. De même,

nous serions bien imprudents en affirmant qu’un projet quelconque utilisera une base de données

Sql Server 2008. Bon, et qu’est-ce que cela change pour nous si nous n’avons pas la base de données

sous la main ? Et bien, l’extraction automatique des métadonnées se glisse hors de notre portée et

nous devrons trouver un moyen d’obtenir la structure de la base de données. Rappelons que le

Figure 6 : requêtes Linq to Sql

mapping peut-être effectué par un fichier Dbml ou Xml, qui sont globalement des fichiers textes et

donc, faciles à échanger via le réseau car de taille modeste. Visual Studio 2008 offre la possibilité

d’extraire facilement un fichier Dbml depuis une base de données, ne reste alors plus qu’à le rendre

disponible via un service web ou un procédé semblable. D’accord, mais que se passera-t-il si notre

base de données n’est pas (ou n’est plus) du type Sql Server 2008 ? La réponse va paraître brutale,

cela ne marchera tout simplement pas, Linq to Sql est prévu pour être utilisé uniquement avec Sql

Server. Attention, il est question ici de « Linq to Sql » et rappelons nous qu’il ne s’agit que d’une

implémentation Linq et non de « Linq vers relationnel » dans son ensemble. Plus qu’un tour de

passe-passe linguistique, il s’agit de deux choses différentes. Linq to Sql peut être vu comme un

raccourci simplifié pour les utilisateurs de Sql Server alors que les autres variantes de Linq offrent

d’autres possibilités pour relier le monde relationnel et celui des objets. Nous détaillerons ces autres

possibilités dans les sections qui leur sont dédiées.

4.2.4 Erreurs et difficultés

Pour terminer l’analyse pratique de Linq to Sql, nous allons envisager deux cas plus complexes mais

néanmoins tout à fait plausibles. Tout d’abord le cas où nous n’avons tout simplement pas de base

de données, il arrive pour certains projets de devoir créer leur base de données « from scratch ».

Nous allons voir quelle aide Linq peut apporter dans ce contexte et quelles en sont les inconvénients.

Nous allons ensuite examiner la gestion des accès concurrentiels et les diverses erreurs considérées

comme typiques. Cette sous-section est tirée de [1], plus particulièrement du chapitre 5 : Managing

Sql Data.

Dans le cas où nous n’avons pas encore de base de données, nous pouvons soit la créer de manière

classique, soit utiliser Linq pour le faire. Rappelons-nous lors de notre premier test, nous avions à

notre disposition une boîte à outil fournie par Visual Studio. Cette boîte à outil porte en réalité le

nom de « Object Relational Designer » ou Concepteur Objet Relationnel pour rester dans la langue

de Molière. Ce concepteur nous permet de définir de nouvelles entités, sans nous préoccuper des

données relationnelles existantes (c’est-à-dire que cette possibilité est disponible même si la

génération des entités depuis une base de données a été utilisée). Ces entités ne correspondant à

aucune table relationnelle, elles sont inutilisables telles quelles. Néanmoins une instance d’un

DataContext peut créer une base de données depuis son schéma de mapping en faisant appel à la

fonction CreateDatabase définie pour la classe DataContext. L’ouvrage [1] nous met en garde contre

une telle pratique, précisant que ce n’est à utiliser que lorsque les classes entités sont jugées plus

importantes que la structure relationnelle. En effet, le passage d’une base de données à un fichier

Dbml n’est pas parfaitement bidirectionnel et certaines opérations ne sont pas possibles comme par

exemple la création de procédures stockées ou de triggers. Autrement dit, nous pouvons utiliser

cette méthode lorsque la base de données est utiliser pour servir l’application et non le contraire,

auquel cas nous devrions créer notre base de données avec du code Sql. Cela reste très intéressant

car nous pouvons désormais réaliser la persistance d’objets sans devoir écrire la moindre ligne de

code Sql et nous pouvons profiter de la validation des requêtes à la compilation, ce qui simplifie les

tests et réduit le temps de développement.

Les bases de données sont énormément utilisées dans le monde du web. Cet environnement à la

particularité de pouvoir provoquer des accès concurrentiels n’importe quand. Profitons de passage

pour préciser ce que nous entendons par « accès concurrentiels ». Nous allons parler ici d’accès

concurrentiels pour désigner les problématiques différentes que sont les opérations concourantes

sur des données (typiquement demande de lecture d’une donnée en cours de modification ou à

modifier), les transactions (avec mécanismes de roll-back si satisfaire la transaction s’avère

impossible) et la gestion des exceptions Sql [12]. Dans ces trois domaines, la presque totalité des

problèmes pouvant survenir sont découverts à l’exécution, ce qui est toujours gênant lorsqu’on

souhaite offrir un accès en ligne aux ressources. Linq manipule les objets entités en mémoire et donc

crée un délai supplémentaire durant lequel des accès concurrentiels peuvent apparaître. Un objet

entité contient plusieurs indicateurs permettant de savoir s’il correspond ou non à son homologue en

base de données et le DataContext peut donc détecter lorsqu’un conflit survient. Il est possible

d’assigner une politique de gestion de conflits plus ou moins fine selon ce qui est désiré. Nous

n’allons pas trop nous étendre là-dessus, les heuristiques de résolution de tels conflits dépassent

largement le cadre de ce travail. Nous nous contenterons de dire que les opérations concourantes

ont été envisagées et que le DataContext est en mesure de réaliser une gestion de conflits assez

performantes pour peu qu’une politique adaptée lui soit fournie. Les informations de

synchronisations jouent également un rôle important à ce niveau. En effet, lorsque le comportement

est laissé par défaut (il s’agit du IsDbGenerated), le DataContext ira régulièrement s’informer auprès

de la base de données pour s’enquérir d’éventuels changements survenus pour chaque attribut

ayant ce comportement. Le comportement inverse est également possible. Lorsqu’une entité doit

modifier l’une de ses propriétés, elle en avertit le DataContext et celui-ci va marquer l’entité avec le

statut « modifié » le temps de répercuter les changements dans la base de données. Cela nous laisse

déjà entrevoir les possibilités de conflits engendrés par des opérations concourantes. Microsoft, par

le biais de la bibliothèque en ligne MSDN [4], propose plusieurs politiques de gestion de conflits mais,

comme dit plus haut, tout cela dépasse le cadre de notre étude.

Les transactions sont utilisées systématiquement par les objets de type DataContext. Cela n’a rien

d’étrange quand nous avons vu que le DataContext est l’unique lien entre la base de données et

l’ensemble des entités qui lui sont associées. Lorsque le DataContext se voit soumettre une ou

plusieurs requêtes, il effectue en priorité les changements sur les entités en mémoire et va, selon la

disponibilité de la connexion à la base de données, soumettre à son tour les changements à la base

de données. Chaque requête soumise au DataContext est encapsulée dans une transaction au sens

relationnel du terme. Il est néanmoins possible pour le programmeur d’élargir une transaction, en y

ajoutant des modifications. La classe TransactionScope (disponible dans la bibliothèque

System.Transactions) permet cette opération le plus naturellement qui soit [4]. Il suffit d’instancier

un nouvel objet TransactionScope sans paramètre et d’y inclure l’ensemble des opérations

souhaitées, comme le montre le morceau de code suivant :

using (TransactionScope ts = new TransactionScope())

{

monDataContext.SubmitChanges();

//Soumission de l'ensemble des requêtes en attente

//... code supplémentaire éventuel

ts.Complete();

}

Nous avons encore à aborder le problème des exceptions. Les auteurs de « Programming Linq » [1]

nous présentent trois sources principales d’exceptions, la création d’un DataContext, les accès à une

base données en lecture et ceux en écriture. Notons que Linq ne possède pas d’exceptions qui lui

sont propres. La plupart des exceptions classiques peuvent être rencontrées en utilisant « Linq to

Sql » néanmoins nous n’allons détailler que celles qui sont typiques de son utilisation.

Les erreurs susceptibles d’apparaître lors de la création d’un DataContext sont du type

InvalidOperationException et indiquent un problème au niveau du mapping. De par leur nature, ces

exceptions « sont considérées comme étant irrécupérables ( « unrecoverable » ) la plupart du

temps » [1]. Ce genre d’erreurs est « très peu probable si le mapping est généré automatiquement »

[1].

Lors de l’accès en lecture à une base de données, plusieurs types de problèmes peuvent survenir :

l’accès peut être impossible, refusé, la structure des tables peut être différente de ce qui était prévu

et ainsi de suite avec la presque totalité des problèmes pouvant survenir lors de l’accès à une base de

données relationnelle. Le principal problème avec l’accès en lecture est qu’il est impossible de

prévoir quand il aura lieu exactement. En effet, nous avions vu dans les précédentes sections que

Linq to Sql ne permettait de parler directement qu’au DataContext (c’est-à-dire aux représentations

en mémoire que sont les entités) et jamais à la base de données. Et pire encore, le DataContext va

lui-même effectuer le travail en arrière-plan (groupement de requêtes et analyses pour maintenir la

synchronisation principalement) responsable d’avoir déclenché l’exception sans que vous puissiez

avoir directement connaissance de la nature de ces menues opérations ni du moment précis où elles

auront lieu. En imaginant une application multi tiers, les causes d’erreur sur les accès à la base de

données peuvent se retrouvés en de nombreux points (nous parlons seulement ici des accès en

lecture, rappelons-le). Nous pourrons toutefois nous consoler en constatant que la plupart des

erreurs courantes auront été détectées dès la compilation et la syntaxe Sql, véritable nid à problèmes

pour le développeur orienté objet, est l’affaire du DataContext et non celle du programmeur

désormais.

Les accès en écriture ont lieu quant à eux à des moments relativement contrôlés. A ces instants, le

DataContext répercute toutes les modifications apportées aux entités sur la base de données. Du fait

de cette écriture décalée et d’éventuelles opérations concourantes, nous devons envisager la

possibilité que des opérations inattendues aient lieu à chaque écriture. Ceci veut dire qu’une écriture

est susceptible de déclencher n’importe quel type d’exception Sql [11][1][4]. Ceci rejoint grandement

le problème des opérations concourantes et de la mise en place d’une gestion de conflits. Nous

devions cependant souligner le fait que d’autres erreurs peuvent survenir lors de la phase d’écriture.

C’est le cas des violations de contraintes d’intégrité, des dépassements de délais lors de l’attente

d’une réponse et ainsi de suite, la liste étant longue. Linq to Sql ne propose aucun remède miracle

contre l’apparition de ces exceptions et elles doivent être gérées comme dans le cadre d’un accès

relationnel classique.

4.2.5 Performances

Les questions de performance sont toujours à garder à l’œil lorsque mapping il y a. En effet, le

mapping n’est jamais qu’une ou plusieurs couches insérées dans une pile parfois déjà conséquente.

La performance n’est pas toujours facile à cerner, en revanche. Nous pouvons d’ores et déjà parler

de performances exprimées en temps développeurs car nous avons vu ce qu’il en était de la syntaxe

et de la simplicité de mise en œuvre. Le travail est plus rapide et moins porteur d’erreurs ([1] utilise

les termes de « less error prone » pour désigner le développement avec Linq to Sql). Si le temps de

mise en œuvre est réduit par rapport à une approche ADO.NET qu’en est-il de la qualité ? Les codes

auto-générés sont souvent pointés du doigt dans ce domaine. Et les temps de réponses, seront-ils

satisfaisant ? Nous allons réaliser plusieurs tests afin de nous forger notre propre opinion.

4.2.5.1 Test d’insertions

Nous allons ici envisager un scénario très simple, à savoir une succession d’insertions dans une table

relationnelle vierge. Nous mettrons en compétition une version Linq to Sql avec une version

ADO.NET. Les requêtes seront groupées ainsi que le ferait une application avec base de données

locale, dans un but de réduction du nombre d’accès disques. La situation est exprimée ci-dessous en

pseudo code :

DateTime star t = now ; //start pour le test 1

for(int i=1 ; i < nbrOps ; ++i)

{

requete.ajout(insertion unitaire);

}

requete.Execute() ;

DateTime end = now ; //Ecart = end – start

Les codes utilisés lors de ce test sont disponibles en annexe (Annexe 2 : codes de tests de

performances pour Linq to Sql). Après plusieurs exécutions de la version Linq pour un même nombre

d’opérations, un phénomène curieux apparaît. Cette version est plus lente à la première exécution

du test, et les temps de réponses se stabilisent dès la deuxième exécution du test. C’est assez

inattendu et rien, à ce stade de notre étude, ne semble pouvoir justifier cela. [1] nous apprend que le

DataContext rassemble toutes les informations nécessaires au mapping lors de sa première

utilisation. Le mapping est gardé en mémoire jusqu’à son remplacement par un autre mapping ou

jusqu’à la fermeture de Visual Studio. Le gain de temps observé varie entre quelques dixièmes à deux

secondes, quel que soit le nombre d’opérations demandées par la suite. Pour revenir à notre test,

nous pouvons désormais comparer la version ADO.NET aux deux temps de réponse de la version

Linq. De manière surprenante, la version Linq a été observée comme étant plus rapide pour ses deux

temps de réponse. Ceci semble aller contre toute la logique du mapping et en réalité, c’est là qu’un

examen plus approfondi du code permettra de dénicher l’astuce. Pour ce premier test, la version

ADO.NET a été construite de manière très naïve. La requête étant représenté par un string, celui-ci

est modifié à chaque tour de boucle pour représenter une requête de plus en plus longue. La

manipulation de ce string en mémoire va entraîner un nombre d’opérations responsables de

l’accumulation de ce retard. Notons déjà ici une des forces de Linq to Sql qui est de réaliser

l’optimisation pour le programmeur alors que la version ADO.NET peut fonctionner en étant très mal

programmée. En ne considérant que les temps d’accès à la base de données, la version ADO.NET

s’est montrée plus rapide, ce qui était beaucoup plus prévisible. Le temps d’exécution nécessaire

varie presque du simple au double entre les deux approches dans ces conditions. Ces temps varient

linéairement avec le nombre d’opérations à effectuer. L’ergonomie et la productivité du développeur

Linq se paient par une hausse conséquente des temps d’accès (ceci est confirmé par [11] et [1]). Le

graphique ci-dessous montre les résultats moyens observés durant les deux précédents tests. L’axe

des ordonnées représente les temps moyen d’exécution exprimés en millisecondes, alors que les

abscisses représentent le nombre d’opérations d’insertion successives effectuées durant le test. Les

courbes suivies de la mention « (with Q) » désignent les résultats du premier test, où le temps

d’exécution comprend également le temps nécessaire à la construction de la requête. Pour les

courbes qui ne sont pas suivies par cette mention, il s’agit des résultats obtenus lors du second test

où le temps d’exécution considéré ne comprend que le temps nécessaire à la connexion directe à la

base de données. Voici la situation exprimée en pseudo code :

DateTime star t = now ; //start pour le test 1

for(int i=1 ; i < nbrOps ; ++i)

{

requete.ajout(insertion unitaire);

}

DateTime start2 = now ; //start pour le test 2

requete.Execute() ;

DateTime end = now ; //Ecart = end – start

Figure 7 : test d'insertions (temps de réponse (ms) en ordonnées, nbr d'insertions en abscisse)

4.2.5.2 Test en situation concurrentielle

Le test suivant porte sur un contexte où les connexions sont nombreuses et indépendantes. Pour

simuler cette situation, nous allons simplement imposer au programme de ne considérer que des

insertions simples directement transmises à la base de données. Comme précédemment, un grand

nombre d’insertions seront effectuées mais cette fois elles seront transmises directement à la base

de données sans regroupement. Bien qu’il ne s’agisse pas d’un scénario vraiment réaliste, cela suffira

pour se faire une idée des performances relatives de Linq et d’une connexion plus traditionnelle.

Le graphique qui suit ce paragraphe montre les temps nécessaires (exprimés en millisecondes) à

l’envoi de toutes les requêtes une à une pour les deux méthodes, le nombre de requêtes de chaque

lot étant repris en ordonnée. On voit clairement que la courbe intitulée « Linq sequence» croît

exponentiellement avec l’augmentation du nombre de requêtes à satisfaire. L’explication principale

en est que Linq doit manipuler des objets en mémoire comme nous l’avons vu dans la section

« Opérations fondamentales » de ce chapitre, dernier paragraphe. Chaque ajout créé un objet

supplémentaire et cette modification a lieu en mémoire centrale, ce qui sera fait beaucoup plus

rapidement que l’envoi de la requête à la base de données. Chaque soumission entraîne l’analyse du

marquage de chaque objet, ce qui ralentit l’ensemble du programme. La méthode ADO.NET,

représentée par la courbe « ADO sequence », ne manipule pas d’autres objets que les strings utilisés

pour construire les requêtes, celles-ci étant ensuite transmises au gestionnaire de la base de

données. Les temps d’accès varient donc selon une progression linéaire. La troisième courbe

présente sur le graphe représente les temps d’accès correspondant pour une utilisation idéalisée de

Linq. Utilisation idéalisée car l’envoi des requêtes n’est fait qu’au dernier moment, économisant ainsi

sur les temps d’analyse de marquages par la sentinelle. L’appel à la fonction SubmitChanges réalise

l’appel à la base de données et toutes les entités marquées comme étant « à insérer » vont générer

une requête Sql correspondant à leur insertion. En n’effectuant cette tâche qu’une seule fois, on

aperçoit que le gain de temps est conséquent10. Dans la réalité pratique, on pourrait regrouper

plusieurs requêtes par petits lots afin d’économiser un peu sur les accès successifs à la base de

données. Mais un appel unique est impensable, du fait que « la fin » est impossible à prévoir en

pratique ainsi qu’à cause des problèmes d’accès concurrentiels aux données. Les possibilités

d’optimiser Linq relèvent principalement de la gestion des conflits, de la synchronisation et du design

de l’application. Les performances en situation réelles sont certainement moins bonnes que le cas

idéal mais vraisemblablement plus proches de cette valeur que de celle de la version naïve.

10

Cette observation semble n’apparaître nulle part, *4+ le laisse tout juste sous-entendre.

Figure 8 : tests en situation concurrentielle (temps de réponse (ms) en ordonnées, nbr d'insertions en abscisse)

4.2.6 Conclusion

Linq to Sql effectue la correspondance entre objets et tables relationnelles avec des fichiers

spécifiques. Nous avons vu comment ces fichiers peuvent être automatiquement générés, délivrant

le développeur de la tâche ardue qu’est la réalisation d’un mapping objet relationnel. Les

mécanismes de gestion et de modifications des entités sont intuitifs et rapidement mis en œuvre,

comme nous l’avons fait avec un exemple concret. Plusieurs pièges restent tendus, guettant le

développeur inattentif mais ceux-ci sont nettement moins nombreux que dans un contexte d’accès

aux données en Sql. La plupart du code est vérifié à la compilation et un système de gestion de

conflits diminuera encore davantage les risques liés aux accès concurrentiels. En ce qui concerne la

performance, Linq s’est montré plus lent qu’une approche ADO.NET mais tout en restant dans des

limites acceptables. Le seul dommage semble être que Linq to Sql n’est en réalité que Linq to Sql

Server, ce qui limite fortement son champ d’application11.

11

Cette information est relativement dissimulée. C’est par le réseau développez.com que j’en ai eu connaissance *13+, ce n’est apparu dans *1+ qu’au 12

e chapitre !

4.3 Linq to DataSet Un DataSet est quelque chose de très proche de la notion des classes entités vues précédemment. Il

s’agit d’une représentation en mémoire d’une collection de données. Précisons qu’un DataSet est

une représentation générique puisque la collection de données est une représentation d’une source

de données que cette dernière soit d’origine relationnelle ou non. Les DataSets font partie de la

couche de données ADO.NET et n’ont donc pas été introduits par Linq. La couche d’accès aux

données ADO.NET fait un grand usage des DataSets, notamment en lecture pour conserver des

informations de structure liées aux données. Il est assez logique que Linq propose une

implémentation destinée à interroger ces structures. Nous allons poursuivre notre étude avec

l’analyse cette implémentation nommée Linq to DataSet. Cette section est grandement inspirée de

[1] et de [4]. Comme son nom l’indique, cette implémentation a pour cibles des objets de type

DataSet. Cette section ne sera pas découpée en sous-parties car Linq n’introduit que peu de

nouveautés en ce qui concerne les DataSets. L’étude approfondie des mécanismes sous-jacents aux

DataSets en général n’entre pas dans le cadre de ce travail et nous les passerons sous silence. Pour

pouvoir être utilisable par une implémentation Linq, il est nécessaire d’implémenter l’interface

IEnumerable<T>. Sans nous étendre sur le sujet, signalons qu’il y a moyen de créer des DataSets

typés mais en toute généralité un DataSet doit être considéré comme non typé et n’implémente

donc pas la fameuse interface. Nous ne considèrerons ici que les DataSets non typés et nous

tenterons d’éclaircir comment les utiliser en tant que IEnumerable<T>.

Etant un concept abstrait, les DataSets peuvent être construits depuis n’importe quel type de base

de données. Ils disposent en outre de mécanismes pour recréer la structure des tables relationnelles

sous-jacentes. Un mapping ? En effet, cela y ressemble fortement, mais celui-ci doit être

explicitement spécifié « à la main » par le développeur. Pour interroger de telles structures, Linq a

réalisé une implémentation spécifique appelée « Linq to DataSet ». Ceci permet de bénéficier de

toutes les améliorations apportées par Linq sur des structures pouvant s’interfacer sur n’importe

quel type de base de données. Ceci n’est en aucun cas le remède universel au vide laissé par Linq to

Sql puisque ce ne sont que les DataSets qui sont interrogeables. Leur construction ainsi que le suivi

des modifications vers la base de données sont encore à faire. Un DataSet représentant une cache de

base de données est classiquement construit à partir d’un DataAdapter (c’est-à-dire en utilisant la

couche ADO.NET). A noter qu’il est possible d’utiliser Linq pour remplir un DataSet sans passer par un

DataAdapter, mais cela suppose d’avoir préalablement récupéré les données et de les avoir sous

forme IEnumerable<T>.

Pour pouvoir utiliser Linq sur une table d’un DataSet, il faut donc explicitement créer un mapping

avec la fonction TableMappings.Add(string sourceTable, string DataSetTable) du DataSet. Pour que

cette table soit énumérable, il faut utiliser l’opérateur AsEnumerable qui sera, dans ce cas-ci, utilisé

sur une DataTable. Voyons un exemple utilisant une base de données MySql (sa procédure de

construction est détaillée dans l’annexe 3 : utilisation d’une base de données MySql avec Linq to

DataSet).

using MySql.Data;

using MySql.Data.MySqlClient;

class Program

{

static void Main(string[] args)

{

MySqlConnection con = new MySqlConnection

("Database=MyLinqSql;Uid='root'");

con.Open();

DataSet ds = new DataSet("MySqlDataSet");

string selectString = @"SELECT * FROM objects1";

MySqlDataAdapter da = new

MySqlDataAdapter(selectString,con);

da.TableMappings.Add("objects1", "Table");

da.Fill(ds);

//interrogation du DataSet ds avec Linq to DataSet

DataTable dt1 = ds.Tables["Table"];

var dataSetQuery = from o in dt1.AsEnumerable()

select new {ID = o.Field<int>("Id"), TEXT =

o.Field<string>("Desc")};

foreach(var res in dataSetQuery)

{

Console.WriteLine(res);

}

//Fermeture de la connexion

con.Close();

Console.WriteLine("Appuyez sur Enter pour terminer...");

Console.ReadLine();

}

}

Le résultat de ce code, pour la base de données considérée est le suivant :

Figure 9 : Linq to DataSet pour interroger un DataSet construit avec MySql

Revenons maintenant sur les particularités de ce code. Nous avons bien créé une connexion MySql

que nous ouvrons et nous instancions un nouveau DataSet nommé MySqlDataSet. Nous créé un

nouveau DataAdapter avec l’instruction de sélectionner toutes les lignes de la table objects1. Un

mapping est ensuite créé pour pouvoir retrouver dans le DataSet la table équivalente à objects1 sous

le nom de Table. Le DataSet est rempli et nous définissons une nouvelle DataTable avec Table. Vient

ensuite notre requête qui a toutes les apparences d’une requête Linq qui devrait maintenant nous

être familière. Néanmoins, la référence à la table utilise l’opérateur AsEnumerable car une DataTable

n’implémente pas IEnumerable<T>, comme nous l’avions dit plus haut. Mais nous voyons également

que les attributs de cette table doivent être accédés par la méthode Field<T> en précisant

explicitement la nature de T, car le compilateur ne pourrait réaliser l’inférence depuis un champ

d’une DataTable. Ceci n’est simplement pas prévu. Peut-être cela changera-t-il dans le futur, mais

pour l’heure nous devons nous-mêmes fournir les informations qui sont en réalité des informations

de mapping encore une fois. Pour en revenir à l’opérateur AsEnumerable, il s’agit d’un opérateur

réalisant la conversion à la volée. Il s’applique à une séquence d’éléments et va les encapsuler dans

une nouvelle séquence d’objets implémentant IEnumerable<T>. Cet opérateur peut être redéfini,

offrant ainsi la possibilité de créer des variantes adaptées au contexte.

En ce qui concerne les performances, la question est de mettre en balance ce que Linq apporte et les

délais introduits. Le mapping est réalisé par le DataSet lui-même et des instructions explicites au

niveau du code. Aucune logique de mapping automatique ne vient alourdir l’usage classique d’un

DataSet. Les performances en termes de temps de réponses seront fonction du contexte et non du

choix d’avoir ou non utilisé Linq. Notons tout de même que l’utilisation de telles structures n’est pas

toujours aisée et en particulier traduire une requête en langage naturel devrait être beaucoup plus

simple en utilisant Linq.

Avec Linq to DataSet, nous avons une solution portable vers tout type de base de données et même

vers d’autres systèmes de persistance car, rappelons-le, un DataSet représente simplement une

collection de données. Cela permet d’utiliser le confort lié aux requêtes de Linq, certes, mais Linq to

DataSet demande pas mal de réglages de la part du développeur, notamment d’assurer le suivi entre

le DataSet et la base de données. Il s’agit là d’un inconvénient majeur puisque cela peut amener des

problèmes liés à la synchronisation ainsi que toute la panoplie d’erreurs classiques liées à l’accès aux

données en Sql. Ceci ne constitue pas alternative à Linq to Sql mais plutôt comme un complément

pour répondre aux besoins des développeurs ayant choisi de manipuler des DataSets ou ayant à

maintenir des applications qui en font toujours usage. [4] maintient que « les DataSets sont une

composante importante de la couche ADO.NET », ce qui fait de Linq to DataSet un outil appréciable.

4.4 Linq to Entities Une autre implémentation ayant pour cible des données relationnelles s’appelle Linq to Entities. Le

terme Entities dans son nom fait référence à la technologie Entity Framework, qui est une évolution

de la couche ADO.NET que nous appellerons par raccourci EF. EF est légèrement plus récent encore

que Linq. Nous allons brièvement introduire les idées fondatrices de cette technologie ainsi que ses

principaux concepts. Nous tenterons ensuite de mettre en lumière les forces et faiblesses de ce

framework, tout en particularisant cela aux contributions de Linq to Entities. Cette section puise ses

informations principalement de [5] pour EF et de [1] pour Linq to Entities.

4.4.1 Concepts fondateurs

L’idée au départ de l’EF est celle-là même qui a amené Linq. Nous pourrions résumer cette idée de

base en « une application orientée objet ne devrait pas dépendre des mécanismes de persistance ».

Nous n’allons pas expliquer à nouveau le problème de correspondance entre l’orienté objet et le

relationnel mais nous allons plutôt ce problème dans le contexte de la deuxième édition du

framework .NET. A cette époque pas si lointaine d’ailleurs, les accès relationnels se faisaient en

utilisant les services de la couche ADO.NET, elle-même une évolution des premiers services ADO

(ADO signifie ActiveX Data Object). Cette nouvelle couche apportait son lot de nouveautés, parmi

lesquelles l’interface IDbConnection, qui permettait de considérer une connexion générique à une

base de données. Chaque fournisseur de données (il faut comprendre par là tant MySql que Oracle

ou Sql Server) implémentait cette interface au sein d’une classe concrète nommée client

(MySqlClient, OracleClient …). De même, la lecture des données se faisait en remplissant un DataSet

pouvant lui-même garder une structure très proche des tables relationnelles de la source. Pour en

revenir à l’idée énoncée plus haut, une application de l’époque dépendait de la nature de la source

de données en ce qui concernait la construction du DataSet et l’instanciation de la connexion. La

dépendance envers la source en elle-même et non son type est localisée au niveau des requêtes Sql

qui imposent de nommer les tables et attributs relationnels. Un changement dans la structure des

tables ou dans le type de la base de données imposent de réécrire tout le code responsable de la

connexion à la base de données et de la construction du DataSet. Selon les cas, il pouvait être

nécessaire de réécrire également certaines requêtes, en particulier lorsque les dialectes Sql parlés

par l’ancienne et la nouvelle source ne coïncidaient pas. Reprenons l’idée de départ mais

transformons-la en une nouvelle formulation dont le sens global sera identique à celui de la

première : « Une application orientée objet devrait pouvoir utiliser la logique conceptuelle des

données en faisant abstraction de son mode de stockage ». Avec la version 2 du framework, il n’est

pas possible d’utiliser la logique telle qu’elle serait définie dans un modèle conceptuel entités-

relations, nous sommes condamnés à utiliser les tables telles que définies dans la base de données.

De même, il n’est pas possible de vérifier entièrement la logique dès que celle-ci interagit un temps

soit peu avec du code Sql. L’Entity Framework a été créé pour résoudre ces problèmes et se présente

comme une couche ajoutée par-dessus les mécanismes ADO.NET classiques. Manipuler des entités

conceptuelles sans directement agir sur la base de données, nous pouvons en effet comprendre que

ce concept est similaire à celui de Linq. Là où Linq propose une syntaxe unifiée pour les requêtes, EF

propose un accès conceptuel aux données défini indépendamment de la base de données.

4.4.2 Fonctionnement

Commençons par préciser quelques termes de vocabulaires et mettons-les en rapport avec la réalité

concrète du moins aussi concrète que peut être la réalité des accès aux données. Sans entrer dans

toutes les spécificités techniques, nous tenterons de mettre en lumière qui sont les principaux

acteurs de cet Entity Framework et quel est leur rôle.

Service objet est le terme officiel utiliser par Microsoft [5][1] pour décrire l’action d’EF. Les données

relationnelles n’ont pas changé, elles sont rendues disponibles en tant qu’objets. C’est ainsi que peut

être perçu le concept de service objet. Il est important de comprendre qu’il s’agit bien d’un service et

donc d’une couche supplémentaire posée sur la pile des mécanismes ADO.NET.

Une entité au sens d’EF est un concept décrit dans une modélisation entités-relations. Microsoft les

décrit parfois sous le nom de data object, littéralement objet donnée. Il s’agit donc de classes qui

vont jouer un rôle similaire à celui des entités au sens de Linq. Les entités d’EF regroupent des

données pouvant se retrouver sur plusieurs tables relationnelles et en termes relationnels, ce qui

approchent le plus de ces entités, ce sont les vues. Il est important de noter que les entités sont

mappées aux données relationnelles mais cette correspondance n’associe aucune ressource

directement aux entités. Cela pourrait se résumer de cette manière : les entités sont les objets

métiers qui décrivent la logique de l’application et lorsqu’on en vient à parler de la persistance, le

mapping permet aux entités de savoir où aller regarder.

Une relation est un lien entre deux entités ou plus. Il s’agit là des mêmes concepts que ceux présents

dans le fameux schéma entités-relations. Une relation peut être de diverses multiplicités : un à un, un

à plusieurs ou plusieurs à plusieurs. Une relation peut elle-même posséder des attributs et est donc

représentée par une classe.

Un objet contexte, ou context object en Anglais, est le terme pour désigner la classe en charge de la

gestion du modèle. La classe ou plutôt un de ses objets. Ce rôle s’apparente fort à celui du

DataContext de Linq to Sql et cet objet contexte gère à la fois le mapping et les instanciations

d’entités et de relations. Un seul objet contexte peut exister pour un modèle donné. Il intercepte

toutes les demandes, qu’elles soient faites aux entités, aux relations ou au modèle lui-même. L’objet

contexte est le point d’entrée de la couche des services objets (c’est ainsi qu’il est présenté dans la

documentation officielle fournie par Microsoft [5]).

Le modèle de données, appelé Entity Data Model ou EDM par Microsoft, regroupe les entités et leurs

relations. C’est en quelque sorte un schéma entités-relations mais les données peuvent y être

spécifiées déjà finement, notamment au niveau des types. Ce modèle peut être créé depuis une base

de données disposant d’un schéma entités-relations. Il peut également être créé à la main ou depuis

la structure de tables relationnelles d’une base de données. Dans tous les cas, des modifications sont

toujours possibles à postériori.

Le fonctionnement général d’EF est le suivant : l’application utilise des concepts dont la forme

correspond à la logique de l’application. Cet assemblage d’objets est manipulé directement en

mémoire et aucune interférence due à la couche relationnelle n’a lieu à ce niveau. Il n’y a que les

entités et relations ayant un rôle à jouer qui sont instanciées, les autres disparaissent dès leur rôle

terminé. L’objet contexte ne maintient que le minimum d’objets nécessaires. Lorsque ceux-ci

nécessitent un rafraîchissement ou lorsqu’ils font l’objet d’une demande d’écriture, l’objet contexte

va utiliser les informations de mapping à sa disposition pour créer les commandes Sql appropriées.

Sql étant devenu standard (ou presque), la seule question délicate est de gérer l’accès direct aux

données. Là et seulement là, le mapping contient des informations qui seront dépendantes de la

nature de la base de données. Ces informations sont utilisées pour créer des connexions et des

commandes avec la couche ADO.NET classique, c’est-à-dire telle qu’elle permet déjà de le faire

depuis la version 2 du framework .NET. Un changement du mode de stockage des données impose

de seulement changer quelques lignes au niveau du mapping. Les informations de structure pouvant

être utilisée pour recréer des tables relationnelles en correspondance avec le mapping actuel. Voici

le principe de l’Entity Framework.

4.4.3 Mapping

Le mapping est réalisé de manière distribuée, dans le sens où plusieurs fichiers y participent, chacun

apportant des informations différentes. Sans entrer dans les détails de leur syntaxe, un premier

fichier représente la structure des entités et relations, un deuxième fichier représente la structure

des données au niveau de la source relationnelle et le troisième fichier fait la correspondance entre

les deux [5]. Ceci se fait (presque) indépendamment de la nature de la base de données et la

structure de cette dernière peut être récupérée ou recréée à l’envie. A l’envie signifie dans la limite

où ce type de base de données est supporté par EF sans quoi ces étapes doivent être faites à la main.

Ceci est d’autant plus intéressant que le développeur n’a pratiquement aucune ligne de code à écrire

pour réaliser ce fameux mapping, un éditeur de modèle EDM graphique est fourni avec EF.

4.4.4 Rôle de Linq

En plus de tous ces mécanismes, Linq propose une implémentation ayant pour cible l’EF, nommée

Linq to Entities. L’EF disposant de son propre moteur de requêtes, Linq se présente comme une

syntaxe unifiée, qui resterait valable que l’on ait recours à l’EF ou non. Tant les requêtes Linq que

celles de l’EF utilisent les services objets, aucune des deux alternatives n’est réellement plus rapide

que l’autre12 [1][5]. Précisons toutefois que les requêtes EF sont écrites dans un langage proche du

Sql nommé Entity Sql, ce qui laisse apparaître du code non vérifiable à la compilation, ce que Linq

permet justement d’éviter. De plus, nous pouvons faire la même remarque que celle faite lorsque

nous parlions de l’implémentation objet : Linq propose un formalisme de requêtes nettement plus

lisible et plus compact.

La difficulté de la mise en œuvre d’une application Linq to Entities tient davantage à la construction

du modèle de données qu’à la mise en place des requêtes. C’est pourquoi nous ne ferons pas

mention d’exemple dans cette section, la syntaxe Linq restant inchangée. Signalons tout de même

que certains opérateurs sont à manier avec prudence, en particulier les opérateurs d’agrégation

personnalisés qui peuvent ne pas être supportés, en toute généralité, par le gestionnaire de bases de

données.

4.5 Db Linq Il s’agit ici d’une implémentation particulière, dans le sens où elle n’émane pas directement de

Microsoft mais d’une équipe mixte, regroupant des développeurs de Microsoft et d’autres

contributeurs indépendants. Son nom est d’ailleurs toujours un nom de code et non une désignation

standard « Linq to something ». Cette implémentation, résumée en une phrase, a pour but de faire

comme Linq to Sql mais mieux que Linq to Sql. La plupart des informations la concernant sont

disponibles sur [4] mais tout est rassemblé sur le site du projet en lui-même [14]. En effet, cette

implémentation se base sur l’API de Linq to Sql mais intègre d’autres types de bases de données que

Sql Server. Citons parmi eux MySql, Oracle et PostGreSql qui sont probablement les plus célèbres.

Pas grand-chose à ajouter sur cette implémentation, si ce n’est qu’elle est toujours en

développement. La dernière release est la version 0.20 datée du 16 avril 2010, ce qui en fait la plus

récente à l’heure d’écrire ce paragraphe.

Microsoft ne semble pas vouloir intégrer cette implémentation dans les librairies standards, pour une

raison indéterminée. Même si tous les outils de génération de code disponibles pour Linq to Sql n’ont

pas d’équivalents pour Db Linq, se contraindre à Sql Server semble peu judicieux. Nous reparlerons

plus loin du projet Mono qui a choisi quant à lui la solution Db Linq [14][15].

12

Il est d’ailleurs possible d’écrire des requêtes Entity Sql avec Linq en utilisant les objets ObjectQuery<Entity>.

Chapitre 5 : Implémentation Xml Le recours à des bases de données Xml s’est fortement répandu ces dernières années et tout

naturellement Linq propose une implémentation pour interagir avec ces structures. Xml est

cependant une technologie difficile à interfacer avec les langages orientés objet, en particulier

lorsque ceux-ci désirent créer du contenu Xml. L’approche classique utilise DOM13 qui oblige à créer

l’ensemble des éléments avant de les rassembler. Linq to Xml propose une nouvelle syntaxe pour

créer du contenu Xml, la construction fonctionnelle. Elle permet de créer les éléments selon une

structure hiérarchique beaucoup plus compacte. Cette syntaxe est encore plus évidente dès lors que

nous utilisons Visual Basic 2008, car ce dernier a introduit dans sa dernière version les littéraux Xml.

Avec cela nous pouvons écrire directement le code en Xml, comme dans l’exemple ci-

dessous (inspiré de [1]) où nous créons un étudiant :

Dim exempleXml As XDocument = _

<?xml version="1.0" encoding="UTF-16" standalone="yes"?>

<etudiant matricule="023951">

<nom>Schoonenbergh</nom>

<faculte>Ecole polytechnique</faculte>

<annee>MA2</annee>

<adresse>

<rue>Kleine Daalstraat</rue>

<numero>79</numero>

<cp>1930</cp>

<localite>Zaventem</localite>

</adresse>

</etudiant>

Linq to Xml a été pensé pour manipuler du Xml tel que défini par le W3C14, c'est-à-dire pour

manipuler directement du contenu Xml sans passer par un intermédiaire. Cela permet d’interagir

avec de telles structures tout en restant dans un paradigme objet. La construction fonctionnelle est

supportée par l’assemblage System.Xml.Linq et peut être utilisée même sans recours direct à Linq.

Plusieurs classes sont définies pour représenter les éléments structurels du langage Xml, citons parmi

elles quelques exemples intéressants. La classe XObject tout d’abord qui est la classe de base pour

représenter n’importe quel objet Xml. Cette classe propose des méthodes qui représentent toute la

gestion structurelle de l’arbre Xml qui sera créé en mémoire. Des méthodes telles que AddBeforeSelf

ou AddAfterSelf permettent ainsi d’ajouter un élément à un endroit précis de l’arbre. Cette classe

supporte également l’annotation Xml. La classe XStreamingElement contient un arbre d’objets

IEnumerable<T> et peuvent donc faire l’objet de requête. Les objets de cette classe réalisent

l’exécution différée des requêtes chargeant partiellement le résultat, ce qui permet de mieux doser

la quantité d’objets chargés en mémoire, bien utile dans une optique de performance. Parlons enfin

de la classe XDocument représentant un document Xml. Les objets de cette classe n’ont pas besoin

de références à un fichier pour être instanciés. Ils sont créés selon le pattern Factory et peuvent être

associés dans le futur à une référence (ou une URI) qui les reliera directement à du contenu existant,

le cas échéant.

13

DOM signifie Document Object Model. Il s’agit d’un ensemble de règles définissant les premières structures Xml. 14

World Wide Web Consortium ; organisme de standardisation des technologies web.

Une particularité de l’implémentation Xml est que les résultats restent valables même si la source de

données est déconnectée. Ce qui veut dire que les entités présentes dans un résultat ne seront

resynchronisées que si la demande explicite leur parvient. Il est également possible d’imaginer

l’exécution d’une requête sur un document Xml particulier, récupérer ensuite le résultat et ré

exécuter la même requête sur un fichier différent (celui-ci devra vraisemblablement avoir une

structure très similaire au premier). Une autre possibilité d’utilisation de cette propriété est qu’il est

très facile d’utiliser un résultat issu d’une source de données relationnelle pour effectuer une

requête sur une structure Xml. Les entités Linq to Sql par exemple se resynchronisent

périodiquement mais ce n’est pas le cas du Xml. Les deux implémentations étant indépendantes, il

ne pose aucun problème d’utiliser l’une de ces sources de données pour supporter une requête sur

l’autre. Par exemple, nous pourrions extraire d’un fichier Xml tous les étudiants inscrits dans chaque

faculté et ensuite pour chacun de ces étudiants, faire une requête en base de données relationnelle

pour obtenir ses informations personnelles.

Chapitre 6 : En pratique

6.1 Ressenti général Après ce vaste tour d’horizon de Linq, il est temps de prendre un peu de recul et de faire le point sur

ce que nous pouvons en retirer. Qu’a apporté cette étude ? Voila une question qu’il est bon de se

poser. La découverte de Linq s’est faite depuis les bases, tout comme ce fut relaté dans ce rapport.

Nous avons introduit un ensemble de mécanismes fondateurs avant de décortiquer les différentes

implémentations proposées par Linq. L’exploration de ces diverses implémentations et la mise en

œuvre de cas concrets se sont avérés parfois surprenants, révélant des comportements inattendus

(cf tests de performance pour Linq to Sql) ou au contraire des avantages conséquents (comme pour

l’exemple de l’implémentation objet). La méthode appliquée tout au long de cette étude a été de

partir des bases théoriques en allant progressivement vers des considérations de plus en plus en

phase avec la réalité concrète. Bien qu’il soit tentant d’explorer encore et encore certaines options,

nous nous sommes efforcés de nous en tenir au sujet initial qui ne concerne que Linq. Des ouvertures

vers des technologies passionnantes comme l’Entity Framework ont été tentées mais les essais et

explications sont restés centrés sur Linq.

La plus grande difficulté rencontrée durant cette étude concerne la disponibilité de la

documentation. En effet, Linq était partie intégrante d’une solution propriétaire, une très large

majorité des informations techniques est concentrée sur le site officiel de Microsoft ou dans des

livres édités par Microsoft Press. Certains sujets sont encore mal documentés du fait de leur jeunesse

et de leurs rapides mutations. Un des problèmes rencontrés à ce sujet fut d’avoir accès à des

explications tronquées, certaines parties étant apparemment considérées comme triviales. Lorsque

ce n’est pas l’avis du lecteur, c’est rapidement problématique pour ce dernier. De même, certains

sujets très récents comme l’Entity Framework ne proposent que des informations fragmentaires et

parfois contradictoires, le plus souvent à cause de changements survenus durant la version beta non

répercutés dans la documentation. Heureusement la communauté qui soutient le développement

.NET est nombreuse et les accès en avant-première à certaines technologies encore en

développement permettent à certains spécialistes de rédiger des explications complémentaires

souvent utiles (il s’agit des sources *13+ et *6+ principalement). Ce passage est également l’occasion

de remercier tous ces contributeurs pour leur travail et leur dévouement. Proposant généralement

des informations dispersées sur des blogs ou des forums communautaires, ils sont parfois auteurs

d’une simple phrase permettant le déclic. Leurs interventions sont aussi l’occasion d’offrir un point

de vue différent de l’omniprésent Microsoft. Dans la documentation officielle, tout semble toujours

simple et harmonieux. Plusieurs retours d’expérience ont également permis d’adopter une vision

plus objective sur l’ensemble des sujets abordés.

6.2 Points forts, points faibles La première remarque qui s’impose après ce que nous avons vu, c’est de se dire que Linq permet

d’écrire du code beaucoup plus simplement. Les requêtes y sont écrites dans un langage plus proche

du langage naturel et les erreurs sont détectées à la compilation. Durant les différents tests réalisés

préalablement à l’écriture de ce rapport, le temps nécessaire à l’écriture de requêtes Sql a été

constaté comme étant entre deux et trois fois supérieur au temps requis pour produire des requêtes

Linq équivalentes (est compris dans ces mesures le temps consacré à l’écriture des requêtes et à leur

débogage). En termes de productivité, Linq constitue déjà une avancée majeure. Au-delà de ce gain

de performances humaines, Linq améliore la maintenabilité d’un code car il est plus lisible d’une part

et plus abstrait vis-à-vis des données d’autre part. En termes de performances machines, seules

certaines implémentations en pâtissent, en particulier Linq to Sql. Les autres implémentations ne

rajoutent que peu de choses entre le code et la gestion du stockage des données, ceci n’influence

donc que légèrement sur les performances. Les possibilités d’optimisation sont également plus

visibles pour Linq qu’en ce qui concerne la couche ADO.NET où une connaissance des subtilités est

souvent nécessaire. Ceci n’est nullement le fruit d’une mesure cependant, simplement une

impression personnelle partagée par divers interlocuteurs rencontrés sur des sites internet

spécialisés. Bien qu’il ne s’agisse pas d’une réelle information, cette tendance semble montrer que

l’utilisation de Linq continuera de se répandre tant qu’il sera question de requêtes sur des données

relationnelles.

Les techniques d’accès aux données relationnelles datant de la deuxième version du framework .NET

sont sans comparaison avec le confort offert par Linq. Mais si nous nous plaçons dans une optique de

continuité, l’arrivée de Linq ne signifie nullement l’abandon de la couche ADO.NET. Certes manipuler

la couche d’accès aux données signifie utiliser un autre langage avec tous les désagréments que cela

comporte, mais Linq est une alternative nouvelle. La plupart des développeurs chargés d’écrire des

requêtes ont probablement toujours une bonne connaissance du Sql et des bases de données en

général. Linq est très plaisant du point de vue du débutant mais certaines personnes décrient son

utilisation abusive, arguant du fait que son utilisation à outrance finira par provoquer l’oubli des

mécanismes subtils pour accéder aux données [13]. Ne plus être conscient de la cuisine interne

réalisée par les outils de génération de code peut certes s’avérer dangereux, mais est-ce vraiment ce

à quoi nous risquons d’arriver ? Probablement pas, car les bases de données en elles-mêmes sont

toujours la solution numéro un dans le monde de la persistance et ces bases de données se doivent

d’être gérées et maintenues par des experts. Des spécialistes du relationnels pour qui les requêtes ne

sont qu’une toute petite partie de ce que peut réaliser une base de données. Créer des procédures

stockées, des déclencheurs (triggers) et ce genre de choses, cela aura toujours son lot d’artisans

œuvrant avec des lignes de commandes bien loin de Linq et de ses interfaces graphiques.

Linq se présente comme le compagnon idéal du développeur d’application qui souhaite ne pas être

entravé par la présence des mécanismes de persistance. La couche ADO.NET restera de toute façon

utilisée en arrière-plan pour réaliser les dialogues intimes avec les bases de données. Linq est donc

probablement plus un complément qu’un concurrent pour ADO.NET. Il est néanmoins dommage que

des implémentations comme Db Linq ne soient pas plus soutenues par Microsoft, mais cela sera

peut-être amené à changer.

Si nous sortons du seul point de vue relationnel, Linq permet avant tout de manipuler des données

sans se préoccuper de savoir d’où elles viennent. C’est à la fois une force et une faiblesse. La force

est d’augmenter l’abstraction du code vis-à-vis du stockage, ce qui est vital nous ne le répèterons

jamais assez. La faiblesse est qu’il devient beaucoup plus facile de commettre des erreurs par

ignorance. Ainsi une persistance assurée par plusieurs bases de données redondantes permettra des

réponses rapides, mais un même code pourra s’avérer problématique s’il prend pour cible un unique

fichier Xml de très grande taille. Il faut rester conscient qu’utiliser Linq ne sera pas systématiquement

une bonne chose et devra toujours être attentivement examiné. Linq dans son ensemble commence

à atteindre la maturité nécessaire pour s’attirer la confiance des développeurs, son utilisation va sans

doute devenir généralisée dans le monde .NET.

6.3 Quelles alternatives Comme nous l’avions précédemment fait remarquer, le problème de la dépendance par rapport au

stockage des données n’est pas nouveau. Linq y apporte sa contribution et nous avons résumé les

principales techniques qui étaient utilisées à l’époque de la deuxième version de .NET. Mais qu’en

est-il des autres langages ? .NET n’a pas le monopole de l’orienté objet et Linq est résolument lié au

framework. Il est impensable qu’un langage orienté objet tel que Java n’ait pas développé de

solutions de son crû pour tenter de créer davantage d’abstraction vis-à-vis de la couche de

persistance des données. Nous allons maintenant nous pencher sur différentes technologies et leurs

points communs avec Linq.

6.3.1 Java

Java reste une pointure dans le domaine des langages de programmation orientés objet. En

perpétuelle concurrence avec son grand rival .NET, leurs évolutions respectives se sont toujours plus

ou moins suivies de près. Ainsi, les accès aux données relationnelles se faisaient via un pilote Odbc15

en C# 2.0 et Java 5 faisait de même avec Jdbc, pour sa part. Voyons maintenant quelles sont les

solutions actuellement proposées pour réaliser une persistance abstraite en Java.

6.3.1.1 JDO

Les paragraphes consacrés à l’étude de JDO sont presqu’entièrement inspirés de *8+ et de la

documentation qui s’y trouve. JDO est l’acronyme désignant Java Data Object. Il s’agit d’une librairie,

plus exactement d’une API, standard pour réaliser la persistance des objets. La première version de

JDO est apparue en 2002 et la seconde version est disponible depuis 2006. Comme presque tout ce

qui concerne Java, cette librairie est open source. La version 2.1 appelée aussi Apache JDO est

toujours en cours de développement à l’heure où ces lignes sont écrites. Cette librairie est décrite

comme supportant tout type de stockage et représentant les données stockées à l’aide de POJO

(Plain Old Java Object), ce qui pourrait se traduire par de bons vieux objets Java. JDO se décline en

plusieurs interfaces, comme le fait Linq, et ces implémentations permettent d’utiliser différents

stockages de données parmi lesquels des bases de données relationnelles, objet ou Xml, un système

de fichiers ou même certains stockages plus exotiques comme les bases de données Big Table de

Google16.

JDO présente la persistance du point de vue du développeur objet, de manière transparente. Pour

décrire brièvement les classes qui entrent en jeu, nous avons un gestionnaire de persistance, des

requêtes et des transactions. Le gestionnaire est responsable de la maintenance des instances de

persistance qui ne sont en fait que l’équivalent des entités en mémoire au sens de Linq. Le

gestionnaire est également chargé d’exécuter les requêtes et transactions qui lui sont passées. Le

principe est, sur le fond, très similaire à Linq to Sql : nous avons un gestionnaire de contexte qui gère

des entités persistantes. Pour interroger les sources de données, JDO a recours à un langage qui lui

est propre, le langage JDOQL. Cet acronyme est l’abréviation de Java Data Object (JDO) Query

Language et n’est pas directement lié à OQL comme nous pourrions l’imaginer. Il est intéressant de

noter que ce langage propose deux syntaxes alternatives, la syntaxe dite déclarative et celle dite du

Single String. Pour ne pas trop nous perdre dans l’analyse de toutes les possibilités syntaxiques, nous

allons simplement examiner cela avec un exemple tiré du site officiel du projet Apache JDO [8], situé

page suivante. 15

Il existe d’autres pilotes plus anciens, odbc était le dernier en date lors de la release de C# 2. 16

Ceci est mentionné à l’adresse suivante : http://db.apache.org/jdo/impls.html

Declarative JDOQL :

Query query = pm.newQuery(mydomain.Product.class,"price < limit");

query.declareParameters("double limit");

query.setOrdering("price ascending");

List results = (List)query.execute(150.00);

Single-String JDOQL :

Query query = pm.newQuery("SELECT FROM mydomain.Product WHERE " +

"price < limit PARAMETERS double limit ORDER BY price

ASCENDING");

List results = (List)query.execute(150.00);

Pour bien comprendre l’exemple ci-dessus, précisons que l’objet nommé pm désigne le gestionnaire

de persistance qui est le point de passage obligé pour toute requête ou transaction effectuée sur ses

données. Cette manière de procéder est différente de celle de Linq. JDO considère une requête

comme un objet qui sera traité de manière centralisé alors que nous avons vu que Linq adresse ses

requêtes directement sur les collections de données. Le recours au gestionnaire n’avait lieu que

lorsqu’une opération générale était nécessaire, comme dans le cas du SubmitChanges que nous

avions abordé dans la partie sur les implémentations relationnelles.

En ce qui concerne le mapping, il est réalisé à la fois par des dénominations explicites dans le code et

par un fichier Xml. Au niveau du code, les informations ajoutées servent à avertir les objets s’ils

doivent ou non s’occuper de leur persistance ou de celle de leurs attributs. En ce qui concerne le

fichier Xml, les informations spécifient quelle classe est la représentation de quelles données et où

trouver ces dernières. Diverses autres informations participent au mapping mais nous ne les

détaillerons pas ici. L’important est de constater que le mapping se fait au niveau du code avec des

indications qui permettent de savoir comment gérer les objets persistants et aussi avec un fichier de

correspondance qui indique quel attribut correspond à quelle donnée enregistrée. La seule

différence notable entre Linq et JDO en ce qui concerne le mapping, c’est que Linq tente d’encore

faciliter la tâche du développeur avec l’intégration d’éditeurs graphiques.

6.3.1.2 Hibernate

Une grande partie des informations relatives à Hibernate provient de [16]. Hibernate a été conçu à

l’origine pour simplifier la persistance d’objets dans des bases de données relationnelles. Ses

fonctionnalités se sont quelque peu élargies mais Hibernate reste un outil cantonné au domaine du

relationnel. Son rôle est d’offrir un mapping objet relationnel plus aisé que lorsqu’il est réalisé à la

main avec la technologie Jdbc. Hibernate gère les mécanismes de persistance, c’est-à-dire les

enregistrements et chargements depuis la base de données, mais il laisse le développeur écrire des

requêtes dans le langage de son choix parmi lesquels le langage propre à Hibernate (HQL), le langage

générique de l’interface de persistance implémentée par Hibernate17 (JPQL) ou directement en Sql.

Là où Linq fait figure de canif suisse intégrant de multiples possibilités via un outil passe-partout,

Hibernate est la boîte à outils du développeur désireux d’avoir le choix des armes.

A noter que Hibernate est souvent considéré comme une solution à n’utiliser que lorsque la situation

est adaptée [13]18. Alors que Linq se veut être une solution flexible, avec abstraction du stockage,

17

Il s’agit en réalité d’une interface définie dans la librairie JPA, mais par abus de langage nous dirons que l’interface implémentée est JPA. JPA signifie Java Persistence API. 18

Il s’agit là d’interventions non officielles mais néanmoins non(ou très peu) démenties par la communauté*13+

Hibernate est un choix lié au monde relationnel offrant un ensemble de fonctionnalités pas toujours

aisées à maîtriser. Précisons tout de même qu’Hibernate est bien plus ancien que Linq, puisque sa

création remonte à 2001. Bien que les interfaces JPA aient été créées pour englober plusieurs

utilitaires de persistance dont Hibernate, ce dernier reste davantage un ORM qu’une solution

intégrée au langage. De manière générale, le rôle des implémentations de JPA reste borné à

effectuer une persistance plus simple lorsque les données sont stockées sous forme relationnelle.

6.3.2 .NET

Bien que Linq y soit récent et que, au contraire de Java, ce langage ne s’enrichisse pas directement

sur la base d’une communauté de contributeurs, .NET et C# en particulier proposent d’autres

alternatives pour réaliser la persistance des données sans passer par toute la panoplie des

commandes ADO.NET. Les autres méthodes que nous allons aborder sont, de manière générale,

destinées à un autre public que celui prévu pour Linq.

6.3.2.1 NHibernate

Comme son nom l’indique, il s’agit là d’une version d’Hibernate adaptée au langage C# (le N est une

abréviation courante en préfixe d’une technologie pour signifier qu’elle s’applique à .NET). Les

informations relatives à NHibernate émanent de [17]. Le cœur d’Hibernate a été porté vers .NET et

son utilisation a été quelque peu adaptée pour mieux répondre aux attentes des développeurs .NET.

Cet outil a été développé par la même équipe que celle en charge de la version Java. De par sa

structure et son mode de fonctionnement, il s’agit encore une fois d’un ORM qui vient se greffer sur

une architecture déjà existante, celle de l’incontournable couche ADO.NET. NHibernate est présenté

comme une solution mature, capable de réaliser la correspondance objet relationnel de manière

transparente pour le développeur. NHibernate répond à une demande particulière : ne pas être

limité par l’ORM en étant sûr que, de toute façon, les données seront sauvées dans une base de

données relationnelle. Il s’agit là d’une optique quelque peu hors des objectifs de Linq qui souhaite

créer une abstraction avec le stockage tout en mettant davantage en avant la facilité de mise en

œuvre. A noter que depuis peu, NHibernate propose un module complémentaire Linq to NHibernate

permettant d’utiliser la syntaxe Linq pour réaliser des requêtes avec NHibernate [18].

6.3.2.2 Entity Framework

Voici une figure connue, les informations à son sujet sont toujours majoritairement issues de [5].

L’Entity Framework, que nous avions brièvement introduit dans la section Linq to Entities, est lui

aussi en soi une alternative à Linq. Il serait mieux de parler de complémentarité entre ces deux outils

que de concurrence, néanmoins il reste possible d’utiliser l’un sans l’autre. L’Entity Framework

permet essentiellement deux choses : choisir la forme des entités à manipuler et automatiser toute

la cuisine interne nécessaire pour pouvoir fournir des entités personnalisées de manière efficace.

Comme nous l’avions vu, cela ajoute également un niveau d’abstraction vis-à-vis de la base de

données, car le code manipulant les entités est bien séparé des manipulations relationnelles. Linq

apporte quant à lui la puissance et la simplicité d’utilisation de son langage de requêtes. L’Entity

Framework a donc pour vocation d’aider à définir la « business logic » de l’application sans entrave

de la part du stockage des données, alors que Linq permet de récupérer des éléments précis plus

aisément. Il est tout à fait envisageable de vouloir garder la main sur le langage de requêtes car, en

effet, celles-ci seront traduites tôt ou tard en Sql. Dans cette optique-là, l’utilisation de l’Entity

Framework se fera sans l’aide de Linq. En résumé, si la difficulté se situe au niveau de l’architecture

du code ou de la jonction entre la logique métier et la couche de persistance des données, Entity

Framework est une solution envisageable. Le champ d’application de Linq sera plutôt atteint si la

difficulté réside dans la capacité à produire ou à vérifier des requêtes vers des données persistantes.

Ces outils répondent à des besoins séparés et n’entrent pas en concurrence directe.

6.3.3 Autres langages

Nous avons vu un peu ce qui se faisait en Java comme en .NET pour mettre en place une aide à la

persistance. Ces deux langages sont probablement les porte-drapeaux du paradigme orienté objet et

pourtant ils n’en sont pas non plus les seuls représentants. Le PHP est très fort présent dans le

domaine du développement web et ce domaine fait généralement un usage extensif des moyens de

persistance. Nous allons maintenant parcourir des approches plus globales, permettant de nous faire

une meilleure idée des types de persistance recherchées et pourquoi. Les paragraphes suivants sont

issus de diverses sources, principalement [19],[20] et [21].

6.3.3.1 Persistance personnalisée

De nombreux ORM existent sur le marché et ce depuis maintenant quelques années. Les plus connus

restent ceux liés à Java et .NET. Pourquoi ? La première chose qui devrait nous mettre la puce à

l’oreille est que ces langages sont fortement typés, à l’inverse du PHP ou de Python. Il est impératif

de connaître le type d’une variable avant de pouvoir l’utiliser. Or les types sont quelque peu

différents lorsque nous passons du monde relationnel à celui des objets, ou vice versa. De même, les

grandes applications orientées objet s’appuient sur des architectures parfois titanesques et cela

entraîne invariablement une complexification structurelle des données à stocker. Les langages

comme PHP et Python ont quant à eux une réputation de légèreté et de facilité de mise en œuvre.

Vu de cet angle là, la multiplication des utilitaires de persistance pour Java et .NET s’explique mieux.

Leur supériorité numérique également. Nous avons vu le cas de Hibernate et de NHibernate dans les

paragraphes précédents, précisons tout de même qu’il en existe des dizaines d’autres. Il existe

malgré tout des ORM pour PHP (Doctrine en est le plus connu) et Python (avec l’ORM SQLAlchemy).

Ceux-ci sont souvent des versions d’ORM utilisés en Java portées vers le langage cible. Des langages

comme PHP et .NET sont difficilement comparables lorsque le domaine d’application est défini, il

n’est donc pas réellement question de concurrence entre Linq et ces différents outils.

Une autre situation qui se généralise de plus en plus est d’embarquer sa propre couche de

persistance, offrant l’abstraction désirée. C’est particulièrement vrai pour des outils de conception

« tout en un », tels des fournisseurs d’ERP (TinyErp s’appuie sur du code Python [24]) ou de

gestionnaire de contenu CMS lorsqu’ils sont développés en PHP ou Python. Les gestionnaires de

contenu intègrent souvent la possibilité d’insérer du code à plusieurs niveaux, que ce soit lors de la

création d’une page ou l’écriture d’un script dont l’appel est géré par le CMS. Bien qu’il s’agisse

essentiellement d’outils spécialisés dans le développement web, notons que ces solutions intègrent

chaque jour plus de fonctionnalités ce qui les approchent très fortement du statut d’environnement

de développement. Parmi ceux-ci, nous pouvons citer des exemples comme eZ Publish [22] ou

Drupal [23]. Ces outils de gestion de contenu permettent d’insérer du code pratiquement n’importe

où et constituent en eux-mêmes des outils de développements (tous deux s’appuient sur du PHP). Ils

ont chacun à leur manière établis une couche de persistance tentant de faire abstraction de la nature

de la source de données. Un ensemble de classes prennent en charge les différents types de bases de

données et les interactions avec le stockage sont créées à la volée, selon le type de stockage détecté.

L’abstraction vis-à-vis du support de persistance est une idée similaire à celle de Linq mais les

requêtes demeurent écrites en Sql ou en XQuery. Ceci n’est pas très surprenant car les langages non

typés comme PHP n’ont généralement pas recours à une phase de compilation. Dès lors, la

vérification syntaxique n’a plus de sens. Simplifier l’écriture des requêtes pourrait être fait mais

gardons en tête que la majorité des applications réalisées avec ces CMS et ERP sont depuis

longtemps liées à des bases de données. Pour ces développeurs, il est envisageable que l’écriture de

requêtes soit un chapitre depuis longtemps maîtrisé, ou du moins avec lequel ils ont établis leurs

habitudes.

6.3.3.2 Bases de données objets

Les bases de données objets sont restées, depuis leur apparition, en marge des autres systèmes de

persistance. Certains indices laissent croire qu’elles font l’objet d’un regain d’intérêt, spécialement

pour des applications légères. Leur gros problème a toujours été de pouvoir justifier le changement

de toute une couche de persistance pourtant déjà fonctionnelle [3]. Un tel changement implique une

migration des données en plus d’une réécriture conséquente des applications qui font usage de la

persistance.

Une base de données objet constitue un moyen de réaliser physiquement la persistance et non de

fournir une aide au programmeur en soi. Bien sûr la syntaxe du langage Oql, permettant d’interroger

ces bases de données, est très proche des langages orientés objet. Mais elle n’est pas réellement

intégrée au langage comme l’est Linq. Si nous en parlons ici c’est pour faire un plus ample lien entre

les deux. Plusieurs logiciels de gestion de bases de données orientées objet ont littéralement sauté

sur l’occasion pour se mettre dans la mouvance de Linq. Ainsi nous pouvons citer db4o qui utilise

Linq comme langage de requêtes19 mais aussi Eloquera, une base de données destinée au

multimédia, ou encore Siaqodb20 qui intègre même un éditeur de requêtes pour Linq. L’écriture de

requêtes avec Linq se fait déjà dans une optique orientée objet, que des bases de données objet

tentent de s’aligner dessus n’est finalement pas surprenant. Est-ce que Linq contribuera à donner un

second souffle aux bases de données orientées objet ? L’avenir nous le dira.

19

Information vue sur http://www.db4o.com/s/linqdb.aspx 20

Information vue sur http://siaqodb.com/

6.4 Evolutions de Linq Nous allons maintenant parler d’avenir. De ce qui est en projets, en développement et de ce qui

pourrait le devenir. Microsoft a montré sa volonté d’avancer dans le domaine de la gestion des

données persistantes mais bien des choses restent à faire.

6.4.1 Version 4

La quatrième version du framework .NET est en phase de beta test au moment où sont rédigées ces

lignes. Lors d’une annonce faite le 28 octobre 200821, l’équipe en charge du développement de Linq

précisait que Linq to Sql serait désormais considéré comme une moins prioritaire, tous les efforts

étant dirigés vers Linq to Entities. Ceci en raison de sa capacité à s’adapter à de plus nombreux types

de bases de données relationnelles. Linq to Sql n’a pas été abandonné pour autant, mais les

changements annoncés ressemblent plus à un planning de résolutions de bogues qu’à la mise en

place d’une évolution. Linq to Entities quant à lui semble avoir le vent en poupe, tout comme son

compagnon l’Entity Framework. Des améliorations concernant l’ergonomie d’utilisation et des

possibilités étendues comme la création d’une base de données depuis un modèle édité

graphiquement sont actuellement déployées dans la version en test du framework et de Visual

Studio 2010. Quelques changements précieux comme la capacité de « self tracking » des objets

entités vont encore alléger la charge du développeur. En ce qui concerne strictement Linq, les

améliorations concernent la flexibilité d’utilisation en offrant davantage de réglages optionnels. Leur

mise en pratique semble encore un peu floue et les informations obtenues sont parfois

contradictoires, il est donc dur d’être précis. Ce qui est sûr c’est que Linq to Entities continuera son

petit bonhomme de chemin.

En ce qui concerne les autres implémentations de Linq il n’y a pas grand-chose à signaler. Les

changements apportés par la version 422 semblent surtout tenir de la correction.

6.4.2 Ce qui reste à améliorer

Le passage par l’Entity Framework résout en partie le problème posé par Linq to Sql et sa non

portabilité vers d’autres bases de données que Sql Server. Tant qu’il y aura des options non

proposées par Linq, son utilisation restera l’objet d’un compromis. La synchronisation des entités

n’est par exemple pas toujours évidente et les mappings auto-générés offrent toujours le risque que

les mécanismes cachés soient mal compris. Ces fonctionnalités feront sans doute l’objet

d’améliorations progressives mais pour l’heure nous ne pouvons qu’espérer. Il est bon de noter que

Microsoft et l’équipe ADO.NET en particulier essaient de se montrer comme étant à l’écoute du

développeur, précisant régulièrement que les retours de la communauté seront pris en compte.

Malgré cela, plusieurs implémentations de Linq comme Linq to NHibernate citée précédemment ne

sont pas suivies par Microsoft. Bien que cela puisse paraître normal puisqu’ils n’en sont pas les

auteurs, cela laisse le risque qu’une modification des spécifications de Linq vienne mettre hors jeu

ces implémentations extérieures. Bien sûr ce risque existe pour de nombreuses technologies, mais

l’évolution de Linq semble encore hésitante, parlant tantôt d’ajouter telle fonctionnalité, tantôt de

laisser tomber telle autre, livrant les informations officielles au tout dernier moment. Ce qui nous

amène à parler d’une amélioration plus que souhaitable, celle concernant la documentation. En

dehors de certains ouvrages, l’ensemble de la documentation sur Linq se trouve en ligne, sur le site

21

http://blogs.msdn.com/adonet/archive/2008/10/29/update-on-linq-to-sql-and-linq-to-entities-roadmap.aspx 22

Consulter à ce sujet : http://damieng.com/blog/2009/06/01/linq-to-sql-changes-in-net-40

de la bibliothèque MSDN. Les livres deviennent rapidement dépassés, constituant rapidement des

sources d’informations obsolètes et parfois dangereuses. La documentation en ligne est dispersée et

ne propose pas la même visibilité que celle de l’API de Java. Cela amène rapidement à devoir récolter

des informations morcelées et de tenter de les mettre soi-même en place. Le but de cette étude était

d’explorer et de tester, ce qui fait que les miettes d’informations ne constituaient pas réellement un

problème dans la mesure où des tests étaient faits de toute façon. Pour une application

professionnelle en revanche, jouer à la devinette est à déconseiller. Une meilleure présentation de la

documentation y constituerait une aide précieuse.

6.4.3 Linq et Mono

Avant de faire le point sur ce tour d’horizon de Linq, il nous reste un dernier point à traiter qui est

celui du projet Mono. Le projet Mono consiste à porter les spécifications du framework .NET vers les

plateformes Linux. La version actuelle de Mono est la 2.6, sortie le 14 décembre 2009. L’ensemble de

cette section tire ses informations de [15] et de [14] en ce qui concerne Db Linq. Elle intègre, parmi

d’autres nouveautés, les fonctionnalités Linq to Sql avec quelques variantes. Il s’agit en réalité de

l’implémentation Db Linq partageant une partie de l’API de Linq to Sql. La plus importante différence

est que la version Mono de Linq to Sql n’est pas limitée à Sql Server mais permet de gérer des

fournisseurs de données comme FireBird, MySql, PostGreSql, Ingres, Oracle et SqlLite [14]. [15]

précise que « La totalité du code n’est pas encore portée », il est par exemple signalé que le support

de l’assemblage System.Data.Linq n’est pas total. Cet assemblage contient la définition des entités et

l’ensemble des extensions de méthode. Il est amusant de constater que Linq avec Mono est plus

polyvalent que dans sa version Microsoft qui, pour une raison mystérieuse, ne semble pas vouloir

soutenir activement l’implémentation Db Linq. Notons que le projet Mono est développé en

partenariat avec les équipes Microsoft et qu’un tel échange devrait pourtant être possible. Affaire à

suivre.

Bien que n’offrant qu’une implémentation, Mono permet d’utiliser Linq avec des bases de données

relationnelles. Ceci donne une dimension supplémentaire à Linq, qui devient une solution beaucoup

plus portable. L’abstraction vis-à-vis du stockage des données est une chose, la réutilisabilité d’un

code en est une autre. Avec le support de Mono, Linq devient un choix viable dans un contexte multi

plateformes, ce qui permet de concurrencer les solutions « à la Java ».

Conclusion Nous avons vu quels étaient les mécanismes permettant au langage d’implémenter les bases de Linq.

Ces mécanismes sont pour la plupart des évolutions de ce qui était proposé dans le deuxième

framework .NET. Partis de ces bases théoriques, nous avons découvert le formalisme de requêtes

proposé par Linq, formalisme proposant une syntaxe unifiée pour toutes les implémentations. Linq

se décline en une série d’implémentations, chacune destinée à des types de données particuliers.

L’implémentation objet a d’abord été abordée, en détaillant les opérateurs jugés comme étant les

plus intéressants. Un cas pratique a été envisagé, l’implémentation objet de Linq y a montré son

intérêt vis-à-vis de techniques plus classiques. Plusieurs implémentations relationnelles ont ensuite

été abordées, avec une attention toute particulière pour Linq to Sql, implémentation vedette de Linq.

Les concepts y ont été vus en détails avant d’analyser les performances de cette implémentation

confrontée à un accès classique aux données. D’autres implémentations ont ensuite été examinées,

mettant en avant l’autre technologie phare de Microsoft, l’Entity Framework. L’exploration de Linq

s’est poursuivie avec l’étude de l’implémentation Xml et des nouveautés qu’elles apportaient, en

particulier dans la gestion de données directement en Xml. Avec un peu de recul, le point a été fait

sur l’expérience Linq ainsi que sur ses forces et faiblesses. L’abstraction suppélmentaire du code vis-

à-vis de la couche de persistance est un apport très positif. Les anciennes techniques d’accès

relationnels ne sont pas obsolètes, le besoin de gérer plus finement des bases de données sera

toujours présent. Malgré tout, Linq constitue un danger pour le développeur non averti. Les tests ont

montré qu’une connaissance des mécanismes utilisés en interne s’avère indispensable pour éviter les

pièges qu’une trop grande facilité pourrait masquer. Le manque de documentation claire et

accessible est également à déplorer. L’étude s’est poursuivie par l’examen des alternatives

envisageables à Linq et leurs correspondances relatives. De nombreux concurrents existent mais très

peu proposent une abstraction aussi forte que celle de Linq. Seul JDO en Java semble constituer une

alternative sérieuse à Linq et .NET. Pour finir ce tour d’horizon, les possibles évolutions de Linq ont

été discutées. Il est difficile de prévoir ce que l’avenir nous réserve mais pour l’heure Linq semble se

stabiliser. Le projet Mono en est encore à ses débuts avec Linq mais la portabilité qu’il offre au projet

Linq tout entier laisse croire que son évolution à lui aussi se poursuivra. Linq, nouveau paradigme,

annonciateur de la programmation orientée données ? Un long chemin reste à accomplir, l’avenir

nous donnera la réponse.

Bibliographie 1. « Programming Microsoft Linq », Paolo Pialorsi et Marco Russo, éditions Microsoft Press,

2008.

2. « C# et .NET, version 2 », Gérard Lebrun, éditions Eyrolles, 2007.

3. « L’orienté objet, troisième édition », Hugues Bersini, éditions Eyrolles, 2007.

4. « Documentation en ligne du réseau de développeurs Microsoft »,

http://msdn.microsoft.com, technical references on C# 3.0 and 3.5

5. « Technical preview of Entity Framework », http://msdn.microsoft.com/en-

us/library/aa697427%28v=VS.80%29.aspx

6. « Tout sur WPF, Linq, C# et .NET en général », Thomas Lebrun,

http://blogs.developpeur.org/tom/default.aspx

7. « Programming Entity Framework », Julie Lerman, O’Reilly Media, janvier 2009.

8. « The Apache JDO web page », hosted by Apache Software Foundation,

http://db.apache.org/jdo/

9. « db4o, an object oriented datastore », http://www.db4o.com/s/linqdb.aspx

10. « Siaqodb first full Linq-supported object database », http://siaqodb.com/

11. « Linq in Action », par Fabrice Marguerie, Steve Eichert et Jim Wooley, O’Reilly Media, 2008.

12. « Bases de données », Esteban Zimànyi, cours donné en MA1 de la faculté des sciences

appliquées de l’Université Libre de Bruxelles, 2008.

13. « Forum des développeurs C# », forum de la communauté Developpez.com,

http://www.developpez.net/forums/f484/dotnet/langages/csharp/

14. « DbLinq 2007 », site du projet Db Linq, http://code.google.com/p/dblinq2007/

15. « Mono 2.6 Release Notes », site du projet Mono, partie relative à la version 2.6,

http://www.mono-project.com/Release_Notes_Mono_2.6

16. « Hibernate in Action », par Christian Bauer et Gavin King, O’Reilly.

17. « NHibernate for .NET », site hébergé par la communauté JBoss,

http://community.jboss.org/wiki/NHibernateforNET

18. « Linq to NHibernate page », http://www.hookedonlinq.com/LINQToNHibernate.ashx

19. « Doctrine ORM page », http://www.doctrine-project.org/

20. « ORM comparison and benchmark », ormeter.net/

21. « Apache Cayenne : Orm comparison for Java », https://cwiki.apache.org/CAY/orm-

comparison.html

22. « eZ Publish », site du projet open-source eZ Publish, ez.no

23. « Drupal community plumbing», site de la communauté Drupal, drupal.org

24. « OpenERP », site de OpenERP anciennement tinyErp, http://www.openerp.com/

Annexe 1 : Code de l’exemple Linq to object Contenu du fichier Ville.cs :

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace exLinq

{

class Ville

{

private string nom;

private int nbrHab;

public List<string> communes;

public List<Ville> liaisons;

public Ville(string n, int nbr)

{

nom = n;

nbrHab = nbr;

communes = new List<string>();

liaisons = new List<Ville>();

}

public string Nom {get{return nom;} set{nom=value;}}

public int Hab { get { return nbrHab; } set { if(value>0) nbrHab =

value; } }

public void AjouterCommune(string nom)

{

communes.Add(nom);

}

public void RetirerCommune(string nom)

{

communes.Remove(nom);

}

public void RelierA(Ville v)

{

liaisons.Add(v);

}

public void EnleverLiaisonAvec(Ville v)

{

liaisons.Remove(v);

}

}

}

Contenu du fichier program.cs :

using System;

using System.Collections.Generic;

using System.Linq;

using System.Linq.Expressions;

using System.Text;

namespace exLinq

{

class Program

{

static void Main(string[] args)

{

Villes();

Console.Read();

}

static void Villes()

{

List<Ville> reseauRoutier = InitialiseLeReseau();

var requete = from v in reseauRoutier

from c in v.liaisons

where v.communes.Count() >= 2

&& c.Hab <= 10000

select new {Ville = v.Nom, Connexion = c.Nom,

Habitants=c.Hab};

foreach (var v in requete)

{

Console.WriteLine(v);

}

}

static List<Ville> InitialiseLeReseau()

{

Ville v = new Ville("Charleroi", 45000);

v.AjouterCommune("Marcinelle");

v.AjouterCommune("Courcelles");

v.AjouterCommune("Marchienne au pont");

Ville v2 = new Ville("Zaventem", 8500);

v2.AjouterCommune("Nossegem");

v2.AjouterCommune("Sint Stevens Woluwe");

Ville v3 = new Ville("Wavre", 18000);

v3.AjouterCommune("Limal");

Ville v4 = new Ville("Braine l'Alleud", 22000);

v4.AjouterCommune("Ophain");

v4.AjouterCommune("Witterzée");

Ville v5 = new Ville("Louvain",26000);

Ville v6 = new Ville("Woluwe Saint Lambert", 20500);

v6.AjouterCommune("Paduwa");

v.RelierA(v4);

v4.RelierA(v);

v.RelierA(v3);

v3.RelierA(v);

v2.RelierA(v5);

v5.RelierA(v2);

v2.RelierA(v4);

v4.RelierA(v2);

v2.RelierA(v6);

v6.RelierA(v2);

v3.RelierA(v5);

v5.RelierA(v3);

v4.RelierA(v6);

v6.RelierA(v4);

v5.RelierA(v6);

v6.RelierA(v5);

List<Ville> reseau = new List<Ville>();

reseau.Add(v);

reseau.Add(v2);

reseau.Add(v3);

reseau.Add(v4);

reseau.Add(v5);

reseau.Add(v6);

return reseau;

}

}

}

Vu les valeurs données lors de l’initialisation, on constate que « l’ensemble des villes ayant au moins

deux communes et étant connectées à au moins une ville de maximum 10000 habitants » se résume

à Braine l’Alleud, qui est la seule ville connectée à Zaventem (la seule de moins de 10000 habitants) à

posséder au moins deux communes.

Annexe 2 : Code de tests de performance pour Linq to Sql Contenu de la classe ADOManager.cs :

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data.Common;

using System.Data.SqlClient;

namespace Linq_demo_console_3

{

public class ADOManager

{

private SqlConnection db;

private SqlDataReader rdr;

public void Connect()

{

Console.WriteLine();

db = new SqlConnection("Data

Source=.\\SQLEXPRESS;AttachDbFilename=\"G:\\Documents and

Settings\\Charles\\Mes documents\\Visual Studio

2008\\Projects\\Linq_demo_console_3\\LinqSelectTest\\LinqDB.mdf\";Integrate

d Security=True;Connect Timeout=30;User Instance=True");

rdr = null;

try {

Console.WriteLine("ADO Initializing");

Console.WriteLine("Manual Connection to DataBase");

db.Open();

Console.WriteLine("DataSource : "+db.DataSource);

Console.WriteLine("DataBase : " + db.Database);

}

catch(Exception e)

{

Console.WriteLine(e.Message);

}

}

public void LaunchInsertTest(int charge)

{

this.Connect();

Console.WriteLine();

Console.WriteLine("*** ADO Test running ***");

//Compute Time interval required to construct query

DateTime start = DateTime.Now;

Console.WriteLine("Starting test at " + start.Hour + ":" +

start.Minute + ":" + start.Second + ":" + start.Millisecond);

string sql = "INSERT INTO

Customers(Name,Address_city,Address_zip,Address_street,Address_number)";

string sql2;

string totalQuery = "";

for(int i=1;i<=charge; ++i)

{

sql2 = "

VALUES('cust"+i+"','city"+i+"','"+i+"','street"+i+"',"+i+"); ";

totalQuery += sql+sql2;

}

//Compute Time interval required by actual query

DateTime qStart = DateTime.Now;

Console.WriteLine("Starting query at " + qStart.Hour + ":" +

qStart.Minute + ":" + qStart.Second + ":" + qStart.Millisecond);

SqlCommand com = new SqlCommand(totalQuery, db);

com.ExecuteNonQuery();

DateTime end = DateTime.Now;

Console.WriteLine("End of test at " + end.Hour + ":" +

end.Minute + ":" + end.Second + ":" + end.Millisecond);

Console.WriteLine("Query duration : "+(end-qStart).ToString());

Console.WriteLine("Test Duration : " + (end -

start).ToString());

this.Deconnect();

}

public void SingleQuery(int i)

{

string sql = "INSERT INTO

Customers(Name,Address_city,Address_zip,Address_street,Address_number)";

sql += " VALUES('cust" + i + "','city" + i + "','" + i +

"','street" + i + "'," + i + "); ";

SqlCommand com = new SqlCommand(sql, db);

com.ExecuteNonQuery();

}

public void Deconnect()

{

if (db != null)

db.Close();

}

}

}

Contenu de la classe LinqBenchmarking.cs :

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace Linq_demo_console_3

{

public class Linq_benchmarking

{

private CustomerDataContext db;

public Linq_benchmarking()

{

db = new CustomerDataContext();

}

public void LaunchInsertTest(int charge)

{

Console.WriteLine("*** LINQ Test running ***");

//Compute Time interval required to construct query

DateTime dt = System.DateTime.Now;

Console.WriteLine("Starting test at

"+dt.Hour+":"+dt.Minute+":"+dt.Second+":"+dt.Millisecond);

for (int loop = 1; loop <= charge; ++loop)

{

Customers c = new Customers();

c.ID =loop;

c.Name="customer"+loop.ToString();

c.Address_city="city"+loop.ToString();

c.Address_number=loop;

c.Address_street="street"+loop.ToString();

c.Address_zip=loop.ToString();

db.Customers.InsertOnSubmit(c);

}

//db.Log = Console.Out;

//Compute Time Interval required by actual query

DateTime qStart = System.DateTime.Now;

Console.WriteLine("Starting query at " + qStart.Hour + ":" +

qStart.Minute + ":" + qStart.Second + ":" + qStart.Millisecond);

db.SubmitChanges();

//db.Log = null;

DateTime dt2 = DateTime.Now;

Console.WriteLine("End of test at " + dt2.Hour + ":" +

dt2.Minute + ":" + dt2.Second + ":" + dt2.Millisecond);

Console.WriteLine("Query duration : " +

this.TimeBetween(qStart,dt2));

Console.WriteLine("Test Duration : "+this.TimeBetween(dt,dt2));

}

public void AllCustomers()

{

var cust = from c in db.Customers

select c.Name;

foreach (string cst in cust)

{

Console.WriteLine(cst);

}

}

public void CleanDB()

{

var cust = from c in db.Customers

select c;

foreach (Customers cst in cust)

{

db.Customers.DeleteOnSubmit(cst);

}

db.SubmitChanges();

}

public string TimeBetween(DateTime start, DateTime end)

{

string time = "";

time += (end - start).ToString();

return time;

}

public void SingleQuery(int n)

{

Customers c = new Customers();

c.ID = n;

c.Name = "customer" + n.ToString();

c.Address_city = "city" + n.ToString();

c.Address_number = n;

c.Address_street = "street" + n.ToString();

c.Address_zip = n.ToString();

db.Customers.InsertOnSubmit(c);

//db.SubmitChanges();

}

public void Submit()

{

db.SubmitChanges();

}

}

}

Contenu de Customer.dbml :

#pragma warning disable 1591

//-------------------------------------------------------------------------

-----

// <auto-generated>

// Ce code a été généré par un outil.

// Version du runtime :2.0.50727.3603

//

// Les modifications apportées à ce fichier peuvent provoquer un

comportement incorrect et seront perdues si

// le code est régénéré.

// </auto-generated>

//-------------------------------------------------------------------------

-----

namespace Linq_demo_console_3

{

using System.Data.Linq;

using System.Data.Linq.Mapping;

using System.Data;

using System.Collections.Generic;

using System.Reflection;

using System.Linq;

using System.Linq.Expressions;

using System.ComponentModel;

using System;

[System.Data.Linq.Mapping.DatabaseAttribute(Name="LinqDB")]

public partial class CustomerDataContext :

System.Data.Linq.DataContext

{

private static System.Data.Linq.Mapping.MappingSource

mappingSource = new AttributeMappingSource();

#region Extensibility Method Definitions

partial void OnCreated();

partial void InsertCustomers(Customers instance);

partial void UpdateCustomers(Customers instance);

partial void DeleteCustomers(Customers instance);

#endregion

public CustomerDataContext() :

base(global::Linq_demo_console_3.Properties.Settings.Default.LinqDBCo

nnectionString, mappingSource)

{

OnCreated();

}

public CustomerDataContext(string connection) :

base(connection, mappingSource)

{

OnCreated();

}

public CustomerDataContext(System.Data.IDbConnection

connection) :

base(connection, mappingSource)

{

OnCreated();

}

public CustomerDataContext(string connection,

System.Data.Linq.Mapping.MappingSource mappingSource) :

base(connection, mappingSource)

{

OnCreated();

}

public CustomerDataContext(System.Data.IDbConnection

connection, System.Data.Linq.Mapping.MappingSource mappingSource) :

base(connection, mappingSource)

{

OnCreated();

}

public System.Data.Linq.Table<Customers> Customers

{

get

{

return this.GetTable<Customers>();

}

}

}

[Table(Name="dbo.Customers")]

public partial class Customers : INotifyPropertyChanging,

INotifyPropertyChanged

{

private static PropertyChangingEventArgs emptyChangingEventArgs

= new PropertyChangingEventArgs(String.Empty);

private int _ID;

private string _Name;

private string _Address_city;

private string _Address_zip;

private string _Address_street;

private System.Nullable<int> _Address_number;

private string _Email;

#region Extensibility Method Definitions

partial void OnLoaded();

partial void OnValidate(System.Data.Linq.ChangeAction action);

partial void OnCreated();

partial void OnIDChanging(int value);

partial void OnIDChanged();

partial void OnNameChanging(string value);

partial void OnNameChanged();

partial void OnAddress_cityChanging(string value);

partial void OnAddress_cityChanged();

partial void OnAddress_zipChanging(string value);

partial void OnAddress_zipChanged();

partial void OnAddress_streetChanging(string value);

partial void OnAddress_streetChanged();

partial void OnAddress_numberChanging(System.Nullable<int> value);

partial void OnAddress_numberChanged();

partial void OnEmailChanging(string value);

partial void OnEmailChanged();

#endregion

public Customers()

{

OnCreated();

}

[Column(Storage="_ID", AutoSync=AutoSync.OnInsert, DbType="Int

NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)]

public int ID

{

get

{

return this._ID;

}

set

{

if ((this._ID != value))

{

this.OnIDChanging(value);

this.SendPropertyChanging();

this._ID = value;

this.SendPropertyChanged("ID");

this.OnIDChanged();

}

}

}

[Column(Storage="_Name", DbType="VarChar(50) NOT NULL",

CanBeNull=false)]

public string Name

{

get

{

return this._Name;

}

set

{

if ((this._Name != value))

{

this.OnNameChanging(value);

this.SendPropertyChanging();

this._Name = value;

this.SendPropertyChanged("Name");

this.OnNameChanged();

}

}

}

[Column(Storage="_Address_city", DbType="VarChar(50) NOT NULL",

CanBeNull=false)]

public string Address_city

{

get

{

return this._Address_city;

}

set

{

if ((this._Address_city != value))

{

this.OnAddress_cityChanging(value);

this.SendPropertyChanging();

this._Address_city = value;

this.SendPropertyChanged("Address_city");

this.OnAddress_cityChanged();

}

}

}

[Column(Storage="_Address_zip", DbType="VarChar(8) NOT NULL",

CanBeNull=false)]

public string Address_zip

{

get

{

return this._Address_zip;

}

set

{

if ((this._Address_zip != value))

{

this.OnAddress_zipChanging(value);

this.SendPropertyChanging();

this._Address_zip = value;

this.SendPropertyChanged("Address_zip");

this.OnAddress_zipChanged();

}

}

}

[Column(Storage="_Address_street", DbType="VarChar(50)")]

public string Address_street

{

get

{

return this._Address_street;

}

set

{

if ((this._Address_street != value))

{

this.OnAddress_streetChanging(value);

this.SendPropertyChanging();

this._Address_street = value;

this.SendPropertyChanged("Address_street");

this.OnAddress_streetChanged();

}

}

}

[Column(Storage="_Address_number", DbType="Int")]

public System.Nullable<int> Address_number

{

get

{

return this._Address_number;

}

set

{

if ((this._Address_number != value))

{

this.OnAddress_numberChanging(value);

this.SendPropertyChanging();

this._Address_number = value;

this.SendPropertyChanged("Address_number");

this.OnAddress_numberChanged();

}

}

}

[Column(Storage="_Email", DbType="VarChar(50)")]

public string Email

{

get

{

return this._Email;

}

set

{

if ((this._Email != value))

{

this.OnEmailChanging(value);

this.SendPropertyChanging();

this._Email = value;

this.SendPropertyChanged("Email");

this.OnEmailChanged();

}

}

}

public event PropertyChangingEventHandler PropertyChanging;

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void SendPropertyChanging()

{

if ((this.PropertyChanging != null))

{

this.PropertyChanging(this,

emptyChangingEventArgs);

}

}

protected virtual void SendPropertyChanged(String propertyName)

{

if ((this.PropertyChanged != null))

{

this.PropertyChanged(this, new

PropertyChangedEventArgs(propertyName));

}

}

}

}

#pragma warning restore 1591

Contenu du fichier program.cs :

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data;

using MySql.Data;

using MySql.Data.MySqlClient;

namespace LinqToDataSet

{

class Program

{

static void Main(string[] args)

{

MySqlConnection con = new

MySqlConnection("Database=MyLinqSql;Uid='root';Pwd='C1$oon'");

string cmdString = "show tables";

MySqlCommand cmd = new MySqlCommand(cmdString, con);

con.Open();

MySqlDataReader dr = cmd.ExecuteReader();

Console.WriteLine("Tables présentes dans la Db :");

while (dr.Read())

{

Console.WriteLine(dr.GetValue(0));

}

//Fermeture du reader pour eviter conflit avec DataAdapter

dr.Close();

DataSet ds = new DataSet("MySqlDataSet");

string selectString = @"SELECT * FROM objects1";

MySqlDataAdapter da = new MySqlDataAdapter(selectString,con);

da.TableMappings.Add("objects1", "Table");

da.Fill(ds);

//interrogation du DataSet ds avec Linq to DataSet

DataTable dt1 = ds.Tables["Table"];

var dataSetQuery = from o in dt1.AsEnumerable()

select new {ID = o.Field<int>("Id"), TEXT =

o.Field<string>("Desc")};

Console.WriteLine("Requête adressée au DataSet construit depuis une

Db MySql");

foreach(var res in dataSetQuery)

{

Console.WriteLine(res);

}

//Fermeture de la connexion

con.Close();

Console.WriteLine("Appuyez sur Enter pour terminer...");

Console.ReadLine();

}

}

}

Annexe 3 : Utilisation d’une base de données MySQL avec Linq to

DataSet Phase 1 : Création de la base de données avec MySQL

Téléchargement de MySQL Community Server (disponible à cette adresse :

http://dev.mysql.com/downloads/ ). Après installation et première configuration, lancement de

l’utilitaire MySQL en ligne de commande. Les commandes entrées sont, dans l’ordre :

Create Database MyLinqSql \g

\u MyLinqSql

Create Table objects1 ( `Id` int not null, `Desc` varchar(20) default ‘’, primary key(Id) );

Create Table objects2 ( `Id` int not null, `Code` int not null, `ShortDesc` varchar(20) default ‘’,

primary key (Id), foreign key (Code) references objects1(Id) );

Insert into objects1(`Id`,`Desc`) values (1,”description 1”); Insert into objects1(`Id`,`Desc`)

values (2,”description 2”); insert into objects1(`Id`,`Desc`) values (3,”description 3”);

Insert into objects2 values (1,1,”obj2 pointe vers 1”); Insert into objects2 values

(2,1,”obj2.2”); Insert into objects2 values (3,2,”obj2.3”); Insert into objects2 values

(4,1,”obj2.4”); Insert into objects2 values (5,2,”obj2.5”);

Ce qui doit donner comme contenu pour objects1 (Select * from objects1 ;) :

Et pour objects2 :