LINQ: language integrated query en C♯ 2008 9782744023071, 9782744041068, 2382382422, 2744023078


265 47 5MB

French Pages X-616 p.: couv. ill. en coul.; 23 cm [632] Year 2009

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Table des matières......Page 4
À propos de l’auteur......Page 12
Traducteur et relecteurs techniques......Page 14
Partie I LINQ et C# 2008......Page 16
1 Hello LINQ......Page 18
2 Améliorations de C# 3.0 pour LINQ......Page 34
Partie II LINQ to Objects......Page 66
3 Introduction à LINQ to Objects......Page 68
4 Les opérateurs différés......Page 78
5 Les opérateurs non différés......Page 146
Partie III LINQ to XML......Page 196
6 Introduction à LINQ to XML......Page 198
7 L’API LINQ to XML......Page 204
8 Les opérateurs LINQ to XML......Page 284
9 Les autres possibilités de XML......Page 310
Partie IV LINQ to DataSet......Page 346
10 LINQ to DataSet......Page 348
11 Possibilités complémentaires des DataSet......Page 382
Partie V LINQ to SQL......Page 390
12 Introduction à LINQ to SQL......Page 392
13 Astuces et outils pour LINQ to SQL......Page 406
14 Opérations standard sur les bases de données......Page 432
15 Les classes d’entité LINQ to SQL......Page 476
16 La classe DataContext......Page 524
17 Les conflits d’accès concurrentiels......Page 586
18 Informations complémentaires sur SQL......Page 608
Index......Page 622
Recommend Papers

LINQ: language integrated query en C♯ 2008
 9782744023071, 9782744041068, 2382382422, 2744023078

  • 0 0 0
  • Like this paper and download? You can publish your own PDF file online for free in a few minutes! Sign Up
File loading please wait...
Citation preview

Référence

LINQ

Language Integrated Query en C# 2008 Joseph C. Rattz

Réseaux et télécom Programmation

Génie logiciel

Sécurité Système d’exploitation

Linq FM Prél Page I Mercredi, 18. février 2009 8:15 08

LINQ Language Integrated Query en C# 2008 Joseph C. Rattz, Jr.

Traduction : Michel Martin, MVP Relecture technique : Mitsuru Furuta, Microsoft France Pierrick Gourlain, MVP Client Application Matthieu Mezil, MVP C#

Linq.book Page II Mercredi, 18. février 2009 7:58 07

Pearson Education France a apporté le plus grand soin à la réalisation de ce livre afin de vous fournir une information complète et fiable. Cependant, Pearson Education France n’assume de responsabilités, ni pour son utilisation, ni pour les contrefaçons de brevets ou atteintes aux droits de tierces personnes qui pourraient résulter de cette utilisation. Les exemples ou les programmes présents dans cet ouvrage sont fournis pour illustrer les descriptions théoriques. Ils ne sont en aucun cas destinés à une utilisation commerciale ou professionnelle. Pearson Education France ne pourra en aucun cas être tenu pour responsable des préjudices ou dommages de quelque nature que ce soit pouvant résulter de l’utilisation de ces exemples ou programmes. Tous les noms de produits ou marques cités dans ce livre sont des marques déposées par leurs propriétaires respectifs.

Publié par Pearson Education France 47 bis, rue des Vinaigriers 75010 PARIS Tél. : 01 72 74 90 00 www.pearson.fr

Titre original : Pro LINQ Language Integrated Query in C# 2008 Traduit de l’américain par Michel Martin

Mise en pages : TyPAO

Relecture technique : Mitsuru Furuta, Pierrick Gourlain, Matthieu Mezil

ISBN : 978-2-7440-4106-8 Copyright © 2009 Pearson Education France Tous droits réservés

ISBN original : 978-1-59059-789-9 Copyright © 2007 by Joseph C. Rattz, Jr. All rights reserved Édition originale publiée par Apress 2855 Telegraph Avenue, Suite 600, Berkeley, CA 94705 www.apress.com

Aucune représentation ou reproduction, même partielle, autre que celles prévues à l’article L. 122-5 2˚ et 3˚ a) du code de la propriété intellectuelle ne peut être faite sans l’autorisation expresse de Pearson Education France ou, le cas échéant, sans le respect des modalités prévues à l’article L. 122-10 dudit code. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

Linq.book Page III Mercredi, 18. février 2009 7:58 07

Table des matières À propos de l’auteur..........................................................................................................

XI

Traducteur et relecteurs techniques ................................................................................

XIII

Partie I LINQ et C# 2008 1 Hello LINQ.................................................................................................................... Un changement de paradigme .................................................................................... Interrogation XML .......................................................................................... Interrogation d’une base de données SQL Server ........................................... Introduction ................................................................................................................ LINQ et l’interrogation des données ............................................................... Composants ..................................................................................................... Comment travailler avec LINQ ....................................................................... LINQ ne se limite pas aux requêtes ............................................................................ Quelques conseils avant de commencer ..................................................................... Utilisez le mot-clé var si vous n’êtes pas à l’aise ........................................... Utilisez les opérateurs Cast ou OfType pour les collections héritées ............. Préférez l’opérateur OfType à l’opérateur Cast .............................................. Les requêtes aussi peuvent être boguées ......................................................... Sachez tirer parti des requêtes différées .......................................................... Utiliser le log du DataContext ....................................................................... Utilisez le forum LINQ ................................................................................... Résumé .......................................................................................................................

3 3 4 5 6 7 7 9 9 12 12 14 15 15 16 17 18 18

2 Améliorations de C# 3.0 pour LINQ .......................................................................... Les nouveautés du langage C# 3.0 ............................................................................. Les expressions lambda ................................................................................... Arbres d’expressions ....................................................................................... Le mot-clé var, l’initialisation d’objets et les types anonymes ...................... Méthodes d’extension ..................................................................................... Méthodes partielles ......................................................................................... Expressions de requête .................................................................................... Résumé .......................................................................................................................

19 19 20 25 26 31 37 39 49

Linq.book Page IV Mercredi, 18. février 2009 7:58 07

IV

Table des matières

Partie II LINQ to Objects 3 Introduction à LINQ to Objects..................................................................................

53

Vue d’ensemble de LINQ to Objects .......................................................................... IEnumerable, séquences et opérateurs de requête standard ................................ IEnumerable, yield et requêtes différées ......................................................... Délégués Func ................................................................................................. Les opérateurs de requête standard ............................................................................. Résumé ............................................................................................................

53 54 55 58 59 61

4 Les opérateurs différés.................................................................................................

63

Espaces de noms référencés ....................................................................................... Assemblies référencés ................................................................................................ Classes communes ...................................................................................................... Les opérateurs différés, par groupes fonctionnels ...................................................... Restriction ....................................................................................................... Projection ........................................................................................................ Partage ............................................................................................................ Concaténation .................................................................................................. Tri .................................................................................................................... Opérateurs de jointure ..................................................................................... Opérateurs de regroupement ........................................................................... Opérateurs d’initialisation ............................................................................... Opérateurs de conversion ................................................................................ Opérateurs dédiés aux éléments ...................................................................... Opérateurs de génération ................................................................................. Résumé .......................................................................................................................

63 64 64 65 65 67 76 83 85 100 104 110 115 122 126 129

5 Les opérateurs non différés .........................................................................................

131

Espaces de noms référencés ....................................................................................... Classes communes ...................................................................................................... Les opérateurs non différés, par groupes fonctionnels ............................................... Opérateurs de conversion ................................................................................ Opérateurs d’égalité ........................................................................................ Opérateurs agissant au niveau des éléments ................................................... Quantificateurs ................................................................................................ Fonctions de comptage .................................................................................... Résumé .......................................................................................................................

131 131 134 134 145 148 160 165 178

Linq.book Page V Mercredi, 18. février 2009 7:58 07

Table des matières

V

6 Introduction à LINQ to XML ..................................................................................... Introduction ................................................................................................................ Se passer de l’API W3C DOM XML ......................................................................... Résumé .......................................................................................................................

183 185 185 187

7 L’API LINQ to XML.................................................................................................... Espaces de noms référencés ....................................................................................... Améliorations de l’API ............................................................................................... La construction fonctionnelle simplifie la création d’arbres XML ................. L’élément, point central d’un objet XML ....................................................... Noms, espaces de noms et préfixes ................................................................. Extraction de valeurs de nœuds ....................................................................... Le modèle d’objet LINQ to XML .............................................................................. Exécution différée des requêtes, suppression de nœuds et bogue d’Halloween ......... Création XML ............................................................................................................ Création d’éléments avec XElement ............................................................... Création d’attributs avec XAttribute ............................................................ Création de commentaires avec XComment ..................................................... Création de conteneurs avec XContainer ....................................................... Création de déclarations avec XDeclaration ................................................. Création de types de documents avec XDocumentType .................................. Création de documents avec XDocument ......................................................... Création de noms avec XName ......................................................................... Création d’espaces de noms avec XNamespace ............................................... Création de nœuds avec XNode ........................................................................ Création d’instructions de traitement avec XProcessingInstruction ......... Création d’éléments streaming avec XStreamingElement .......................... Création de textes avec XText ......................................................................... Définition d’un objet CData avec XCData ....................................................... Sauvegarde de fichiers XML ...................................................................................... Sauvegardes avec XDocument.Save() ........................................................... Sauvegarde avec XElement.Save ................................................................... Lecture de fichiers XML ............................................................................................ Lecture avec XDocument.Load() ................................................................... Lecture avec XElement.Load() ..................................................................... Extraction avec XDocument.Parse() ou XElement.Parse() ....................... Déplacements XML .................................................................................................... Propriétés de déplacement ............................................................................... Méthodes de déplacement ...............................................................................

189 189 190 190 192 194 196 199 200 202 202 205 206 207 207 208 209 210 211 211 211 213 215 215 216 216 217 218 218 219 220 221 222 225

Partie III LINQ to XML

Linq.book Page VI Mercredi, 18. février 2009 7:58 07

VI

Table des matières

Modification de données XML ................................................................................... Ajout de nœuds ............................................................................................... Suppression de nœuds ..................................................................................... Mise à jour de nœuds ...................................................................................... XElement.SetElementValue() sur des objets enfants de XElement ............ Attributs XML ............................................................................................................ Création d’un attribut ...................................................................................... Déplacements dans un attribut ........................................................................ Modification d’attributs ................................................................................... Annotations XML ....................................................................................................... Ajout d’annotations avec XObject.AddAnnotation() .................................. Accès aux annotations avec XObject.Annotation() ou XObject.Annotations() .......................................................................... Suppression d’annotations avec XObject.RemoveAnnotations() .............. Exemples d’annotations .................................................................................. Événements XML ....................................................................................................... XObject.Changing ....................................................................................... XObject.Changed ........................................................................................ Quelques exemples d’événements .................................................................. Le bogue d’Halloween .................................................................................... Résumé .......................................................................................................................

238 238 242 245 248 250 250 250 253 258 258

8 Les opérateurs LINQ to XML..................................................................................... Introduction aux opérateurs LINQ to XML ............................................................... Opérateur Ancestors ................................................................................................. Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur AncestorsAndSelf ................................................................................... Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur Attributes ............................................................................................... Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur DescendantNodes ..................................................................................... Prototype ......................................................................................................... Exemple ........................................................................................................... Opérateur DescendantNodesAndSelf ....................................................................... Prototype ......................................................................................................... Exemple ........................................................................................................... Opérateur Descendants ............................................................................................. Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur DescendantsAndSelf ............................................................................... Prototypes ........................................................................................................ Exemples .........................................................................................................

269 270 270 270 271 274 274 275 277 277 277 279 279 279 280 280 281 282 282 282 284 284 284

258 258 259 262 262 262 263 267 267

Linq.book Page VII Mercredi, 18. février 2009 7:58 07

Table des matières

VII

Opérateur Elements ................................................................................................... Prototypes ........................................................................................................ Exemples ......................................................................................................... Opérateur InDocumentOrder ..................................................................................... Prototype ......................................................................................................... Exemple ........................................................................................................... Opérateur Nodes ......................................................................................................... Prototype ......................................................................................................... Exemple ........................................................................................................... Opérateur Remove ....................................................................................................... Prototypes ........................................................................................................ Exemples ......................................................................................................... Résumé .......................................................................................................................

287 287 287 289 289 289 290 290 291 292 292 292 294

9 Les autres possibilités de XML ................................................................................... Espaces de noms référencés ....................................................................................... Requêtes ..................................................................................................................... La description du chemin n’est pas une obligation ......................................... Une requête complexe ..................................................................................... Transformations .......................................................................................................... Transformations avec XSLT ............................................................................ Transformations avec la construction fonctionnelle ........................................ Astuces ............................................................................................................ Validation .................................................................................................................... Les méthodes d’extension ............................................................................... Prototypes ........................................................................................................ Obtention d’un schéma XML .......................................................................... Exemples ......................................................................................................... XPath .......................................................................................................................... Prototypes ........................................................................................................ Résumé .......................................................................................................................

295 295 296 296 298 303 304 306 308 314 314 314 315 317 328 328 329

Partie IV LINQ to DataSet 10 LINQ to DataSet ......................................................................................................... Référence des assemblies ........................................................................................... Espaces de noms référencés ....................................................................................... Code commun utilisé dans les exemples .................................................................... Opérateurs dédiés aux DataRow .................................................................................. Opérateur Distinct ........................................................................................ Opérateur Except ............................................................................................ Opérateur Intersect ......................................................................................

333 334 334 334 336 336 340 342

Linq.book Page VIII Mercredi, 18. février 2009 7:58 07

VIII

Table des matières

Opérateur Union .............................................................................................. Opérateur SequencialEqual .......................................................................... Opérateurs dédiés aux champs ................................................................................... Opérateur Field ........................................................................................ Opérateur SetField .................................................................................. Opérateurs dédiés aux DataTable .............................................................................. Opérateur AsEnumerable ................................................................................ Opérateur CopyToDataTable ........................................................ Résumé .......................................................................................................................

344 346 347 351 356 359 359 360 365

11 Possibilités complémentaires des DataSet................................................................ Espaces de noms référencés ....................................................................................... DataSets typés ........................................................................................................... Un exemple plus proche de la réalité .......................................................................... Résumé .......................................................................................................................

367 367 367 369 372

Partie V LINQ to SQL 12 Introduction à LINQ to SQL..................................................................................... Introduction à LINQ to SQL ...................................................................................... La classe DataContext ................................................................................... Classes d’entités .............................................................................................. Associations .................................................................................................... Détection de conflit d’accès concurrentiel ...................................................... Résolution de conflit d’accès concurrentiel .................................................... Prérequis pour exécuter les exemples ......................................................................... Obtenir la version appropriée de la base de données Northwind .................... Génération des classes d’entité de la base de données Northwind ................. Génération du fichier de mappage XML de la base de données Northwind ... Utilisation de l’API LINQ to SQL ............................................................................. IQueryable ......................................................................................................... Quelques méthodes communes .................................................................................. La méthode GetStringFromDb() ................................................................... La méthode ExecuteStatementInDb() ......................................................... Résumé .......................................................................................................................

377 378 380 381 382 383 383 383 384 384 385 386 386 386 387 388 388

13 Astuces et outils pour LINQ to SQL......................................................................... Introduction aux astuces et aux outils pour LINQ to SQL ......................................... Astuces ....................................................................................................................... La propriété DataContext.Log ...................................................................... La méthode GetChangeSet() ......................................................................... Utilisation de classes partielles ou de fichiers de mappage ............................. Utilisation de méthodes partielles ...................................................................

391 391 392 392 393 393 394

Linq.book Page IX Mercredi, 18. février 2009 7:58 07

Table des matières

IX

Outils .......................................................................................................................... SQLMetal ........................................................................................................ Le Concepteur Objet/Relationnel .................................................................... Utiliser SQLMetal et le Concepteur O/R ................................................................... Résumé .......................................................................................................................

394 394 401 414 415

14 Opérations standard sur les bases de données......................................................... Prérequis pour exécuter les exemples ......................................................................... Méthodes communes ....................................................................................... Utilisation de l’API LINQ to SQL .................................................................. Opérations standard de bases de données ................................................................... Insertions ......................................................................................................... Requêtes .......................................................................................................... Mises à jour ..................................................................................................... Suppressions .................................................................................................... Surcharger les méthodes de mise à jour des bases de données .................................. Surcharge de la méthode Insert .................................................................... Surcharge de la méthode Update .................................................................... Surcharge de la méthode Delete .................................................................... Exemple ........................................................................................................... Surcharge dans le Concepteur Objet/Relationnel ............................................ Considérations ................................................................................................. Traduction SQL .......................................................................................................... Résumé .......................................................................................................................

417 417 418 418 418 418 423 446 450 453 453 454 454 454 457 457 457 459

15 Les classes d’entité LINQ to SQL ............................................................................. Prérequis pour exécuter les exemples ......................................................................... Les classes d’entité ..................................................................................................... Création de classes d’entité ............................................................................. Schéma de fichier de mappage externe XML ................................................. Projection dans des classes d’entité/des classes de non-entité ........................ Dans une projection, préférez l’initialisation d’objet à la construction paramétrée ............................................................. Extension des classes d’entité avec des méthodes partielles ...................................... Les classes API importantes de System.Data.Linq ................................................. EntitySet ................................................................................................. EntityRef ................................................................................................. Table ......................................................................................................... IExecuteResult ............................................................................................. ISingleResult ........................................................................................ IMultipleResults ......................................................................................... Résumé .......................................................................................................................

461 461 461 462 493 494 496 499 501 502 502 504 505 506 506 508

16 La classe DataContext................................................................................................ Prérequis pour exécuter les exemples .........................................................................

509 509

Linq.book Page X Mercredi, 18. février 2009 7:58 07

X

Table des matières

Méthodes communes ....................................................................................... Utilisation de l’API LINQ to SQL .................................................................. La classe [Your]DataContext .................................................................................. La classe DataContext .............................................................................................. Principaux objectifs ......................................................................................... Datacontext() et [Your]DataContext() .................................................... SubmitChanges() ........................................................................................... DatabaseExists() ......................................................................................... CreateDatabase() ......................................................................................... DeleteDatabase() ........................................................................................ CreateMethodCallQuery() ........................................................................... ExecuteQuery() ............................................................................................ Translate() ................................................................................................... ExecuteCommand() ......................................................................................... ExecuteMethodCall() ................................................................................... GetCommand() ................................................................................................. GetChangeSet() ............................................................................................. GetTable() ..................................................................................................... Refresh() ....................................................................................................... Résumé .......................................................................................................................

509 509 510 510 513 520 532 539 540 541 542 543 546 547 549 557 558 560 562 568

17 Les conflits d’accès concurrentiels ............................................................................

571

Prérequis pour exécuter les exemples ......................................................................... Méthodes communes ....................................................................................... Utilisation de l’API LINQ to SQL .................................................................. Conflits d’accès concurrentiels ................................................................................... Contrôle d’accès concurrentiel optimiste ........................................................ Contrôle d’accès concurrentiel pessimiste ...................................................... Une approche alternative pour les middle-tier et les serveurs ......................... Résumé .......................................................................................................................

571 571 571 571 572 585 588 591

18 Informations complémentaires sur SQL ..................................................................

593

Prérequis pour exécuter les exemples ......................................................................... Utilisation de l’API LINQ to SQL .................................................................. Utilisation de l’API LINQ to XML ................................................................. Les vues d’une base de données ................................................................................. Héritage des classes d’entité ....................................................................................... Transactions ................................................................................................................ Résumé .......................................................................................................................

593 593 593 593 595 601 603

Index ...................................................................................................................................

607

Linq.book Page XI Mercredi, 18. février 2009 7:58 07

À propos de l’auteur Joseph C. Rattz Jr a commencé sa carrière de développeur en 1990, lorsqu’un ami lui a demandé de l’aide pour développer l’éditeur de texte "ANSI Master" sur un ordinateur Commodore Amiga. Un jeu de pendu (The Gallows) lui a rapidement fait suite. Après ces premiers programmes écrits en Basic compilé, Joe s’est tourné vers le langage C, à des fins de vitesse et de puissance. Il a alors développé des applications pour les magazines JumpDisk (périodique avec CD consacré aux ordinateurs Amiga) et Amiga World. Comme il développait dans une petite ville et sur une plate-forme isolée, Joe a appris toutes les "mauvaises" façons d’écrire du code. C’est en tentant de faire évoluer ses applications qu’il a pris conscience de l’importance de la maintenabilité du code. Deux ans plus tard, Joe a intégré la société Policy Management Systems en tant que programmeur pour développer une application client/serveur dans le domaine de l’assurance pour OS/2 et Presentation Manager. D’année en année, il a ajouté le C++, Unix, Java, ASP, ASP.NET, C#, HTML, DHTML et XML à sa palette de langages alors qu’il travaillait pour SCT, DocuCorp, IBM et le comité d’Atlanta pour les jeux Olympiques, CheckFree, NCR, EDS, Delta Technology, Radiant Systems et la société Genuine Parts. Joe apprécie particulièrement le développement d’interfaces utilisateurs et de programmes exécutés côté serveur. Sa phase favorite de développement est le débogage. Joe travaille actuellement pour la société Genuine Parts Company (maison mère de NAPA), dans le département Automotive Parts Group Information System, où il développe le site web Storefront. Ce site gère les stocks de NAPA et fournit un accès à leurs comptes et données à travers un réseau d’ordinateurs AS/400. Vous pouvez le contacter sur le site www.linqdev.com.

Linq.book Page XII Mercredi, 18. février 2009 7:58 07

Linq.book Page XIII Mercredi, 18. février 2009 7:58 07

Traducteur et relecteurs techniques À propos du traducteur Michel Martin est un passionné des technologies Microsoft. Nommé MVP par Microsoft depuis 2003, il anime des ateliers de formation, réalise des CD-ROM d’autoformation vidéo et a écrit plus de 250 ouvrages techniques, parmi lesquels Développez des gadgets pour Windows Vista et Windows Live (Pearson, 2007) et le Programmeur Visual Basic 2008 (Pearson, 2008). Il a récemment créé le réseau social eFriends Network, accessible à l’adresse http://www.efriendsnetwork.com.

À propos des relecteurs techniques Mitsuru Furuta est responsable technique en charge des relations développeurs chez Microsoft France. Il blogue sur http://blogs.msdn.com/mitsufu. Pierrick Gourlain est architecte logiciel. Nommé MVP par Microsoft depuis 2007, il est passionné de nouvelles technologies, plus particulièrement de LINQ, WPF, WCF, WF et des langages dynamiques. Il collabore à plusieurs projets open-source hébergés sur codeplex (http://www.codeplex.com). Matthieu Mezil est consultant formateur, nommé MVP C# par Microsoft depuis avril 2008. Passionné par .NET, il s’est spécialisé sur l’Entity Framework. Il blogue sur http://blogs.codes-sources.com/matthieu (fr) et http://msmvps.com/blogs/matthieu (en).

Linq.book Page XIV Mercredi, 18. février 2009 7:58 07

Linq.book Page 1 Mercredi, 18. février 2009 7:58 07

I LINQ et C# 2008

Linq.book Page 2 Mercredi, 18. février 2009 7:58 07

Linq.book Page 3 Mercredi, 18. février 2009 7:58 07

1 Hello LINQ Listing 1.1 : Hello Linq. using System; using System.Linq; string[] greetings = {"hello world", "hello LINQ", "hello Pearson"}; var items = from s in greetings where s.EndsWith("LINQ") select s; foreach (var item in items) Console.WriteLine(item);

INFO Le code du Listing 1.1 a été inséré dans un projet basé sur le modèle "Application Console", de Visual Studio 2008. Si cette directive n’est pas déjà présente dans le squelette de l’application, ajoutez une instruction using System.Linq pour référencer cet espace de noms.

L’exécution de ce code avec le raccourci clavier Ctrl+F5 affiche le message suivant dans la console : Hello LINQ

Un changement de paradigme Avez-vous remarqué un changement par rapport à votre style de programmation ? En tant que développeur .NET, vous n’êtes certainement pas passé à côté. À travers cet exemple trivial, une requête SQL (Structured Query Language) a été exécutée sur un

Linq.book Page 4 Mercredi, 18. février 2009 7:58 07

4

LINQ et C# 2008

Partie I

tableau de Strings1. Intéressez-vous à la clause where. Vous ne rêvez pas, j’ai bien utilisé la méthode EndsWidth sur un objet String. Vous vous demandez certainement quel est le type de cette variable. C# fait-il toujours des vérifications statiques des types ? Oui, à la compilation ! Cette prouesse est rendue possible par LINQ (Language INtegrated Query). Interrogation XML Après avoir examiné le code du Listing 1.1, ce deuxième exemple va commencer à vous faire entrevoir le potentiel mis entre les mains du développeur .NET par LINQ. En utilisant l’API LINQ to XML, le Listing 1.2 montre avec quelle facilité il est possible d’interagir et d’interroger des données XML (eXtensible Markup Language). Remarquez en particulier comment les données XML sont manipulées à travers l’objet books. Listing 1.2 : Requête XML basée sur LINQ to XML. using System; using System.Linq; using System.Xml.Linq; XElement books = XElement.Parse( @"

Pro LINQ: Language Integrated Query en C# 2008 Joe Rattz

Pro WF: Windows Workflow en .NET 3.0 Bruce Bukovics

Pro C# 2005 et la plateforme.NET 2.0, Troisième édition Andrew Troelsen

"); var titles = from book in books.Elements("book") where (string) book.Element("author") == "Joe Rattz" select book.Element("title"); foreach(var title in titles) Console.WriteLine(title.Value);

INFO Si l’assembly System.Xml.Linq.dll n’apparaît pas dans les références du projet, ajoutezla. Remarquez également la référence à l’espace de noms System.Xml.Linq.

1. L’ordre d’interrogation est inversé par rapport à une requête SQL traditionnelle. Par ailleurs, une instruction "s in" a été ajoutée pour fournir une référence à l’ensemble des éléments source. Ici, le tableau de chaînes "hello world", "hello LINQ" et "hello Pearson".

Linq.book Page 5 Mercredi, 18. février 2009 7:58 07

Chapitre 1

Hello LINQ

5

Appuyez sur Ctrl+F5 pour exécuter ce code. Voici le résultat affiché dans la console. Pro LINQ: Language Integrated Query en C# 2008

Avez-vous remarqué comment les données XML ont été découpées dans un objet de type XElement sans qu’il ait été nécessaire de définir un objet XmlDocument ? Les extensions de l’API XML sont un des avantages de LINQ to XML. Au lieu d’être centré sur les objets XmlDocument, comme le préconise le W3C Document Object Model (DOM), LINQ to XML permet au développeur d’interagir à tous les niveaux du document en utilisant la classe XElement. INFO Outre ses possibilités d’interrogation, LINQ to XML fournit également une interface de travail XML plus puissante et plus facile à utiliser.

Notez également que la même syntaxe SQL est utilisée pour interroger les données XML, comme s’il s’agissait d’une base de données. Interrogation d’une base de données SQL Server Ce nouvel exemple montre comment utiliser LINQ to SQL pour interroger des tables dans des bases de données. Le Listing 1.3 interroge la base de données exemple Microsoft Northwind. Listing 1.3 : Une simple interrogation de base de données basée sur une requête LINQ to SQL. using System; using System.Linq; using System.Data.Linq; using nwind; Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var custs = from c in db.Customers where c.City == "Rio de Janeiro" select c; foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);

INFO Ce code fait référence à l’assembly System.Data.Linq.dll. Si cette assembly n’est pas spécifiée dans les premières lignes du listing, ajoutez-la. Notez qu’il est également fait référence à l’espace de noms System.Data.Linq.

Linq.book Page 6 Mercredi, 18. février 2009 7:58 07

6

LINQ et C# 2008

Partie I

Pour que cet exemple fonctionne, il est nécessaire de faire appel à l’utilitaire en ligne de commande SQLMetal ou au concepteur d’objets relationnels, afin de générer des classes d’entités qui pointent vers la base de données Northwind. Reportez-vous au Chapitre 12 pour en savoir plus sur l’utilisation de SQLMetal. Les classes d’entités de cet exemple faisant partie de l’espace de noms nwind, la clause using nwind; a été utilisée en début de listing pour y faire référence. INFO Il se peut que vous deviez changer la chaîne de connexion passée au constructeur Northwind dans ce listing. Reportez-vous aux sections relatives à DataContext() et [Your]DataContext() du Chapitre 16 pour prendre connaissance des différents modes de connexion possibles.

Appuyez sur Ctrl+F5 pour exécuter ce code. Le résultat ci-après devrait s’afficher dans la console : Hanari Carnes Que Delícia Ricardo Adocicados

Cet exemple utilise la table Customers de la base de données Northwind. Il se contente de sélectionner les clients qui résident à Rio de Janeiro. À première vue, il n’y a rien de nouveau ou de différent dans ce code. Vous remarquerez pourtant que la requête est intégrée dans le code. Les fonctionnalités de l’éditeur sont donc également accessibles au niveau de la requête ; en particulier la vérification de la syntaxe et l’Intellisense. L’écriture "à l’aveuglette" des requêtes et la détection des erreurs à l’exécution font donc bel et bien partie du passé ! Vous voulez baser une clause where sur un champ de la table Customers, mais vous n’arrivez pas à vous rappeler le nom des champs ? Intellisense affichera les noms des champs et vous n’aurez plus qu’à choisir dans la liste. Dans l’exemple précédent, il suffit de taper c. pour qu’Intellisense liste tous les champs de la table Customers. Vous verrez au Chapitre 2 que les requêtes LINQ peuvent utiliser deux syntaxes : la syntaxe "à point" object.method(), traditionnelle dans le langage C#, et une nouvelle syntaxe propre à LINQ. Les requêtes présentées jusqu’ici utilisent cette nouvelle syntaxe mais, bien entendu, vous pouvez continuer à utiliser la syntaxe traditionnelle.

Introduction La plate-forme .NET et les langages qui l’accompagnent (C# et VB) sont aujourd’hui éprouvés. Cependant, il reste un point douloureux pour les développeurs : l’accès aux sources de données. La manipulation de bases de données et de code XML se révèle généralement lourde et parfois problématique.

Linq.book Page 7 Mercredi, 18. février 2009 7:58 07

Chapitre 1

Hello LINQ

7

Les problèmes rencontrés dans la manipulation des bases de données sont multiples. Pour commencer, le langage n’est pas en mesure d’interagir avec les données au niveau natif. Cela signifie que, fréquemment, les erreurs de syntaxe ne sont pas détectées jusqu’à l’exécution. De même, les champs incorrectement référencés ne sont pas détectés. De telles erreurs peuvent être désastreuses, en particulier si elles se produisent pendant l’exécution d’une routine de gestion d’erreurs. Rien n’est plus frustrant qu’un mécanisme de gestion d’erreurs mis en échec à cause d’une erreur syntaxique qui n’a jamais été détectée ! Un autre problème peut provenir d’une différence entre les types des données stockés dans une base de données ou dans des éléments XML, par exemple, et les types gérés par le langage de programmation. Les données date et heure sont en particulier concernées. L’extraction, l’itération et la manipulation de données XML risquent également d’être très fastidieuses. Souvent, alors qu’un simple fragment XML doit être manipulé, il est nécessaire de créer un XmlDocument pour se conformer à l’API W3C DOM XML. Au lieu d’ajouter de nouvelles classes et méthodes pour pallier ces déficiences, les ingénieurs de Microsoft ont décidé d’aller plus loin en modifiant la syntaxe des requêtes d’interrogation. C’est ainsi que LINQ a vu le jour. Cette technologie, directement accessible dans les langages de programmation, permet d’interroger tous types de données, des tableaux mémoire aux collections en passant par les bases de données, les documents XML et bien d’autres ensembles de données. LINQ et l’interrogation des données LINQ est essentiellement un langage d’interrogation. Il peut retourner un ensemble d’objets, un objet unique ou un sous-ensemble de champs appartenant à un objet ou à un ensemble d’objets. Cet ensemble d’objets est appelé une "séquence". La plupart des séquences LINQ sont de type IEnumerable, où T est le type des objets stockés dans la séquence. Par exemple, une séquence d’entiers est stockée dans une variable de type IEnumerable. Comme vous le verrez dans la suite du livre, la plupart des méthodes LINQ retournent un IEnumerable. Dans les exemples étudiés jusqu’ici, toutes les requêtes ont retourné un IEnumerable ou un type hérité. Le mot-clé "var" a parfois été utilisé par souci de simplification. Vous verrez au Chapitre 2 qu’il s’agit d’un raccourci d’écriture. Composants La puissance et l’universalité de LINQ devraient le faire adopter dans de nombreux domaines. En fait, tous les types de données stockés sont de bons candidats aux requêtes LINQ. Ceci concerne les bases de données, Active Directory, le Registre de Windows, le système de fichiers, les feuilles de calcul Excel, etc.

Linq.book Page 8 Mercredi, 18. février 2009 7:58 07

8

LINQ et C# 2008

Partie I

Microsoft a défini plusieurs domaines de prédilection pour LINQ. Il ne fait aucun doute que cette liste sera complétée par la suite. LINQ to Objects LINQ to Objects est le nom donné à l’API IEnumerable pour les opérateurs de requête standard. Vous l’utiliserez par exemple pour requêter des tableaux et des collections de données en mémoire. Les opérateurs de requête standard LINQ to Objects sont les méthodes statiques de la classe System.Linq.Enumerable. LINQ to XML LINQ to XML est le nom de l’API dédiée au travail sur les données XML (cette interface était précédemment appelée XLINQ). LINQ to XML ne se contente pas de définir des librairies XML afin d’assurer la compatibilité avec LINQ. Il apporte également une solution à plusieurs déficiences du standard XML DOM et facilite le travail avec les données XML. À titre d’exemple, il n’est désormais plus nécessaire de créer un XmlDocument pour traiter une portion réduite de XML. Qui s’en plaindra ? Pour pouvoir travailler avec LINQ to XML, vous devez faire référence à l’assembly System.Xml.Linq.dll dans votre projet : using System.Xml.Linq;

LINQ to DataSet LINQ to DataSet est le nom de l’API permettant de travailler avec des DataSets. De nombreux développeurs utilisent ces types d’objets. Sans qu’aucune réécriture de code ne soit nécessaire, ils pourront désormais tirer avantage de la puissance de LINQ pour interroger leurs DataSets. LINQ to SQL LINQ to SQL est le nom de l’API IQueryable, qui permet d’appliquer des requêtes LINQ aux bases de données Microsoft SQL Server (cette interface était précédemment connue sous le nom DLinq). Pour pouvoir utiliser LINQ to SQL, vous devez faire référence à l’assembly System.Data.Linq.dll : using System.Data.Linq;

LINQ to Entities LINQ to Entities est une API alternative utilisée pour interfacer des bases de données. Elle découple le modèle objet entity de la base de données elle-même en ajoutant un mappage logique entre les deux. Ce découplage procure une puissance et une flexibilité accrues. Étant donné que LINQ to Entities ne fait pas partie du framework LINQ, nous ne nous y intéresserons pas dans cet ouvrage. Cependant, si LINQ to SQL ne vous

Linq.book Page 9 Mercredi, 18. février 2009 7:58 07

Chapitre 1

Hello LINQ

9

semble pas assez flexible, vous devriez vous intéresser à LINQ to Entities ; en particulier si vous avez besoin d’une plus grande souplesse entre les entités et la base de données, si vous manipulez des données provenant de plusieurs tables ou si vous voulez personnaliser la modélisation des entités. Comment travailler avec LINQ Il n’existe aucun produit LINQ à acheter ou à installer : c’est juste le nom qui a été donné à l’outil d’interrogation de C# 3.0 et au Framework .NET 3.5, apparu dans Visual Studio 2008. Pour obtenir des informations à jour sur LINQ et Visual Studio 2008, connectezvous sur les pages www.linqdev.com et http://apress.com/book/bookDisplay .html?bID=10241.

LINQ ne se limite pas aux requêtes LINQ étant l’abréviation de Language INtegrated Query (langage d’interrogation intégré), vous pourriez penser qu’il se limite à l’interrogation de données. Comme vous le verrez dans la suite du livre, son domaine d’action va beaucoup plus loin... Vous est-il déjà arrivé de devoir remanier les données renvoyées par une méthode avant de pouvoir les passer en argument à une autre méthode ? Supposons par exemple que vous appeliez la méthode A. Cette méthode retourne un tableau de string contenant des valeurs numériques stockées en tant que chaînes de caractères. Vous devez alors appeler une méthode B qui demande un tableau d’entiers en entrée. Puis mettre en place une boucle pour convertir un à un les éléments du tableau. Quelle plaie ! LINQ apporte une réponse élégante à ce problème. Supposons que nous ayons un tableau de string reçu d’une méthode A, comme indiqué dans le Listing 1.4. Listing 1.4 : Une requête XML basée sur LINQ to XML. string[] numbers = { "0042", "010", "9", "27" };

Dans cet exemple, le tableau de string a été déclaré de façon statique. Avant d’appeler la méthode B, il est nécessaire de convertir ce tableau de chaînes en un tableau d’entiers : int[] nums = numbers.Select(s => Int32.Parse(s)).ToArray();

Cette conversion pourrait-elle être plus simple ? Voici le code à utiliser pour afficher le tableau d’entiers nums : foreach(int num in nums) Console.WriteLine(num);

Linq.book Page 10 Mercredi, 18. février 2009 7:58 07

10

LINQ et C# 2008

Partie I

Et voici l’affichage résultant dans la console : 42 10 9 27

Peut-être pensez-vous que cette conversion s’est contentée de supprimer les zéros devant les nombres. Pour nous en assurer, nous allons trier les données numériques. Si tel est le cas, 9 sera affiché en dernier et 10, en premier. Le Listing 1.5 effectue la conversion et le tri des données. Listing 1.5 : Conversion d’un tableau de chaînes en entiers et tri croissant. string[] numbers = { "0042", "010", "9", "27" }; int[] nums = numbers.Select(s => Int32.Parse(s)).OrderBy(s => s).ToArray(); foreach(int num in nums) Console.WriteLine(num);

Voici le résultat : 9 10 27 42

Cela fonctionne, mais il faut bien avouer que cet exemple est simpliste. Nous allons maintenant nous intéresser à des données plus complexes. Supposons que nous disposions de la classe Employee et qu’une de ses méthodes retourne le nom des employés. Supposons également que nous disposions d’une classe Contact et qu’une de ses méthodes liste les contacts d’un des employés. Supposons enfin que vous souhaitiez obtenir la liste des contacts de chacun des employés. La tâche semble assez simple. Cependant, la méthode qui retourne le nom des employés fournit un ArrayList d’objets Employee, et la méthode qui liste les contacts nécessite un tableau de type Contact. Voici le code des classes Employee et Contact : namespace LINQDev.HR { public class Employee { public int id; public string firstName; public string lastName; public static ArrayList GetEmployees() { // Le "vrai" code ferait certainement une requête // sur une base de données à ce point précis ArrayList al = new ArrayList();

Linq.book Page 11 Mercredi, 18. février 2009 7:58 07

Chapitre 1

Hello LINQ

11

// Ajout des données dans le tableau ArrayList al al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz"} ); al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates"} ); al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg"} ); return(al); } } } namespace LINQDev.Common { public class Contact { public int Id; public string Name; public static void PublishContacts(Contact[] contacts) { // Cette méthode se contente d’afficher les contacts dans la console foreach(Contact c in contacts) Console.WriteLine("Contact Id: {0} Contact: {1}", c.Id, c.Name); } } }

Comme vous pouvez le voir, la classe Employee et la méthode GetEmployee sont dans l’espace de noms LINQDev.HR, et la méthode GetEmployees retourne un ArrayList. Quant à la méthode PublishContacts, elle se trouve dans l’espace de noms LINQDev.Common et demande un tableau d’objets Contact en entrée. Avant l’arrivée de LINQ, vous auriez dû passer en revue les ArrayList retournés par la méthode GetEmployees et créer un nouveau tableau de type Contact afin d’assurer la compatibilité avec la méthode PublishContacts. Comme le montre le Listing 1.6, LINQ facilite grandement les choses. Listing 1.6 : Appel des méthodes GetEmployees et PublishContacts. ArrayList alEmployees = LINQDev.HR.Employee.GetEmployees(); LINQDev.Common.Contact[] contacts = alEmployees .Cast() .Select(e => new LINQDev.Common.Contact { Id = e.id, Name = string.Format("{0} {1}", e.firstName, e.lastName) }) .ToArray(); LINQDev.Common.Contact.PublishContacts(contacts);

Pour convertir le tableau ArrayList d’objets Employee en un tableau d’objets Contact, nous l’avons transformé en une séquence IEnumerable en utilisant l’opérateur de requête standard Cast. Cette transformation est nécessaire car une collection héritée ArrayList est renvoyée par GetEmployees. Syntaxiquement parlant, ce sont les objets de la classe System.Object et non ceux de la classe Employee qui sont stockés dans l’ArrayList. Le casting vers des objets Employee est donc nécessaire. Si la méthode GetEmployees avait renvoyé une collection générique List, cette étape n’aurait pas été nécessaire. Malheureusement, ce type de collection n’était pas disponible lors de l’écriture de ce code hérité.

Linq.book Page 12 Mercredi, 18. février 2009 7:58 07

12

LINQ et C# 2008

Partie I

Le casting terminé, l’opérateur Select est appliqué sur la séquence d’objets Employee. Dans l’expression lambda (le code passé comme argument de la méthode Select), un objet Contact est instancié et initialisé en utilisant les valeurs retournées par les objets Employee (vous en saurez plus en consultant la section réservées aux méthodes anonymes au Chapitre 2). Pour terminer, la séquence d’objets Contact est convertie en un tableau d’objets Contact en utilisant l’opérateur ToArray. Ceci afin d’assurer la compatibilité avec la méthode PublishContacts. Voici le résultat affiché dans la console : Contact Id: 1 Contact: Joe Rattz Contact Id: 2 Contact: William Gates Contact Id: 3 Contact: Anders Hejlsberg

J’espère que vous êtes maintenant convaincu que LINQ ne se limite pas à l’interrogation de données. En parcourant les autres chapitres de ce livre, essayez de trouver de nouveaux champs d’application de LINQ.

Quelques conseils avant de commencer Pendant l’écriture de cet ouvrage, j’ai parfois été troublé, embrouillé, voire bloqué alors que j’expérimentais LINQ. Pour vous éviter de tomber dans les mêmes pièges, je vais vous donner quelques conseils. Tous les concepts propres à LINQ n’ayant pas encore été introduits, il serait logique que ces conseils figurent à la fin de l’ouvrage. Rassurez-vous : je ne vais pas vous imposer la lecture complète de l’ouvrage ! Mais ne vous formalisez pas si vous ne comprenez pas entièrement ce qui va être dit dans les pages suivantes… Utilisez le mot-clé var si vous n’êtes pas à l’aise Il n’est pas nécessaire d’utiliser le mot-clé var lorsque vous affectez une séquence de classes anonymes à une variable, mais cela peut vous aider à passer l’étape de la compilation, en particulier si vous ne savez pas exactement quel type de données vous êtes en train de manipuler. Bien entendu, il est préférable de connaître le type des données T des IEnumerable mais, parfois, en particulier lorsque vous commencez en programmation LINQ, cela peut se révéler difficile. Si le code ne veut pas se compiler à cause d’une incompatibilité dans un type de données, pensez à transformer ce type en utilisant le mot-clé var. Supposons que vous ayez le code suivant : // Ce code produit une erreur à la compilation Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders);

Linq.book Page 13 Mercredi, 18. février 2009 7:58 07

Chapitre 1

Hello LINQ

13

Il se peut que vous ne sachiez pas exactement quel est le type des données de la séquence d’IEnumerable. Une astuce bien pratique consiste à affecter le résultat de la requête à une variable dont le type est spécifié automatiquement grâce au mot-clé var, puis à obtenir son type grâce à la méthode GetType (voir Listing 1.7). Listing 1.7 : Un exemple de code qui utilise le mot-clé var. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders); Console.WriteLine(orders.GetType());

Dans cet exemple, le type de la variable orders est spécifié par l’intermédiaire du motclé var. Voici le type affiché dans la console : System.Data.Linq.DataQuery`1[nwind.Order]

Dans tout le charabia retourné par le compilateur, nwind.Order est certainement la partie la plus importante, puisqu’elle indique le type de la séquence. Si l’expression affichée dans la console vous intrigue, exécutez l’exemple dans le débogueur et examinez la variable orders dans la fenêtre Espion Express. Son type est le suivant : System.Linq.IQueryable {System.Data.Linq.DataQuery}

La séquence est donc de type nwind.Order. Il s’agit en fait d’un IQueryable, mais vous pouvez l’affecter à un IEnumerable, puisque IQueryable hérite de IEnumerable. Vous pouvez donc réécrire le code précédent et passer en revue les résultats en utilisant les instructions du Listing 1.8. Listing 1.8 : Le même code que dans le Listing 1.7, sauf au niveau des codes explicites. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders); foreach(Order item in orders) Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);

INFO Pour que ce code fonctionne, vous devez spécifier une directive using pour les espaces de noms System.Collections.Generic et System.Linq (ce deuxième espace de noms est obligatoire dès que vous utilisez des instructions en rapport avec LINQ).

Linq.book Page 14 Mercredi, 18. février 2009 7:58 07

14

LINQ et C# 2008

Partie I

Ce code produit le résultat suivant : 3/21/1997 12:00:00 AM - 10482 - Lazy K Kountry Store 5/22/1997 12:00:00 AM - 10545 - Lazy K Kountry Store … 4/17/1998 12:00:00 AM - 11032 - White Clover Markets 5/1/1998 12:00:00 AM - 11066 - White Clover Markets

Utilisez les opérateurs Cast ou OfType pour les collections héritées La grande majorité des opérateurs de requête LINQ ne peut être utilisée que sur des collections qui implémentent l’interface IEnumerable. Aucune des collections héritées de C# (celles présentes dans l’espace de noms System.Collection) n’implémente cette interface. Mais, alors, comment utiliser LINQ avec des collections héritées ? Deux opérateurs de requête standard sont là pour convertir des collections héritées en séquences IEnumerable : Cast et OfType (voir Listing 1.9). Listing 1.9 : Conversion d’une collection héritée en un IEnumerable avec l’opérateur Cast. // Création d’une collection héritée ArrayList arrayList = new ArrayList(); // L’initialisation de collections ne fonctionne pas // avec les collections héritées arrayList.Add("Adams"); arrayList.Add("Arthur"); arrayList.Add("Buchanan"); IEnumerable names = arrayList.Cast().Where(n => n.Length < 7); foreach(string name in names) Console.WriteLine(name);

Le Listing 1.10 représente le même exemple, en utilisant cette fois-ci l’opérateur OfType. Listing 1.10 : Utilisation de l’opérateur OfType. // Création d’une collection héritée ArrayList arrayList = new ArrayList(); // L’initialisation de collections ne fonctionne pas // avec les collections héritées arrayList.Add("Adams"); arrayList.Add("Arthur"); arrayList.Add("Buchanan"); IEnumerable names = arrayList.OfType().Where(n => n.Length < 7); foreach(string name in names) Console.WriteLine(name);

Ces deux exemples produisent le même résultat : Adams Arthur

Ces deux opérateurs sont quelque peu différents : Cast essaye de convertir tous les éléments de la collection dans le type spécifié. Une exception est générée si un des

Linq.book Page 15 Mercredi, 18. février 2009 7:58 07

Chapitre 1

Hello LINQ

15

éléments ne peut pas être converti. Au contraire, OfType ne convertit que les éléments qui peuvent l’être. Préférez l’opérateur OfType à l’opérateur Cast Les génériques ont été implémentés dans C# pour permettre une vérification de type statique (c’est-à-dire pendant la compilation) sur les collections. Avant l’apparition des génériques, il n’y avait aucun moyen de s’assurer que les éléments d’une collection héritée (un ArrayList ou un Hashtable, par exemple) étaient tous de même type et avaient le type requis. Rien par exemple n’empêchait l’insertion d’un objet Textbox dans un ArrayList supposé ne contenir que des objets Label. Avec l’apparition des génériques dans C# 2.0, les développeurs peuvent désormais s’assurer qu’une collection ne contient que des éléments dont le type est spécifié. Bien que les opérateurs OfType et Cast soient utilisables sur une collection héritée, Cast nécessite que tous les objets de la collection aient le type attendu. Pour éviter de générer des exceptions en cas d’incompatibilité de type, préférez-lui l’opérateur OfType. Par son intermédiaire, seuls les objets du type spécifié seront stockés dans la séquence IEnumerable, et aucune exception ne sera générée. Le cas échéant, les objets dont le type n’est pas celui attendu ne seront pas convertis. Les requêtes aussi peuvent être boguées Au Chapitre 3, vous verrez que les requêtes LINQ sont souvent différées. Elles ne sont donc pas exécutées dès leur invocation. Considérez par exemple le code suivant, extrait du Listing 1.1 : var items = from s in greetings where s.EndsWith("LINQ") select s; foreach (var item in items) Console.WriteLine(item);

Contrairement à ce que vous pourriez penser, la requête n’est pas exécutée à l’initialisation de la variable items. Elle ne sera exécutée que lorsqu’une ligne de code aura besoin de son résultat ; typiquement lors de l’énumération du résultat de la requête. Ici, le résultat de la requête n’est pas calculé jusqu’à ce que l’instruction foreach soit exécutée. On oublie souvent que l’exécution d’une requête est différée jusqu’à l’énumération de sa séquence. Une requête mal formulée pourrait ainsi produire une erreur bien des lignes plus loin, lorsque sa séquence est énumérée, et le programmeur pourrait avoir du mal à penser que la requête en est l’origine. Examinons le code du Listing 1.11.

Linq.book Page 16 Mercredi, 18. février 2009 7:58 07

16

LINQ et C# 2008

Partie I

Listing 1.11 : Cette requête contient une erreur intentionnelle qui n’est levée qu’à l’énumération. string[] strings = { "un", "deux", null, "trois" }; Console.WriteLine("Avant l’appel à Where()"); IEnumerable ieStrings = strings.Where(s => s.Length == 3); Console.WriteLine("Après l’appel à Where()"); foreach(string s in ieStrings) { Console.WriteLine("Traitement " + s); }

Le troisième élément du tableau a pour valeur null. L’expression null.Length va produire une exception lors de l’énumération de la séquence ieStrings, et en particulier de son troisième élément. Pourtant, la ligne à l’origine de l’erreur est allègrement passée… Voici le résultat obtenu à l’exécution de ce code : Avant l’appel à Where() Après l’appel à Where() Traitement un Traitement deux Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object. …

L’opérateur Where n’a pas produit d’exception. L’exception a seulement été levée lorsque l’on a essayé de lire le troisième élément de la séquence. Imaginez que la séquence ieStrings soit passée à une fonction qui énumère la séquence dans une liste déroulante ou un contrôle équivalent. Penseriez-vous que l’exception provient de la requête LINQ ? Il y a de grandes chances pour que vous cherchiez l’erreur dans le code de la fonction… Sachez tirer parti des requêtes différées Au Chapitre 3, vous en apprendrez bien plus sur les requêtes différées. Cependant, je voudrais dès à présent insister sur le fait que, si une requête différée retourne un IEnumerable, cet objet peut être énuméré autant de fois que nécessaire sans pour autant devoir rappeler la requête. La plupart des codes de cet ouvrage appellent une requête et stockent l’ IEnumerable retourné dans une variable. Une instruction foreach est alors appliquée sur la séquence IEnumerable à des fins démonstratives. Si ce code est exécuté à plusieurs reprises, il n’est pas nécessaire de rappeler la requête à chaque exécution. Il serait plus judicieux d’écrire une méthode d’initialisation et d’y placer toutes les requêtes nécessaires. Cette méthode serait appelée une fois. Vous pourriez alors énumérer la séquence de votre choix pour obtenir la dernière version des résultats. Openmirrors.com

Linq.book Page 17 Mercredi, 18. février 2009 7:58 07

Chapitre 1

Hello LINQ

17

Utiliser le log du DataContext Lorsque vous travaillerez avec LINQ to SQL, vous devrez garder à l’esprit que la classe relative à la base de données, générée par SQLMetal, hérite de System.Data.Linq.DataContext. Cette classe dispose donc de quelques fonctionnalités préinstallées. Entre autres de l’objet TextWriter Log. Si vous avez déjà expérimenté une rupture de code liée aux données, vous serez ravi d’apprendre qu’il est possible d’utiliser l’objet Log du DataContext pour observer les données résultant de la requête, tout comme vous le feriez dans SQL Server Enterprise Manager ou Query Analyzer (voir l’exemple du Listing 1.12). Listing 1.12 : Un exemple d’utilisation du log du DataContext. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; IQueryable orders = from c in db.Customers from o in c.Orders where c.Country == "USA" && c.Region == "WA" select o; foreach(Order item in orders) Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);

Ce code produit la sortie suivante dans la console : SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[RequiredDate], [t1].[ShippedDate], [t1].[ShipVia], [t1].[Freight], [t1].[ShipName], [t1].[ShipAddress], [t1].[ShipCity], [t1].[ShipRegion], [t1].[ShipPostalCode], [t1].[ShipCountry] FROM [dbo].[Customers] AS [t0], [dbo].[Orders] AS [t1] WHERE ([t0].[Country] = @p0) AND ([t0].[Region] = @p1) AND ([t1].[CustomerID] = [t0].[CustomerID]) -- @p0: Input String (Size = 3; Prec = 0; Scale = 0) [USA] -- @p1: Input String (Size = 2; Prec = 0; Scale = 0) [WA] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 3/21/1997 12:00:00 AM - 10482 - Lazy K Kountry Store 5/22/1997 12:00:00 AM - 10545 - Lazy K Kountry Store 6/19/1997 12:00:00 AM - 10574 - Trail’s Head Gourmet Provisioners 6/23/1997 12:00:00 AM - 10577 - Trail’s Head Gourmet Provisioners 1/8/1998 12:00:00 AM - 10822 - Trail’s Head Gourmet Provisioners 7/31/1996 12:00:00 AM - 10269 - White Clover Markets 11/1/1996 12:00:00 AM - 10344 - White Clover Markets 3/10/1997 12:00:00 AM - 10469 - White Clover Markets 3/24/1997 12:00:00 AM - 10483 - White Clover Markets 4/11/1997 12:00:00 AM - 10504 - White Clover Markets 7/11/1997 12:00:00 AM - 10596 - White Clover Markets 10/6/1997 12:00:00 AM - 10693 - White Clover Markets 10/8/1997 12:00:00 AM - 10696 - White Clover Markets 10/30/1997 12:00:00 AM - 10723 - White Clover Markets 11/13/1997 12:00:00 AM - 10740 - White Clover Markets 1/30/1998 12:00:00 AM - 10861 - White Clover Markets 2/24/1998 12:00:00 AM - 10904 - White Clover Markets 4/17/1998 12:00:00 AM - 11032 - White Clover Markets 5/1/1998 12:00:00 AM - 11066 - White Clover Markets

Linq.book Page 18 Mercredi, 18. février 2009 7:58 07

18

LINQ et C# 2008

Partie I

Utilisez le forum LINQ Il y a fort à parier que, tôt ou tard, vous vous retrouverez dans une situation bloquante en expérimentant LINQ. N’hésitez pas à faire appel au forum dédié à LINQ sur MSDN.com, en vous connectant à l’adresse www.linqdev.com. Ce forum est suivi par les développeurs Microsoft. Vous y trouverez de nombreuses ressources très intéressantes.

Résumé Je sens que vous êtes impatient de passer au chapitre suivant. Je voudrais cependant vous rappeler quelques petites choses avant que vous ne tourniez les pages. LINQ va changer la façon dont les développeurs .NET interrogent leurs données. Les éditeurs de logiciels vont certainement ajouter un sticker "Compatible LINQ" sur leurs produits, tout comme ils le font actuellement avec XML. Gardez bien en mémoire que LINQ n’est pas juste une nouvelle librairie que vous ajoutez à vos projets. Il s’agit d’une tout autre approche pour interroger vos données, consistant en plusieurs composants qui dépendent de la source de données à interroger. Alors que nous écrivons ces lignes, vous pouvez utiliser LINQ pour interroger des collections de données en mémoire avec LINQ to Objects, des fichiers XML avec LINQ to SQL, des DataSets avec LINQ to DataSets et des bases de données SQL Server avec LINQ to SQL. Rappelez-vous également que LINQ n’est pas simplement un langage de requête. Dans un de mes projets, j’ai utilisé LINQ avec succès non seulement pour interroger des sources de données, mais également pour modifier le format des données afin de les présenter dans une fenêtre WinForm. Enfin, j’espère que vous tiendrez compte des astuces que j’ai mentionnées à la fin de ce chapitre. Si vous ne comprenez pas entièrement certaines d’entre elles, ce n’est pas un problème. Vous en saisirez toutes les subtilités au fur et à mesure de votre progression dans le livre. Stockez-les dans un coin de votre tête : elles vous feront gagner du temps. Après vous être intéressé aux exemples et conseils de ce chapitre, vous êtes peut-être perplexe devant la syntaxe de LINQ. Ne vous en faites pas, au prochain chapitre vous allez découvrir en détail toutes les modifications apportées au langage C# 3.0 par Microsoft et comprendrez plus facilement le code.

Openmirrors.com

Linq.book Page 19 Mercredi, 18. février 2009 7:58 07

2 Améliorations de C# 3.0 pour LINQ Le chapitre précédent vous a initié au monde merveilleux de LINQ. J’y ai donné quelques exemples pour attiser votre appétit et des astuces qui pourront vous paraître quelque peu prématurées. Certaines syntaxes vous laissent peut-être perplexe, car le code revêt un aspect entièrement nouveau. C# a en effet dû être remanié pour supporter les fonctionnalités avancées de LINQ. Dans ce chapitre, vous allez découvrir les facettes les plus innovantes de C# 3.0.

Les nouveautés du langage C# 3.0 Pour que LINQ s’intègre parfaitement dans C#, des améliorations significatives ont dû être apportées au langage. Toutes les améliorations déterminantes ont été dictées par le support de LINQ. Bien que chacune d’entre elles soit intéressante en tant que telle, c’est l’ensemble qui fait de C# 3.0 un langage si puissant. Pour bien comprendre la syntaxe de LINQ, vous devez au préalable vous intéresser à certaines nouvelles fonctionnalités de C# 3.0. Ce chapitre va passer en revue les nouveautés suivantes : m

les expressions lambda ;

m

les arbres d’expressions ;

m

le mot-clé var, l’initialisation des objets et des collections et les types anonymes ;

m

les méthodes d’extension ;

m

les méthodes partielles ;

m

les expressions de requête.

Linq.book Page 20 Mercredi, 18. février 2009 7:58 07

20

LINQ et C# 2008

Partie I

Les assemblies et espaces de noms nécessaires à la bonne exécution des exemples de ce chapitre ne seront pas mentionnés s’ils ont déjà été utilisés au Chapitre 1. En revanche, les nouveaux assemblies et espaces de noms seront signalés lors de leur première utilisation. Les expressions lambda Bien qu’inventées en 1936 par le mathématicien américain Alonzo Church et utilisées dans des langages aussi anciens que LISP, les expressions lambda sont une nouveauté du langage C# 3.0. Leur but premier vise à simplifier la syntaxe des algorithmes. Avant de nous intéresser aux expressions lambda, nous allons nous attarder quelques instants sur la possibilité de passer un algorithme dans un argument d’une méthode. Utilisation de méthodes nommées Avant la sortie de C# 2.0, lorsqu’une méthode/une variable avait besoin d’un délégué, le développeur devait créer une méthode nommée et passer ce nom à chaque utilisation du délégué.

Supposons que deux développeurs travaillent sur un même projet. Le développeur numéro 1 crée un code réutilisable et le développeur numéro 2 utilise ce code pour créer une application. Supposons que le développeur 1 définisse une méthode générique permettant de filtrer des tableaux d’entiers, en permettant de spécifier l’algorithme de tri à utiliser. Dans un premier temps, il crée un délégué qui reçoit un entier et retourne la valeur true si la valeur passée peut être incluse dans le tableau. Ainsi, il créé une classe utilitaire et ajoute le délégué et la méthode de filtre. Voici le code utilisé : public class Common { public delegate bool IntFilter(int i); public static int[] FilterArrayOfInts(int[] ints, IntFilter filter) { ArrayList aList = new ArrayList(); foreach (int i in ints) { if (filter(i)) { aList.Add(i); } } return ((int[])aList.ToArray(typeof(int))); } }

Le développeur numéro 1 a placé le délégué et la méthode FilterArrayOfInt() dans une DLL (Dynamic Link Library) afin de les rendre accessibles dans plusieurs applications. Openmirrors.com

Linq.book Page 21 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

21

La méthode FilterArrayOfInt() du listing précédent admet deux paramètres en entrée : le tableau à trier et un délégué qui fait référence à la méthode de tri à utiliser. Le tableau d’entiers trié est renvoyé par la méthode. Supposons maintenant que le développeur numéro 2 veuille limiter le tri aux entiers impairs. Voici la méthode de tri utilisée : public class Application { public static bool IsOdd(int i) { return ((i & 1) == 1); } }

En se basant sur le code de la méthode FilterArrayOfInts, la méthode IsOdd sera appelée pour tous les entiers du tableau qui lui seront passés. Ce filtre ne retournera la valeur true que dans le cas où l’entier passé est impair. Le Listing 2.1 donne un exemple d’utilisation de la méthode FilterArrayOfInts. Listing 2.1 : Appel de la méthode commune FilterArrayOfInts. using System.Collections; int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums, Application.IsOdd); foreach (int i in oddNums) Console.WriteLine(i);

Voici le résultat : 1 3 5 7

Comme vous pouvez le remarquer, pour passer le délégué dans le second paramètre de la méthode FilterArrayOfInts, il suffit d’indiquer son nom. En définissant un autre filtre, le résultat peut être tout autre. Il est ainsi possible de définir un filtre pour les nombres pairs, pour les nombres premiers ou pour un tout autre critère. Les délégués sont intéressants chaque fois que le code doit être utilisé à plusieurs reprises. Utiliser des méthodes anonymes Cet exemple fonctionne à la perfection, mais à la longue il peut être fastidieux d’écrire tous les filtres et autres délégués dont vous avez besoin : la plupart de ces méthodes seront appelées une seule fois et il peut être frustrant de créer autant de méthodes que de tris nécessaires. Depuis C# 2.0, les développeurs peuvent faire appel aux méthodes anonymes, afin de passer du code comme argument et ainsi d’éviter l’utilisation de délégués.

Linq.book Page 22 Mercredi, 18. février 2009 7:58 07

22

LINQ et C# 2008

Partie I

Dans cet exemple, plutôt que créer la méthode IsOdd, le code de filtrage est passé dans l’argument (voir Listing 2.2). Listing 2.2 : Appel du filtre par l’intermédiaire d’une méthode anonyme. int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums, delegate(int i) { return ((i & 1) == 1); }); foreach (int i in oddNums) Console.WriteLine(i);

Comme vous le voyez, il n’est plus nécessaire de définir une méthode de filtrage. Cette technique est particulièrement intéressante si le code qui remplace le délégué a peu de chances d’être utilisé à plusieurs reprises. Le résultat est bien entendu identique à celui de l’exemple précédent : 1 3 5 7

Les méthodes anonymes ont un inconvénient : elles sont verbeuses et difficiles à lire. Il serait vraiment agréable de pouvoir écrire le code de la méthode d’une manière plus concise ! Utiliser les expressions lambda En C#, les expressions lambda consistent en une liste de paramètres séparés entre eux par des virgules1, suivis de l’opérateur lambda (=>) puis d’une expression ou d’une déclaration. (param1, param2, …paramN) => expr

Si l’expression/la déclaration est plus complexe, vous pouvez utiliser un bloc délimité par les caractères { et } : (param1, param2, …paramN) => { statement1; statement2; ... statementN; return(lambda_expression_return_type); }

Dans cet exemple, le type de données renvoyé par l’instruction return doit correspondre au code de retour spécifié par le délégué. Voici un exemple d’expression lambda : x => x

1. Si les paramètres sont au nombre de deux (ou plus), ils doivent être délimités par des parenthèses.

Openmirrors.com

Linq.book Page 23 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

23

Cette expression lambda pourrait se lire "x conduit à x" ou encore "entrée x sortie x". Cela signifie que la variable d’entrée x est également renvoyée par l’expression lambda. Étant donné que la fonction ne compte qu’un seul paramètre en entrée, il n’est pas nécessaire de l’entourer de parenthèses. Il est important d’avoir à l’esprit que le délégué détermine le type de l’entrée x ainsi que le type qui doit être retourné. Par exemple, si le délégué définit une chaîne en entrée et retourne un booléen, l’expression x => x ne peut pas être utilisée. Dans ce cas, la partie à droite de l’opérateur lambda doit retourner un booléen. Par exemple : x => x.Length > 0

Cette expression lambda pourrait se lire "x conduit à x.Length > 0" ou encore "entrée x, sortie x.Length > 0". Étant donné que la partie à droite de l’opérateur lambda est équivalente à un booléen, le délégué doit indiquer que la méthode renvoie un booléen, sans quoi une erreur se produira à la compilation. L’expression lambda ci-après tente de retourner la longueur de l’argument fourni en entrée. Le délégué doit donc spécifier que la valeur retournée est de type entier ( int). s => s.Length

Si plusieurs paramètres sont passés en entrée de l’expression lambda, séparez-les par des virgules et entourez-les par des parenthèses, comme dans l’expression suivante : (x, y) => x == y

Les expressions lambda complexes peuvent être spécifiées à l’intérieur d’un bloc, comme dans : (x, y) => { if (x > y) return (x); else return (y); }

ATTENTION Gardez à l’esprit que le délégué doit indiquer le type des paramètres en entrée et de l’élément renvoyé. Dans tous les cas, assurez-vous que ces éléments sont en accord avec les types définis dans le délégué.

Pour vous rafraîchir la mémoire, voici la déclaration delegate définie par le programmeur numéro 1 : delegate bool IntFilter(int i);

L’application développée par le programmeur numéro 2 devra accepter un paramètre de type int et retourner une valeur de type bool. Cela peut se déduire de la méthode appelée et du but du filtre, mais dans tous les cas rappelez-vous que c’est le délégué qui dicte les types en entrée et en sortie.

Linq.book Page 24 Mercredi, 18. février 2009 7:58 07

24

LINQ et C# 2008

Partie I

En utilisant une expression lambda, l’exemple précédent se transforme en le Listing 2.3. Listing 2.3 : Appel du filtre avec une expression lambda. int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums, i => ((i & 1) == 1)); foreach (int i in oddNums) Console.WriteLine(i);

Ce code est vraiment concis. S’il vous semble quelque peu déroutant, une fois que vous y serez habitué vous verrez à quel point il est réutilisable et facile à maintenir. Bien entendu, les résultats sont les mêmes que dans les exemples précédents : 1 3 5 7

Pour récapituler, voici quelques instructions concernant les trois approches dont nous venons de parler : int[] oddNums = // Approche méthode nommée Common.FilterArrayOfInts(nums, Application.IsOdd); int[] oddNums = // Approche méthode anonyme Common.FilterArrayOfInts(nums, delegate(int i){return((i & 1) == 1);}); int[] oddNums = // Approche expression lambda Common.FilterArrayOfInts(nums, i => ((i & 1) == 1));

La première version semble plus courte que les autres, mais vous devez garder à l’esprit qu’elle est associée à une méthode nommée dans laquelle est défini le traitement à effectuer. Cette alternative sera certainement le meilleur choix si la méthode doit être réutilisée et/ou si l’algorithme mis en œuvre est complexe et/ou doit être confié à des spécialistes. ASTUCE Les algorithmes complexes et/ou réutilisés sont mieux gérés par des méthodes nommées. Ils sont alors accessibles à tout développeur, même s’il ne saisit pas toutes les nuances du code mis en œuvre.

C’est au développeur de choisir quelle méthode est la plus appropriée dans son cas précis : une méthode nommée, une méthode anonyme ou une expression lambda. Les expressions lambda peuvent être passées comme argument des requêtes LINQ. Étant donné que ces requêtes ont toutes les chances d’utiliser des arguments à usage unique ou en tout cas peu réutilisés, l’alternative des opérateurs lambda offre une Openmirrors.com

Linq.book Page 25 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

25

grande flexibilité et n’oblige pas le programmeur à écrire une méthode nommée pour chaque requête. Arbres d’expressions Les arbres d’expressions permettent de représenter sous la forme d’arbres les expressions lambda utilisées dans des requêtes. Ils autorisent l’évaluation simultanée de tous les opérateurs impliqués dans une requête. Ils semblent donc parfaitement adaptés à la manipulation de sources de données telles que celles embarquées dans une base de données. Dans la plupart des exemples passés en revue jusqu’ici, les opérateurs de requête ont été exécutés de façon séquentielle. Examinons le code ci-après : int[] nums = new int[] { 6, 2, 7, 1, 9, 3 }; IEnumerable numsLessThanFour = nums .Where(i => i < 4) .OrderBy(i => i);

Cette requête utilise les opérateurs Where et OrderBy, qui attendent des méthodes déléguées en argument. Lorsque ce code est compilé, L’IL (Intermediate Language) .NET fabriqué est identique à celui que produirait une méthode anonyme pour chacun des opérateurs des expressions lambda. À l’exécution, les opérateurs Where puis OrderBy sont appelés successivement. Cette exécution séquentielle des opérateurs semble convenir dans cet exemple, mais supposez que cette requête soit appliquée dans une source de données volumineuse (une base de données, par exemple). Cela aurait-il un sens de filtrer les données une première fois avec l’opérateur Where, puis une seconde avec l’opérateur OrderBy. Cette technique n’est évidemment pas applicable aux requêtes de bases de données ni potentiellement à d’autres types de requêtes. C’est ici que les arbres d’expressions prennent toute leur importance. Ils autorisent en effet l’évaluation et l’exécution simultanées de tous les opérateurs d’une requête. Le compilateur est donc maintenant en mesure de coder deux types de codes pour une expression lambda : du code IL ou un arbre d’expressions. C’est le prototype de l’opérateur qui détermine quel type de code sera généré. Si sa déclaration l’autorise à accepter une méthode déléguée, du code IL sera généré. Si sa déclaration l’autorise à accepter une expression d’une méthode déléguée, un arbre d’expressions sera généré. À titre d’exemple, nous allons nous intéresser à deux implémentations différentes de l’opérateur Where. La première est l’opérateur de requête standard Where de l’API LINQ to Objects, définie dans la classe System.Linq.Enumerable : public static IEnumerable Where( this IEnumerable source, Func predicate);

Linq.book Page 26 Mercredi, 18. février 2009 7:58 07

26

LINQ et C# 2008

Partie I

La seconde implémentation de l’opérateur Where provient de l’API LINQ to SQL et de la classe System.Linq.Queryable : public static IQueryable Where( this IQueryable source, System.Linq.Expressions.Expression predicate);

Comme vous pouvez le voir, le premier opérateur Where accepte la méthode déléguée Func en argument. Du code IL sera donc généré par le compilateur pour l’expression lambda de cet opérateur. Reportez-vous au Chapitre 3 pour avoir plus d’informations sur le délégué Func. Pour l’instant, il vous suffit de comprendre que le délégué Func définit la signature de l’argument. Le deuxième opérateur Where accepte un arbre d’expressions (Expression) en argument. Le compilateur générera donc un arbre d’expressions pour représenter les données. Les opérateurs qui admettent une séquence IEnumerable comme premier argument utilisent des délégués pour manipuler les expressions lambda. En revanche, les opérateurs qui admettent une séquence IQueryable comme premier argument utilisent des arbres d’expressions. INFO Le compilateur produit du code IL pour les méthodes d’extension des séquences IEnumerable, alors qu’il produit des arbres d’expressions pour les méthodes d’extension des séquences IQueryable.

Le développeur qui se contente d’utiliser LINQ n’est pas obligé de connaître les tenants et les aboutissants des arbres d’expressions. C’est la raison pour laquelle cet ouvrage n’ira pas plus loin dans les fonctionnalités avancées des arbres d’expressions. Le mot-clé var, l’initialisation d’objets et les types anonymes Il est quasiment impossible de s’intéresser au mot-clé var et à l’inférence de type sans aborder l’initialisation des objets et les types anonymes. De même, il est quasiment impossible de s’intéresser à l’initialisation d’objets et aux types anonymes en passant sous silence le mot-clé var. Étant donné leurs fortes imbrications, plutôt que décrire séparément ces trois nouveautés du langage C#, je vais vous les présenter simultanément. Examinez la déclaration ciaprès : var1 mySpouse = new {2 FirstName = "Vickey"3, LastName = "Rattz" };

1. Le mot-clé var apparaît clairement devant le nom de la variable. 2. Un type anonyme sera utilisé, car l’opérateur new est utilisé sans préciser une classe nommée. 3. L’objet anonyme sera explicitement initialisé en utilisant la nouvelle fonctionnalité d’initialisation d’objet.

Openmirrors.com

Linq.book Page 27 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

27

Dans cet exemple, la variable mySpouse est déclarée en utilisant le mot-clé var. Cette variable se voit assigner un type anonyme… dont le type est connu grâce aux nouveautés de C# en matière d’initialisation d’objets. Cette simple ligne de code tire parti du mot-clé var, des types anonymes et de l’initialisation d’objets. Pour résumer, le mot-clé var permet de déduire le type d’un objet en tenant compte du type des données utilisées pour l’initialiser. Les types anonymes permettent donc de créer des types de classes à la volée. Comme le laisse prévoir le mot "anonyme", ces nouveaux types de données n’ont pas de nom. Il n’est pas simple de créer une donnée anonyme sans connaître ses variables membres, et vous ne pouvez pas connaître ses variables membres sans connaître leurs types. Enfin, vous ne pouvez pas connaître le type de ses membres jusqu’à ce qu’ils soient initialisés. Mais, rassurez-vous, la fonctionnalité d’initialisation de C# 3.0 gère tout ce fatras pour vous ! Lorsque cette ligne de code passera entre les mains du compilateur, une nouvelle classe de type anonyme sera créée. Elle contiendra deux membres de type String : FirstName et LastName. Le mot-clé var est implicitement typé pour les variables locales L’introduction des types anonymes dans le langage C# a induit un problème sousjacent : si une variable dont le type n’est pas défini est instanciée avec un objet de type anonyme, quel sera le type de la variable ? Considérez le code ci-après : // Ce code n’est pas compilable ! ??? unnamedTypeVar = new {firstArg = 1, secondArg = "Joe" };

Quel type déclareriez-vous pour la variable unnamedTypeVar ? Pour résoudre ce problème, le mot-clé var a été défini par les ingénieurs en charge du développement du langage C# chez Microsoft. Ce mot-clé informe le compilateur qu’il doit implicitement définir le type de la variable en utilisant l’initialiseur de la variable. Si vous ne définissez pas un initialiseur, il en résultera une erreur à la compilation. Le Listing 2.4 représente un code qui déclare une variable avec le mot-clé var sans l’initialiser. Listing 2.4 : Une déclaration de variable invalide utilisant le mot-clé var. var name;

Voici l’erreur générée par le compilateur. Implicitly-typed local variables must be initialized

Étant donné que le type des variables est vérifié de façon statique à la compilation, il est nécessaire de définir un initialiseur pour que le compilateur puisse faire son travail jusqu’au bout. Mais, attention, vous ne devrez pas affecter une valeur d’un autre type à

Linq.book Page 28 Mercredi, 18. février 2009 7:58 07

28

LINQ et C# 2008

Partie I

cette variable dans la suite du code, sans quoi une erreur se produira à la compilation. Examinons le code du Listing 2.5. Listing 2.5 : Une affectation incorrecte à une variable déclarée avec le mot-clé var. var name = "Joe"; // Jusqu’ici, tout va bien name = 1; // Ceci est incorrect ! Console.WriteLine(name);

Ce code ne passera pas l’étape de la compilation, car le type de la variable est implicitement défini à String par sa première affectation. Il est donc impossible de lui affecter une valeur entière par la suite. Voici l’erreur générée par le compilateur : Cannot implicitly convert type ’int’ to ’string’

Comme vous le voyez, le compilateur s’occupe de la cohérence du type des données affectées à la variable. Pour en revenir à la déclaration du type anonyme unnamedTypeVar, la syntaxe à utiliser est celle du Listing 2.6. Listing 2.6 : Un type anonyme affecté à une variable déclarée avec le mot-clé var. var unnamedTypeVar = new {firstArg = 1, secondArg = "Joe" }; Console.WriteLine(unnamedTypeVar.firstArg + ". " + unnamedTypeVar.secondArg);

Voici le résultat de ce code : 1.Joe

L’utilisation du mot-clé var apporte deux avantages : la vérification de type statique et la flexibilité apportée par le support des types anonymes. Ce dernier point deviendra très important lorsque nous nous intéresserons aux opérateurs de projection dans la suite de l’ouvrage. Dans les exemples passés en revue jusqu’ici, le mot-clé var était obligatoire. En effet, si vous affectez un objet résultant d’une classe anonyme à une variable, cette dernière doit être déclarée avec le mot-clé var. Notez cependant que le mot-clé var peut être utilisé à chaque déclaration de variable, à condition que cette dernière soit correctement initialisée. Pour des questions de maintenance du code, il n’est cependant pas conseillé d’abuser de cette technique : les développeurs devraient toujours connaître le type des données qu’ils manipulent. Bien sûr, vous connaissez le type de vos données aujourd’hui, mais qu’en sera-t-il dans six mois ? Et si un autre programmeur prend la relève ? ASTUCE Afin de faciliter la maintenance de votre code, n’abusez pas du mot-clé var. Ne l’utilisez que lorsque cela est nécessaire. Par exemple lorsque vous affectez un objet de type anonyme à une variable.

Openmirrors.com

Linq.book Page 29 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

29

Expressions d’initialisation d’objets et de collections Les types anonymes autorisant l’utilisation de types de données dynamiques, le mode d’initialisation des objets et des collections a été simplifié, essentiellement grâce aux expressions lambda ou aux arbres d’expressions.

Initialisation d’objets Vous pouvez désormais spécifier les valeurs des membres et propriétés public d’une classe pendant son instanciation : public class Address { public string address; public string city; public string state; public string postalCode; }

Sans la fonctionnalité d’initialisation ajoutée à C# 3.0, vous n’auriez pas pu utiliser un constructeur spécialisé, et vous auriez dû définir un objet de type Address, comme dans le Listing 2.7. Listing 2.7 : Instanciation et initialisation de la classe avec l’ancienne méthode. Address address = new Address(); address.address = "105 Elm Street"; address.city = "Atlanta"; address.state = "GA"; address.postalCode = "30339";

Cette technique serait très lourde dans une expression lambda. Supposons que vous ayez défini une requête à partir d’une source de données et que vous vouliez projeter certains membres dans un objet Address en utilisant l’opérateur Select : // Ce code ne passera pas la compilation IEnumerable addresses = somedatasource .Where(a => a.State = "GA") .Select(a => new Address(???)???);

Il n’existe aucun moyen simple d’initialiser les membres de l’objet Address. N’ayez crainte : l’initialisation d’objet de C# 3.0 est la solution. Bien sûr, il serait possible de créer un constructeur qui vous permettrait de passer les valeurs à initialiser à l’instanciation de l’objet. Mais quel travail ! Le Listing 2.8 montre comment résoudre le problème par l’intermédiaire d’un type anonyme construit à la volée. Listing 2.8 : Instanciation et initialisation de la classe avec la nouvelle méthode. Address address = new Address { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" };

Linq.book Page 30 Mercredi, 18. février 2009 7:58 07

30

LINQ et C# 2008

Partie I

Les expressions lambda autorisent ce genre de manipulation, y compris en dehors des requêtes LINQ ! Le compilateur instancie les membres nommés avec les valeurs spécifiées. Les éventuels membres non spécifiés utiliseront le type de données par défaut. Initialisation de collections Les ingénieurs de Microsoft ont également mis au point une technique d’initialisation de collections. Il vous suffit pour cela de spécifier les valeurs de la collection, tout comme vous le feriez pour un objet. Une restriction : la collection doit implémenter l’interface System.Collections.Generic.ICollection. Les collections C# héritées (celles qui se trouvent dans l’espace de noms System.Collection) ne sont pas concernées. Le Listing 2.9 donne un exemple d’initialisation de collection. Listing 2.9 : Un exemple d’initialisation de collection. using System.Collections.Generic; List presidents = new List { "Adams", "Arthur", "Buchanan" }; foreach(string president in presidents) { Console.WriteLine(president); }

Voici le résultat obtenu lorsque vous exécutez le programme en appuyant sur Ctrl+F5 : Adams Arthur Buchanan

Vous pouvez également utiliser cette technique pour créer facilement des collections initialisées dans le code, même si vous n’utilisez pas LINQ. Types anonymes C# étant dans l’impossibilité de créer de nouveaux types de données à la compilation, il est difficile de définir une nouvelle API agissant au niveau du langage pour les requêtes génériques. Les ingénieurs qui ont mis au point le langage C# 3.0 ont relevé cette prouesse : désormais, il est possible de créer dynamiquement des classes non nommées et des propriétés dans ces classes. Ce type de classe est appelé "type anonyme".

Un type anonyme n’a pas de nom et est généré à la compilation, en initialisant un objet en cours d’instanciation. Étant donné que la classe n’a pas de type, toute variable affectée à un objet d’un type anonyme doit pouvoir le déclarer. C’est là qu’intervient le motclé new de C# 3.0. Un type anonyme ne peut pas être estimé s’il est issu d’un opérateur Select ou SelectMany. Sans les types anonymes, des classes nommées devraient être définies pour receOpenmirrors.com

Linq.book Page 31 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

31

voir des données issues des opérateurs Select ou SelectMany. Ceci se révélerait très lourd et peu pratique à mettre en place. Dans la section relative à l’initialisation d’objets, j’ai introduit le code d’instanciation et d’initialisation suivant : Address address = new Address { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" };

Pour utiliser un type anonyme à la place de la classe nommée Address, il suffit d’omettre le nom de la classe. Notez cependant qu’il est impossible de stocker le nouvel objet instancié dans une variable de type Address, car l’objet n’est pas encore de type Address. Son type n’est connu que du compilateur. Il est donc également nécessaire de changer le type de données de la variable address en utilisant le mot-clé var (voir Listing 2.10). Listing 2.10 : Instanciation et initialisation d’un type anonyme en utilisant l’initialisation d’objets. var address = new { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" }; Console.WriteLine("address = {0} : city = {1} : state = {2} : zip = {3}", address.address, address.city, address.state, address.postalCode); Console.WriteLine("{0}", address.GetType().ToString());

La dernière ligne a été ajoutée pour afficher le nom de la classe anonyme générée par le compilateur. Voici le résultat : address = 105 Elm Street : city = Atlanta : state = GA : zip = 30339 f__AnonymousType5`4[System.String,System.String,System.String,System.String]

Ce nom peu orthodoxe laisse clairement entendre qu’il a été généré par un compilateur (le nom généré par votre compilateur a de grandes chances d’être différent). Méthodes d’extension Une méthode d’extension est une méthode ou une classe statique qui peut être invoquée comme s’il s’agissait d’une méthode d’instance d’une classe différente. Vous pourriez par exemple créer la méthode statique d’extension ToDouble dans la classe statique StringConversions. Cette méthode serait appelée comme s’il s’agissait d’une méthode d’un objet de type string.

Linq.book Page 32 Mercredi, 18. février 2009 7:58 07

32

LINQ et C# 2008

Partie I

Avant d’entrer dans le détail des méthodes d’extension, nous allons nous intéresser au problème qui leur a donné naissance. Nous allons comparer les méthodes statiques (class) aux méthodes d’instance (object). Les méthodes d’instance peuvent seulement être appelées dans les instances d’une classe, aussi appelées objets. Il est impossible d’appeler une méthode d’instance dans la classe elle-même. Au contraire, les méthodes statiques ne peuvent être appelées qu’à l’intérieur d’une classe. Rappel sur les méthodes d’instance et les méthodes statiques La méthode ToUpper de la classe string est un exemple d’une méthode d’instance : elle ne peut être appelée que sur un objet string. En aucun cas sur la classe string elle-même.

Dans le code du Listing 2.11, la méthode ToUpper est appelée sur l’objet name. Listing 2.11 : Appel d’une méthode d’instance d’un objet. // Ce code passe l’étape de la compilation string name = "Joe"; Console.WriteLine(name.ToUpper());

Ce code est compilable. Son exécution affiche la conversion en majuscules de la variable name : JOE

Si vous essayez d’appeler la méthode ToUpper sur la classe string, vous obtiendrez une erreur de compilation, car ToUpper est une méthode d’instance. Elle ne peut donc être appelée qu’à partir d’un objet et non d’une classe. Le Listing 2.12 donne un exemple d’un tel code. Listing 2.12 : Tentative d’appel d’une méthode d’instance sur une classe. // Ce code ne passe pas l’étape de la compilation string.ToUpper();

Voici l’erreur affichée par le compilateur : An object reference is required for the nonstatic field, method, or property ’string.ToUpper()’

Cet exemple peut sembler un peu bizarre, puisque aucune valeur n’a été communiquée à ToUpper. Si vous essayiez de passer une valeur à ToUpper, cela reviendrait à appeler une variante de la méthode ToUpper. Ceci est impossible puisqu’il n’existe aucun prototype de ToUpper dont la signature contienne un string. Faites la différence entre la méthode ToUpper et la méthode Format de la classe string. Cette dernière est statique. Elle doit donc être appliquée à la classe string et non à un Openmirrors.com

Linq.book Page 33 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

33

objet string. Essayons d’invoquer cette méthode sur un objet string (voir Listing 2.13). Listing 2.13 : Tentative d’appel d’une méthode de classe sur un objet. string firstName = "Joe"; string lastName = "Rattz"; string name = firstName.Format("{0} {1}", firstName, lastName); Console.WriteLine(name);

Ce code produit l’erreur suivante lors de la compilation : Member ’string.Format(string, object, object)’ cannot be accessed with an instance reference; qualify it with a type name instead

Appliquons maintenant la méthode Format sur la classe string elle-même (voir Listing 2.14). Listing 2.14 : Appel d’une méthode de classe sur une classe. string firstName = "Joe"; string lastName = "Rattz"; string name = string.Format("{0} {1}", firstName, lastName); Console.WriteLine(name);

Ce code passe la compilation et donne le résultat suivant à l’exécution : Joe Ratz

Outre le mot-clé static, il suffit souvent d’observer la signature d’une méthode pour savoir qu’il s’agit d’une méthode d’instance. Considérez par exemple la méthode ToUpper. Elle ne comprend aucun autre argument que la version surchargée de la référence à l’objet. Si elle ne dépend pas d’une instance string d’une donnée interne, quelle valeur string pourrait-elle mettre en majuscules ? Résolution du problème par les méthodes d’extension Supposons que vous soyez un développeur et que vous deviez mettre en place une nouvelle façon d’interroger des objets. Supposons que vous décidiez de créer une méthode Where pour traiter la clause Where. Comment procéderiez-vous ?

L’opérateur Where devrait-il être traité dans une méthode d’instance ? Dans ce cas, à quelle classe ajouteriez-vous cette méthode, étant donné que vous voulez que la méthode Where puisse interroger toute collection d’objets. Aucune réponse logique à cette question ! En adoptant cette approche, vous devriez modifier un très grand nombre de classes si vous vouliez que la méthode soit universelle. La méthode doit donc être statique. Comme nous allons le voir dans les lignes suivantes, si l’on se réfère aux requêtes SQL traditionnelles, incluant plusieurs clauses where, jointures, regroupements et/ou tris, une méthode statique n’est pas vraiment appropriée.

Linq.book Page 34 Mercredi, 18. février 2009 7:58 07

34

LINQ et C# 2008

Partie I

Supposons que vous ayez défini un nouveau type de données : une séquence d’objets génériques que nous appellerons Enumerable. La méthode Where devrait opérer sur un Enumerable et retourner un autre Enumerable filtré. De plus, la méthode Where devrait accepter un argument qui permette au développeur de préciser la logique utilisée pour filtrer les enregistrements de données depuis ou dans l’Enumerable. Cet argument, que j’appellerai le prédicat, pourrait être spécifié dans une méthode nommée, une méthode anonyme ou une expression lambda. ATTENTION Les trois codes qui suivent sont purement démonstratifs. Ils ne passeront pas l’étape de la compilation.

Étant donné que la méthode Where demande une entrée à filtrer de type Enumerable, et que la méthode est statique, cette entrée doit être spécifiée dans un argument de la méthode Where. Ceci pourrait se matérialiser comme suit : static Enumerable Enumerable.Where(Enumerable input, LambdaExpression predicate) { … }

En ignorant pour l’instant la sémantique d’une expression lambda, un appel à la méthode Where pourrait s’effectuer par les instructions suivantes : Enumerable enumerable = {"one", "two", "three"}; Enumerable filteredEnumerable = Enumerable.Where(enumerable, lambdaExpression);

Cela ne s’annonce pas trop mal. Mais que faire si nous avons besoin de plusieurs clauses Where ? Puisque l’Enumerable sur lequel travaille la méthode Where doit être un argument de la méthode, le chaînage des méthodes revient à les imbriquer. Voici comment appeler trois clauses Where : Enumerable enumerable = {"one", "two", "three"}; Enumerable finalEnumerable = Enumerable.Where(Enumerable.Where(Enumerable.Where(enumerable, lX1), lX2), lX3);

Vous devez lire la dernière instruction de la partie la plus interne vers la partie la plus externe. Très difficile à lire ! Pouvez-vous imaginer à quoi ressemblerait une requête plus complexe ? Si seulement il y avait un autre moyen… La solution Une solution élégante consisterait à appeler la méthode statique Where sur chaque objet Enumerable, plutôt que sur la classe. Il ne serait alors plus nécessaire de passer chaque Enumerable dans la méthode Where, puisque l’objet Enumerable aurait accès à ses propres Enumerable. La requête précédente deviendrait donc : Enumerable enumerable = {"one", "two", "three"}; Enumerable finalEnumerable = enumerable.Where(lX1).Where(lX2).Where(lX3);

Openmirrors.com

Linq.book Page 35 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

35

ATTENTION Les codes qui précèdent ainsi que le code qui suit sont purement démonstratifs. Ils ne passeront pas l’étape de la compilation.

Ce code pourrait être réécrit comme suit : Enumerable enumerable = {"one", "two", "three"}; Enumerable finalEnumerable = enumerable .Where(lX1) .Where(lX2) .Where(lX3);

Ce code est bien plus lisible : la déclaration peut maintenant être lue de gauche à droite et de haut en bas. Comme vous pouvez le voir, cette syntaxe est très simple à suivre. C’est la raison pour laquelle vous verrez de nombreuses requêtes LINQ exprimées de la sorte dans la documentation officielle et dans cet ouvrage. Pour terminer, vous avez besoin d’une méthode statique qui puisse être appelée dans une méthode de classe. Ce sont exactement les possibilités offertes par les méthodes d’extension. Elles ont été ajoutées à C# pour permettre d’appeler élégamment une méthode statique sans avoir à passer le premier argument de la méthode. Cela permet d’appeler la méthode d’extension comme s’il s’agissait de la méthode du premier argument. Les appels chaînés aux méthodes d’extension sont donc bien plus lisibles. Les méthodes d’extension permettent à LINQ d’appliquer des opérateurs de requête standard aux types qui implémentent l’interface IEnumerable. INFO Les méthodes d’extension peuvent être appelées sur une instance de classe (un objet) et non sur la classe elle-même.

Déclarations et invocations de méthodes d’extension Il suffit d’utiliser le mot-clé this comme premier argument d’une méthode pour la transformer en une méthode d’extension.

La méthode d’extension peut être utilisée sur n’importe quel objet dont le type est le même que celui de son premier argument. Si, par exemple, le premier argument de la méthode d’extension est de type string, elle apparaîtra comme une méthode d’instance string et pourra être appliquée à tout objet string. Ayez toujours à l’esprit que les méthodes d’extension ne peuvent être déclarées que dans des classes statiques.

Linq.book Page 36 Mercredi, 18. février 2009 7:58 07

36

LINQ et C# 2008

Partie I

Voici un exemple d’une méthode d’extension : namespace Netsplore.Utilities { public static class StringConversions { public static double ToDouble(this string s) { return Double.Parse(s); } public static bool ToBool(this string s) { return Boolean.Parse(s); } } }

Les classes et méthodes utilisées sont toutes statiques. Pour utiliser ces méthodes d’extension, il suffit d’appeler les méthodes statiques sur des instances d’objets, comme dans le Listing 2.15. Étant donné que la méthode ToDouble est statique et que son premier argument est this, ToDouble est une méthode d’extension. Listing 2.15 : Appel d’une méthode d’extension. using Netsplore.Utilities; double pi = "3.1415926535".ToDouble(); Console.WriteLine(pi);

Voici le résultat du WriteLine : 3.1415926535

Il est important de spécifier la directive using sur l’espace de noms Netsplore.Utilities. Si vous l’omettez, le compilateur ne trouvera pas les méthodes d’extension et vous obtiendrez une erreur du type suivant : ’string’ does not contain a definition for ’ToDouble’ and no extension method ’ToDouble’ accepting a first argument of type ’string’ could be found (are you missing a using directive or an assembly reference?)

Comme indiqué précédemment, il n’est pas permis de déclarer une méthode d’extension à l’intérieur d’une classe non statique. Si vous le faites, vous obtiendrez le message d’erreur suivant : Extension methods must be defined in a non-generic static class

Précédence des méthodes d’extension Les instances d’objets conventionnelles ont une précédence sur les méthodes d’extension lorsque leur signature est identique à la signature d’appel.

Les méthodes d’extension sont un concept très utile, en particulier si vous voulez étendre une classe "scellée" ou dont vous ne connaissez pas le code. Les méthodes d’extension Openmirrors.com

Linq.book Page 37 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

37

précédentes ajoutent des méthodes à la classe string. Si les méthodes d’extension n’existaient pas, vous ne pourriez pas le faire, car la classe string est scellée. Méthodes partielles Les méthodes partielles ajoutent un mécanisme de gestion d’événements ultraléger au langage C#. Oubliez les conclusions que vous êtes certainement en train de tirer sur les méthodes partielles : le seul point commun entre les méthodes partielles et les classes partielles est qu’une méthode partielle ne peut exister que dans une classe partielle. Avant de passer en revue les autres règles sur les méthodes partielles, nous allons nous intéresser à leur nature. Le prototype ou la définition d’une méthode partielle est spécifié dans sa déclaration, mais cette dernière n’inclut pas l’implémentation de la méthode. Aucun code IL n’est donc émis par le compilateur lors de la déclaration de la méthode, l’appel de la méthode ou l’évaluation des arguments passés à la méthode. C’est comme si la méthode n’avait jamais existé ! Le terme "méthode partielle" peut sembler inapproprié si l’on compare le comportement d’une méthode partielle à celui d’une classe partielle. Le terme "méthode fantôme" aurait certainement été plus judicieux… Un exemple de méthode partielle Voici un exemple de classe partielle dans lequel est définie une méthode partielle.

La classe MyWidget public partial class MyWidget { partial void MyWidgetStart(int count); partial void MyWidgetEnd(int count); public MyWidget() { int count = 0; MyWidgetStart(++count); Console.WriteLine("In the constructor of MyWidget."); MyWidgetEnd(++count); Console.WriteLine("count = " + count); } }

Cette classe partielle MyWidget contient une méthode partielle également nommée MyWidget. Les deux premières lignes définissent les méthodes partielles MyWidgetStart et MyWidgetStop. Toutes deux acceptent un paramètre et retournent void (cette dernière caractéristique est une obligation des méthodes partielles). Le bloc de code suivant est le constructeur. Comme vous pouvez le voir, il définit l’int count et l’initialise à 0. La méthode MyWidgetStart est alors appelée, un message est affiché dans la console, la méthode MyWidgetStop est appelée puis la valeur de count est affichée dans la console. La valeur de count est incrémentée à chaque passage dans

Linq.book Page 38 Mercredi, 18. février 2009 7:58 07

38

LINQ et C# 2008

Partie I

la méthode partielle. Ceci afin de prouver que, si une méthode partielle n’est pas implémentée, ses arguments ne sont pas évalués. Le code du Listing 2.16 définit un objet de classe MyWidget. Listing 2.16 : Instanciation de la classe MyWidget. MyWidget myWidget = new MyWidget();

Appuyez sur Ctrl+F5 pour exécuter le code. Voici le résultat obtenu dans la console : In the constructor of MyWidget. count = 0

Comme vous pouvez le voir, après que le constructeur de MyWidget eut incrémenté à deux reprises la variable count, la valeur affichée à la fin du constructeur est égale à zéro. Ceci vient du fait que les arguments des méthodes partielles ne sont pas implémentés. Aucun code IL n’est donc émis par le compilateur. Nous allons maintenant ajouter une implémentation pour les deux méthodes partielles : Une autre déclaration de MyWidget contenant l’implémentation des méthodes partielles public partial class MyWidget { partial void MyWidgetStart(int count) { Console.WriteLine("In MyWidgetStart(count is {0})", count); } partial void MyWidgetEnd(int count) { Console.WriteLine("In MyWidgetEnd(count is {0})", count); } }

L’implémentation ayant été rajoutée, exécutez à nouveau le code du Listing 2.16. Vous obtiendrez l’affichage suivant dans la console : In MyWidgetStart(count is 1) In the constructor of MyWidget. In MyWidgetEnd(count is 2) count = 2

Comme vous pouvez le voir, les méthodes partielles ont été implémentées et les arguments, passés et évalués (la variable count vaut 2 à la fin de la sortie écran). Pourquoi utiliser les méthodes partielles ? Vous vous demandez peut-être pourquoi utiliser des méthodes partielles. Certains rétorqueront qu’elles s’apparentent à l’héritage et aux méthodes virtuelles. Mais, alors, pourquoi alourdir le langage avec les méthodes partielles ? Tout simplement parce qu’elles sont plus efficaces si vous prévoyez d’utiliser des procédures potentiellement

Openmirrors.com

Linq.book Page 39 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

39

non implémentées. Elles permettent d’écrire du code pouvant être étendu par une personne tierce via le paradigme des classes partielles sans dégradation de performances. Les méthodes partielles ont certainement été ajoutées à C# pour les besoins des outils de génération de classes d’entités de LINQ to SQL. À titre d’exemple, chaque propriété mappée d’une classe d’entités possède une méthode partielle qui est appelée avant que la propriété ne change et une autre qui est appelée après que la propriété eut changé. Ceci permet d’ajouter un autre module en déclarant la même classe d’entité, d’implémenter ces méthodes partielles et d’être averti chaque fois qu’une propriété est sur le point d’être modifiée et après sa modification. Cela n’est-il pas intéressant ? Le code ne sera ni plus volumineux ni plus lent. Alors, ne vous en privez pas ! Les règles Les méthodes partielles doivent respecter quelques règles. Ces dernières ne sont pas trop contraignantes, et l’on y gagne vraiment au change en termes de flexibilité et de possibilités offertes au programmeur. Les voici : m

Elles ne doivent être définies et implémentées que dans des classes partielles.

m

Elles doivent être préfixées par le mot-clé partiel.

m

Elles sont privées mais ne doivent pas utiliser le mot-clé private, sinon une erreur sera générée à la compilation.

m

Elles doivent retourner void.

m

Elles peuvent ne pas être implémentées.

m

Elles peuvent être static.

m

Elles peuvent avoir des arguments.

Expressions de requête Un des avantages du langage C# est la déclaration foreach. Cette instruction est remplacée par le compilateur par une boucle qui appelle des méthodes telles que GetEnumerator et MoveNext. La simplicité de cette instruction l’a rendue universelle lorsqu’il s’agit d’énumérer des tableaux et collections. La syntaxe des requêtes LINQ est très proche de celle de SQL et vraiment appréciée par les développeurs. Les exemples des pages précédentes utilisent cette syntaxe, propre à C# 3.0, connue sous le nom "expressions de requêtes". Pour réaliser une requête LINQ, il n’est pas obligatoire d’utiliser une expression de requête. Une alternative consiste à utiliser la notation "à point" standard de C#, en appliquant des méthodes à des objets et des classes. Dans de nombreux cas, l’utilisation de la notation standard est favorable au niveau des instructions, car très démonstrative. Plusieurs exemples de ce livre préfèrent la syntaxe "à point" traditionnelle aux expressions

Linq.book Page 40 Mercredi, 18. février 2009 7:58 07

40

LINQ et C# 2008

Partie I

de requête. Il n’y a aucune concurrence entre ces deux types d’écritures. Cependant, la facilité avec laquelle vous écrirez vos premières expressions de requête peut se révéler enthousiasmante… Pour avoir une idée des différences entre les deux types de notations, le Listing 2.17 met en œuvre une requête fondée sur la syntaxe traditionnelle de C#. Listing 2.17 : Une requête utilisant la notation à point traditionnelle. string[] names = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable sequence = names .Where(n => n.Length < 6) .Select(n => n); foreach (string name in sequence) { Console.WriteLine("{0}", name); }

Le Listing 2.18 est la requête équivalente fondée sur les expressions de requête. Listing 2.18 : La requête équivalente fondée sur les expressions de requête. string[] names = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable sequence = from n in names where n.Length < 6 select n; foreach (string name in sequence) { Console.WriteLine("{0}", name); }

La première chose qui saute aux yeux quant à l’expression de requête est que, contrairement au SQL, la déclaration from précède le select. Une des raisons majeures ayant motivé ce changement vient de l’IntelliSense. Sans cette inversion, si vous tapiez select suivi d’une espace dans l’éditeur de Visual Studio 2008, IntelliSense n’aurait aucune idée des éléments à afficher dans la liste déroulante. En indiquant d’où proviennent les données, IntelliSense a une idée précise des variables à proposer dans la liste déroulante. Openmirrors.com

Linq.book Page 41 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

41

Ces deux exemples donnent le résultat suivant : Adams Bush Ford Grant Hayes Nixon Polk Taft Tyler

Grammaire des expressions de requête Les expressions de requête doivent se conformer aux règles de grammaire suivantes :

1. Une expression de requête doit toujours commencer par la clause from. 2. Peuvent ensuite venir zéro, une ou plusieurs clauses from, let et/ou where. La clause from définit une ou plusieurs énumérations qui passent en revue les éléments d’une ou de plusieurs séquences. La clause let définit une variable et lui affecte une valeur. La clause where filtre les éléments d’une séquence ou réalise une jointure de plusieurs séquences dans la séquence de sortie. 3. La suite de l’expression de requête peut contenir une clause orderby qui trie les données sur un ou plusieurs champs. Le tri peut être ascendant ( ascending) ou descendant (descending). 4. Une clause select ou group doit alors faire suite. 5. La suite de l’expression de requête peut contenir une clause de continuation optionnelle into, zéro, une ou plusieurs clauses join, ainsi qu’un ou plusieurs autres blocs syntaxiques, à partir du point numéro 2. La clause into redirige les résultats de la requête dans une séquence de sortie imaginaire. Cette séquence se comporte comme une clause from pour l’expression suivante commençant par le point numéro 2. Pour une description plus technique de la grammaire des expressions de requête, utilisez le diagramme suivant provenant de la documentation officielle MSDN sur LINQ. Expression de requête from-clause query-body Clause from from typeopt identifier in expression join-clausesopt Clauses join join-clause join-clauses join-clause Clause join join typeopt identifier in expression on expression equals expression join typeopt identifier in expression on expression equals expression into identifier Corps de la requête from-let-where-clausesopt orderby-clauseopt select-or-group-clause query-continuationopt

Linq.book Page 42 Mercredi, 18. février 2009 7:58 07

42

LINQ et C# 2008

Partie I

Clauses from, let et where from-let-where-clause from-let-where-clauses from-let-where-clause Clause from, let et where from-clause let-clause where-clause Clause let let identifier = expression Clause where where boolean-expression Clause orderby orderby orderings Tris ordering orderings , ordering Tri expression ordering-directionopt Direction du tri ascending descending Clause select ou group select-clause group-clause Clause select select expression Clause group group expression by expression Continuation de la requête into identifier join-clausesopt query-body

Traduction des expressions de requête Supposons que vous ayez créé une expression de requête syntaxiquement correcte. Pour la traduire en code "à point" C#, le compilateur recherche des "motifs". La traduction s’effectue en plusieurs étapes. Chacune d’entre elles recherche un ou plusieurs motifs spécifiques. Le compilateur réitère la traduction pour tous les motifs correspondant à l’étape actuelle avant de passer à la suivante. Par ailleurs, l’étape n de la traduction ne peut se faire que si les n–1 étapes précédentes ont été achevées.

Identificateurs transparents Certaines traductions insèrent des variables d’énumération comprenant des identificateurs transparents. Dans les descriptions de la section suivante, les identificateurs transparents sont identifiés par des astérisques (*). Ce signe ne doit pas être confondu avec le caractère de remplacement "*". Lors de la traduction, il arrive que certaines énumérations additionnelles soient générées par le compilateur et que des identificateurs transparents soient utilisés pour les énumérer (ces identificateurs n’existent que pendant le processus de traduction). Openmirrors.com

Linq.book Page 43 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

43

Étapes de la traduction Dans cette section, nous allons utiliser les conventions du Tableau 2.1, où des lettres représentent les variables utilisées dans des portions spécifiques d’une requête. Tableau 2.1 : Variables de traduction.

Variable

Description

Exemple

c

Variable temporaire générée par le compilateur

aucun

e

Variable d’énumération

from e in customers

f

Champ sélectionné ou nouveau type anonyme

from e in customers select f

g

Un élément groupé

from e in s group g by k

i

Un imaginaire dans une séquence

from e in s into i

k

Élément clé groupé ou joint

from e in s group g by k

l

Une variable définie avec let

from e in s let l = v

o

Un élément classé

from e in s orderby o

s

La séquence d’entrée

from e in s

v

Une valeur affectée à une variable par let

from e in s let l = v

w

Une clause where

from e in s where w

Attention ! Le processus de traduction est complexe. Que cela ne vous décourage pas ! En effet, vous n’avez pas besoin de comprendre ce qui va être dit dans les détails pour écrire des requêtes LINQ. Les informations données dans cette section sont un plus. Il y a fort à parier que vous n’en aurez que rarement besoin, voire jamais. Dans la suite, les étapes de la traduction seront spécifiées sous la forme motif –> traduction. Je vais présenter ces étapes en me conformant à l’enchaînement logique du compilateur. Il serait sans doute plus simple de comprendre le processus de traduction en utilisant l’enchaînement inverse de celui du compilateur. En effet, la première étape ne met en œuvre que le premier motif. Elle donne naissance à plusieurs autres motifs non traduits qu’il faut encore traiter. Étant donné que chaque étape de traduction nécessite que l’étape précédente soit entièrement traduite, lorsque le processus est terminé il ne reste plus aucun terme à traduire. C’est la raison pour laquelle la dernière étape de la traduction est plus aisée à comprendre que la première. Et la description inversée des étapes de traduction est également la meilleure façon de comprendre ce qui se passe. Ceci étant dit, voici les étapes de traduction, décrites dans l’ordre du compilateur.

Linq.book Page 44 Mercredi, 18. février 2009 7:58 07

44

LINQ et C# 2008

Partie I

Clauses Select et Group avec une clause into Si une expression de requête contient une clause into, la traduction suivante est effectuée : from …1 into i …2

–>

from i in from …1 …2

Voici un exemple : from c in customers group c by c.Country into g select new { Country = g.Key, CustCount = g.Count() }

–>

from g in from c in customers group c by c.Country select new { Country = g.Key, custCount = g.Count() }

Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en : customers.GroupBy(c => c.Country) .Select(g => new { Country = g.Key, CustCount = g.Count() })

Types explicites de variables d’énumération Si votre expression de requête contient une clause from qui spécifie explicitement le type d’une variable d’énumération, la traduction suivante sera effectuée : –> from e in s.Cast() from T e in s Voici un exemple : from Customer c in customers –> from c in customers.Cast() select c

Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en : customers.Cast()

Si l’expression de requête contient une clause join qui spécifie explicitement un type de variable d’énumération, la traduction suivante est effectuée : join T e in s on k1 equals k2

–>

join e in s.Cast() on k1 equals k2

–>

from c in customers join o in orders.Cast() on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }

Voici un exemple : from c in customers join Order o in orders on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }

Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en : .Join(orders.Cast(), c => c.CustomerID, o => o.CustomerID, new { c.Name, o.OrderDate, o.Total })

Openmirrors.com

Linq.book Page 45 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

45

ASTUCE La saisie explicite de variables d’énumération est nécessaire lorsque la collection de données énumérée est héritée des collections de C# (ArrayList, par exemple). Le casting opéré convertit la collection héritée en une séquence qui implémente IEnumerable afin d’assurer la compatibilité avec les opérateurs de requête.

Clauses join Si l’expression de requête contient une clause from suivie d’une clause join, mais pas d’une clause into suivie d’une clause select, la traduction suivante est opérée (t est une variable temporaire créée par le compilateur) : from e1 in s1 join e2 in s2 on k1 equals k2 select f

–>

from t in s1 .Join(s2, e1 => k1, e2 => k2, (e1, e2) => f) select t

Voici un exemple : from c in customers join o in orders on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }

–>

from t in customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) =>new { c.Name, o.orderDate o.Total }) select t

Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en : customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c.Name, o.OrderDate, o.Total })

Si l’expression de requête contient une clause from suivie d’une clause join, puis d’une clause into suivie d’une clause select, la traduction suivante est opérée (t est une variable temporaire créée par le compilateur) : from e1 in s1 join e2 in s2 on k1 equals k2 into i select f

from t in s1 .GroupJoin(s2, e1 => k1, –> e2 => k2, (e1, i) => f) select t

Voici un exemple : from c in customers join o in orders on c.CustomerID equals o.CustomerID into co select new { c.Name, Sum = co.Sum(o => o.Total) } Sum = co.Sum( o => co.Total)

from t in customers .groupJoin(orders, –> c => c.CustomerID, o => o.CustomerID, (c, co) => new { c.Name,

Select t

Linq.book Page 46 Mercredi, 18. février 2009 7:58 07

46

LINQ et C# 2008

Partie I

En utilisant les étapes de traduction suivantes, le code est finalement traduit en : Customers .GroupJoin(orders, c => c.CustomerIDc.CustomerID, o => o.CustomerID, (c, co) => new { c.Name, Sum = co.Sum(o = o.Total) })

Si l’expression de requête contient une clause from suivie d’une clause join mais pas d’une clause into suivie par quelque chose d’autre qu’une clause select, la traduction suivante est opérée (* est un opérateur transparent) : from e1 in s1 join e2 in s2 on k1 equals k2 …

–>

from * in from e1 in s1 join e2 in s2 on k1 equals k2 select new { e1, e2 }

Le motif généré correspond au premier motif de la section "Clauses Join" : la requête contient une clause from suivie d’une clause join. La clause into est absente, mais une clause select est présente. Une nouvelle traduction sera donc opérée. Si l’expression de requête contient une clause from suivie d’une clause join, puis d’une clause into suivie par quelque chose d’autre qu’une clause select, la traduction suivante est opérée (* est un opérateur transparent) : from e1 in s1 join e2 in s2 on k1 equals k2 into i …

–>

from * from e1 in s1 join e2 in s2 on k1 equals k2 into i select new { e1, i }

Le motif généré correspond au deuxième motif de la section "Clauses Join" : on trouve une clause from suivie d’une clause join, d’une clause into puis d’une clause select. Une nouvelle traduction sera donc opérée. Les clauses Let et Where Si l’expression de requête contient une clause from suivie immédiatement d’une clause let, la traduction suivante est effectuée (* est un identificateur transparent) : from e in s let l = v

from * in from e1 in s1 select new { e, l = v }

–>

Voici un exemple (t est un identificateur généré par le compilateur. Il reste invisible et inaccessible par le code) : from c in customers let cityStateZip = c.City + ", " + c.State + " " + c.Zip select new { c.Name, cityStateZip }

select new { c.Name, cityStateZip }

Openmirrors.com

–>

from * in from c in customers select new { c, cityStateZip = c.City + ", " + c.State + " " + c.Zip }

Linq.book Page 47 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

47

Une fois les autres étapes de traduction terminées, le code est finalement transformé en : customers .Select(c => new { c, cityStateZip = c.City + ", " + c.State + " " + c.Zip }) .Select(t => new { t.c.Name, t.cityStateZip })

Si l’expression de requête contient une clause from suivie d’une clause where, la traduction suivante est opérée : from e in s where w

from e in s .Where(e => w)

–>

Voici un exemple : from c in customers where c.Country == "USA" select new { c.Name, c.Country }

from c in customers .Where (c => c.Country == "USA") select new { c.Name, c.Country }

–>

Une fois les autres étapes de traduction terminées, le code est finalement transformé en : customers .Where(c => c.Country == "USA") .Select(c => new { c.Name, c.Country })

Clauses from multiples Si l’expression de requête contient deux clauses from suivies par une requête select, la traduction suivante est opérée : from e1 in s1 from e2 in s2 select f

–>

from c in s1 .SelectMany(e1 => from e2 in s2 select f) select c

Voici un exemple (t est une variable temporaire générée par le compilateur) : from c in customers from o in c.Orders select new { c.Name, o.OrderID, o.OrderDate }

–>

from t in customers .SelectMany(c => from o in c.Orders select new { c.Name, o.OrderID, o.OrderDate }) Select t

Une fois les autres étapes de traduction terminées, le code est finalement transformé en : customers .SelectMany(c => c.Orders.Select(o => new { c.Name, o.OrderID, o.OrderDate }))

Si l’expression de requête contient deux clauses from suivies par quelque chose d’autre qu’une clause select, la traduction suivante est opérée (* est un identificateur transparent) : from e1 in s1 from e2 in s2 …

–>

from * in from e1 in s1 from e2 in s2 select new { e1, e2 }

Linq.book Page 48 Mercredi, 18. février 2009 7:58 07

48

LINQ et C# 2008

Partie I

Voici un exemple (* est un identificateur transparent) : from c in customers from o in c.Orders orderby o.OrderDate descending select new {c.Name, o.OrderID, o.OrderDate }

–>

from * in from c in customers from o in c.Orders select new { c, o } orderby o.OrderDate descending select new { c.Name, o.OrderID, o.OrderDate }

Le code ainsi obtenu doit réitérer la première étape de traduction. En effet, le motif résultant contient une clause from suivie par une autre clause from puis par une clause select, ce qui correspond au premier modèle de la section "Clauses from multiples". Il s’agit donc d’un exemple dans lequel certaines étapes doivent être appelées plusieurs fois pour que la traduction soit complète. Une fois toutes les étapes de traduction terminées, le code est finalement transformé en : customers .SelectMany(c => c.Orders.Select(o => new { c, o })) .OrderByDescending(t => t.o.OrderDate) .Select(t => new { t.c.Name, t.o.OrderID, t.o.OrderDate})

Clauses OrderBy Les traductions suivantes prennent place dans un tri ascendant : from e in s orderby o1, o2

–>

from e in s .OrderBy(e => o1).ThenBy(e => o2)

Voici un exemple : from c in customers orderby c.Country, c. Name select new { c.Country, c.Name} select new { c.Country, c.Name }

from c in customers .OrderBy(c => c.Country) .TheBy(c.Name)

Une fois toutes les étapes de traduction terminées, le code est finalement transformé en : customers .OrderBy(c => c.Country) .ThenByDescending(c.Name) .Select(c => new { c.Country, c.Name }

Clauses Select Dans une expression de requête, si vous sélectionnez la totalité de l’élément stocké dans la séquence, l’élément sélectionné a le même identificateur que la variable d’énumération de la séquence. La traduction suivante est opérée : from e in s select f

–>

s

–>

customers

Voici un exemple : from c in customers select c

Openmirrors.com

Linq.book Page 49 Mercredi, 18. février 2009 7:58 07

Chapitre 2

Améliorations de C# 3.0 pour LINQ

49

Si l’élément sélectionné n’a pas le même identificateur que la variable d’énumération de la séquence, cela signifie qu’il n’est pas sélectionné en totalité (la sélection peut porter sur un membre de l’élément ou sur un type anonyme construit à partir de plusieurs membres de l’élément). La traduction suivante est effectuée : from e in s select f

–>

s.Select(e => f)

–>

customers.Select(c => c.Name)

Voici un exemple : from c in customers select c.Name

Clauses group Dans l’expression de requête, si l’élément regroupé a le même identificateur que l’énumérateur de la séquence, cela signifie que le regroupement porte sur la totalité de l’élément stocké dans la séquence. La traduction est la suivante : from e in s group g by k

–>

s.GroupBy(e => k)

–>

customers.GroupBy(c => c.Country)

Voici un exemple : from c in customers group c by c.Country

Si l’élément regroupé n’a pas le même identificateur que l’énumérateur de la séquence, cela signifie qu’il n’est pas regroupé en totalité. La traduction suivante est effectuée : from e in s group g by k

–>

s.GroupBy(e => k, e => g)

–>

customers .GroupBy(c => c.Country, c => new { c.Country c.Name })

Voici un exemple : from c in customers group new { c.Country, c. Name} by c.Country

Toutes les étapes de la traduction ont été effectuées et l’expression de requête a été entièrement traduite en une notation "à point" traditionnelle.

Résumé De nombreuses fonctionnalités ont été ajoutées au langage C#. Bien que ces ajouts aient été dictés par l’implémentation de LINQ, vous avez tout intérêt à les utiliser en dehors du contexte LINQ. Les expressions d’initialisation d’objets et de collections sont particulièrement intéressantes, car elles réduisent la taille du code de façon drastique. Cette fonctionnalité, combinée avec le mot-clé var et aux types anonymes, facilite grandement la création de données et de types de données à la volée.

Linq.book Page 50 Mercredi, 18. février 2009 7:58 07

50

LINQ et C# 2008

Partie I

Les méthodes d’extension permettent d’ajouter des fonctionnalités aux classes scellées et aux classes dont vous n’avez pas le code source. Si elles n’éliminent pas la raison d’être des méthodes anonymes, les expressions lambda représentent une nouvelle façon de définir de nouvelles fonctionnalités, simplement et de façon concise. Lorsque vous commencerez à les utiliser, vous serez peut-être déconcerté, mais, le temps aidant, vous les apprécierez à leur juste valeur. Les arbres d’expressions permettent aux éditeurs de logiciels tiers de conserver un mode de stockage propriétaire tout en supportant les performances avancées de LINQ. Les méthodes partielles ajoutent un mécanisme de gestion d’événements ultraléger au langage C#. Elles sont utilisées pour accéder à des moments clés dans les classes d’entités LINQ to SQL. Si les expressions de requête peuvent sembler confuses de prime abord, il ne faut pas bien longtemps pour qu’un développeur se sente à l’aise à leur contact. Elles ont en effet un air de parenté avec les requêtes SQL. Chacune de ces améliorations du langage est intéressante en soi, mais c’est leur utilisation conjointe qui est à la base de LINQ. LINQ devrait être la prochaine grande tendance en programmation. Les développeurs .NET apprécieront certainement de pouvoir l’inscrire dans leur CV. En tout cas, moi, j’en suis fier ! Vous avez maintenant une idée de ce qu’est LINQ, ainsi que des fonctionnalités et syntaxes C# afférentes. Il est temps de passer à la prochaine étape. En tournant les pages, vous allez apprendre à appliquer des requêtes LINQ à des collections en mémoire (array ou arraylist, par exemple) et aux collections génériques de C# 2.0, et vous découvrirez différentes fonctions pour alimenter vos requêtes. Cette portion de LINQ est aujourd’hui connue sous le nom de "LINQ to Objects".

Openmirrors.com

Linq.book Page 51 Mercredi, 18. février 2009 7:58 07

II LINQ to Objects

Linq.book Page 52 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 53 Mercredi, 18. février 2009 7:58 07

3 Introduction à LINQ to Objects Listing 3.1 : Une requête LINQ to Objects élémentaire. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string president = presidents.Where(p => p.StartsWith("Lin")).First(); Console.WriteLine(president);

INFO Ce code a été ajouté au prototype d’une application console Visual Studio 2008.

Le Listing 3.1 donne une idée de ce qu’est LINQ to Objects : par son intermédiaire, il est possible d’interroger des données en mémoire à l’aide de requêtes proches du langage SQL. Lancez le programme avec Ctrl+F5. Vous obtenez le résultat suivant : Lincoln

Vue d’ensemble de LINQ to Objects Si LINQ est aussi agréable et facile à utiliser, c’est en partie parce qu’il est parfaitement intégré dans le langage C#. Plutôt qu’avoir à composer avec de nouvelles classes spécifiques à LINQ, vous pouvez utiliser les mêmes collections1 et tableaux que précédemment. Vous avez donc les avantages inhérents à LINQ sans devoir retoucher (ou très 1. Les collections doivent implémenter l’interface IEnumerable ou IEnumerable pour pouvoir être interrogeables par LINQ.

Linq.book Page 54 Mercredi, 18. février 2009 7:58 07

54

LINQ to Objects

Partie II

peu) le code existant. LINQ to Objects s’exécute à travers l’interface IEnumerable, les séquences et les opérateurs de requête standard. À titre d’exemple, pour trier un tableau d’entiers, vous pouvez utiliser une requête LINQ, tout comme s’il s’agissait d’une requête SQL. Un autre exemple. Si vous voulez trouver un objet Customer spécifique dans un ArrayList of Customer, LINQ to Objects est assurément la réponse. Pour beaucoup d’entre vous, les chapitres sur LINQ to Objects seront utilisés en tant que référence. Ils ont été construits dans cette optique et je vous conseille de les parcourir en totalité. Ne vous contentez pas de lire les sections des seuls opérateurs qui vous intéressent, sans quoi votre formation sera incomplète.

IEnumerable, séquences et opérateurs de requête standard IEnumerable, prononcé "Iénumérable de T", est une interface implémentée par les tableaux et les classes de collections génériques de C# 2.0. Cette interface permet d’énumérer les éléments d’une collection.

Une séquence est un terme logique d’une collection qui implémente l’interface IEnumerable. Si vous avez une variable de type IEnumerable, vous pouvez dire que vous avez une séquence de T. Par exemple, si vous avez un IEnumerable de string, ce qui s’écrit IEnumerable, vous pouvez dire que vous avez une séquence de string. INFO Toutes les variables déclarées en tant que IEnumerable sont considérées comme séquences de T.

La plupart des opérateurs de requête standard sont des méthodes d’extension de la classe statique System.Linq.Enumerable et ont un premier argument prototypé par un IEnumerable. Étant donné que ces opérateurs sont des méthodes d’extension, il est préférable de les appeler à travers une variable de type IEnumerable plutôt que passer une variable de type IEnumerable en premier argument. Les méthodes d’opérateurs de requête standard de la classe System.Linq.Enumerable qui ne sont pas des méthodes d’extension sont des méthodes statiques. Elles doivent être appelées dans la classe System.Linq.Enumerable. La combinaison de ces méthodes d’opérateurs de requête standard vous permet d’effectuer des requêtes complexes sur une séquence IEnumerable. Les collections héritées – ces collections non génériques qui existaient avant C# 2.0 – supportent l’interface IEnumerable, et non l’interface IEnumerable. Cela signifie que vous ne pouvez pas appeler directement ces méthodes d’extension dont le premier Openmirrors.com

Linq.book Page 55 Mercredi, 18. février 2009 7:58 07

Chapitre 3

Introduction à LINQ to Objects

55

argument est un IEnumerable sur une collection héritée. Cependant, vous pouvez toujours exécuter des requêtes LINQ sur des collections héritées en invoquant l’opérateur de requête standard Cast ou OfType. Cet opérateur produira une séquence qui implémente l’interface IEnumerable, vous permettant ainsi d’accéder à la panoplie complète des opérateurs de requête standard. INFO Utilisez les opérateurs Cast ou OfType pour exécuter des requêtes LINQ sur des collections C# héritées et non génériques.

Pour accéder aux opérateurs de requête standard, vous devez ajouter une directive using System.Linq; dans votre code (si cette dernière n’est pas déjà présente). Il n’est pas nécessaire d’ajouter une référence à un assembly car le code nécessaire est contenu dans l’assembly System.Core.dll, qui est automatiquement ajouté aux projets par Visual Studio 2008.

IEnumerable, yield et requêtes différées La plupart des opérateurs de requête standard sont prototypés pour retourner un IEnumerable (une séquence). Mais, attention, les éléments de la séquence ne sont pas retournés dès l’exécution de l’opérateur : ils ne seront "cédés" que lors de l’énumération de la séquence. C’est la raison pour laquelle on dit que ces requêtes sont différées. Le terme "céder" fait référence au mot-clé yield, ajouté dans C# 2.0 pour faciliter l’écriture d’énumérateurs. Examinez le code du Listing 3.2. Listing 3.2 : Une requête triviale. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Where(p => p.StartsWith("A")); foreach(string item in items) Console.WriteLine(item);

La requête apparaît en gras dans ce listing. Lorsque cette ligne s’exécute, elle retourne un objet. Ce n’est que pendant l’énumération de cet objet que la requête Where est réellement exécutée. Si une erreur se produit dans la requête, elle ne sera détectée qu’à l’énumération.

Linq.book Page 56 Mercredi, 18. février 2009 7:58 07

56

LINQ to Objects

Partie II

Voici le résultat de la requête : Adams Arthur

Cette requête s’est comportée comme prévu. Nous allons maintenant introduire une erreur intentionnelle dans la requête. Le code qui suit va essayer d’effectuer un tri en se basant sur le cinquième caractère du nom des présidents. Lorsque l’énumération atteint un nom dont la longueur est inférieure à cinq caractères, une exception sera générée. Rappelez-vous que l’exception ne se produira pas avant l’énumération de la séquence résultat (voir Listing 3.3). Listing 3.3 : Une requête triviale avec une exception introduite intentionnellement. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Where(s => Char.IsLower(s[4])); Console.WriteLine("After the query."); foreach (string item in items) Console.WriteLine(item);

Ce code ne produit aucune erreur à la compilation, mais voici les résultats affichés dans la console : Adams Arthur Buchanan Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array. …

Tout se passe bien jusqu’au quatrième élément. Bush produit une exception lors de l’énumération. La leçon à tirer de cet exemple est qu’une compilation réussie ne suffit pas pour assurer qu’une requête est vierge de tout bogue. Sachez par ailleurs que, les requêtes qui retournent un IEnumerable étant différées, il suffit d’exécuter une seule fois le code de la requête. Vous pouvez ensuite énumérer les données autant de fois que vous le souhaitez. Si, entre deux énumérations, les données changent, les résultats seront différents (voir Listing 3.4). Listing 3.4 : Un exemple dans lequel les résultats de la requête changent d’une énumération à l’autre. // Création d’un tableau de int int[] intArray = new int[] { 1,2,3 };

Openmirrors.com

Linq.book Page 57 Mercredi, 18. février 2009 7:58 07

Chapitre 3

Introduction à LINQ to Objects

57

IEnumerable ints = intArray.Select(i => i); // Affichage des résultats foreach(int i in ints) Console.WriteLine(i); // Modification d’un élément dans la source intArray[0] = 5; Console.WriteLine("---------"); // Nouvel affichage des résultats foreach(int i in ints) Console.WriteLine(i);

Lorsque l’opérateur Select est appelé, un objet est retourné et stocké dans la variable IEnumerable ints. La requête n’a pas encore été exécutée. Elle est juste stockée dans l’objet ints. Les résultats de la requête n’existent donc pas encore, mais l’objet ints sait comment les obtenir. Lorsque l’instruction foreach est appelée pour la première fois, ints exécute la requête et obtient successivement les différents éléments de la séquence. Un peu plus bas, un des éléments est modifié dans son tableau d’origine, intArray[]. L’instruction foreach est appelée à nouveau. Cela provoque une nouvelle exécution de la requête. Cette énumération retourne tous les éléments de intArray[] et donc également l’élément qui a été modifié. Dans cet ouvrage (et dans beaucoup d’autres relatifs à LINQ), vous pourrez lire qu’une requête retourne une séquence et non un objet qui implémente l’interface IEnumerable. Ceci est un abus de langage : les éléments de la séquence ne sont obtenus qu’à son énumération. Voici les résultats affichés par ce code : 1 2 3 --------5 2 3

La requête n’a été appelée qu’une fois et, pourtant, les résultats des deux énumérations sont différents. Cela confirme – si besoin était – que la requête est bien différée. Dans le cas contraire, les résultats des deux énumérations seraient identiques. Selon les cas, ceci peut être un avantage ou un inconvénient. Si vous ne voulez pas que la requête soit différée, utilisez un opérateur qui ne retourne pas un IEnumerable. Par exemple ToArray, ToList, ToDictionary ou ToLookup. Les résultats seront alors figés dans une mémoire cache et ne changeront pas.

Linq.book Page 58 Mercredi, 18. février 2009 7:58 07

58

LINQ to Objects

Partie II

Le Listing 3.5 est le même que le précédent, à un détail près : en utilisant un opérateur ToList, la requête retourne non pas un IEnumerable mais un List. Listing 3.5 : En retournant un objet List, la requête est exécutée immédiatement et les résultats sont mis dans un cache. // Création d’un tableau de int int[] intArray = new int[] { 1,2,3 }; List ints = intArray.Select(i => i).ToList; // Affichage des résultats foreach(int i in ints) Console.WriteLine(i); // Modification d’un élément dans la source intArray[0] = 5; Console.WriteLine("---------"); // Nouvel affichage des résultats foreach(int i in ints) Console.WriteLine(i);

Voici les résultats : 1 2 3 --------1 2 3

Comme on pouvait s’y attendre, les résultats ne changent pas d’une énumération à la suivante. La requête est donc bien exécutée immédiatement. L’opérateur Select est différé, et l’opérateur ToList ne l’est pas. En appliquant ToList au résultat du Select, l’objet retourné par Select est énuméré et la requête n’est plus différée. Délégués Func Plusieurs des opérateurs de requête standard sont prototypés pour accepter un délégué Func comme argument. Cela vous évite d’avoir à déclarer des délégués explicitement. Voici les déclarations de délégués Func : public public public public public

delegate delegate delegate delegate delegate

TR TR TR TR TR

Func(); Func(T0 a0); Func(T0 a0, T1 a1); Func(T0 a0, T1 a1, T2 a2); Func(T0 a0, T1 a1, T2 a2, T3 a3);

Dans ces déclarations, TR fait référence au type de donnée retournée. Cet argument est toujours le dernier de la liste. Quant à T0 à T3, ils représentent les paramètres passés à la méthode. Plusieurs déclarations sont nécessaires, car tous les opérateurs de requête Openmirrors.com

Linq.book Page 59 Mercredi, 18. février 2009 7:58 07

Chapitre 3

Introduction à LINQ to Objects

59

standard n’utilisent pas le même nombre de paramètres en entrée. Dans tous les cas, les délégués admettent un nombre maximal de 4 paramètres. Examinons un des prototypes de l’opérateur Where : public static IEnumerable Where( this IEnumerable source, Func predicate);

En observant le prédicat Func, vous pouvez en déduire que la méthode ou l’expression lambda n’accepte qu’un seul argument, T, et retourne un booléen. Cette dernière déduction vient du fait que le type de retour est toujours le dernier paramètre de la liste. Vous utiliserez la déclaration Func, comme indiqué dans le Listing 3.6. Listing 3.6 : Cet exemple utilise une déclaration de délégué Func. // Création d’un tableau d’entiers int[] ints = new int[] { 1,2,3,4,5,6 }; // Déclaration du délégué Func GreaterThanTwo = i => i > 2; // Mise en place (et non exécution) de la requête IEnumerable intsGreaterThanTwo = ints.Where(GreaterThanTwo); // Affichage des résultats foreach(int i in intsGreaterThanTwo) Console.WriteLine(i);

L’exécution de ce code produit les résultats suivants : 2 4 5 6

Les opérateurs de requête standard Le Tableau 3.1 dresse la liste alphabétique des principaux opérateurs de requête standard. Les prochains chapitres vont séparer les opérateurs différés des opérateurs non différés. Ce tableau facilitera donc votre repérage dans le livre. Tableau 3.1 : Les opérateurs de requête standard

Opérateur

Objet

Aggregate

Agrégat

All

Dénombrement

Any

Dénombrement

AsEnumerable

Conversion

Différé

u

Supporte l’expression de requête

Linq.book Page 60 Mercredi, 18. février 2009 7:58 07

60

LINQ to Objects

Partie II

Tableau 3.1 : Les opérateurs de requête standard (suite)

Opérateur

Objet

Différé

Average

Agrégat

Cast

Conversion

u

Concat

Concaténation

u

Contains

Dénombrement

Count

Agrégat

DefaultIfEmpty

Élément

u

Distinct

Ensemble

u

ElementAt

Élément

Supporte l’expression de requête

ElementAtOrDefault Élément Empty

Génération

u

Except

Ensemble

u

First

Élément

FirstOrDefault

Élément

GroupBy

Regroupement

u

u

GroupJoin

Jointure

u

u

Intersect

Ensemble

u

Join

Jointure

u

Last

Élément

LastOrDefault

Élément

LongCount

Agrégat

Max

Agrégat

Min

Agrégat

OfType

Conversion

u

OrderBy

Tri

u

u

OrderByDescending

Tri

u

u

Range

Génération

u

Repeat

Génération

u

Reverse

Tri

u

Select

Projection

u

Openmirrors.com

u

u

Linq.book Page 61 Mercredi, 18. février 2009 7:58 07

Chapitre 3

Introduction à LINQ to Objects

61

Tableau 3.1 : Les opérateurs de requête standard (suite)

Opérateur

Objet

Différé

Supporte l’expression de requête

SelectMany

Projection

u

u

SequenceEqual

Égalité

Single

Élément

SingleOrDefault

Élément

Skip

Partage

u

SkipWhile

Partage

u

Sum

Agrégat

Take

Partage

u

TakeWhile

Partage

u

ThenBy

Tri

u

u

ThenByDescending

Tri

u

u

ToArray

Conversion

ToDictionary

Conversion

ToList

Conversion

ToLookup

Conversion

Union

Ensemble

u

Where

Restriction

u

u

Résumé Ce chapitre a introduit le terme "séquence" et le type de données associé, IEnumerable. Si vous n’êtes pas à l’aise avec ces expressions, soyez rassuré : elles deviendront vite une seconde nature pour vous ! Pour l’instant, contentez-vous de voir les IEnumerable comme une séquence d’objets auxquels vous allez appliquer des traitements via des méthodes. Ce chapitre a mis en évidence l’importance de l’exécution différée des requêtes. Selon les cas, elle peut constituer un avantage ou un inconvénient. Cette caractéristique est vraiment importante. C’est pourquoi nous allons séparer les opérateurs différés (au Chapitre 4) des opérateurs non différés (au Chapitre 5) dans la suite de cet ouvrage.

Linq.book Page 62 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 63 Mercredi, 18. février 2009 7:58 07

4 Les opérateurs différés Au chapitre précédent, nous nous sommes intéressés aux séquences, aux types de données qui les représentent et aux conséquences de leur exécution différée. Conscient de l’importance de ce dernier point, j’ai choisi de traiter des opérateurs différés et non différés dans deux chapitres séparés. Ce chapitre va s’intéresser aux opérateurs différés, par groupes fonctionnels. Il est facile de reconnaître un tel opérateur : il retourne un IEnumerable ou un IOrderEnumerable. Attention, pour pouvoir exécuter les exemples de ce chapitre, assurez-vous que vous avez référencé les espaces de noms (directive using), les assemblies et les codes communs nécessaires !

Espaces de noms référencés Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq, System.Collections, System.Collections.Generic et System.Data.Linq. Si elles ne sont pas déjà présentes, vous devez donc ajouter les directives using suivantes dans votre code : using System.Linq; using System.Collections; using System.Collections.Generic; using System.Data.Linq;

Si vous parcourez le code source (disponible sur le site www.pearson.fr), vous verrez que j’ai également ajouté une directive using sur l’espace de noms System.Diagnostic. Cette directive n’est pas nécessaire si vous saisissez directement les exemples de ce chapitre. Elle n’est là que pour les besoins propres du code source.

Linq.book Page 64 Mercredi, 18. février 2009 7:58 07

64

LINQ to Objects

Partie II

Assemblies référencés Pour que le code de ce chapitre fonctionne, vous devez également référencer l’assembly System.Data.Linq.dll.

Classes communes Certains exemples de ce chapitre nécessitent des classes additionnelles pour fonctionner en totalité. En voici la liste. La classe Employee permet de travailler sur les employés d’une entreprise. Elle contient des méthodes statiques qui retournent un tableau d’employés de type ArrayList. public class Employee { public int id; public string firstName; public string lastName; public static ArrayList GetEmployeesArrayList() { ArrayList al = new ArrayList(); al.Add(new Employee al.Add(new Employee al.Add(new Employee al.Add(new Employee al.Add(new Employee return (al);

{ { { { {

id id id id id

= = = = =

1, firstName = 2, firstName = 3, firstName = 4, firstName = 101, firstName

"Joe", lastName = "Rattz" }); "William", lastName = "Gates" }); "Anders", lastName = "Hejlsberg" }); "David", lastName = "Lightman" }); = "Kevin", lastName = "Flynn" });

} public static Employee[] GetEmployeesArray() { return ((Employee[])GetEmployeesArrayList().ToArray()); } }

La classe EmployeeOptionEntry représente le montant des stock-options des employés. Elle contient une méthode statique qui retourne un tableau de stock-options. public class EmployeeOptionEntry { public int id; public long optionsCount; public DateTime dateAwarded; public static EmployeeOptionEntry[] GetEmployeeOptionEntries() { EmployeeOptionEntry[] empOptions = new EmployeeOptionEntry[] { new EmployeeOptionEntry { id = 1, optionsCount = 2, dateAwarded = DateTime.Parse("1999/12/31") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1992/06/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1994/01/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 5000, dateAwarded = DateTime.Parse("1997/09/30") },

Openmirrors.com

Linq.book Page 65 Mercredi, 18. février 2009 7:58 07

Chapitre 4

new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("2003/04/01") new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") new EmployeeOptionEntry { id = 4, optionsCount = 1500, dateAwarded = DateTime.Parse("1997/12/31") new EmployeeOptionEntry { id = 101, optionsCount = 2, dateAwarded = DateTime.Parse("1998/12/31") };

Les opérateurs différés

65

},

},

},

},

}

return (empOptions); } }

Les opérateurs différés, par groupes fonctionnels Dans les pages qui suivent, nous avons organisé les différents opérateurs de requête standard différés par grands groupes fonctionnels. Restriction Les opérateurs de restriction sont utilisés pour ajouter ou enlever des éléments dans une séquence d’entrée. L’opérateur Where L’opérateur Where est utilisé pour filtrer des éléments d’une séquence.

Prototypes Deux prototypes de l’opérateur Where seront étudiés dans ce livre. Premier prototype public static IEnumerable Where( this IEnumerable source, Func predicate);

Ce prototype demande deux paramètres : une séquence d’entrée et un prédicat (délégué générique). Il renvoie un objet énumérable dont seuls les éléments pour lesquels le prédicat renvoie true sont accessibles. INFO Comme Where est une méthode d’extension, la séquence d’entrée n’est pas réellement passée dans le premier argument : tant que Where est appliqué sur un objet du même type que le premier argument, ce dernier peut être remplacé par le mot-clé this.

Linq.book Page 66 Mercredi, 18. février 2009 7:58 07

66

LINQ to Objects

Partie II

Lorsque vous appelez la méthode Where, un délégué est passé à un prédicat. Cette dernière doit accepter une entrée de type T (où T est le type des éléments contenus dans la séquence d’entrée) et retourner un booléen. L’opérateur Where communique chacun des éléments contenus dans la séquence d’entrée au prédicat. L’élément n’est retourné dans la séquence de sortie que dans le cas où le prédicat retourne la valeur true. Second prototype public static IEnumerable Where( this IEnumerable source, Func predicate);

Ce second prototype est semblable au premier si ce n’est que le prédicat reçoit un argument complémentaire entier. Cet argument correspond à l’index de l’élément dans la séquence. Il commence à zéro et se termine au nombre d’éléments de la séquence moins un. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.1 est un exemple d’appel du premier prototype Where. Listing 4.1 : Un exemple d’appel du premier prototype Where. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable sequence = presidents.Where(p => p.StartsWith("J")); foreach (string s in sequence) Console.WriteLine("{0}", s);

Cet exemple applique la méthode Where à la séquence d’entrée et définit une expression lambda. Cette dernière retourne un booléen dont la valeur indique si l’élément doit ou ne doit pas être inclus dans la séquence de sortie. Dans cet exemple, seuls les éléments qui commencent par la lettre "J" seront retournés. Voici les résultats affichés dans la console lorsque vous appuyez sur Ctrl+F5 : Jackson Jefferson Johnson

Le Listing 4.2 est un exemple d’appel du second prototype Where. Ce code se contente d’utiliser l’index i pour filtrer les éléments de la séquence. Seuls les éléments d’indice impair seront retournés. Openmirrors.com

Linq.book Page 67 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

67

Listing 4.2 : Un exemple d’appel du second prototype Where. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable sequence = presidents.Where((p, i) => (i & 1) == 1); foreach (string s in sequence) Console.WriteLine("{0}", s);

L’exécution de ce code produit la sortie suivante dans la console : Arthur Bush Cleveland Coolidge Fillmore Garfield Harding Hayes Jackson Johnson Lincoln McKinley Nixon Polk Roosevelt Taylor Tyler Washington

Projection Les opérateurs de projection retournent une séquence d’éléments sélectionnés dans la séquence d’entrée ou instanciés à partir de portions d’éléments de la séquence d’entrée. Le type des éléments de la séquence de sortie peut être différent du type des éléments de la séquence d’entrée. L’opérateur Select L’opérateur Select est utilisé pour créer une séquence de sortie S d’un type d’élément en partant d’une séquence d’entrée T d’un autre type d’élément. Ces deux types ne sont pas forcément identiques.

Prototypes Deux prototypes de l’opérateur Select seront étudiés dans ce livre.

Linq.book Page 68 Mercredi, 18. février 2009 7:58 07

68

LINQ to Objects

Partie II

Premier prototype public static IEnumerable Select( this IEnumerable source, Func selector);

Ce prototype admet deux arguments en entrée : une séquence source et un délégué. Il retourne un objet dont l’énumération produit une séquence d’éléments de type S. Comme signalé précédemment, les types T et S ne sont pas forcément identiques. Pour utiliser ce prototype, vous devez passer un délégué à une méthode de sélection via l’argument selector. Ce dernier doit accepter un élément de type T (où T est le type des éléments contenus dans la séquence d’entrée) et retourner un élément de type S. L’opérateur Select appelle la méthode selector pour chacun des éléments de la séquence d’entrée. La méthode selector choisit une portion de l’élément passé, crée un nouvel élément, éventuellement d’un autre type (y compris le type anonyme) et le retourne. Second prototype public static IEnumerable Select( this IEnumerable source, Func selector);

Ce second prototype est semblable au premier si ce n’est qu’un argument complémentaire de type entier est passé au délégué. Cet argument correspond à l’index de l’élément dans la séquence (l’index du premier élément est 0). Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.3 est un exemple d’appel du premier prototype. Listing 4.3 : Un exemple d’utilisation du premier prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable nameLengths = presidents.Select(p => p.Length); foreach (int item in nameLengths) Console.WriteLine(item);

La méthode selector est passée par l’intermédiaire d’une expression lambda. Cette dernière retourne la longueur des éléments de la séquence d’entrée. Remarquez que les types des séquences d’entrée et de sortie diffèrent : string pour la première, integer pour la deuxième. Openmirrors.com

Linq.book Page 69 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

69

Voici le résultat de ce code lorsque vous appuyez sur Ctrl+F5 : 5 6 8 4 6 9 7 8 10 8 4 8 5 7 8 5 6 7 9 7 7 7 7 8 6 5 6 4 6 9 4 6 6 5 9 10 6

Cet exemple est très simple, puisqu’il ne génère aucune classe. Le Listing 4.4 donne un exemple plus élaboré du premier prototype de l’opérateur Select. Listing 4.4 : Un autre exemple d’utilisation du premier prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; var nameObjs = presidents.Select(p => new { p, p.Length }); foreach (var item in nameObjs) Console.WriteLine(item);

Ici, l’expression lambda instancie un nouveau type anonyme. Le compilateur génère dynamiquement un objet de type anonyme qui contient un string p et un int

Linq.book Page 70 Mercredi, 18. février 2009 7:58 07

70

LINQ to Objects

Partie II

p.Length, et la méthode selector retourne cet objet. Étant donné que l’élément retourné est de type anonyme, il n’existe aucun type pour y faire référence. Contrairement à l’exemple précédent, où la séquence de sortie avait été affectée à un IEnumerable, il est impossible d’affecter la séquence de sortie à un IEnumerable d’un type connu. C’est la raison pour laquelle le mot-clé var a été utilisé. INFO Les opérateurs de projection dont les méthodes selector instancient des types anonymes doivent affecter leur séquence de sortie à une variable déclarée avec le mot-clé var.

Voici la sortie dans la console lorsque vous appuyez sur Ctrl+F5 : { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { {

p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p

= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

Adams, Length = 5 } Arthur, Length = 6 } Buchanan, Length = 8 } Bush, Length = 4 } Carter, Length = 6 } Cleveland, Length = 9 } Clinton, Length = 7 } Coolidge, Length = 8 } Eisenhower, Length = 10 } Fillmore, Length = 8 } Ford, Length = 4 } Garfield, Length = 8 } Grant, Length = 5 } Harding, Length = 7 } Harrison, Length = 8 } Hayes, Length = 5 } Hoover, Length = 6 } Jackson, Length = 7 } Jefferson, Length = 9 } Johnson, Length = 7 } Kennedy, Length = 7 } Lincoln, Length = 7 } Madison, Length = 7 } McKinley, Length = 8 } Monroe, Length = 6 } Nixon, Length = 5 } Pierce, Length = 6 } Polk, Length = 4 } Reagan, Length = 6 } Roosevelt, Length = 9 } Taft, Length = 4 } Taylor, Length = 6 } Truman, Length = 6 } Tyler, Length = 5 } Van Buren, Length = 9 } Washington, Length = 10 } Wilson, Length = 6 }

Dans son état actuel, ce code a un inconvénient : il ne permet pas d’agir sur les membres de la classe anonyme générée dynamiquement. Cependant, grâce à la fonctionnalité d’initialisation d’objets de C# 3.0, il est possible de spécifier les noms des membres de la classe anonyme dans une expression lambda (voir Listing 4.5). Openmirrors.com

Linq.book Page 71 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

71

Listing 4.5 : Un troisième exemple du premier prototype de l’opérateur Select. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; var nameObjs = presidents.Select(p => new { LastName = p, Length = p.Length }); foreach (var item in nameObjs) Console.WriteLine("{0} contient {1} caractères", item.LastName, item.Length);

Comme vous pouvez le voir, le nom des membres a été spécifié dans l’expression lambda, et on a accédé aux membres par leurs noms dans la méthode Console.WriteLine. Voici le résultat de ce code : Adams contient 5 caractères Arthur contient 6 caractères Buchanan contient 8 caractères Bush contient 4 caractères Carter contient 6 caractères Cleveland contient 9 caractères Clinton contient 7 caractères Coolidge contient 8 caractères Eisenhower contient 10 caractères Fillmore contient 8 caractères Ford contient 4 caractères Garfield contient 8 caractères Grant contient 5 caractères Harding contient 7 caractères Harrison contient 8 caractères Hayes contient 5 caractères Hoover contient 6 caractères Jackson contient 7 caractères Jefferson contient 9 caractères Johnson contient 7 caractères Kennedy contient 7 caractères Lincoln contient 7 caractères Madcontienton contient 7 caractères McKinley contient 8 caractères Monroe contient 6 caractères Nixon contient 5 caractères Pierce contient 6 caractères Polk contient 4 caractères Reagan contient 6 caractères Roosevelt contient 9 caractères Taft contient 4 caractères Taylor contient 6 caractères Truman contient 6 caractères Tyler contient 5 caractères Van Buren contient 9 caractères Washington contient 10 caractères Wilson contient 6 caractères

Pour illustrer le second prototype, nous allons insérer l’index passé à la méthode selector dans la séquence de sortie (voir Listing 4.6).

Linq.book Page 72 Mercredi, 18. février 2009 7:58 07

72

LINQ to Objects

Partie II

Listing 4.6 : Un exemple d’utilisation du second prototype de l’opérateur Select. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; var nameObjs = presidents.Select((p, i) => new { Index = i, LastName = p }); foreach (var item in nameObjs) Console.WriteLine("{0}. {1}", item.Index + 1, item.LastName);

Pour chaque élément de la séquence d’entrée, cet exemple affiche la valeur de l’index augmentée de 1, puis le nom de l’élément. Voici les résultats affichés dans la console : 1. Adams 2. Arthur 3. Buchanan 4. Bush 5. Carter … 34. Tyler 35. Van Buren 36. Washington 37. Wilson

Opérateur SelectMany L’opérateur SelectMany est utilisé pour créer une ou plusieurs séquences à partir de la séquence passée en entrée. Contrairement à l’opérateur Select, qui retourne un élément en sortie pour chaque élément en entrée, SelectMany peut retourner zéro, un ou plusieurs éléments en sortie pour chaque élément en entrée.

Prototypes Deux prototypes de l’opérateur Select seront étudiés dans ce livre. Premier prototype public static IEnumerable SelectMany( this IEnumerable source, Func selector);

Ce prototype admet deux entrées : une séquence source d’éléments de type T et un délégué pour effectuer la sélection des données. Il retourne un objet dont l’énumération passe chaque élément de la séquence d’entrée au délégué. Lors de l’énumération de la méthode selector, zéro, un ou plusieurs éléments de type S sont retournés dans une séquence de sortie intermédiaire. L’opérateur SelectMany retourne les différentes séquences de sortie concaténées. Openmirrors.com

Linq.book Page 73 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

73

Second prototype public static IEnumerable SelectMany( this IEnumerable source, Func selector);

Ce prototype est en tout point semblable au précédent, si ce n’est qu’un index des éléments de la séquence d’entrée est passé à la méthode selector (l’index du premier élément est 0). Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.7 donne un exemple d’appel du premier prototype. Listing 4.7 : Un exemple du premier prototype de l’opérateur SelectMany. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable chars = presidents.SelectMany(p => p.ToArray()); foreach (char ch in chars) Console.WriteLine(ch);

Dans cet exemple, la méthode de sélection reçoit un paramètre string. En lui appliquant la méthode ToArray, on obtient un tableau de chaînes qui est transformé en une chaîne de sortie de type char. Pour une unique séquence en entrée (ici, un string), le sélecteur retourne une séquence de caractères. L’opérateur SelectMany concatène toutes ces séquences de caractères dans une seule qui devient la séquence de sortie. Voici le texte affiché dans la console suite à l’exécution du code : A d a m s A r t h u r

Linq.book Page 74 Mercredi, 18. février 2009 7:58 07

74

LINQ to Objects

Partie II

B u c h a n a nB u s h … W a s h i n g t o n W i l s o n

Cette requête est simple à comprendre, mais pas très démonstrative de la façon dont l’opérateur SelectMany est généralement utilisé. Dans le prochain exemple, nous utiliserons les classes communes Employee et EmployeeOptionEntry pour être plus proches de la réalité. L’opérateur SelectMany va être appliqué sur un tableau d’éléments Employee. Pour chacun de ces éléments, la méthode de sélection (le délégué) retournera zéro, un ou plusieurs éléments de la classe anonyme. Ces éléments contiendront les champs id et optionsCount du tableau d’éléments EmployeeOptionEntry de l’objet Employee (voir Listing 4.8). Listing 4.8 : Un exemple plus complexe du premier prototype de l’opérateur SelectMany. Employee[] employees = Employee.GetEmployeesArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .SelectMany(e => empOptions .Where(eo => eo.id == e.id) .Select(eo => new { id = eo.id, optionsCount = eo.optionsCount })); foreach (var item in employeeOptions) Console.WriteLine(item);

Chaque employé du tableau Employee est passé dans l’expression lambda utilisée dans l’opérateur SelectMany. Par l’intermédiaire de l’opérateur Where, l’expression lambda Openmirrors.com

Linq.book Page 75 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

75

retrouve alors les éléments EmployeeOptionEntry dont le champ id correspond au champ id de l’employé actuel. Ce code effectue donc une jointure des tableaux Employee et EmployeeOptionEntry sur le champ id. L’opérateur Select de l’expression lambda crée alors un objet anonyme composé des membres id et optionsCount pour chacun des enregistrements sélectionnés dans le tableau EmployeeOptionEntry. L’expression lambda retourne donc une séquence de zéro, un ou plusieurs objets anonymes pour chacun des employés sélectionnés. Le résultat final est une séquence de séquences concaténées par l’opérateur SelectMany. Voici le résultat de ce code, affiché dans la console : { { { { { { { { {

id id id id id id id id id

= = = = = = = = =

1, optionsCount = 2, optionsCount = 2, optionsCount = 2, optionsCount = 3, optionsCount = 3, optionsCount = 3, optionsCount = 4, optionsCount = 101, optionsCount

2 } 10000 } 10000 } 10000 } 5000 } 7500 } 7500 } 1500 } = 2 }

Bien qu’un peu tiré par les cheveux, le Listing 4.9 donne un exemple d’appel du second prototype de l’opérateur SelectMany. Listing 4.9 : Un exemple d’appel du second prototype de l’opérateur SelectMany. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable chars = presidents .SelectMany((p, i) => i < 5 ? p.ToArray() : new char[] { }); foreach (char ch in chars) Console.WriteLine(ch);

L’expression lambda teste la valeur de l’index. S’il est inférieur à 5, le tableau de caractères de la chaîne passée en entrée est retourné. Voici le résultat affiché dans la console : A d a m s A r t h u r

Linq.book Page 76 Mercredi, 18. février 2009 7:58 07

76

LINQ to Objects

Partie II

B u c h a n a n B u s h C a r t e r

Cette expression lambda n’est pas particulièrement efficace, en particulier si le nombre d’éléments en entrée est élevé. Elle est en effet appelée pour chacun des éléments passés en entrée, y compris pour ceux dont l’index est supérieur à 5. Dans ce cas, un tableau vide est retourné. Pour une plus grande efficacité, vous préférerez l’opérateur Take (voir la section suivante). L’opérateur SelectMany peut également être utilisé lorsqu’il s’agit de concaténer plusieurs séquences. Reportez-vous à la section relative à l’opérateur Concat, un peu plus loin dans ce chapitre, pour avoir un exemple de concaténation. Partage Les opérateurs de partage retournent une séquence qui est un sous-ensemble de la séquence d’entrée. Opérateur Take L’opérateur Take retourne un certain nombre d’éléments de la séquence d’entrée, à partir du premier.

Prototype Un seul prototype de l’opérateur Take sera étudié dans ce livre : public static IEnumerable Take( this IEnumerable source, int count);

L’opérateur Take admet deux paramètres en entrée : une séquence source et l’entier count, qui indique combien d’éléments doivent être retournés. Il renvoie un objet dont l’énumération produira les count premiers éléments de la séquence d’entrée. Si count est plus grand que le nombre d’éléments contenus dans la séquence d’entrée, la totalité de la séquence d’entrée est retournée. Openmirrors.com

Linq.book Page 77 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

77

Exceptions L’exception ArgumentNullException est levée si la séquence source a pour valeur null. Exemples Le Listing 4.10 donne un exemple d’appel de l’opérateur Take. Listing 4.10 : Un exemple d’appel de l’unique prototype Take. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Take(5); foreach (string item in items) Console.WriteLine(item);

Ce code retourne les cinq premiers éléments du tableau presidents : Adams Arthur Buchanan Bush Carter

Dans l’exemple précédent, j’ai indiqué que le code serait plus efficace si l’opérateur Take était utilisé pour limiter le nombre d’entrées soumises à l’expression lambda. Le code auquel je faisais référence se trouve dans le Listing 4.11. Listing 4.11 : Un autre exemple d’appel du prototype Take. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable chars = presidents.Take(5).SelectMany(s => s.ToArray()); foreach (char ch in chars) Console.WriteLine(ch);

La sortie console est identique à celle du Listing 4.9 : A d a m s

Linq.book Page 78 Mercredi, 18. février 2009 7:58 07

78

LINQ to Objects

Partie II

A r t h u r B u c h a n a n B u s h C a r t e r

Contrairement au Listing 4.9, seuls les cinq premiers éléments sont passés en entrée de l’opérateur SelectMany. Cette technique est bien plus efficace, en particulier si de nombreux éléments ne doivent pas être passés à SelectMany. L’opérateur TakeWhile L’opérateur TakeWhile renvoie les éléments de la séquence d’entrée, en commençant par le premier, tant qu’une condition est vérifiée. Les éléments restants sont ignorés.

Prototypes Deux prototypes de l’opérateur TakeWhile seront étudiés dans ce livre. Premier prototype public static IEnumerable TakeWhile( this IEnumerable source, Func predicate);

Dans ce prototype, l’opérateur TakeWhile admet deux paramètres en entrée : une séquence source et un prédicat. Il retourne un objet dont l’énumération fournit des éléments jusqu’à ce que le prédicat renvoie la valeur false. Les éléments suivants ne sont pas traités. Second prototype public static IEnumerable TakeWhile( this IEnumerable source, Func predicate);

Ce second prototype est semblable au premier si ce n’est que le prédicat reçoit également un entier qui correspond à l’index de l’élément dans la séquence source. Openmirrors.com

Linq.book Page 79 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

79

Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.12 donne un exemple d’appel du premier prototype. Listing 4.12 : Un exemple d’appel du premier prototype de l’opérateur TakeWhile. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.TakeWhile(s => s.Length < 10); foreach (string item in items) Console.WriteLine(item);

Seuls les éléments contenant dix caractères au maximum sont retournés : Adams Arthur Buchanan Bush Carter Cleveland Clinton Coolidge

L’énumération s’est arrêtée sur le nom Eisenhower, long de 10 caractères. Voici maintenant un exemple d’appel du second prototype de l’opérateur TakeWhile. Listing 4.13 : Un exemple d’appel du second prototype TakeWhile. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents .TakeWhile((s, i) => s.Length < 10 && i < 5); foreach (string item in items) Console.WriteLine(item);

Linq.book Page 80 Mercredi, 18. février 2009 7:58 07

80

LINQ to Objects

Partie II

Cet exemple arrête l’énumération lorsqu’un élément en entrée a une longueur supérieure à 9 caractères ou lorsque la sixième entrée est atteinte. Voici le résultat : Adams Arthur Buchanan Bush Carter

Ici, l’énumération s’est arrêtée lorsque la sixième entrée a été atteinte. Opérateur Skip L’opérateur Skip saute un certain nombre d’éléments dans la séquence d’entrée et retourne les suivants.

Prototype Un seul prototype de l’opérateur Skip sera étudié dans ce livre : public static IEnumerable Skip( this IEnumerable source, int count);

L’opérateur Skip admet deux paramètres : une séquence source et l’entier count, qui indique le nombre d’éléments à sauter. Ce prototype renvoie un objet dont l’énumération exclut les count premiers éléments. Si la valeur de count est supérieure au nombre d’éléments de la séquence d’entrée, cette dernière ne sera pas énumérée et la séquence de sortie sera vide. Exceptions L’exception ArgumentNullException est levée si la séquence d’entrée a pour valeur null. Exemples Le Listing 4.14 est un exemple d’appel du prototype Skip. Listing 4.14 : Un exemple d’utilisation du prototype de l’opérateur Skip. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Skip(1); foreach (string item in items) Console.WriteLine(item);

Openmirrors.com

Linq.book Page 81 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

81

Dans cet exemple, seul le premier élément est ignoré. Tous les éléments suivants sont donc renvoyés par l’opérateur Skip : Arthur Buchanan Bush … Van Buren Washington Wilson

Opérateur SkipWhile L’opérateur SkipWhile ignore les éléments de la séquence d’entrée tant qu’une condition est vérifiée. Les éléments suivants sont alors renvoyés dans la séquence de sortie.

Prototypes Deux prototypes de l’opérateur SkipWhile seront étudiés dans ce livre. Premier prototype public static IEnumerable SkipWhile( this IEnumerable source, Func predicate);

Ce premier prototype admet deux paramètres : une séquence source et un prédicat. Il renvoie un objet dont l’énumération exclut les éléments de la séquence d’entrée tant que le prédicat retourne la valeur true. Dès qu’une valeur false est retournée, tous les éléments suivants sont envoyés dans la séquence de sortie. Second prototype public static IEnumerable SkipWhile( this IEnumerable source, Func predicate);

Ce second prototype est semblable au premier si ce n’est que le prédicat reçoit également un entier qui correspond à l’index de l’élément dans la séquence source. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.15 donne un exemple d’appel du premier prototype de l’opérateur SkipWhile. Listing 4.15 : Un exemple d’appel du premier prototype de l’opérateur SkipWhile. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson",

Linq.book Page 82 Mercredi, 18. février 2009 7:58 07

82

LINQ to Objects

Partie II

"Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.SkipWhile(s => s.StartsWith("A")); foreach (string item in items) Console.WriteLine(item);

Dans cet exemple, tous les éléments qui commencent par la lettre A sont ignorés. Les éléments suivants sont passés à la séquence de sortie : Buchanan Bush Carter … Van Buren Washington Wilson

Le Listing 4.16 donne un exemple d’utilisation du second prototype de l’opérateur SkipWhile. Listing 4.16 : Un exemple d’utilisation du second prototype de l’opérateur SkipWhile. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents .SkipWhile((s, i) => s.Length > 4 && i < 10); foreach (string item in items) Console.WriteLine(item);

Dans cet exemple, tous les éléments dont la longueur est inférieure ou égale à 4 caractères ou supérieure ou égale à 10 caractères sont ignorés. Les éléments suivants constituent la séquence de sortie : Bush Carter Cleveland … Van Buren Washington Wilson

L’élément Bush compte 4 caractères. Il a donc mis fin au SkipWhile. Openmirrors.com

Linq.book Page 83 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

83

Concaténation Les opérateurs de concaténation accolent plusieurs séquences d’entrée dans la séquence de sortie. Opérateur Concat L’opérateur Concat accole deux séquences d’entrée dans la séquence de sortie.

Prototype Un seul prototype de l’opérateur Concat sera étudié dans ce livre : public static IEnumerable Concat( this IEnumerable first, IEnumerable second);

Deux séquences de même type T sont fournies en entrée de ce prototype : first et second. L’énumération de l’objet retourné renvoie tous les éléments de la première séquence d’entrée suivis de tous les éléments de la seconde séquence d’entrée. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.17 donne un exemple d’utilisation des opérateurs Concat, Take et Skip. Listing 4.17 : Un exemple d’utilisation du prototype de l’opérateur Concat. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Take(5).Concat(presidents.Skip(5)); foreach (string item in items) Console.WriteLine(item);

Ce code concatène les cinq premiers éléments de la séquence d’entrée presidents aux éléments de cette même séquence d’entrée, en excluant les cinq premiers. Le résultat contient donc tous les éléments de la séquence d’entrée : Adams Arthur Buchanan Bush Carter Cleveland Clinton

Linq.book Page 84 Mercredi, 18. février 2009 7:58 07

84

LINQ to Objects

Partie II

Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson

Pour effectuer une concaténation, vous pouvez également utiliser l’opérateur SelectMany (voir Listing 4.18). Listing 4.18 : Un exemple effectuant une concaténation sans l’opérateur Concat. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = new[] { presidents.Take(5), presidents.Skip(5) } .SelectMany(s => s); foreach (string item in items) Console.WriteLine(item);

Le tableau item a été instancié par l’intermédiaire de deux séquences : une créée avec l’opérateur Take et une autre, avec l’opérateur Skip. Cet exemple est comparable au précédent mais, ici, on fait appel à l’opérateur SelectMany. Openmirrors.com

Linq.book Page 85 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

85

ASTUCE Si vous devez concaténer plusieurs séquences, vous utiliserez l’opérateur SelectMany. L’opérateur Concat, quant à lui, est limité à la concaténation de deux séquences.

Voici le résultat affiché dans la console : Adams Arthur Buchanan Bush Carter Cleveland Clinton Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson

Tri Les opérateurs de tri permettent de classer des séquences. Les opérateurs OrderBy et OrderByDescending nécessitent tous deux une séquence d’entrée de type IEnumerable et retournent une séquence de type IOrderedEnumerable. Il est impossible de passer un IOrderedEnumerable en entrée des opérateurs OrderBy et OrderByDescending. Tout chaînage est donc impossible. Si vous avez besoin de trier conjointement plusieurs éléments, utilisez les opérateurs ThenBy ou ThenByDescending. Ces opérateurs peuvent être chaînés car ils admettent et retournent des IOrderedEnumerable.

Linq.book Page 86 Mercredi, 18. février 2009 7:58 07

86

LINQ to Objects

Partie II

À titre d’exemple, cet appel n’est pas valide : inputSequence.OrderBy(s => s.LastName).OrderBy(s => s.FirstName)…

Pour effectuer ce traitement, vous utiliserez la syntaxe suivante : inputSequence.OrderBy(s => s.LastName).ThenBy(s => s.FirstName)…

L’opérateur OrderBy L’opérateur OrderBy trie une séquence d’entrée en utilisant la méthode keySelector. Cette méthode retourne une valeur clé pour chaque élément en entrée et une séquence de sortie de type IOrderedEnumerable. Dans cette dernière, les éléments seront classés dans un ordre croissant, en se basant sur les valeurs clés retournées.

Le tri effectué par l’opérateur OrderBy est connu pour être "instable" : si deux éléments ayant la même valeur clé sont passés à OrderBy, leur ordre initial peut aussi bien être maintenu qu’inversé. Vous ne devez donc jamais vous fier à l’ordre des éléments issus de ces opérateurs OrderBy et OrderByDescending pour les champs qui ne sont pas spécifiés dans la méthode. ATTENTION Le tri effectué par les opérateurs OrderBy et OrderByDescending est "instable".

Prototypes Deux prototypes de l’opérateur OrderBy seront étudiés dans ce livre. Premier prototype public static IOrderedEnumerable OrderBy( this IEnumerable source, Func keySelector) where K : IComparable;

Ce prototype admet deux entrées : une séquence source et le délégué keySelector. L’énumération de l’objet retourné passe tous les éléments de la séquence d’entrée à la méthode KeySelector afin d’obtenir leurs clés et de procéder à leur tri. La méthode KeySelector se voit passer un élément de type T. Elle retourne la valeur clé de type K. Les types T et K peuvent être similaires ou différents. En revanche, le type de la valeur retournée par la méthode KeySelector doit implémenter l’interface IComparable. Second prototype public static IOrderedEnumerable OrderBy( this IEnumerable source, Func keySelector, IComparer comparer);

Ce prototype est le même que le précédent, si ce n’est qu’un objet comparer complémentaire lui est passé. Si vous utilisez cette version de l’opérateur OrderBy, le type K n’est pas forcé d’implémenter l’interface IComparable. Openmirrors.com

Linq.book Page 87 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

87

Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le Listing 4.19 est un exemple d’utilisation du premier prototype. Listing 4.19 : Un exemple du premier prototype de l’opérateur OrderBy. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.OrderBy(s => s.Length); foreach (string item in items) Console.WriteLine(item);

Cet exemple classe les présidents par la longueur de leurs noms. Voici les résultats : Bush Ford Polk Taft Adams Grant Hayes Nixon Tyler Arthur Carter Hoover Monroe Pierce Reagan Taylor Truman Wilson Clinton Harding Jackson Johnson Kennedy Lincoln Madison Buchanan Coolidge Fillmore Garfield Harrison McKinley Cleveland Jefferson Roosevelt Van Buren Eisenhower Washington

Linq.book Page 88 Mercredi, 18. février 2009 7:58 07

88

LINQ to Objects

Partie II

Nous allons maintenant donner un exemple d’utilisation du deuxième prototype. Mais, auparavant, prenons quelques instants pour examiner l’interface IComparer : interface IComparer { int Compare(T x, T y); }

Cette interface utilise la méthode Compare. Cette dernière admet deux arguments de type T en entrée et retourne une valeur int. Sa valeur est : m

négative si le premier argument est inférieur au second ;

m

nulle si les deux arguments sont égaux ;

m

positive si le second argument est supérieur au premier.

Remarquez à quel point les génériques de C# 2.0 sont utiles dans cette interface et ce prototype. Pour faire fonctionner cet exemple, une classe spécifique qui implémente l’interface IComparer a été créée. Cette classe réarrangera les éléments par rapport à leur ratio nombre de voyelles/nombre de consonnes. Implémentation de l’interface IComparer pour illustrer le second prototype OrderBy public class MyVowelToConsonantRatioComparer : IComparer { public int Compare(string s1, string s2) { int vCount1 = 0; int cCount1 = 0; int vCount2 = 0; int cCount2 = 0; GetVowelConsonantCount(s1, ref vCount1, ref cCount1); GetVowelConsonantCount(s2, ref vCount2, ref cCount2); double dRatio1 = (double)vCount1/(double)cCount1; double dRatio2 = (double)vCount2/(double)cCount2; if(dRatio1 < dRatio2) return(-1); else if (dRatio1 > dRatio2) return(1); else return(0); } // Cette méthode est publique. Le code qui utilise ce comparateur // pourra donc y accéder si cela est nécessaire public void GetVowelConsonantCount(string s, ref int vowelCount, ref int consonantCount) { string vowels = "AEIOUY"; // Initialize the counts. vowelCount = 0; consonantCount = 0;

Openmirrors.com

Linq.book Page 89 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

89

// Conversion en majuscules pour ne pas être sensible à la casse string sUpper = s.ToUpper(); foreach(char ch in sUpper) { if(vowels.IndexOf(ch) < 0) consonantCount++; else vowelCount++; } return; } }

Cette classe contient deux méthodes : Compare et GetVowelConsonantCount. La méthode Compare est nécessaire pour l’interface IComparer. La méthode GetConsonantVowelCount calcule le nombre de voyelles et de consonnes de la chaîne qui lui est passée. Par son intermédiaire, il est ainsi possible d’obtenir les valeurs à afficher lors de l’énumération de la séquence réordonnée. La logique utilisée à l’intérieur de la méthode n’a pas d’importance. Il est en effet peu probable que vous ayez un jour à classer des données en tenant compte de leur ratio nombre de voyelles/nombre de consonnes, et encore moins de comparer deux chaînes selon ce ratio. Ce qui est important, en revanche, c’est la technique qui a permis de créer une classe qui implémente l’interface IComparer en implémentant la méthode Compare. Pour cela, examinez le bloc if … else à la fin de la méthode Compare. Comme vous le voyez, les valeurs retournées sont -1, 1 ou 0, ce qui assure la compatibilité avec l’interface IComparer. Le Listing 4.20 donne un exemple d’appel du code. Listing 4.20 : Un exemple d’appel du second prototype OrderBy. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer(); IEnumerable namesByVToCRatio = presidents .OrderBy((s => s), myComp); foreach (string item in namesByVToCRatio) { int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount; Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount); }

Linq.book Page 90 Mercredi, 18. février 2009 7:58 07

90

LINQ to Objects

Partie II

L’objet mycomp a été instancié avant d’appeler l’opérateur OrderBy. Une référence est donc créée, et il est possible de l’utiliser dans la boucle foreach. Voici les résultats de ce code : Grant - 0.25 - 1:4 Bush - 0.333333333333333 - 1:3 Ford - 0.333333333333333 - 1:3 Polk - 0.333333333333333 - 1:3 Taft - 0.333333333333333 - 1:3 Clinton - 0.4 - 2:5 Harding - 0.4 - 2:5 Jackson - 0.4 - 2:5 Johnson - 0.4 - 2:5 Lincoln - 0.4 - 2:5 Washington - 0.428571428571429 - 3:7 Arthur - 0.5 - 2:4 Carter - 0.5 - 2:4 Cleveland - 0.5 - 3:6 Jefferson - 0.5 - 3:6 Truman - 0.5 - 2:4 Van Buren - 0.5 - 3:6 Wilson - 0.5 - 2:4 Buchanan - 0.6 - 3:5 Fillmore - 0.6 - 3:5 Garfield - 0.6 - 3:5 Harrison - 0.6 - 3:5 McKinley - 0.6 - 3:5 Adams - 0.666666666666667 - 2:3 Nixon - 0.666666666666667 - 2:3 Tyler - 0.666666666666667 - 2:3 Kennedy - 0.75 - 3:4 Madison - 0.75 - 3:4 Roosevelt - 0.8 - 4:5 Coolidge - 1 - 4:4 Eisenhower - 1 - 5:5 Hoover - 1 - 3:3 Monroe - 1 - 3:3 Pierce - 1 - 3:3 Reagan - 1 - 3:3 Taylor - 1 - 3:3 Hayes - 1.5 - 3:2

Les présidents sont classés par ratio voyelle/consonne croissant. L’opérateur OrderByDescending Cet opérateur a les mêmes prototypes et comportement que OrderBy, excepté que les éléments sont classés dans un ordre décroissant.

Prototypes Deux prototypes de l’opérateur OrderByDescending seront étudiés dans ce livre. Premier prototype public static IOrderedEnumerable OrderByDescending( this IEnumerable source, Func keySelector) where K : IComparable;

Openmirrors.com

Linq.book Page 91 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

91

ATTENTION Le tri effectué par les opérateurs OrderBy et OrderByDescending est "instable".

Second prototype public static IOrderedEnumerable OrderByDescending( this IEnumerable source, Func keySelector, IComparer comparer);

Ce prototype est le même que le précédent, si ce n’est qu’un objet comparer complémentaire lui est passé. Si vous utilisez cette version de l’opérateur OrderByDescending, le type K n’est pas forcé d’implémenter l’interface IComparable. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Dans l’exemple du Listing 4.21, nous allons classer les présidents des États-Unis en utilisant un ordre inverse alphabétique sur leurs noms. Listing 4.21 : Un exemple d’utilisation du premier prototype d’OrderDescending. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.OrderByDescending(s => s); foreach (string item in items) Console.WriteLine(item);

Les présidents sont bien classés en utilisant un ordre inverse alphabétique sur leurs noms. Wilson Washington Van Buren Tyler Truman Taylor Taft Roosevelt Reagan Polk Pierce Nixon Monroe McKinley Madison Lincoln

Linq.book Page 92 Mercredi, 18. février 2009 7:58 07

92

LINQ to Objects

Partie II

Kennedy Johnson Jefferson Jackson Hoover Hayes Harrison Harding Grant Garfield Ford Fillmore Eisenhower Coolidge Clinton Cleveland Carter Bush Buchanan Arthur Adams

Nous allons maintenant donner un exemple d’appel du second prototype d’ OrderByDescending. Nous utiliserons le même code (y compris au niveau du comparateur MyVowelToConsonantRatioComparer) que dans la section relative à l’opérateur OrderBy. Mais, ici, c’est l’opérateur OrderByDescending qui sera appelé (voir Listing 4.22). Listing 4.22 : Un exemple d’appel du second prototype d’OrderByDescending. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer(); IEnumerable namesByVToCRatio = presidents .OrderByDescending((s => s), myComp); foreach (string item in namesByVToCRatio) { int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount; Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount); }

Voici les résultats de cet exemple : Hayes - 1.5 - 3:2 Coolidge - 1 - 4:4 Eisenhower - 1 - 5:5

Openmirrors.com

Linq.book Page 93 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

93

Hoover - 1 - 3:3 Monroe - 1 - 3:3 Pierce - 1 - 3:3 Reagan - 1 - 3:3 Taylor - 1 - 3:3 Roosevelt - 0.8 - 4:5 Kennedy - 0.75 - 3:4 Madison - 0.75 - 3:4 Adams - 0.666666666666667 - 2:3 Nixon - 0.666666666666667 - 2:3 Tyler - 0.666666666666667 - 2:3 Buchanan - 0.6 - 3:5 Fillmore - 0.6 - 3:5 Garfield - 0.6 - 3:5 Harrison - 0.6 - 3:5 McKinley - 0.6 - 3:5 Arthur - 0.5 - 2:4 Carter - 0.5 - 2:4 Cleveland - 0.5 - 3:6 Jefferson - 0.5 - 3:6 Truman - 0.5 - 2:4 Van Buren - 0.5 - 3:6 Wilson - 0.5 - 2:4 Washington - 0.428571428571429 - 3:7 Clinton - 0.4 - 2:5 Harding - 0.4 - 2:5 Jackson - 0.4 - 2:5 Johnson - 0.4 - 2:5 Lincoln - 0.4 - 2:5 Bush - 0.333333333333333 - 1:3 Ford - 0.333333333333333 - 1:3 Polk - 0.333333333333333 - 1:3 Taft - 0.333333333333333 - 1:3 Grant - 0.25 - 1:4

Ces résultats sont les mêmes que dans l’exemple de la section précédente mais, ici, le classement a été effectué du plus grand au plus petit ratio voyelles/consonnes. Opérateur ThenBy L’opérateur ThenBy trie une séquence de type IOrderedEnumerable en se basant sur une méthode keySelector qui lui retourne une valeur clé. Il renvoie une séquence de sortie de type IOrderedEnumerable. INFO Les opérateurs ThenBy et ThenByDescending demandent tous deux un paramètre dont le type est inhabituel : IOrderedEnumerable. L’opérateur OrderBy ou OrderByDescending doit être appelé en premier lieu pour créer un objet IOrderedEnumerable.

INFO Contrairement aux opérateurs OrderBy et OrderByDescending, ThenBy et ThenByDescending sont stables. Ils préservent donc l’ordre original des éléments qui possèdent la même clé.

Linq.book Page 94 Mercredi, 18. février 2009 7:58 07

94

LINQ to Objects

Partie II

Prototypes Deux prototypes de l’opérateur ThenBy seront étudiés dans ce livre. Premier prototype public static IOrderedEnumerable ThenBy( this IOrderedEnumerable source, Func keySelector) where K : IComparable;

Dans ce prototype, l’opérateur ThenBy reçoit une séquence d’entrée de type IOrderedEnumerable et un délégué keySelector. Ce dernier se voit passer l’élément d’entrée de type T et retourne le champ de type K de cet élément qui sera utilisé comme valeur clé. Les types T et K peuvent être identiques ou différents. La valeur retournée par la méthode KeySelector doit implémenter l’interface ICompare. L’opérateur ThenBy classe la séquence d’entrée par ordre croissant selon la clé retournée par keySelector. Second prototype public static IOrderedEnumerable ThenBy( this IOrderedEnumerable source, Func keySelector, IComparer comparer);

Ce prototype est identique au précédent, si ce n’est qu’un objet comparer complémentaire lui est passé. Si vous utilisez cette version de l’opérateur ThenBy, le type K n’est pas forcé d’implémenter l’interface IComparable. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null (voir Listing 4.23). Exemples Listing 4.23 : Un exemple d’appel du premier prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.OrderBy(s => s.Length).ThenBy(s => s); foreach (string item in items) Console.WriteLine(item);

Dans un premier temps, ce code classe les éléments (ici, les noms des présidents des États-Unis) selon leur longueur. Dans un second temps, les éléments sont classés dans Openmirrors.com

Linq.book Page 95 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

95

un ordre alphabétique. Si plusieurs noms ont la même longueur, ils apparaîtront donc dans l’ordre alphabétique. Bush Ford Polk Taft Adams Grant Hayes Nixon Tyler Arthur Carter Hoover Monroe Pierce Reagan Taylor Truman Wilson Clinton Harding Jackson Johnson Kennedy Lincoln Madison Buchanan Coolidge Fillmore Garfield Harrison McKinley Cleveland Jefferson Roosevelt Van Buren Eisenhower Washington

Pour illustrer le second prototype de l’opérateur ThenBy, nous allons utiliser le comparateur MyVowelConsonantRatioComparer, introduit quelques pages précédemment. Pour être en mesure d’appeler l’opérateur ThenBy, il faut au préalable appeler l’opérateur OrderBy ou OrderByDescending. Le but de cet exemple est de classer les noms par longueurs croissantes puis, à l’intérieur de chaque groupe de longueurs, par ratio voyelles/consonnes (voir Listing 4.24). Listing 4.24 : Un exemple d’appel du second prototype de ThenBy. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft",

Linq.book Page 96 Mercredi, 18. février 2009 7:58 07

96

LINQ to Objects

Partie II

"Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer(); IEnumerable namesByVToCRatio = presidents .OrderBy(n => n.Length) .ThenBy((s => s), myComp); foreach (string item in namesByVToCRatio) { int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount; Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount); }

Voici le résultat de ce code : Bush - 0.333333333333333 - 1:3 Ford - 0.333333333333333 - 1:3 Polk - 0.333333333333333 - 1:3 Taft - 0.333333333333333 - 1:3 Grant - 0.25 - 1:4 Adams - 0.666666666666667 - 2:3 Nixon - 0.666666666666667 - 2:3 Tyler - 0.666666666666667 - 2:3 Hayes - 1.5 - 3:2 Arthur - 0.5 - 2:4 Carter - 0.5 - 2:4 Truman - 0.5 - 2:4 Wilson - 0.5 - 2:4 Hoover - 1 - 3:3 Monroe - 1 - 3:3 Pierce - 1 - 3:3 Reagan - 1 - 3:3 Taylor - 1 - 3:3 Clinton - 0.4 - 2:5 Harding - 0.4 - 2:5 Jackson - 0.4 - 2:5 Johnson - 0.4 - 2:5 Lincoln - 0.4 - 2:5 Kennedy - 0.75 - 3:4 Madison - 0.75 - 3:4 Buchanan - 0.6 - 3:5 Fillmore - 0.6 - 3:5 Garfield - 0.6 - 3:5 Harrison - 0.6 - 3:5 McKinley - 0.6 - 3:5 Coolidge - 1 - 4:4 Cleveland - 0.5 - 3:6 Jefferson - 0.5 - 3:6 Van Buren - 0.5 - 3:6 Roosevelt - 0.8 - 4:5 Washington - 0.428571428571429 - 3:7 Eisenhower - 1 - 5:5

Comme prévu, les noms sont classés par longueurs, puis par ratio voyelles/consonnes. Openmirrors.com

Linq.book Page 97 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

97

Opérateur ThenByDescending Cet opérateur utilise les mêmes prototypes et se comporte comme l’opérateur ThenBy, mais il classe les données dans un ordre décroissant.

Prototypes Deux prototypes de l’opérateur ThenByDescending seront étudiés dans ce livre. Premier prototype public static IOrderedEnumerable ThenByDescending( this IOrderedEnumerable source, Func keySelector) where K : IComparable;

Ce prototype se comporte comme le premier prototype de l’opérateur ThenBy, mais il classe les données dans un ordre décroissant. Second prototype public static IOrderedEnumerable ThenByDescending( this IOrderedEnumerable source, Func keySelector, IComparer comparer);

Ce prototype est identique au précédent, si ce n’est qu’un objet comparer complémentaire lui est passé. Si vous utilisez cette version de l’opérateur ThenByDescending, le type K n’est pas forcé d’implémenter l’interface IComparable. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Nous allons utiliser le même exemple que dans la section précédente, mais ici l’opérateur ThenByDescending sera utilisé à la place de ThenBy (voir Listing 4.25). Listing 4.25 : Un exemple d’appel du premier prototype de l’opérateur ThenByDescending. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.OrderBy(s => s.Length).ThenByDescending(s => s); foreach (string item in items) Console.WriteLine(item);

Linq.book Page 98 Mercredi, 18. février 2009 7:58 07

98

LINQ to Objects

Partie II

Ce code classe les noms des présidents par longueur croissante puis, à l’intérieur de chaque groupe, par ordre inverse alphabétique. Taft Polk Ford Bush Tyler Nixon Hayes Grant Adams Wilson Truman Taylor Reagan Pierce Monroe Hoover Carter Arthur Madison Lincoln Kennedy Johnson Jackson Harding Clinton McKinley Harrison Garfield Fillmore Coolidge Buchanan Van Buren Roosevelt Jefferson Cleveland Washington Eisenhower

Pour illustrer le second prototype de l’opérateur ThenByDescending, nous utiliserons le même code que dans l’exemple du second prototype de l’opérateur ThenBy, à ceci près que l’opérateur ThenByDescending remplacera l’opérateur ThenBy (voir Listing 4.26). Listing 4.26 : Un exemple d’appel du second prototype de l’opérateur ThenByDescending. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

Openmirrors.com

Linq.book Page 99 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

99

MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer(); IEnumerable namesByVToCRatio = presidents .OrderBy(n => n.Length) .ThenByDescending((s => s), myComp); foreach (string item in namesByVToCRatio) { int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount; Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount); }

Voici les informations affichées dans la console suite à l’exécution de ce code : Bush - 0.333333333333333 - 1:3 Ford - 0.333333333333333 - 1:3 Polk - 0.333333333333333 - 1:3 Taft - 0.333333333333333 - 1:3 Hayes - 1.5 - 3:2 Adams - 0.666666666666667 - 2:3 Nixon - 0.666666666666667 - 2:3 Tyler - 0.666666666666667 - 2:3 Grant - 0.25 - 1:4 Hoover - 1 - 3:3 Monroe - 1 - 3:3 Pierce - 1 - 3:3 Reagan - 1 - 3:3 Taylor - 1 - 3:3 Arthur - 0.5 - 2:4 Carter - 0.5 - 2:4 Truman - 0.5 - 2:4 Wilson - 0.5 - 2:4 Kennedy - 0.75 - 3:4 Madison - 0.75 - 3:4 Clinton - 0.4 - 2:5 Harding - 0.4 - 2:5 Jackson - 0.4 - 2:5 Johnson - 0.4 - 2:5 Lincoln - 0.4 - 2:5 Coolidge - 1 - 4:4 Buchanan - 0.6 - 3:5 Fillmore - 0.6 - 3:5 Garfield - 0.6 - 3:5 Harrison - 0.6 - 3:5 McKinley - 0.6 - 3:5 Roosevelt - 0.8 - 4:5 Cleveland - 0.5 - 3:6 Jefferson - 0.5 - 3:6 Van Buren - 0.5 - 3:6 Eisenhower - 1 - 5:5 Washington - 0.428571428571429 - 3:7

Comme vous pouvez le voir, les noms sont classés par longueur croissante, puis par ratio voyelles/consonnes décroissant.

Linq.book Page 100 Mercredi, 18. février 2009 7:58 07

100

LINQ to Objects

Partie II

Opérateur Reverse Cet opérateur renvoie une séquence du même type que celle passée en entrée, mais en inversant ses éléments.

Prototype Un seul prototype de l’opérateur Reverse sera étudié dans ce livre : public static IEnumerable Reverse( this IEnumerable source);

Ce prototype retourne une séquence IEnumerable dont l’énumération produit l’ordre inverse des éléments de la séquence d’entrée. Exceptions L’exception ArgumentNullException est levée si l’argument a pour valeur null (voir Listing 4.27). Exemples Listing 4.27 : Un exemple d’appel de l’opérateur Reverse. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable items = presidents.Reverse(); foreach (string item in items) Console.WriteLine(item);

Ce code affiche les informations suivantes dans la fenêtre Console. Comme on pouvait s’y attendre, les noms des présidents apparaissent dans l’ordre inverse de ceux passés en entrée : Wilson Washington Van Buren … Bush Buchanan Arthur Adams

Opérateurs de jointure Les opérateurs de jointure effectuent un assemblage de plusieurs séquences. Openmirrors.com

Linq.book Page 101 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

101

Opérateur Join L’opérateur Join effectue une jointure entre deux séquences, en se basant sur les clés extraites des différents éléments des deux séquences.

Prototype Un seul prototype de l’opérateur Join sera abordé dans cet ouvrage : public static IEnumerable Join( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector);

Le premier élément de la méthode a pour nom outer (extérieur). Comme il s’agit d’une méthode d’extension, on parlera de "séquence extérieure" pour faire référence à la séquence sur laquelle l’opérateur Join est appelé. L’opérateur Join retourne un objet. Son énumération produit, dans un premier temps, une séquence inner d’éléments de type U. Pour ce faire, la méthode innerKeySelector est appelée sur chaque élément de la séquence inner et un tableau de référencement est créé pour mémoriser les couples élément/valeur clé. Dans un second temps, l’objet retourné énumère la séquence outer d’éléments de type T. Pour ce faire, la méthode outerKeySelector est appelée sur chaque élément de la séquence outer afin d’obtenir sa clé et de retrouver la séquence inner correspondante dans le tableau de référencement. Pour chaque élément de la paire séquence outer/séquence inner, l’objet retourné appelle enfin la méthode resultSelector, en lui passant les éléments outer et inner. Un objet instancié de type V est alors retourné par la méthode resultSelector, puis placé dans la séquence de sortie de type V. L’ordre des éléments de la séquence outer est préservé, ainsi que celui des éléments inner de chaque séquence outer. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Cet exemple utilise les deux classes communes définies au début de ce chapitre : Employee et EmployeeOptionEntry. Le code du Listing 4.28 a été mis en forme un peu différemment afin d’améliorer la lisibilité des arguments de l’opérateur Join. Listing 4.28 : Un exemple d’appel de l’opérateur Join. Employee[] employees = Employee.GetEmployeesArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .Join( empOptions, // séquence inner

Linq.book Page 102 Mercredi, 18. février 2009 7:58 07

102

LINQ to Objects

Partie II

e => e.id, // outerKeySelector o => o.id, // innerKeySelector (e, o) => new // resultSelector { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = o.optionsCount }); foreach (var item in employeeOptions) Console.WriteLine(item);

Ce code effectue une jointure sur deux tableaux de données en utilisant deux classes communes. L’opérateur Join étant appliqué au tableau employees, ce dernier joue le rôle de la séquence externe. Quant à empOptions, il correspond à la séquence interne. Voici les résultats de la jointure : { { { { { { { { {

id id id id id id id id id

= = = = = = = = =

1, name = 2, name = 2, name = 2, name = 3, name = 3, name = 3, name = 4, name = 101, name

Joe Rattz, options = 2 } William Gates, options = 10000 } William Gates, options = 10000 } William Gates, options = 10000 } Anders Hejlsberg, options = 5000 } Anders Hejlsberg, options = 7500 } Anders Hejlsberg, options = 7500 } David Lightman, options = 1500 } = Kevin Flynn, options = 2 }

La méthode resultSelector crée une classe anonyme du même type que la séquence de sortie. Il est facile de voir qu’il s’agit d’une classe anonyme, car aucun nom de classe n’est spécifié dans l’instruction new. Par ailleurs, le mot-clé var est utilisé, ce qui confirme nos soupçons. Il n’est pas possible de le déclarer en tant qu’ IEnumerable, puisque aucun type nommé ne donne les précisions nécessaires pour le déclarer comme tel. ASTUCE Lorsque le dernier opérateur appelé retourne une séquence de type anonyme, vous devez utiliser le mot-clé var pour mémoriser la séquence dans un objet.

L’opérateur GroupJoin L’opérateur GroupJoin effectue une jointure sur deux séquences en se basant sur les clés extraites de chacun des éléments des deux séquences.

Cet opérateur travaille d’une manière comparable à l’opérateur Join, à ceci près que l’opérateur Join ne passe qu’un seul élément de la séquence externe et un élément de la séquence interne à la méthode resultSelector. Cela signifie que, si plusieurs éléments de la séquence interne correspondent à un élément de la séquence interne, plusieurs appels à resultSelect seront nécessaires. Avec l’opérateur GroupJoin, tous les éléments de la séquence interne qui correspondent à un élément de la séquence externe Openmirrors.com

Linq.book Page 103 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

103

sont passés conjointement sous la forme d’une séquence à resultSelector. Un seul appel à cette méthode est donc nécessaire. Prototype Un seul prototype de l’opérateur GroupJoin sera étudié dans cet ouvrage : public static IEnumerable GroupJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector);

Le premier élément de la méthode a pour nom outer (extérieur). Comme il s’agit d’une méthode d’extension, on parlera de "séquence extérieure" pour faire référence à la séquence sur laquelle l’opérateur Join est appelé. L’opérateur GroupJoin retourne un objet. Son énumération produit, dans un premier temps, une séquence inner d’éléments de type U. Pour ce faire, la méthode innerKeySelector est appelée sur chaque élément de la séquence inner et un tableau de référencement est créé pour mémoriser les couples élément/valeur clé. Dans un second temps, l’objet retourné énumère la séquence outer d’éléments de type T. Pour ce faire, la méthode outerKeySelector est appelée sur chaque élément de la séquence outer afin d’obtenir sa clé et de retrouver la séquence inner correspondante dans le tableau de référencement. Pour chaque élément de la paire séquence outer/séquence inner, l’objet retourné appelle enfin la méthode resultSelector, en lui passant l’élément outer et la séquence des éléments inner correspondants. Un objet instancié de type V est alors retourné par la méthode resultSelector, puis placé dans la séquence de sortie de type V. L’ordre des éléments de la séquence outer est préservé, ainsi que celui des éléments inner de chaque séquence outer. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Nous utiliserons les classes Employee et EmployeeOptionEntry déjà évoquées dans la section précédente. Le code du Listing 4.29 réalise une jointure entre les employés et les options et calcule la somme des options de chacun des employés en utilisant l’opérateur GroupJoin. Listing 4.29 : Un exemple d’utilisation de l’opérateur GroupJoin. Employee[] employees = Employee.GetEmployeesArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .GroupJoin(

Linq.book Page 104 Mercredi, 18. février 2009 7:58 07

104

LINQ to Objects

Partie II

empOptions, e => e.id, o => o.id, (e, os) => new { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = os.Sum(o => o.optionsCount) }); foreach (var item in employeeOptions) Console.WriteLine(item);

Ce code est très proche du précédent. Cependant, si vous examinez le deuxième argument passé à l’expression lambda (issu de la méthode resultSelector), vous verrez que l’argument o de l’exemple sur l’opérateur Join est remplacé par os. Cette différence s’explique par le fait que l’opérateur Join travaille sur un seul objet option, alors que l’opérateur GroupJoin travaille sur une séquence d’objets option. L’opérateur Sum initialise donc le dernier membre de l’objet anonyme instancié avec la somme des objets option. Pour l’instant, il vous suffit de savoir que cet opérateur est en mesure de calculer la somme des éléments (ou d’un membre des éléments) qui lui sont passés. Pour en savoir plus sur l’opérateur non différé Join, reportez-vous au Chapitre 5. Voici le résultat du code précédent : { { { { {

id id id id id

= = = = =

1, name = 2, name = 3, name = 4, name = 101, name

Joe Rattz, options = 2 } William Gates, options = 30000 } Anders Hejlsberg, options = 20000 } David Lightman, options = 1500 } = Kevin Flynn, options = 2 }

Dans ces résultats, les valeurs options correspondent à la somme de tous les champs option de chaque employé. Ces résultats sont différents de ceux issus de l’opérateur Join, où une ligne était créée pour chacune des options de chaque employé. Opérateurs de regroupement Ces opérateurs permettent de regrouper les éléments d’une séquence qui possèdent une même clé. Opérateur GroupBy Cet opérateur est utilisé pour regrouper les éléments d’une séquence d’entrée.

Prototypes Tous les prototypes de l’opérateur GroupBy retournent une séquence d’éléments IGrouping. L’interface IGrouping est définie comme suit : public interface IGrouping : IEnumerable { K Key { get; } }

Openmirrors.com

Linq.book Page 105 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

105

Un IGrouping est donc une séquence de type T avec une clé de type K. Quatre prototypes de GroupBy seront étudiés dans cet ouvrage. Premier prototype public static IEnumerable GroupBy( this IEnumerable source, Func keySelector);

Ce prototype retourne un objet dont l’énumération passe en revue les éléments de la séquence d’entrée, appelle la méthode keySelector, mémorise chaque élément avec sa clé et produit une séquence d’instances IGrouping dans laquelle chaque élément IGrouping est une séquence d’éléments qui partagent la même clé. Les clés sont comparées par l’intermédiaire du comparateur d’égalité par défaut, EqualityComparerDefault. Pour dire les choses autrement, la valeur retournée par la méthode GroupBy est une séquence d’objets IGrouping. Chacun d’entre eux contient une clé et une séquence d’éléments issus de la séquence d’entrée et partageant la même clé. L’ordre des instances IGrouping est le même que celui des clés dans la séquence d’entrée. Quant à l’ordre des éléments d’une séquence IGrouping, il est identique à celui des éléments dans la séquence d’entrée. Deuxième prototype public static IEnumerable GroupBy( this IEnumerable source, Func keySelector, IEqualityComparer comparer);

Ce prototype est identique au premier, à ceci près qu’il est possible de choisir le comparateur à utiliser. Troisième prototype public static IEnumerable GroupBy( this IEnumerable source, Func keySelector, Func elementSelector);

Ce prototype est identique au premier mais, ici, la méthode elementSelector est utilisée pour choisir les éléments de la séquence d’entrée qui doivent apparaître dans la séquence de sortie. Quatrième prototype public static IEnumerable GroupBy( this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer comparer);

Ce prototype regroupe les possibilités offertes par les deuxième et troisième prototypes : il est donc possible de choisir un comparateur avec l’argument comparer et de limiter les éléments de la séquence de sortie avec l’argument elementSelector.

Linq.book Page 106 Mercredi, 18. février 2009 7:58 07

106

LINQ to Objects

Partie II

Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Le premier exemple (voir Listing 4.30) utilise la classe commune EmployeeOptionEntries. Les employés seront regroupés par id et affichés. Listing 4.30 : Un exemple d’utilisation du premier prototype. EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); IEnumerable outerSequence = empOptions.GroupBy(o => o.id); // Première énumération de la séquence extérieure de IGroupings foreach (IGrouping keyGroupSequence in outerSequence) { Console.WriteLine("Enregistrements Option pour l’employé " + keyGroupSequence.Key); // Énumération des séquences IGrouping d’éléments EmployeeOptionEntry foreach (EmployeeOptionEntry element in keyGroupSequence) Console.WriteLine("id={0} : optionsCount={1} : dateAwarded={2:d}", element.id, element.optionsCount, element.dateAwarded); }

Ce code énumère la séquence outerSequence. Les éléments obtenus sont des objets qui implémentent l’interface IGrouping. Ils contiennent une clé et une séquence d’éléments EmployeeOptionEntry qui partagent cette même clé. Voici les résultats : Enregistrements Option pour l’employé 1 id=1 : optionsCount=2 : dateAwarded=12/31/1999 Enregistrements Option pour l’employé 2 id=2 : optionsCount=10000 : dateAwarded=6/30/1992 id=2 : optionsCount=10000 : dateAwarded=1/1/1994 id=2 : optionsCount=10000 : dateAwarded=4/1/2003 Enregistrements Option pour l’employé 3 id=3 : optionsCount=5000 : dateAwarded=9/30/1997 id=3 : optionsCount=7500 : dateAwarded=9/30/1998 id=3 : optionsCount=7500 : dateAwarded=9/30/1998 Enregistrements Option pour l’employé 4 id=4 : optionsCount=1500 : dateAwarded=12/31/1997 Enregistrements Option pour l’employé 101 id=101 : optionsCount=2 : dateAwarded=12/31/1998

Pour illustrer le deuxième prototype de l’opérateur GroupBy, nous allons supposer que tous les employés dont le champ id est inférieur à 100 sont des membres fondateurs de l’entreprise. Nous allons lister tous les enregistrements option regroupés selon l’état fondateur/non fondateur des employés. Openmirrors.com

Linq.book Page 107 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

107

Pour ce faire, nous devons définir un comparateur spécifique qui de plus doit implémenter l’interface IEqualityComparer. Avant de parler du comparateur, jetons un œil à cette interface : interface IEqualityComparer { bool Equals(T x, T y); int GetHashCode(T x); }

Cette interface nécessite l’implémentation de deux méthodes : Equals et GetHashCode. La méthode Equals reçoit deux objets de type T. Elle retourne la valeur true si les deux objets sont considérés comme égaux et la valeur false dans le cas contraire. La méthode GetHashCode reçoit un objet de type T et retourne un code (appelé hash code ou clé) de type entier pour cet objet. Le hash code est une valeur numérique qui identifie (généralement) de manière unique un objet. Ordinairement calculé à partir du contenu de l’objet, il est utilisé comme un index qui permettra de retrouver facilement une structure de données. Voici la classe qui implémente l’interface IEqualityComparer : public class MyFounderNumberComparer : IEqualityComparer { public bool Equals(int x, int y) { return(isFounder(x) == isFounder(y)); } public int GetHashCode(int i) { int f = 1; int nf = 100; return (isFounder(i) ? f.GetHashCode() : nf.GetHashCode()); } public bool isFounder(int id) { return(id < 100); } }

La méthode IsFounder a été ajoutée aux méthodes Equals et GetHashCode. Elle détermine si un employé est un fondateur en se basant sur son champ id. Ceci facilite la compréhension du code. La méthode IsFounder est publique. Il est donc possible de l’appeler en dehors de l’interface. Nous verrons cela un peu plus loin dans le code de l’exemple. Le comparateur d’égalité considère que tout entier inférieur à 100 représente un membre fondateur. Si deux entiers font partie d’une de ces deux catégories, ils sont considérés comme égaux. La fonction GetHashCode retourne un entier égal à 1 si l’employé est un membre fondateur ou égal à 100 dans le cas contraire. Listing 4.31 : Un exemple d’utilisation du deuxième prototype. MyFounderNumberComparer comp = new MyFounderNumberComparer(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();

Linq.book Page 108 Mercredi, 18. février 2009 7:58 07

108

LINQ to Objects

Partie II

IEnumerable opts = empOptions .GroupBy(o => o.id, comp); // Énumération de la séquence d’IGrouping foreach (IGrouping keyGroup in opts) { Console.WriteLine("Options pour les " + (comp.isFounder(keyGroup.Key) ? "fondateurs" : "non fondateurs ")); // Énumération de la séquence d’éléments EmployeeOptionEntry foreach (EmployeeOptionEntry element in keyGroup) Console.WriteLine("id={0} : optionsCount={1} : dateAwarded={2:d}", element.id, element.optionsCount, element.dateAwarded); }

Dans cet exemple, le comparateur est instancié en dehors de la méthode GroupBy. La méthode IsFounder peut ainsi être appelée dans la boucle d’affichage foreach. Voici les résultats affichés par ce code : Options pour les fondateurs id=1 : optionsCount=2 : dateAwarded=12/31/1999 id=2 : optionsCount=10000 : dateAwarded=6/30/1992 id=2 : optionsCount=10000 : dateAwarded=1/1/1994 id=3 : optionsCount=5000 : dateAwarded=9/30/1997 id=2 : optionsCount=10000 : dateAwarded=4/1/2003 id=3 : optionsCount=7500 : dateAwarded=9/30/1998 id=3 : optionsCount=7500 : dateAwarded=9/30/1998 id=4 : optionsCount=1500 : dateAwarded=12/31/1997 Options pour les non fondateurs id=101 : optionsCount=2 : dateAwarded=12/31/1998

Comme vous le voyez, les employés dont le champ id est inférieur à 100 sont regroupés sous le libellé "Options pour les fondateurs" et les autres sous le libellé "Options pour les non fondateurs". Pour illustrer le troisième prototype, nous allons extraire les dates de délivrance des options. Le code sera très proche de celui utilisé pour illustrer le premier prototype. Contrairement au Listing 4.30, qui retournait un regroupement d’objets EmployeeOptionEntry, le Listing 4.32 retourne un regroupement de dates. Listing 4.32 : Un exemple d’utilisation du troisième prototype. EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); IEnumerable opts = empOptions .GroupBy(o => o.id, e => e.dateAwarded); // Énumération de la séquence de IGrouping foreach (IGrouping keyGroup in opts) { Console.WriteLine("Enregistrements Option pour l’employé " + keyGroup.Key); // Énumération des éléments DateTime foreach (DateTime date in keyGroup) Console.WriteLine(date.ToShortDateString()); }

Openmirrors.com

Linq.book Page 109 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

109

Dans l’appel à l’opérateur GroupBy, remarquez que le deuxième argument ne retourne que la date de l’option (dateAwarded). Le IGrouping est donc de type DateTime (et non EmployeeOptionEntry). Voici le résultat de l’exécution : Enregistrements 12/31/1999 Enregistrements 6/30/1992 1/1/1994 4/1/2003 Enregistrements 9/30/1997 9/30/1998 9/30/1998 Enregistrements 12/31/1997 Enregistrements 12/31/1998

Option pour l’employé 1 Option pour l’employé 2

Option pour l’employé 3

Option pour l’employé 4 Option pour l’employé 101

Pour illustrer le quatrième prototype, nous allons utiliser la méthode elementSelector et un objet comparer. Cela revient à utiliser une combinaison des exemples du deuxième et du troisième prototypes. Dans le Listing 4.33, nous regroupons les dates des options dans deux groupes : les fondateurs (id < 100) et les non-fondateurs (id > 100). Listing 4.33 : Un exemple d’utilisation du quatrième prototype. MyFounderNumberComparer comp = new MyFounderNumberComparer(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); IEnumerable opts = empOptions .GroupBy(o => o.id, o => o.dateAwarded, comp); // Énumération de la séquence de IGrouping foreach (IGrouping keyGroup in opts) { Console.WriteLine("Enregistrements Option pour les " + (comp.isFounder(keyGroup.Key) ? "fondateurs" : "non fondateurs")); // Énumération de la séquence des éléments EmployeeOptionEntry foreach (DateTime date in keyGroup) Console.WriteLine(date.ToShortDateString()); }

La sortie console n’affiche que des dates regroupées par fondateurs et non-fondateurs : Enregistrements Option pour les fondateurs 12/31/1999 6/30/1992 1/1/1994 9/30/1997 4/1/2003 9/30/1998 9/30/1998 12/31/1997 Enregistrements Option pour les non fondateurs 12/31/1998

Linq.book Page 110 Mercredi, 18. février 2009 7:58 07

110

LINQ to Objects

Partie II

Opérateurs d’initialisation Les opérateurs d’initialisation sont utilisés pour obtenir des valeurs calculées à partir de séquences. ASTUCE Les prototypes des opérateurs d’initialisation passés en revue dans cet ouvrage ne sont pas adaptés aux DataSets. Préférez-leur les prototypes présentés au Chapitre 10.

Opérateur Distinct L’opérateur Distinct supprime les doublons dans la séquence d’entrée.

Prototype Un seul prototype de l’opérateur Distinct sera étudié dans cet ouvrage : public static IEnumerable Distinct( this IEnumerable source);

Cet opérateur retourne un objet dont l’énumération exclut les doublons de la séquence d’entrée. Le critère d’égalité entre deux éléments est déterminé avec les méthodes GetHashCode et Equals. Exceptions L’exception ArgumentNullException est levée si la source a pour valeur null. Exemples Cet exemple fonctionne selon les cinq étapes suivantes : m

affichage du nombre d’éléments contenus dans le tableau presidents ;

m

duplication des éléments du tableau ;

m

affichage de la séquence résultante ;

m

appel de l’opérateur Distinct sur la séquence concaténée ;

m

affichage du nombre d’éléments en sortie de l’opérateur.

Si tout fonctionne correctement, le nombre d’éléments renvoyés par l’opérateur Distinct devrait être égal au nombre initial d’éléments du tableau presidents. Pour obtenir le nombre d’éléments des deux séquences, nous utiliserons l’opérateur de requête standard non différé Count. Si nécessaire, reportez-vous au chapitre suivant pour avoir de plus amples informations sur cet opérateur. Listing 4.34 : Un exemple d’utilisation de l’opérateur Distinct. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield",

Openmirrors.com

Linq.book Page 111 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

111

"Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; // Affichage du nombre d’éléments du tableau presidents Console.WriteLine("Nombre de présidents : " + presidents.Count()); // Duplication des éléments du tableau presidents IEnumerable presidentsWithDupes = presidents.Concat(presidents); // Affichage du nombre d’éléments du tableau presidents Console.WriteLine("Nombre de présidents après la duplication : " + presidentsWithDupes.Count()); // Suppression des doublons et affichage du nombre d’éléments IEnumerable presidentsDistinct = presidentsWithDupes.Distinct(); Console.WriteLine("Nombre de présidents distincts : " + presidentsDistinct.Count());

Voici le résultat de ce code : Nombre de présidents : 37 Nombre de présidents après la duplication : 74 Nombre de présidents distincts : 37

Opérateur Union L’opérateur Union retourne la réunion de deux séquences d’entrée.

Prototype Nous étudierons un seul prototype de l’opérateur Union dans cet ouvrage : public static IEnumerable Union( this IEnumerable first, IEnumerable second);

Ce prototype fournit un objet dont l’énumération retourne les éléments de la première séquence, privés de leurs doublons, suivis des éléments de la seconde séquence, également privés de leurs doublons. L’égalité des éléments est déterminée par les méthodes HashCode et Equals. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Pour montrer la différence entre les opérateurs Union et Concat (voir Listing 4.35), nous allons créer les séquences first et second à partir du tableau presidents. Ces deux séquences auront en commun le cinquième élément du tableau presidents. Nous afficherons le nombre d’éléments du tableau presidents, des séquences premier et second et des séquences premier et second soumises aux opérateurs Concat et Union.

Linq.book Page 112 Mercredi, 18. février 2009 7:58 07

112

LINQ to Objects

Partie II

Listing 4.35 : Un exemple d’utilisation de l’opérateur Union. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable first = presidents.Take(5); IEnumerable second = presidents.Skip(4); // Seul le cinquième élément du tableau presidents // est commun aux séquences premier et second IEnumerable concat = first.Concat(second); IEnumerable union = first.Union(second); Console.WriteLine("Nombre ➥presidents.Count()); Console.WriteLine("Nombre Console.WriteLine("Nombre Console.WriteLine("Nombre ➥concat.Count()); Console.WriteLine("Nombre ➥union.Count());

d’éléments du tableau presidents : " + d’éléments de la première séquence : " + first.Count()); d’éléments de la deuxième séquence : " + second.Count()); d’éléments après concaténation des deux séquences : " + d’éléments après union des deux séquences : " +

Ce code affiche le texte ci-après dans la fenêtre Console : Nombre Nombre Nombre Nombre Nombre

d’éléments d’éléments d’éléments d’éléments d’éléments

du tableau presidents : 37 de la première séquence : 5 de la deuxième séquence : 33 après concaténation des deux séquences : 38 après union des deux séquences : 37

Comme on pouvait s’y attendre : m

La séquence issue de l’opérateur Concat a un élément de plus que le tableau presidents.

m

La séquence issue de l’opérateur Union a le même nombre d’éléments que le tableau presidents.

Opérateur Intersect L’opérateur Intersect retourne l’intersection des deux séquences passées en entrée.

Prototype Nous étudierons un seul prototype de l’opérateur Intersect dans cet ouvrage : public static IEnumerable Intersect( this IEnumerable first, IEnumerable second);

Openmirrors.com

Linq.book Page 113 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

113

Cet opérateur retourne un objet dont l’énumération est obtenue : 1. en dressant la liste des singletons de la première séquence ; 2. en énumérant les éléments de la deuxième séquence et en marquant ceux qui se trouvent dans la liste de la première étape ; 3. en énumérant les éléments marqués dans l’ordre où ils ont été collectés à l’étape 2. L’égalité des éléments est déterminée à l’aide des méthodes GetHashCode et Equals. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Nous allons utiliser les opérateurs Take et Skip pour générer deux séquences qui possèdent un seul élément en commun. Lorsque nous appliquerons l’opérateur Intersect à ces deux séquences, seul cet élément sera retourné. Listing 4.36 : Un exemple d’utilisation de l’opérateur Intersect. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; IEnumerable first = presidents.Take(5); IEnumerable second = presidents.Skip(4); // Seul le cinquième élément du tableau presidents // est commun aux séquences premier et second IEnumerable intersect = first.Intersect(second); Console.WriteLine("Nombre ➥presidents.Count()); Console.WriteLine("Nombre Console.WriteLine("Nombre ➥second.Count()); Console.WriteLine("Nombre ➥intersect.Count());

d’éléments dans le tableau presidents : " + d’éléments dans la première séquence : " + first.Count()); d’éléments dans la deuxième séquence : " + d’éléments après intersection des deux séquences : " +

// Affichage de la séquence résultant de l’opérateur Intersect foreach (string name in intersect) Console.WriteLine(name);

Voici le résultat de l’exécution de ce code : Nombre Nombre Nombre Nombre Carter

d’éléments d’éléments d’éléments d’éléments

dans le tableau presidents : 37 dans la première séquence : 5 dans la deuxième séquence : 33 après intersection des deux séquences : 1

Linq.book Page 114 Mercredi, 18. février 2009 7:58 07

114

LINQ to Objects

Partie II

Opérateur Except Cet opérateur retourne une séquence qui contient tous les éléments de la première séquence qui n’apparaissent pas dans la seconde.

Prototype Nous étudierons un seul prototype de l’opérateur Except dans cet ouvrage : public static IEnumerable Except( this IEnumerable first, IEnumerable second);

Cet opérateur retourne un objet dont l’énumération effectue les actions suivantes : 1. Énumération des éléments de la première séquence en éliminant les doublons. 2. Énumération des éléments de la deuxième séquence en ne conservant que les éléments qui n’ont pas été retenus à la première étape. 3. Création d’une collection de sortie qui contient les éléments retenus à la deuxième étape. L’égalité des éléments est déterminée à l’aide des méthodes GetHashCode et Equals. Exceptions L’exception ArgumentNullException est levée si un des arguments a pour valeur null. Exemples Une fois encore, nous utiliserons le tableau presidents. Supposons que vous effectuiez un traitement sur les éléments du tableau presidents et que les éléments obtenus soient placés dans une séquence. Si de nouveaux éléments sont ajoutés à cette séquence, il sera inutile d’appliquer le traitement aux éléments qui l’ont déjà subi. Pour ne sélectionner que les nouveaux éléments, il suffira de transmettre l’ancienne liste et la nouvelle liste à l’opérateur Except. Vous pourrez alors appliquer le traitement aux nouveaux venus. Dans cet exemple, nous allons supposer que les quatre premiers éléments ont déjà subi le traitement. Pour obtenir la liste des autres éléments, il suffit d’utiliser l’opérateur Except (voir Listing 4.37). Listing 4.37 : Un exemple d’utilisation de l’opérateur Except. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

Openmirrors.com

Linq.book Page 115 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

115

// Définition de la séquence processed IEnumerable processed = presidents.Take(4); IEnumerable exceptions = presidents.Except(processed); foreach (string name in exceptions) Console.WriteLine(name);

Comme on pouvait s’y attendre, les noms des présidents affichés dans la console commencent au cinquième : Carter Cleveland Clinton Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson

Opérateurs de conversion Les opérateurs de conversion représentent une façon simple et pratique de convertir des séquences en des collections d’un autre type. Opérateur Cast L’opérateur Cast convertit tous les éléments de la séquence d’entrée dans le type spécifié et les place dans la séquence de sortie.

Prototype Nous étudierons un seul prototype de l’opérateur Cast dans cet ouvrage : public static IEnumerable Cast( this IEnumerable source);

Linq.book Page 116 Mercredi, 18. février 2009 7:58 07

116

LINQ to Objects

Partie II

Contrairement à la majorité des opérateurs de requête standard différés, le premier argument de l’opérateur Cast est de type IEnumerable, et non IEnumerable. Ceci s’explique par le fait que l’opérateur Cast a été défini pour être appelé sur des classes qui implémentent l’interface IEnumerable. En particulier les collections C# héritées, définies avant la sortie de C# 2.0 et des génériques. Vous pouvez utiliser l’opérateur Cast sur toute collection C# héritée, à condition qu’elle implémente l’interface IEnumerable. Une séquence IEnumerable sera alors créée. Étant donné que la plupart des opérateurs de requête standard ne travaillent qu’avec des séquences de type IEnumerable, vous devrez utiliser l’opérateur Cast ou OfType (voir la section suivante) pour obtenir un type IEnumerable compatible. Ayez bien cela en tête si vous prévoyez d’appliquer des opérateurs de requête standard sur des collections héritées. L’opérateur Cast retourne un objet dont l’énumération transforme les éléments de la séquence d’entrée pour qu’ils soient du type T. Si un élément ne peut pas être converti, une exception est levée. Il est donc important de n’utiliser cet opérateur que lorsque l’on est sûr que tous les éléments de la séquence d’entrée peuvent être convertis. ASTUCE Lorsque vous appliquez une requête LINQ à une collection héritée, n’oubliez pas d’utiliser un opérateur Cast ou OfType pour convertir la collection héritée en une séquence IEnumerable compatible avec les opérateurs de requête standard.

Exceptions L’exception ArgumentNullException est levée si l’argument source a pour valeur null. L’exception InvalidCastException est levée si un des éléments de la séquence d’entrée ne peut pas être converti dans le type T. Exemples Dans cet exemple, nous utiliserons la méthode GetEmployeesArrayList de la classe commune Employee pour obtenir un objet ArrayList hérité (non générique). Cet objet sera alors converti en un IEnumerable avec l’opérateur Cast (voir Listing 4.38). Listing 4.38 : Ce code convertit un ArrayList en un IEnumerable qui peut être utilisé avec les opérateurs de requête standard. ArrayList employees = Employee.GetEmployeesArrayList(); Console.WriteLine("Le type de l’objet employees est " + employees.GetType()); var seq = employees.Cast(); Console.WriteLine("Le type de l’objet seq est " + seq.GetType()); var emps = seq.OrderBy(e => e.lastName); foreach (Employee emp in emps) Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);

Openmirrors.com

Linq.book Page 117 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

117

La première ligne utilise la méthode GetEmployeesArrayList pour obtenir un ArrayList d’objets Employee. Le type de l’objet ainsi obtenu est affiché par la deuxième ligne. Dans la troisième ligne, l’objet employees est converti en une séquence IEnumerable en appelant l’opérateur Cast. Le type de l’objet obtenu est affiché dans la quatrième ligne. Les autres lignes énumèrent l’objet IEnumerable afin de prouver que la conversion a réussi. Voici le résultat de l’exécution de ce code : Le type de l’objet employees est System.Collections.ArrayList Le type de l’objet seq est System.Linq.Enumerable+d__b0`1[LINQChapter4.Employee] Kevin Flynn William Gates Anders Hejlsberg David Lightman Joe Rattz

Le type de l’objet employees est clairement identifiable. Il en va autrement de celui de l’objet seq. Ce que nous pouvons dire, c’est qu’il est différent du précédent et qu’il ressemble à une séquence. Nous pouvons également remarquer le mot CastIterator dans l’intitulé de son type. Vous rappelez-vous de ce qui a été dit à propos des opérateurs différés : ces opérateurs retournent non pas une séquence en sortie, mais un objet dont l’énumération fournit les éléments de la séquence de sortie. L’objet seq est précisément de ce type. ATTENTION L’opérateur Cast essaye de convertir tous les éléments de la séquence d’entrée dans le type spécifié. Si un de ces éléments ne peut pas être converti, une exception InvalidCastException est levée. Si une telle situation est possible, préférez l’opérateur OfType à l’opérateur Cast.

Opérateur OfType Cet opérateur change le type des éléments de la séquence d’entrée qui le permettent et les place dans la séquence de sortie.

Prototype Nous étudierons un seul prototype de l’opérateur OfType dans cet ouvrage : public static IEnumerable OfType( this IEnumerable source);

Le premier argument de l’opérateur OfType est de type IEnumerable, et non IEnumerable. Tout comme Cast, OfType est destiné à être appelé sur des classes qui implémentent l’interface IEnumerable. En particulier les collections C# héritées, définies avant la sortie de C# 2.0 et des génériques.

Linq.book Page 118 Mercredi, 18. février 2009 7:58 07

118

LINQ to Objects

Partie II

Vous pouvez utiliser l’opérateur OfType sur toute collection C# héritée, à condition qu’elle implémente l’interface IEnumerable. Une séquence IEnumerable sera alors créée. Étant donné que la plupart des opérateurs de requête standard ne travaillent qu’avec des séquences de type IEnumerable, vous devrez utiliser l’opérateur OfType ou Cast (voir la section précédente) pour obtenir un type IEnumerable compatible. Ayez bien cela en tête si vous prévoyez d’appliquer des opérateurs de requête standard sur des collections héritées. L’opérateur OfType retourne un objet dont l’énumération transforme les éléments de la séquence d’entrée pour qu’ils soient du type T (seuls les éléments qui supportent la conversion sont convertis). Exceptions L’exception ArgumentNullException est levée si l’argument source a pour valeur null. Exemples Dans l’exemple du Listing 4.39, nous créons un ArrayList contenant des objets issus des classes communes Employee et EmployeeOptionEntry. Appliqué à cet objet, l’opérateur Cast ne parvient pas à effectuer la conversion de type. Quelques lignes plus bas, l’opérateur OfType, appliqué à ce même objet, passe haut la main la conversion. Listing 4.39 : Un exemple d’appel des opérateurs Cast et OfType. ArrayList al = new ArrayList(); al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz" }); al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates" }); al.Add(new EmployeeOptionEntry { id = 1, optionsCount = 0 }); al.Add(new EmployeeOptionEntry { id = 2, optionsCount = 99999999999 }); al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg" }); al.Add(new EmployeeOptionEntry { id = 3, optionsCount = 848475745 }); var items = al.Cast(); Console.WriteLine("Tentative d’énumération de la séquence issue de l’opérateur Cast ..."); try { foreach (Employee item in items) Console.WriteLine("{0} {1} {2}", item.id, item.firstName, item.lastName); } catch (Exception ex) { Console.WriteLine("{0}{1}", ex.Message, System.Environment.NewLine); } Console.WriteLine("Tentative d’énumération de la séquence issue de l’opérateur ➥OfType ..."); var items2 = al.OfType(); foreach (Employee item in items2) Console.WriteLine("{0} {1} {2}", item.id, item.firstName, item.lastName);

Le premier bloc d’instructions crée et remplit l’objet ArrayList al. L’opérateur Cast est alors appliqué à cet objet. Le bloc d’instructions suivant tente d’énumérer les éléments de la séquence issue de l’opérateur Cast (sans ces instructions, l’erreur de Openmirrors.com

Linq.book Page 119 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

119

conversion n’aurait pas été identifiée). L’énumération est protégée par une structure try/catch. Ainsi, un message est affiché lorsqu’une erreur de conversion est détectée. Le code se poursuit par l’application de l’opérateur OfType sur la séquence al. Ici, aucune erreur de conversion n’étant possible, les éléments de la séquence retournée par OfType sont simplement énumérés (la structure try/catch ne devrait pas être retirée d’un code dont la portée dépasse le cadre pédagogique). Voici les résultats de ce code : Tentative d’énumération de la séquence issue de l’opérateur Cast ... 1 Joe Rattz 2 William Gates Unable to cast object of type ’LINQChapter4.EmployeeOptionEntry’ to type ’LINQChapter4.Employee’. Tentative d’énumération de la séquence issue de l’opérateur OfType ... 1 Joe Rattz 2 William Gates 3 Anders Hejlsberg

Il n’a pas été possible d’énumérer tous les résultats de la séquence retournée par l’opérateur Cast sans qu’une exception ne soit générée. En revanche, tous les résultats de la séquence retournée par l’opérateur OfType ont pu être énumérés, et seuls les éléments de type employee ont été inclus dans la séquence de sortie. ASTUCE Si vous voulez convertir une collection non générique (une collection héritée, par exemple) en une séquence IEnumerable, utilisez l’opérateur OfType et non l’opérateur Cast si les données à convertir peuvent être de plusieurs types différents.

Opérateur AsEnumerable L’opérateur AsEnumerable retourne la séquence d’entrée IEnumerable en tant qu’IEnumerable.

Prototype Un seul prototype de l’opérateur AsEnumerable sera étudié dans cet ouvrage : public static IEnumerable AsEnumerable( this IEnumerable source);

Un rapide coup d’œil à ce prototype montre qu’AsEnumerable utilise la séquence d’entrée IEnumerable source et la retourne typée en IEnumerable. Cela peut sembler quelque peu étrange. En effet, quel est l’intérêt de transformer un IEnumerable en un autre IEnumerable ? Les opérateurs de requête standard sont définis pour opérer sur des séquences LINQ to Objects "normales", c’est-à-dire qui implémentent l’interface IEnumerable.

Linq.book Page 120 Mercredi, 18. février 2009 7:58 07

120

LINQ to Objects

Partie II

D’autres types de collections, par exemple celles qui accèdent à des bases de données, peuvent utiliser des séquences et des opérateurs qui leur sont propres. Généralement, lorsque vous appliquez un opérateur de requête sur ces types de collections, cet opérateur est spécifique à la collection. En utilisant l’opérateur AsEnumerable, vous allez pouvoir convertir une séquence d’entrée en une séquence IEnumerable "normale", directement utilisable dans un opérateur de requête standard. À titre d’exemple, lorsque nous nous intéresserons à LINQ to SQL un peu plus loin dans ce livre, vous verrez que cette partie de LINQ utilise des séquences de type IQueryable et implémente ses propres opérateurs. Ces derniers sont spécifiques aux séquences IQueryable. Lorsque vous appelez l’opérateur Where sur une séquence IQueryable, c’est la méthode Where de LINQ to SQL qui est invoquée, et non l’opérateur de requête standard Where de LINQ to Objects ! Si vous essayez d’invoquer un opérateur de requête standard sur un objet IQueryable, une exception sera générée, à moins qu’un opérateur LINQ to SQL de même nom n’existe. L’opérateur AsEnumerable permet de convertir une séquence IQueryable en une séquence IEnumerable, permettant ainsi l’utilisation des opérateurs de requête standard. AsEnumerable se révèle très pratique si vous devez contrôler dans quelle API un opérateur doit être appelé. Exceptions Aucune exception n’est générée par cet opérateur. Exemples Pour mieux comprendre comment fonctionne cet opérateur, nous allons raisonner sur un cas pratique. Nous utiliserons l’exemple LINQ to SQL donné au Chapitre 1. Voici le code utilisé : using using using using

System; System.Linq; System.Data.Linq; nwind;

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var custs = from c in db.Customers where c.City == "Rio de Janeiro" select c; foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);

Et voici les résultats de cet exemple : Hanari Carnes Que Delícia Ricardo Adocicados

Openmirrors.com

Linq.book Page 121 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

121

Pour que ce code soit en mesure de fonctionner, vous devez ajouter à votre projet : m

l’assembly System.Data.Linq.dll ;

m

une directive using qui pointe sur l’espace de noms nwind ;

m

les classes d’entités générées, qui seront étudiées dans les chapitres relatifs à LINQ to SQL.

Supposons que vous deviez inverser l’ordre des enregistrements issus de la base de données. Vous utiliserez l’opérateur Reverse, abordé un peu plus loin dans ce chapitre. Le Listing 4.40 représente le code précédent, modifié pour appeler l’opérateur Reverse. Listing 4.40 : Appel de l’opérateur Reverse. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var custs = (from c in db.Customers where c.City == "Rio de Janeiro" select c) .Reverse(); foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);

Comme vous pouvez le voir, l’unique modification a consisté à appeler la méthode Reverse. Voici les résultats renvoyés dans la console : Exception non gérée : System.NotSupportedException : L’opérateur ’Reverse’ n’est pas ➥supporté. …

Que s’est-il passé ? Étant donné qu’il n’existe aucune méthode Reverse pour l’interface IQueryable, une exception a été générée. C’est là qu’intervient la méthode AsEnumerable. Grâce à elle, la séquence IQueryable va être convertie en une séquence IEnumerable, et il sera possible de lui appliquer la méthode Reverse. Voici dans le Listing 4.41 le code modifié. Listing 4.41 : Appel de l’opérateur AsEnumerable avant l’opérateur Reverse. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var custs = (from c in db.Customers where c.City == "Rio de Janeiro" select c) .AsEnumerable() .Reverse(); foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);

Linq.book Page 122 Mercredi, 18. février 2009 7:58 07

122

LINQ to Objects

Partie II

La méthode AsEnumerable est appelée avant l’opérateur Reverse. C’est donc l’opérateur Reverse de LINQ to Objects qui va être invoqué. Voici les résultats affichés dans la console : Ricardo Adocicados Que Delícia Hanari Carnes

Ces résultats sont bien affichés dans l’ordre inverse de la séquence originale. L’opérateur Reverse a donc bien fonctionné. Opérateurs dédiés aux éléments Ces opérateurs permettent d’extraire des éléments dans la séquence d’entrée. Opérateur DefaultIfEmpty L’opérateur DefaultIfEmpty retourne une séquence qui contient un élément par défaut si la séquence d’entrée est vide.

Prototypes Deux prototypes de l’opérateur DefaultIfEmpty seront étudiés dans cet ouvrage. Premier prototype public static IEnumerable DefaultIfEmpty( this IEnumerable source);

Ce prototype retourne un objet dont l’énumération renvoie chacun des éléments de la séquence d’entrée. Si cette dernière est vide, une séquence de type default(T) contenant un seul élément est retournée. Pour les références et les types nullables, la valeur par défaut est null. Contrairement aux autres opérateurs dédiés aux éléments, DefaultIfEmpty retourne une séquence de type IEnumerable et non de type T. Il existe d’autres opérateurs de type, mais nous ne les étudierons pas dans ce chapitre, car ils ne sont pas différés. Le second prototype permet de spécifier la valeur par défaut. Second prototype public static IEnumerable DefaultIfEmpty( this IEnumerable source, T defaultValue);

Cet opérateur est utile aux opérateurs qui génèrent des exceptions lorsque la séquence d’entrée est vide. Il permet également à l’opérateur GroupJoin de générer des jointures externes à gauche (left outer join). Openmirrors.com

Linq.book Page 123 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

123

Exceptions Une exception ArgumentNullException est levée si l’argument source a pour valeur null. Exemples Dans ce premier exemple, nous allons rechercher le nom "Jones" dans le tableau presidents (voir Listing 4.42). Un message indiquera si ce nom a été ou n’a pas été trouvé. Listing 4.42 : Premier exemple du prototype DefaultIfEmpty, sans l’opérateur DefaultIfEmpty. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string jones = presidents.Where(n => n.Equals("Jones")).First(); if (jones != null) Console.WriteLine("Jones was found"); else Console.WriteLine("Jones was not found");

Voici les résultats affichés dans la console : Exception non gérée : System.InvalidOperationException : La séquence ne contient ➥aucun élément …

Le nom "Jones" n’ayant pas été trouvé, une séquence vide est passée à l’opérateur First. Ce dernier n’appréciant pas les séquences vides, il a généré une exception. Nous allons maintenant ajouter un appel à l’opérateur DefaultIfEmpty entre les opérateurs Where et First. Ainsi, c’est non pas une séquence vide, mais une séquence contenant un élément null qui sera passée à l’opérateur First (voir Listing 4.43). Listing 4.43 : Second exemple du premier prototype, cette fois en utilisant DefaultIfEmpty. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string jones = presidents.Where(n => n.Equals("Jones")).DefaultIfEmpty().First(); if (jones != null) Console.WriteLine("Jones was found."); else Console.WriteLine("Jones was not found.");

Linq.book Page 124 Mercredi, 18. février 2009 7:58 07

124

LINQ to Objects

Partie II

Voici le résultat : Jones n’a pas été trouvé.

Voici maintenant un exemple pour le second prototype (voir Listing 4.44). Ici, nous pouvons choisir la valeur retournée lorsque la séquence d’entrée est vide. Listing 4.44 : Un exemple du second prototype de l’opérateur DefaultIfEmpty. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.Where(n => n.Equals("Jones")).DefaultIfEmpty("Missing").First(); Console.WriteLine(name);

Voici le résultat : Absent

Nous allons maintenant réaliser une jointure externe à gauche en utilisant les opérateurs GroupJoin et DefaultIfEmpty. Nous travaillerons avec deux classes communes, Employee et EmployeeOptionEntry. Dans le Listing 4.45, l’opérateur DefaultIfEmpty n’est pas utilisé. Listing 4.45 : Un exemple sans l’opérateur DefaultIfEmpty. ArrayList employeesAL = Employee.GetEmployeesArrayList(); // Ajout d’un nouvel employé sans enregistrement EmployeeOptionEntry correspondant employeesAL.Add(new Employee { id = 102, firstName = "Michael", lastName = "Bolton" }); Employee[] employees = employeesAL.Cast().ToArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .GroupJoin( empOptions, e => e.id, o => o.id, (e, os) => os .Select(o => new { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = o != null ? o.optionsCount : 0 })) .SelectMany(r => r); foreach (var item in employeeOptions) Console.WriteLine(item);

Openmirrors.com

Linq.book Page 125 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

125

Quelques précisions à propos de cet exemple : m

Le code est très proche de celui qui a été utilisé pour illustrer l’opérateur GroupJoin.

m

Étant donné que chaque employé de la classe commune employee a une correspondance dans la classe commune EmployeeOptionEntry, nous allons ajouter un nouvel employé, Michael Bolton, à l’objet ArrayList des employés, de telle sorte qu’aucun objet EmployeeOptionEntry ne lui corresponde.

m

L’opérateur DefaultIfEmpty ne sera pas appelé dans cet exemple.

Voici les résultats de la requête : { { { { { { { { {

id id id id id id id id id

= = = = = = = = =

1, name = 2, name = 2, name = 2, name = 3, name = 3, name = 3, name = 4, name = 101, name

Joe Rattz, options = 2 } William Gates, options = 10000 } William Gates, options = 10000 } William Gates, options = 10000 } Anders Hejlsberg, options = 5000 } Anders Hejlsberg, options = 7500 } Anders Hejlsberg, options = 7500 } David Lightman, options = 1500 } = Kevin Flynn, options = 2 }

Comme aucun objet ne correspond à l’employé Michael Bolton dans le tableau EmployeeOptionArray, aucune information concernant cet employé n’est affichée dans la console. Nous allons maintenant utiliser l’opérateur DefaultIfEmpty pour créer un enregistrement par défaut pour cet employé (voir Listing 4.46). Listing 4.46 : Un exemple d’utilisation de l’opérateur DefaultIfEmpty. ArrayList employeesAL = Employee.GetEmployeesArrayList(); // Ajout d’un nouvel employé sans enregistrement EmployeeOptionEntry correspondant employeesAL.Add(new Employee { id = 102, firstName = "Michael", lastName = "Bolton" }); Employee[] employees = employeesAL.Cast().ToArray(); EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries(); var employeeOptions = employees .GroupJoin( empOptions, e => e.id, o => o.id, (e, os) => os .DefaultIfEmpty() .Select(o => new { id = e.id,

Linq.book Page 126 Mercredi, 18. février 2009 7:58 07

126

LINQ to Objects

Partie II

name = string.Format("{0} {1}", e.firstName, e.lastName), options = o != null ? o.optionsCount : 0 })) .SelectMany(r => r); foreach (var item in employeeOptions) Console.WriteLine(item);

Le premier bloc de code ajoute l’employé Michael Bolton sans lui associer un objet EmployeeOptionEntry. Le deuxième bloc de code effectue une requête sur les données en faisant appel à l’opérateur DefaultIfEmpty. Voici les résultats : { { { { { { { { { {

id id id id id id id id id id

= = = = = = = = = =

1, name = 2, name = 2, name = 2, name = 3, name = 3, name = 3, name = 4, name = 101, name 102, name

Joe Rattz, options = 2 } William Gates, options = 10000 } William Gates, options = 10000 } William Gates, options = 10000 } Anders Hejlsberg, options = 5000 } Anders Hejlsberg, options = 7500 } Anders Hejlsberg, options = 7500 } David Lightman, options = 1500 } = Kevin Flynn, options = 2 } = Michael Bolton, options = 0 }

L’opérateur DefaultIfEmpty a bien ajouté un objet EmployeeOptionEntry pour l’employé Michael Bolton. Opérateurs de génération Ces opérateurs sont utilisés pour générer des séquences. Opérateur Range L’opérateur Range génère une séquence d’entiers.

Prototype Un seul prototype de l’opérateur Range sera étudié dans cet ouvrage : public static IEnumerable Range( int start, int count);

Ce prototype génère une séquence de count entiers à partir de start. L’opérateur Range n’est pas une méthode d’extension. Il n’étend pas le type IEnumerable. INFO L’opérateur Range n’est pas une méthode d’extension. C’est une méthode statique appelée dans l’assembly System.Linq.Enumerable.

Openmirrors.com

Linq.book Page 127 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

127

Exceptions Une exception ArgumentOutOfRangeException est levée si count est inférieur à zéro ou si start+count-1 est supérieur à int.MaxValue. Exemples Listing 4.47 : Un exemple d’appel de l’opérateur Range. IEnumerable ints = Enumerable.Range(1, 10); foreach(int i in ints) Console.WriteLine(i);

Je tiens à rappeler que l’opérateur Range n’est pas appliqué à une séquence : il s’agit d’une méthode statique de la classe System.Linq.Enumerable. Voici les résultats affichés dans la console : 1 2 3 4 5 6 7 8 9 10

Opérateur Repeat L’opérateur Repeat génère une séquence en répétant plusieurs fois un même élément.

Prototype Un seul prototype de l’opérateur Repeat sera étudié dans cet ouvrage : public static IEnumerable Repeat( T element, int count);

Ce prototype retourne un objet dont l’énumération produit count éléments T. L’opérateur Repeat n’est pas une méthode d’extension. Il n’étend pas le type IEnumerable. INFO L’opérateur Repeat n’est pas une méthode d’extension. C’est une méthode statique appelée dans l’assembly System.Linq.Enumerable.

Exceptions Une exception ArgumentOutOfRangeException est levée si count est inférieur à zéro.

Linq.book Page 128 Mercredi, 18. février 2009 7:58 07

128

LINQ to Objects

Partie II

Exemples Listing 4.48 : Génération d’une séquence de dix éléments Integer initialisés à la valeur 2. IEnumerable ints = Enumerable.Repeat(2, 10); foreach(int i in ints) Console.WriteLine(i);

Voici les résultats affichés dans la console : 2 2 2 2 2 2 2 2 2 2

Opérateur Empty L’opérateur Empty génère une séquence vide de type T.

Prototype Un seul prototype de l’opérateur Empty sera étudié dans cet ouvrage : public static IEnumerable Empty();

Ce prototype renvoie un objet dont l’énumération produit 0 élément de type T. L’opérateur Empty n’est pas une méthode d’extension. Il n’étend pas le type IEnumerable. INFO L’opérateur Empty n’est pas une méthode d’extension. C’est une méthode statique appelée dans l’assembly System.Linq.Enumerable.

Exceptions Aucune. Exemples Cet exemple génère une séquence de type String par l’intermédiaire de l’opérateur Empty. La séquence générée ainsi que son nombre d’éléments sont ensuite affichés dans la console. Listing 4.49 : Génération d’une séquence vide de String. IEnumerable strings = Enumerable.Empty(); foreach(string s in strings)

Openmirrors.com

Linq.book Page 129 Mercredi, 18. février 2009 7:58 07

Chapitre 4

Les opérateurs différés

129

Console.WriteLine(s); Console.WriteLine(strings.Count());

Voici le résultat affiché dans la console : 0

Comme vous le voyez, la boucle foreach ne produit aucun résultat. Ceci est normal, puisqu’il n’y a aucun élément à afficher.

Résumé Ce chapitre a illustré la plupart des prototypes des opérateurs différés, du plus simple au plus complexe. En isolant les opérateurs de requête standard différés de leurs acolytes non différés, j’ai mis l’accent sur l’impact que pouvait avoir l’exécution non instantanée d’une requête. Au chapitre suivant, vous découvrirez les opérateurs de requête standard non différés. Ce sera le dernier chapitre dédié à LINQ to Objects.

Linq.book Page 130 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 131 Mercredi, 18. février 2009 7:58 07

5 Les opérateurs non différés Au chapitre précédent, nous nous sommes intéressés aux opérateurs de requête différés. Ces opérateurs sont faciles à identifier, car ils retournent un IEnumerable ou un OrderedSequence. Nous allons maintenant nous intéresser aux opérateurs de requête standard non différés. Ces opérateurs sont faciles à reconnaître, car le résultat retourné a un type différent de IEnumerable et OrderedSequence. Pour pouvoir exécuter les exemples de ce chapitre, assurez-vous que vous avez référencé les espaces de noms (directive using), les assemblies et les codes communs nécessaires.

Espaces de noms référencés Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq, System.Collections et System.Collections.Generic. Si elles ne sont pas déjà présentes, vous devez donc ajouter les directives using suivantes dans votre code : using System.Linq; using System.Collections; using System.Collections.Generic;

Si vous parcourez le code source mis à disposition sur le site www.pearson.fr, vous verrez que j’ai également ajouté une directive using sur l’espace de noms System.Diagnostic. Cette directive n’est pas nécessaire si vous saisissez directement les exemples de ce chapitre. Elle n’est là que pour les besoins propres du code source.

Classes communes Pour fonctionner entièrement, certains exemples de ce chapitre nécessitent des classes additionnelles. Cette section décrit les quatre classes qui seront utilisées par certains exemples de ce chapitre.

Linq.book Page 132 Mercredi, 18. février 2009 7:58 07

132

LINQ to Objects

Partie II

La classe Employee permet de travailler sur les employés d’une entreprise. Elle contient des méthodes statiques qui retournent un tableau d’employés de type ArrayList. public class Employee { public int id; public string firstName; public string lastName; public static ArrayList GetEmployeesArrayList() { ArrayList al = new ArrayList(); al.Add(new Employee al.Add(new Employee al.Add(new Employee al.Add(new Employee al.Add(new Employee return (al);

{ { { { {

id id id id id

= = = = =

1, firstName = 2, firstName = 3, firstName = 4, firstName = 101, firstName

"Joe", lastName = "Rattz" }); "William", lastName = "Gates" }); "Anders", lastName = "Hejlsberg" }); "David", lastName = "Lightman" }); = "Kevin", lastName = "Flynn" });

} public static Employee[] GetEmployeesArray() { return ((Employee[])GetEmployeesArrayList().ToArray()); } }

La classe EmployeeOptionEntry représente le montant des stock-options des employés. Elle contient une méthode statique qui retourne un tableau de stock-options. public class EmployeeOptionEntry { public int id; public long optionsCount; public DateTime dateAwarded; public static EmployeeOptionEntry[] GetEmployeeOptionEntries() { EmployeeOptionEntry[] empOptions = new EmployeeOptionEntry[] { new EmployeeOptionEntry { id = 1, optionsCount = 2, dateAwarded = DateTime.Parse("1999/12/31") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1992/06/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1994/01/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 5000, dateAwarded = DateTime.Parse("1997/09/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("2003/04/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") },

Openmirrors.com

Linq.book Page 133 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

133

new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") }, new EmployeeOptionEntry { id = 4, optionsCount = 1500, dateAwarded = DateTime.Parse("1997/12/31") }, new EmployeeOptionEntry { id = 101, optionsCount = 2, dateAwarded = DateTime.Parse("1998/12/31") } }; return (empOptions); } }

Plusieurs opérateurs utilisent des classes qui implémentent l’interface IEqualityComparer. Ceci afin de tester l’égalité entre deux éléments. Cette interface est utile lorsque le terme "égalité" doit être pris au sens large. Par exemple, deux chaînes peuvent être considérées égales, même si leur casse diffère. L’interface IEqualityComparer ayant été abordée en détail au chapitre précédent, nous n’y reviendrons pas. Dans les exemples de ce chapitre, nous aurons besoin d’une classe permettant de comparer plusieurs nombres stockés dans des chaînes de caractères. Ainsi, par exemple, les chaînes "17" et "00017" seront considérées comme égales. La classe MyStringifieldNumberComparer se chargera de ce type de comparaison. public class MyStringifiedNumberComparer : IEqualityComparer { public bool Equals(string x, string y) { return(Int32.Parse(x) == Int32.Parse(y)); } public int GetHashCode(string obj) { return Int32.Parse(obj).ToString().GetHashCode(); } }

Cette implémentation de l’interface IEqualityComparer ne fonctionne que sur des variables de type string. La technique utilisée consiste à convertir les valeurs string en int32. Ainsi, par exemple, la valeur "002" sera convertie en un entier de valeur 2, et les éventuels zéros en tête de la chaîne n’affecteront pas la conversion. Dans plusieurs exemples de ce chapitre, nous aurons besoin d’une classe dans laquelle le champ clé des enregistrements n’est pas forcément unique. La classe Actor a été créée dans ce but (le champ birthYear sera utilisé comme clé). public class Actor { public int birthYear; public string firstName; public string lastName;

Linq.book Page 134 Mercredi, 18. février 2009 7:58 07

134

LINQ to Objects

public static Actor[] GetActors() { Actor[] actors = new Actor[] { new Actor { birthYear = 1964, new Actor { birthYear = 1968, new Actor { birthYear = 1960, new Actor { birthYear = 1964, };

Partie II

firstName firstName firstName firstName

= = = =

"Keanu", lastName = "Reeves" }, "Owen", lastName = "Wilson" }, "James", lastName = "Spader" }, "Sandra", lastName = "Bullock" },

return (actors); } }

Les opérateurs non différés, par groupes fonctionnels Dans cette section, nous avons organisé les différents opérateurs de requête standard non différés par grands groupes fonctionnels. Opérateurs de conversion Les opérateurs de conversion sont utilisés pour convertir des séquences dans des collections d’un autre type. L’opérateur ToArray L’opérateur ToArray crée un tableau de type T à partir d’une séquence d’entrée de type T.

Prototype Un seul prototype de l’opérateur ToArray sera étudié dans ce livre : public static T[] ToArray( this IEnumerable source);

Ce prototype admet un seul paramètre : une séquence source d’éléments de type T. Il renvoie un tableau d’éléments de type T. Exceptions L’exception ArgumentNullExpression est levée si l’argument a pour valeur null. Exemples Nous allons créer une séquence IEnumerable en appliquant l’opérateur OfType à un tableau. Une fois la séquence obtenue, nous la passerons à l’opérateur ToArray pour placer les différents éléments dans un tableau (voir Listing 5.1). Listing 5.1 : Un exemple d’appel à l’opérateur ToArray. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson",

Openmirrors.com

Linq.book Page 135 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

135

"Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string[] names = presidents.OfType().ToArray(); foreach (string name in names) Console.WriteLine(name);

Dans un premier temps, le tableau presidents est converti en une séquence IEnumerable avec l’opérateur OfType. Dans un second temps, cette séquence est convertie en un tableau en utilisant l’opérateur ToArray. Le tableau est immédiatement initialisé, car ToArray est un opérateur non différé. Voici le résultat affiché dans la console : Adams Arthur Buchanan Bush Carter Cleveland Clinton Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson

Vous aurez certainement remarqué que ce code est redondant. En effet, le tableau presidents est déjà une séquence, puisque dans C# 3.0 les tableaux implémentent l’interface IEnumerable. L’appel à l’opérateur ToArray aurait donc pu être évité.

Linq.book Page 136 Mercredi, 18. février 2009 7:58 07

136

LINQ to Objects

Partie II

Mais alors qu’auriez-vous pensé de ce code qui se serait contenté de convertir un tableau en… un tableau ? L’opérateur ToArray a deux avantages : il permet de mémoriser une séquence jusqu’à son énumération et de s’assurer que plusieurs énumérations du tableau travailleront sur les mêmes données. Opérateur ToList L’opérateur ToList crée une liste d’éléments de type T à partir d’une séquence d’entrée de type T.

Prototype Un seul prototype de l’opérateur ToList sera étudié dans ce livre : public static List ToList( this IEnumerable source);

Cet opérateur admet un argument : une séquence d’entrée source de type T. Il renvoie une liste d’éléments de type T. Exceptions L’exception ArgumentNullExpression est levée si l’argument source a pour valeur null. Exemples Listing 5.2 : Un appel à l’opérateur ToList. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; List names = presidents.ToList(); foreach (string name in names) Console.WriteLine(name);

Ce code utilise les mêmes données que l’exemple précédent. Mais, ici, l’opérateur OfType n’est pas appelé pour créer une séquence intermédiaire de type IEnumerable : le tableau presidents est directement converti en une liste de type List. Voici les résultats affichés dans la console : Adams Arthur

Openmirrors.com

Linq.book Page 137 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

137

Buchanan Bush Carter Cleveland Clinton Coolidge Eisenhower Fillmore Ford Garfield Grant Harding Harrison Hayes Hoover Jackson Jefferson Johnson Kennedy Lincoln Madison McKinley Monroe Nixon Pierce Polk Reagan Roosevelt Taft Taylor Truman Tyler Van Buren Washington Wilson

Tout comme ToArray, ToList a deux avantages : il permet de mémoriser une séquence jusqu’à son énumération et de s’assurer que plusieurs énumérations travailleront sur les mêmes données. Opérateur ToDictionary Cet opérateur admet au minimum deux paramètres en entrée : une séquence d’entrée de type T et une clé de type K. Il crée un dictionnaire de type .

Si l’argument facultatif elementSelector est spécifié dans le prototype, le dictionnaire créé est de type . Les valeurs stockées sont de type E, différent du type d’entrée T. INFO Si la classe C# Dictionary ne vous est pas familière, sachez qu’elle permet de mémoriser des couples élément/clé (où clé est unique pour chaque élément). Pour retrouver un élément dans la liste, il suffit d’indexer le tableau en utilisant la clé.

Linq.book Page 138 Mercredi, 18. février 2009 7:58 07

138

LINQ to Objects

Partie II

Prototypes Quatre prototypes de l’opérateur ToDictionary seront étudiés dans ce livre. Premier prototype public static Dictionary ToDictionary( this IEnumerable source, Func keySelector);

Ce prototype crée un dictionnaire de type en énumérant la séquence d’entrée source. Le délégué keySelector est appelé pour obtenir une valeur clé pour chaque élément (c’est cette valeur qui sera inscrite dans le dictionnaire). Les éléments stockés dans le dictionnaire sont de même type que ceux de la séquence d’entrée. Aucun comparateur n’étant spécifié dans le prototype, c’est le comparateur par défaut, EqualityComparer.Default, qui sera utilisé. Le deuxième prototype est semblable au premier, mais il permet de spécifier le comparateur à utiliser. Deuxième prototype public static Dictionary ToDictionary( this IEnumerable source, Func keySelector, IEqualityComparer comparer);

Vous utiliserez ce prototype si le comparateur par défaut, EqualityComparer.Default, ne convient pas. Dans ce cas, le comparateur IEqualityComparer sera utilisé pour tout ajout ou lecture d’élément dans le dictionnaire. La classe StringComparer implémente plusieurs classes de comparaison. L’une d’entre elles, par exemple, ignore la casse des chaînes comparées. Ainsi, les clés "Joe" et "joe" seront considérées comme égales. Le troisième prototype est semblable au premier, mais il ajoute un sélectionneur d’élément. Par son intermédiaire, les valeurs stockées dans le dictionnaire peuvent être d’un autre type que celles de la séquence d’entrée. Troisième prototype public static Dictionary ToDictionary( this IEnumerable source, Func keySelector, Func elementSelector);

L’argument elementSelector fait référence à un délégué qui retourne un fragment de l’élément soumis, ou un objet d’un tout autre type. C’est cet élément qui sera stocké dans le dictionnaire. Le quatrième prototype cumule les avantages des deux précédents. Par son intermédiaire, vous pouvez spécifier un elementSelector et un comparateur. Quatrième prototype public static Dictionary ToDictionary( this IEnumerable source, Func keySelector,

Openmirrors.com

Linq.book Page 139 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

139

Func elementSelector, IEqualityComparer comparer);

Exceptions L’exception ArgumentNullExpression est levée si l’argument source, keySelector ou elementSelector, a pour valeur null ou si la clé retournée par keySelector a pour valeur null. L’exception ArgumentException est levée si un sélecteur retourne la même clé pour deux éléments. Exemples Dans cet exemple, nous utiliserons la classe commune Employee. Nous allons créer un dictionnaire de type Dictionary. La clé int représentera l’identifiant id de l’employé et l’objet Employee, l’élément stocké dans le dictionnaire. Listing 5.3 : Un exemple d’utilisation du premier prototype de l’opérateur ToDictionary. Dictionary eDictionary = Employee.GetEmployeesArray().ToDictionary(k => k.id); Employee e = eDictionary[2]; Console.WriteLine("Employé dont le champ id vaut 2 : {0} {1}", e.firstName, ➥e.lastName);

Le champ id est utilisé comme clé. Le premier argument de Dictionary est donc de type int. Ce prototype étant limité à l’enregistrement intégral des données qui lui sont passées, le deuxième argument est de type Employee. En fournissant l’identifiant d’un employé, le prototype Dictionary donne donc accès aux données correspondantes. Voici le résultat affiché dans la console : Employé dont le champ id vaut 2 : William Gates

Pour illustrer le deuxième prototype, nous avons besoin d’une situation dans laquelle l’utilisation d’un comparateur personnalisé se justifie. Supposons que la clé soit une valeur numérique stockée dans une chaîne. Les valeurs "1", "01", "001", etc. ne sont pas identiques, même si elles représentent le même nombre. Nous devons donc utiliser un comparateur qui autorise ce type de "largesse d’écriture". Nous allons légèrement modifier la classe commune Employee pour qu’elle admette une clé de type string. Cette modification va donner naissance à la classe Employee2. La classe utilisée par le deuxième prototype de l’opérateur ToDictionary public class Employee2 { public string id; public string firstName; public string lastName; public static ArrayList GetEmployeesArrayList()

Linq.book Page 140 Mercredi, 18. février 2009 7:58 07

140

LINQ to Objects

Partie II

{ ArrayList al = new ArrayList(); al.Add(new Employee2 { id = "1", firstName = "Joe", lastName = "Rattz" }); al.Add(new Employee2 { id = "2", firstName = "William", lastName = "Gates" }); al.Add(new Employee2 { id = "3", firstName = "Anders", lastName = "Hejlsberg" }); al.Add(new Employee2 { id = "4", firstName = "David", lastName = "Lightman" }); al.Add(new Employee2 { id = "101", firstName = "Kevin", lastName = "Flynn" }); return (al); } public static Employee2[] GetEmployeesArray() { return ((Employee2[])GetEmployeesArrayList().ToArray(typeof(Employee2))); } }

Le type de la clé a été modifié dans un but purement démonstratif, afin d’étayer le fonctionnement du comparateur MyStringifieldNumberComparer. Ce dernier considérera comme égales deux clés qui, littéralement, ne le sont pas. Voyons maintenant comment utiliser la classe Employee2 (voir Listing 5.4). Listing 5.4 : Un exemple d’utilisation du deuxième prototype de l’opérateur ToDictionary. Dictionary eDictionary = Employee2.GetEmployeesArray() .ToDictionary(k => k.id, new MyStringifiedNumberComparer()); Employee2 e = eDictionary["2"]; Console.WriteLine("Employé dont le champ id vaut \"2\" : {0} {1}", e.firstName, e.lastName); e = eDictionary["000002"]; Console.WriteLine("Employé dont le champ id vaut \"000002\" : {0} {1}", e.firstName, e.lastName);

Dans cet exemple, nous tentons d’accéder à l’élément du dictionnaire dont la clé a pour valeur "2", puis "000002". Si la classe de comparaison fonctionne, ces deux clés devraient pointer vers le même employé. Voici les résultats : Employé dont le champ id vaut "2" : William Gates Employé dont le champ id vaut "000002" : William Gates

Les deux clés ayant une même valeur numérique, elles renvoient vers la même entrée dans le dictionnaire. Le troisième prototype permet de stocker dans le dictionnaire un élément d’un autre type que celui de la séquence d’entrée. Pour illustrer son fonctionnement, nous allons travailler avec la classe Employee (voir Listing 5.5). Listing 5.5 : Un exemple d’utilisation du troisième prototype de l’opérateur ToDictionary. Dictionary eDictionary = Employee.GetEmployeesArray() .ToDictionary(k => k.id,

Openmirrors.com

Linq.book Page 141 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

141

i => string.Format("{0} {1}", // elementSelector i.firstName, i.lastName)); string name = eDictionary[2]; Console.WriteLine("Employé dont le champ id vaut 2 : {0}", name);

Dans cet exemple, une expression lambda concatène les champs firstName et lastName et les stocke dans une chaîne. La séquence d’entrée est de type Employee, mais c’est un type string qui est stocké dans le dictionnaire. Voici le résultat : Employé dont le champ id vaut 2 : William Gates

Pour illustrer le quatrième prototype, nous allons utiliser la classe Employee2 et la classe commune MyStringfieldNumberComparer (voir Listing 5.6). Listing 5.6 : Un exemple d’utilisation du quatrième prototype de l’opérateur ToDictionary. Dictionary eDictionary = Employee2.GetEmployeesArray() .ToDictionary(k => k.id, // sélection de la clé i => string.Format("{0} {1}", // sélection de l’élément i.firstName, i.lastName), new MyStringifiedNumberComparer()); // comparateur string name = eDictionary["2"]; Console.WriteLine("Employé dont le champ id vaut \"2\" : {0}", name); name = eDictionary["000002"]; Console.WriteLine("Employé dont le champ id vaut \"000002\" : {0}", name);

Dans ce code : m

le sélecteur elementSelector stocke des valeurs chaînes dans le dictionnaire ;

m

le comparateur MyStringifiedNumberComparer est utilisé pour trouver un élément dans le dictionnaire.

Les deux derniers blocs recherchent l’employé dont l’identifiant vaut " 2", puis "000002". Les chaînes renvoyées sont identiques puisque le comparateur considère ces deux chaînes comme égales. Voici le résultat : Employé dont le champ id vaut 2 : William Gates Employé dont le champ id vaut 000002 : William Gates

Opérateur ToLookup Cet opérateur admet au minimum deux paramètres en entrée : une séquence d’entrée de type T et une clé de type K. Il crée un objet Lookup de type .

Si l’argument facultatif elementSelector est spécifié dans le prototype, l’objet Lookup créé est de type . Les valeurs stockées sont de type E, différent du type d’entrée T. Tous les prototypes de l’opérateur ToLookup créent un objet Lookup qui implémente l’interface ILookup. Nous leur ferons souvent référence en utilisant le simple mot "Lookup".

Linq.book Page 142 Mercredi, 18. février 2009 7:58 07

142

LINQ to Objects

Partie II

INFO Si la classe C# Lookup ne vous est pas familière, sachez qu’elle permet de mémoriser des couples élément/clé (où clé n’est pas forcément unique pour chaque élément). Pour retrouver le ou les éléments qui correspondent à une clé, il suffit d’indexer le tableau en utilisant cette clé.

Prototypes Quatre prototypes de l’opérateur ToLookup seront étudiés dans ce livre. Premier prototype public static ILookup ToLookup( this IEnumerable source, Func keySelector);

Ce prototype crée un Lookup de type en énumérant la séquence d’entrée, source. Le délégué keySelector est appelé pour extraire la valeur clé de chaque élément (c’est cette valeur qui sera inscrite dans le Lookup). Les éléments stockés dans le Lookup sont de même type que ceux de la séquence d’entrée. Aucun comparateur n’étant spécifié dans le prototype, c’est le comparateur par défaut, EqualityComparer.Default, qui sera utilisé. Le deuxième prototype est semblable au premier, mais il permet de spécifier le comparateur à utiliser. Deuxième prototype public static ILookup ToLookup( this IEnumerable source, Func keySelector, IEqualityComparer comparer);

Vous utiliserez ce prototype si le comparateur par défaut, EqualityComparer.Default, ne convient pas. Dans ce cas, le comparateur IEqualityComparer sera utilisé pour tout ajout ou lecture d’élément dans le Lookup. La classe StringComparer implémente plusieurs classes de comparaison. L’une d’entre elles, par exemple, ignore la casse des chaînes comparées. Ainsi, les clés "Joe" et "joe" seront considérées comme égales. Le troisième prototype est semblable au premier, mais il ajoute un sélectionneur d’élément. Par son intermédiaire, les valeurs stockées dans le dictionnaire peuvent être d’un autre type que celles de la séquence d’entrée. Troisième prototype public static ILookup ToLookup( this IEnumerable source, Func keySelector, Func elementSelector);

Openmirrors.com

Linq.book Page 143 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

143

L’argument elementSelector fait référence à un délégué qui retourne un fragment de l’élément soumis, ou un objet d’un tout autre type. C’est cet élément qui sera stocké dans le dictionnaire. Le quatrième prototype cumule les avantages des deux précédents. Par son intermédiaire, vous pouvez spécifier un elementSelector et un comparateur. Quatrième prototype public static ILookup ToLookup( this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer comparer);

Exceptions L’exception ArgumentNullExpression est levée si l’argument source, keySelector ou elementSelector, a pour valeur null ou si la clé retournée par keySelector a pour valeur null. L’exception ArgumentException est levée si un sélecteur retourne la même clé pour deux éléments. Exemples Pour illustrer le premier prototype de l’opérateur ToLookup, nous avons besoin d’une classe dont les éléments contiennent des membres qui peuvent être utilisés comme clés, mais qui ne sont pas forcément uniques. Nous utiliserons pour cela la classe Actor (voir Listing 5.7). Listing 5.7 : Un exemple d’appel du premier prototype de l’opérateur ToLookup. ILookup lookup = Actor.GetActors().ToLookup(k => k.birthYear); // Recherche d’un acteur né en 1964 IEnumerable actors = lookup[1964]; foreach (var actor in actors) Console.WriteLine("{0} {1}", actor.firstName, actor.lastName);

La première instruction crée un Lookup en utilisant le membre Actor.birthYear comme clé. La deuxième instruction indexe le Lookup en utilisant la clé. Il ne reste plus qu’à énumérer l’objet actors pour afficher le ou les résultats : Keanu Reeves Sandra Bullock

Pour illustrer le deuxième prototype, nous allons légèrement modifier la classe Actor. Son membre birthYear, initialement de type int, sera de type string dans la classe modifiée.

Linq.book Page 144 Mercredi, 18. février 2009 7:58 07

144

LINQ to Objects

Partie II

La classe utilisée par le deuxième prototype de l’opérateur ToLookup public class Actor2 { public string birthYear; public string firstName; public string lastName; public static Actor2[] GetActors() { Actor2[] actors = new Actor2[] { new Actor2 { birthYear = "1964", firstName = "Keanu", lastName = "Reeves" }, new Actor2 { birthYear = "1968", firstName = "Owen", lastName = "Wilson" }, new Actor2 { birthYear = "1960", firstName = "James", lastName = "Spader" }, // Une date exprimée sur 5 chiffres new Actor2 { birthYear = "01964", firstName = "Sandra", lastName = "Bullock" }, }; return(actors); } }

Le membre birthYear est maintenant une chaîne de caractères. Il ne reste plus qu’à appeler l’opérateur ToLookup (voir Listing 5.8). Listing 5.8 : Un exemple d’utilisation du deuxième prototype de l’opérateur ToLookup. ILookup lookup = Actor2.GetActors() .ToLookup(k => k.birthYear, new MyStringifiedNumberComparer()); // Recherche d’un acteur né en 1964 IEnumerable actors = lookup["0001964"]; foreach (var actor in actors) Console.WriteLine("{0} {1}", actor.firstName, actor.lastName);

La méthode de comparaison est la même que celle qui avait été utilisée pour illustrer l’opérateur Dictionary. En effet, l’éventuel ou les éventuels "0" en tête de clé n’étant pas significatifs, il est nécessaire de tester l’égalité "au sens large". Voici les résultats : Keanu Reeves Sandra Bullock

La recherche d’éléments dont la clé vaut "0001964" retourne les acteurs Keanu Reeves et Sandra Bullock, dont les clés respectives valent "1964" et "01964". L’objet de comparaison a donc bien fonctionné. Pour illustrer le troisième prototype, nous ferons appel à la classe Actor, qui avait déjà été utilisée dans l’exemple du premier prototype (voir Listing 5.9). Listing 5.9 : Un exemple d’utilisation du troisième prototype de l’opérateur ToLookup. ILookup lookup = Actor.GetActors() .ToLookup(k => k.birthYear, a => string.Format("{0} {1}", a.firstName, a.lastName)); // Recherche d’un acteur né en 1964 IEnumerable actors = lookup[1964];

Openmirrors.com

Linq.book Page 145 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

145

foreach (var actor in actors) Console.WriteLine("{0}", actor);

Dans cet exemple, l’argument elementSelector est une expression lambda qui concatène les champs firstName et lastName. Voici le résultat : Keanu Reeves Sandra Bullock

En utilisant cette troisième variante de l’opérateur ToLookup, le type de données mémorisées dans l’objet Lookup (string) est différent de celui des éléments passés en entrée (Actor). Pour illustrer le quatrième prototype, nous allons utiliser la classe Actor2 et la classe commune MyStringfieldNumberComparer (voir Listing 5.10). Listing 5.10 : Un exemple d’appel du quatrième prototype de l’opérateur ToLookup. ILookup lookup = Actor2.GetActors() .ToLookup(k => k.birthYear, a => string.Format("{0} {1}", a.firstName, a.lastName), new MyStringifiedNumberComparer()); // Recherche d’un acteur né en 1964 IEnumerable actors = lookup["0001964"]; foreach (var actor in actors) Console.WriteLine("{0}", actor);

Voici le résultat : Keanu Reeves Sandra Bullock

Cet exemple recherche des éléments dont la clé vaut "0001964". Les acteurs Keanu Reeves et Sandra Bullock, dont les clés respectives valent "1964" et "01964", correspondent au critère. La comparaison a donc bien fonctionné. Par ailleurs, seules les chaînes nécessaires à la requête (firstName et lastName) sont stockées dans le Lookup. Opérateurs d’égalité Les opérateurs de cette catégorie sont utilisés pour tester l’égalité entre deux séquences. Opérateur SequenceEqual L’opérateur SequenceEqual détermine si deux séquences d’entrée sont égales.

Prototypes Deux prototypes de l’opérateur SequenceEqual seront étudiés dans ce livre. Premier prototype public static bool SequenceEqual( this IEnumerable first, IEnumerable second);

Linq.book Page 146 Mercredi, 18. février 2009 7:58 07

146

LINQ to Objects

Partie II

Cet opérateur énumère les deux séquences en parallèle et compare leurs éléments en utilisant la méthode System.Object.Equals. Si tous les éléments sont égaux et si les deux séquences ont le même nombre d’éléments, l’opérateur retourne la valeur true. Dans le cas contraire, il retourne la valeur false. Second prototype public static bool SequenceEqual( this IEnumerable first, IEnumerable second, IEqualityComparer comparer);

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Exemples Listing 5.11 : Un exemple d’utilisation du premier prototype de l’opérateur SequenceEqual. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool eq = presidents.SequenceEqual(presidents); Console.WriteLine(eq);

Voici le résultat : True

Ceci vous semble un peu trop simple ? Nous allons légèrement compliquer les choses dans le Listing 5.12. Listing 5.12 : Un autre exemple d’utilisation du premier prototype de l’opérateur SequenceEqual. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool eq = presidents.SequenceEqual(presidents.Take(presidents.Count())); Console.WriteLine(eq);

Openmirrors.com

Linq.book Page 147 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

147

Si l’écriture vous semble plus complexe, un rapide examen va vous persuader du contraire. L’opérateur Take limite la comparaison à… tous les éléments du tableau presidents (presidents.Count()). Ce code est donc strictement équivalent au précédent et, bien entendu, il produit le même résultat : True

Nous allons maintenant comparer le tableau presidents avec ses presidents.Count() – 1 premiers éléments (voir Listing 5.13). Listing 5.13 : Un autre exemple d’utilisation du premier prototype de l’opérateur SequenceEqual. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool eq = presidents.SequenceEqual(presidents.Take(presidents.Count() - 1)); Console.WriteLine(eq);

Voici le résultat : False

Les deux séquences n’ayant pas le même nombre d’éléments, il est tout à fait normal que la valeur False soit retournée. Au chapitre précédent, lors de l’étude des opérateurs Take et Skip, il a été dit que, si ces opérateurs étaient utilisés correctement, ils permettaient de retrouver la séquence originale. Nous allons maintenant le prouver en leur adjoignant les opérateurs Concat et SequenceEqual (voir Listing 5.14). Listing 5.14 : Un exemple plus complexe du premier prototype de l’opérateur SequenceEqual. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool eq = presidents.SequenceEqual(presidents.Take(5).Concat(presidents.Skip(5))); Console.WriteLine(eq);

Dans cet exemple, Take(5) extrait les cinq premiers éléments de la séquence originale. Ces éléments sont alors concaténés à la séquence originale (Concat) en sautant les cinq

Linq.book Page 148 Mercredi, 18. février 2009 7:58 07

148

LINQ to Objects

Partie II

premiers éléments (Skip(5)). La séquence obtenue est comparée à la séquence originale (presidents.SequenceEqual()). Comme il se doit, la valeur True est retournée par l’opérateur SequenceEqual : True

Pour illustrer le second prototype, nous allons utiliser deux tableaux de string dont chaque élément est un nombre exprimé sous la forme d’une chaîne. Les éléments des deux tableaux seront définis de telle sorte qu’ils soient égaux, après conversion en entiers. Pour effectuer la comparaison, nous utiliserons la classe MyStringifieldNumberComparer (voir Listing 5.15). Listing 5.15 : Un exemple du second prototype de l’opérateur SequenceEqual. string[] stringifiedNums1 = { "001", "49", "017", "0080", "00027", "2" }; string[] stringifiedNums2 = { "1", "0049", "17", "080", "27", "02" }; bool eq = stringifiedNums1.SequenceEqual(stringifiedNums2, new MyStringifiedNumberComparer()); Console.WriteLine(eq);

En examinant rapidement les deux tableaux, vous pouvez voir que leurs éléments sont égaux, après conversion en entiers. Voici le résultat : True

Opérateurs agissant au niveau des éléments Cette catégorie d’opérateurs vous permet d’obtenir des éléments à partir de la séquence d’entrée. Opérateur First Selon le prototype utilisé, l’opérateur First retourne le premier élément de la séquence d’entrée ou de la séquence correspondant à un prédicat.

Prototypes Deux prototypes de l’opérateur First seront étudiés dans ce livre. Premier prototype public static T First( this IEnumerable source);

Ce prototype retourne le premier élément de la séquence d’entrée source. Openmirrors.com

Linq.book Page 149 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

149

Second prototype public static T First( this IEnumerable source, Func predicate);

Ce prototype retourne le premier élément de la séquence d’entrée pour lequel le prédicat vaut true. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée si le prédicat ne retourne la valeur true pour aucun des éléments de la séquence d’entrée. Exemples Listing 5.16 : Un exemple d’utilisation du premier prototype de l’opérateur First. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.First(); Console.WriteLine(name);

Voici le résultat : Adams

Vous vous demandez peut-être si les opérateurs First et Take(1) sont différents ? Eh bien, oui ! L’opérateur Take retourne une séquence d’éléments (y compris dans le cas où cette séquence ne contient qu’un seul élément). En revanche, l’opérateur First retourne un élément ou génère une exception si aucun élément ne peut être retourné. Listing 5.17 : Un exemple d’utilisation du deuxième prototype de l’opérateur First. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.First(p => p.StartsWith("H")); Console.WriteLine(name);

Linq.book Page 150 Mercredi, 18. février 2009 7:58 07

150

LINQ to Objects

Partie II

Ce code devrait retourner le premier élément de la séquence d’entrée qui commence par la lettre "H". Voici le résultat : Harding

Si aucun élément ne peut être renvoyé par l’opérateur First, une exception InvalidOperationException est levée. Pour éviter ce problème, utilisez l’opérateur FirstOrDefault. Opérateur FirstOrDefault L’opérateur FirstOrDefault est semblable à l’opérateur First, excepté en ce qui concerne son comportement lorsque aucun élément n’est trouvé.

Prototypes Deux prototypes de l’opérateur FirstOrDefault seront étudiés dans ce livre. Premier prototype public static T FirstOrDefault( this IEnumerable source);

Ce prototype retourne le premier élément de la séquence d’entrée source. Si la séquence d’entrée est vide, l’objet default(T) est retourné (la valeur par défaut des types référence et nullable est null). Second prototype public static T FirstOrDefault( this IEnumerable source, Func predicate);

Ce prototype retourne le premier élément de la séquence d’entrée pour lequel le prédicat vaut true. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Exemples Listing 5.18 : Appel du premier prototype de l’opérateur FirstOrDefault. L’élément recherché n’est pas trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

Openmirrors.com

Linq.book Page 151 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

151

string name = presidents.Take(0).FirstOrDefault(); Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat : NULL

Listing 5.19 : Appel du premier prototype de l’opérateur FirstOrDefault. L’élément recherché est trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.FirstOrDefault(); Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat : Adams

Pour illustrer le second prototype de l’opérateur FirstOrDefault, nous allons rechercher le premier élément qui commence par la lettre "B". Listing 5.20 : Appel du second prototype. L’élément recherché est trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.FirstOrDefault(p => p.StartsWith("B")); Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat : Buchanan

Listing 5.21 : Appel du second prototype. L’élément recherché n’est pas trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.FirstOrDefault(p => p.StartsWith("Z")); Console.WriteLine(name == null ? "NULL" : name);

Linq.book Page 152 Mercredi, 18. février 2009 7:58 07

152

LINQ to Objects

Partie II

Aucune réponse n’étant trouvée, voici le résultat : NULL

Opérateur Last Selon le prototype utilisé, l’opérateur Last retourne le dernier élément de la séquence d’entrée ou de la séquence correspondant à un prédicat.

Prototypes Deux prototypes de l’opérateur Last seront étudiés dans ce livre. Premier prototype public static T Last( this IEnumerable source);

Ce prototype retourne le dernier élément de la séquence d’entrée source. Second prototype public static T Last( this IEnumerable source, Func predicate);

Ce prototype retourne le dernier élément de la séquence d’entrée pour lequel le prédicat vaut true. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée si le prédicat ne retourne la valeur true pour aucun des éléments de la séquence d’entrée. Exemples Listing 5.22 : Un exemple d’utilisation du premier prototype de l’opérateur Last. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.Last(); Console.WriteLine(name);

Voici le résultat : Wilson

Openmirrors.com

Linq.book Page 153 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

153

Listing 5.23 : Un exemple d’utilisation du second prototype de l’opérateur Last. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.Last(p => p.StartsWith("H")); Console.WriteLine(name);

Ce code devrait retourner le dernier élément de la séquence d’entrée qui commence par la lettre "H". Voici le résultat : Hoover

Si aucun élément ne peut être renvoyé par l’opérateur Last, une exception InvalidOperationException est levée. Pour éviter ce problème, utilisez l’opérateur LastOrDefault. Opérateur LastOrDefault L’opérateur LastOrDefault est semblable à l’opérateur Last, excepté en ce qui concerne son comportement lorsque aucun élément n’est trouvé.

Prototypes Deux prototypes de l’opérateur LastOrDefault seront étudiés dans ce livre. Premier prototype public static T LastOrDefault( this IEnumerable source);

Ce prototype retourne le dernier élément de la séquence d’entrée source. Si la séquence d’entrée est vide, l’objet default(T) est retourné (la valeur par défaut des types référence et nullable est null). Second prototype public static T LastOrDefault( this IEnumerable source, Func predicate);

Ce prototype retourne le dernier élément de la séquence d’entrée pour lequel le prédicat vaut true. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.

Linq.book Page 154 Mercredi, 18. février 2009 7:58 07

154

LINQ to Objects

Partie II

Exemples Listing 5.24 : Appel du premier prototype de l’opérateur LastOrDefault. L’élément recherché n’est pas trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.Take(0).LastOrDefault(); Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat : NULL

Listing 5.25 : Appel du premier prototype de l’opérateur LastOrDefault. L’élément recherché est trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.LastOrDefault(); Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat : Wilson

Pour illustrer le second prototype de l’opérateur LastOrDefault, nous allons rechercher le dernier élément qui commence par la lettre "B". Listing 5.26 : Appel du second prototype. L’élément recherché est trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.LastOrDefault(p => p.StartsWith("B")); Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat : Bush

Openmirrors.com

Linq.book Page 155 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

155

Listing 5.27 : Appel du second prototype. L’élément recherché n’est pas trouvé. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string name = presidents.LastOrDefault(p => p.StartsWith("Z")); Console.WriteLine(name == null ? "NULL" : name);

Aucune réponse n’étant trouvée, voici le résultat : NULL

Opérateur Single Selon le prototype utilisé, l’opérateur Single retourne le seul élément de la séquence d’entrée, ou le seul élément de la séquence d’entrée correspondant à un prédicat.

Prototypes Deux prototypes de l’opérateur Single seront étudiés dans ce livre. Premier prototype public static T Single( this IEnumerable source);

Ce prototype énumère la séquence d’entrée source et renvoie l’unique élément trouvé. Second prototype public static T Single( this IEnumerable source, Func predicate);

Ce second prototype retourne l’unique élément pour lequel le prédicat a pour valeur true. L’exception InvalidOperationException est levée si le prédicat ne retourne la valeur true pour aucun ou pour plusieurs des éléments de la séquence d’entrée. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée si le prédicat ne retourne la valeur true pour aucun ou pour plusieurs des éléments de la séquence d’entrée, ou si la séquence d’entrée est vide. Exemples Listing 5.28 : Un exemple d’utilisation du premier prototype de l’opérateur Single sur la classe commune Employee. Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 3).Single(); Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);

Linq.book Page 156 Mercredi, 18. février 2009 7:58 07

156

LINQ to Objects

Partie II

La requête retourne un seul et unique élément. Dans cet exemple, tout se passe bien, car un seul employé a un identifiant égal à 3 (Where(e => e.id == 3)). Voici le résultat : Anders Hejlsberg

Listing 5.29 : Un exemple d’appel du second prototype de l’opérateur Single. Employee emp = Employee.GetEmployeesArray() .Single(e => e.id == 3); Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);

Ce code est équivalent au précédent. Mais, ici, au lieu d’invoquer l’opérateur Where pour s’assurer de l’unicité de la réponse, un prédicat est passé en argument de l’opérateur Single. Voici le résultat : Anders Hejlsberg

Opérateur SingleOrDefault L’opérateur SingleOrDefault est semblable à l’opérateur Single, excepté en ce qui concerne son comportement lorsque aucun élément n’est trouvé.

Prototypes Deux prototypes de l’opérateur SingleOrDefault seront étudiés dans ce livre. Premier prototype public static T SingleOrDefault( this IEnumerable source);

Ce prototype énumère la séquence d’entrée source et renvoie l’unique élément trouvé. Si la séquence est vide, l’objet default(T) est retourné (la valeur par défaut des types référence et nullable est null). Si plusieurs éléments sont trouvés, une exception InvalidOperationException est levée. Le second prototype de l’opérateur SingleOrDefault permet de spécifier un prédicat pour indiquer quel élément doit être retourné. Second prototype public static T SingleOrDefault( this IEnumerable source, Func predicate);

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée si le prédicat retourne la valeur true pour plusieurs des éléments de la séquence d’entrée. Openmirrors.com

Linq.book Page 157 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

157

Exemples Le Listing 5.30 illustre le fonctionnement du premier prototype dans le cas où aucun élément n’est trouvé dans la séquence d’entrée. Pour ce faire, nous utilisons l’opérateur Where en spécifiant une clé inexistante. Listing 5.30 : Un exemple d’utilisation du premier prototype de l’opérateur SingleOrDefault. Ici, aucun élément n’est trouvé. Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 5).SingleOrDefault(); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

Ce code effectue une requête sur un employé dont l’identifiant vaut 5, en sachant pertinemment qu’un tel identifiant n’existe pas. Une séquence vide est donc retournée. Voici le résultat : NULL

Le Listing 5.31 illustre le fonctionnement du premier prototype dans le cas où un élément est trouvé dans la séquence d’entrée. Pour ce faire, nous utilisons l’opérateur Where en spécifiant une clé existante et unique. Listing 5.31 : Un exemple d’utilisation du premier prototype de l’opérateur SingleOrDefault. Ici, un élément est trouvé. Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 4).SingleOrDefault(); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

L’identifiant spécifié dans l’opérateur Where existe et est unique. Voici le résultat : David Lightman

Pour illustrer le fonctionnement du second prototype, nous allons cette fois passer un prédicat à l’opérateur SingleOrDefault en choisissant un identifiant qui existe. Listing 5.32 : Un exemple d’utilisation du second prototype de l’opérateur SingleOrDefault. Ici, un élément est trouvé. Employee emp = Employee.GetEmployeesArray() .SingleOrDefault(e => e.id == 4); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

Linq.book Page 158 Mercredi, 18. février 2009 7:58 07

158

LINQ to Objects

Partie II

Ce code est équivalent au précédent. Mais, ici, au lieu d’invoquer l’opérateur Where pour filtrer les données, un prédicat est passé comme argument de l’opérateur SingleOrDefault. Voici le résultat : David Lightman

Nous allons maintenant essayer un prédicat qui ne trouve aucune correspondance dans les données (voir Listing 5.33). Listing 5.33 : Un exemple d’utilisation du second prototype de l’opérateur SingleOrDefault. Ici, aucun élément n’est trouvé. Employee emp = Employee.GetEmployeesArray() .SingleOrDefault(e => e.id == 5); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

Aucune réponse n’étant trouvée, voici le résultat (remarquez qu’aucune exception n’a été générée) : NULL

Opérateur ElementAt L’opérateur ElementAt retourne l’élément de la séquence d’entrée dont l’index est spécifié.

Prototype Un seul prototype de l’opérateur ElementAt sera étudié dans ce livre : public static T ElementAt( this IEnumerable source, int index);

Si la séquence implémente IList, l’interface IList est utilisée pour retrouver l’élément indexé. Dans le cas contraire, la séquence est énumérée jusqu’à ce que l’élément indexé soit atteint. Une exception ArgumentOutOfRangeExeption est levée si l’index est négatif ou supérieur au nombre d’éléments dans la séquence. INFO Dans le langage C#, le premier élément d’un index a pour valeur zéro et le dernier, le nombre d’éléments de la séquence moins un.

Exceptions L’exception ArgumentNullExpression est levée si l’argument source a pour valeur null. L’exception ArgumentOutOfRangeExeption est levée si l’index est négatif ou supérieur au nombre d’éléments dans la séquence. Openmirrors.com

Linq.book Page 159 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

159

Exemples Listing 5.34 : Exemple d’appel de l’opérateur ElementAt. Employee emp = Employee.GetEmployeesArray() .ElementAt(3); Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);

L’élément de rang 3 (c’est-à-dire le quatrième élément) a été demandé. Voici le résultat : David Lightman

Opérateur ElementAtOrDefault L’opérateur ElementAtOrDefault retourne l’élément de la séquence d’entrée dont l’index est spécifié.

Prototype Un seul prototype de l’opérateur ElementAtOrDefault sera étudié dans ce livre : public static T ElementAtOrDefault( this IEnumerable source, int index);

Si la séquence implémente IList, l’interface IList est utilisée pour retrouver l’élément indexé. Dans le cas contraire, la séquence est énumérée jusqu’à ce que l’élément indexé soit atteint. Si l’index est négatif, supérieur ou égal au nombre d’éléments dans la séquence, l’objet default(T) est retourné (la valeur par défaut des types référence et nullable est null). Cette seule caractéristique différencie les opérateurs ElementAtOrDefault et ElementAt. INFO Dans le langage C#, le premier élément d’un index a pour valeur zéro et le dernier, le nombre d’éléments de la séquence moins un.

Exceptions L’exception ArgumentNullExpression est levée si l’argument source a pour valeur null. Exemples Listing 5.35 : Exemple d’appel de l’opérateur ElementAt avec un index valide. Employee emp = Employee.GetEmployeesArray() .ElementAtOrDefault(3); Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

Linq.book Page 160 Mercredi, 18. février 2009 7:58 07

160

LINQ to Objects

Partie II

Voici le résultat : David Lightman

L’élément dont l’index vaut 3 est bien retourné par la requête. Nous allons maintenant transmettre un index invalide à cette même requête (voir Listing 5.36). Listing 5.36 : Exemple d’appel de l’opérateur ElementAt avec un index invalide. Employee emp = Employee.GetEmployeesArray() .ElementAtOrDefault(5); Console.WriteLine(emp == null ? "NULL" :

Étant donné que l’index 5 ne correspond à aucun élément, voici le résultat retourné : NULL

Quantificateurs Les quantificateurs permettent de tester l’existence d’une valeur dans une séquence d’entrée. Opérateur Any L’opérateur Any retourne la valeur true si au moins un élément de la séquence d’entrée vérifie une condition.

Prototypes Deux prototypes de l’opérateur Any seront étudiés dans ce livre. Premier prototype public static bool Any( this IEnumerable source);

Ce prototype retourne la valeur true si la séquence d’entrée contient au moins un élément. Second prototype public static bool Any( this IEnumerable source, Func predicate);

Le second prototype énumère la séquence d’entrée source. Il retourne la valeur true si le prédicat retourne la valeur true sur au moins un élément de la séquence. L’énumération stoppe dès que cette condition est atteinte. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Openmirrors.com

Linq.book Page 161 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

161

Exemples Listing 5.37 : Exemple d’appel du premier prototype avec une séquence d’entrée vide. bool any = Enumerable.Empty().Any(); Console.WriteLine(any);

Voici le résultat : False

Listing 5.38 : Exemple d’appel du premier prototype avec une séquence d’entrée non vide. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool any = presidents.Any(); Console.WriteLine(any);

Voici le résultat : True

Listing 5.39 : Exemple d’appel du second prototype. Ici, aucun élément ne correspond au prédicat. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool any = presidents.Any(s => s.StartsWith("Z")); Console.WriteLine(any);

Le prédicat limite la requête aux éléments dont le nom commence par la lettre " Z". Comme aucun élément ne correspond, la valeur False est retournée : False

Listing 5.40 : Exemple d’appel du second prototype. Ici, au moins un élément correspond au prédicat. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool any = presidents.Any(s => s.StartsWith("A")); Console.WriteLine(any);

Linq.book Page 162 Mercredi, 18. février 2009 7:58 07

162

LINQ to Objects

Partie II

Au moins un élément du tableau presidents correspondant au prédicat, la valeur True est retournée : True

Opérateur All L’opérateur All retourne la valeur true si tous les éléments de la séquence d’entrée vérifient une condition.

Prototype Un seul prototype de l’opérateur All sera étudié dans ce livre : public static bool All( this IEnumerable source, Func predicate);

L’opérateur All énumère la séquence d’entrée. Il retourne la valeur true si le prédicat est vérifié sur tous les éléments de la séquence. Si le prédicat retourne la valeur false pour un élément, l’énumération cesse immédiatement. Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Exemples Listing 5.41 : Exemple d’appel du prototype de l’opérateur All. Ici, le prédicat ne retourne pas la valeur True pour tous les éléments. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool all = presidents.All(s => s.Length > 5); Console.WriteLine(all);

Tous les éléments du tableau presidents n’ayant pas une longueur supérieure à 5 caractères, le prédicat n’est pas toujours vérifié. Le résultat est sans appel : False

Listing 5.42 : Exemple d’appel du prototype de l’opérateur All. Ici, le prédicat retourne la valeur True pour tous les éléments. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley",

Openmirrors.com

Linq.book Page 163 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

163

"Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool all = presidents.All(s => s.Length > 3); Console.WriteLine(all);

Les noms des présidents comprenant un minimum de 4 caractères, le prédicat est vérifié pour tous les éléments du tableau. Voici le résultat : True

Opérateur Contains L’opérateur Contains retourne la valeur true si un des éléments de la séquence d’entrée vérifie la condition.

Prototypes Deux prototypes de l’opérateur All seront étudiés dans ce livre. Premier prototype public static bool Contains( this IEnumerable source, T value);

Dans un premier temps, ce prototype teste si la séquence d’entrée implémente l’interface ICollection. Dans l’affirmative, la méthode Contains de cette interface est appelée. Dans le cas contraire, la séquence d’entrée est énumérée pour voir si un de ses éléments vérifie la condition. Dès qu’une telle situation est atteinte, l’énumération prend fin. La valeur spécifiée est comparée aux éléments de la séquence d’entrée en utilisant la classe de comparaison par défaut : EqualityComparer.Default. Second prototype Le second prototype est en tout point comparable au premier, si ce n’est qu’il permet de spécifier un objet IEqualityComparer. Dans ce cas, c’est ce comparateur qui est utilisé pour comparer les éléments de la séquence d’entrée : public static bool Contains( this IEnumerable source, T value, IEqualityComparer comparer);

Exceptions L’exception ArgumentNullExpression est levée si l’argument source a pour valeur null. Exemples Listing 5.43 : Exemple d’appel du premier prototype. La valeur spécifiée ne se trouve pas dans la séquence. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland",

Linq.book Page 164 Mercredi, 18. février 2009 7:58 07

164

LINQ to Objects

Partie II

"Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool contains = presidents.Contains("Rattz"); Console.WriteLine(contains);

Aucun élément contenant la valeur "Rattz" dans le tableau. Le résultat est donc le suivant : False

Listing 5.44 : Exemple d’appel du premier prototype. La valeur spécifiée se trouve dans la séquence. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; bool contains = presidents.Contains("Hayes"); Console.WriteLine(contains);

Un des éléments du tableau contenant la valeur "Hayes", le résultat est le suivant : True

Pour illustrer le second prototype, nous allons utiliser la classe commune MyStringifieldNumberComparer (voir Listing 5.45). La requête recherchera un nombre stocké au format chaîne et précédé de plusieurs zéros. Le comparateur ne prenant pas en considération les zéros de tête, ce nombre sera retrouvé dans le tableau. Listing 5.45 : Exemple d’appel du second prototype. La valeur spécifiée est trouvée dans la séquence. string[] stringifiedNums = { "001", "49", "017", "0080", "00027", "2" }; bool contains = stringifiedNums.Contains("0000002", new MyStringifiedNumberComparer()); Console.WriteLine(contains);

Le comparateur convertit la chaîne recherchée en un nombre. Les zéros de tête disparaissent et la valeur est trouvée dans la séquence. La variable contains devrait donc avoir pour valeur true. Voici le résultat : True

Openmirrors.com

Linq.book Page 165 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

165

Nous allons maintenant rechercher un élément inexistant dans la séquence d’entrée (voir Listing 5.46). Listing 5.46 : Exemple d’appel du second prototype. La valeur spécifiée n’est pas trouvée dans la séquence. string[] stringifiedNums = { "001", "49", "017", "0080", "00027", "2" }; bool contains = stringifiedNums.Contains("000271", new MyStringifiedNumberComparer()); Console.WriteLine(contains);

L’élément "000271" n’étant pas trouvé dans la séquence d’entrée, voici le résultat : False

Fonctions de comptage Les opérateurs de ce groupe effectuent des comptes (nombre d’éléments, somme, minimum, maximum) sur les éléments de la séquence d’entrée. Opérateur Count L’opérateur Count retourne le nombre d’éléments de la séquence d’entrée.

Prototypes Deux prototypes de l’opérateur Count seront étudiés dans ce livre. Premier prototype public static int Count( this IEnumerable source);

Ce prototype teste si la séquence d’entrée implémente l’interface ICollection. Dans l’affirmative, il obtient le nombre d’éléments de la séquence en utilisant la fonction de comptage de cette interface. Dans la négative, le nombre d’éléments est obtenu en énumérant la séquence d’entrée. Le second prototype renvoie le nombre d’éléments de la séquence d’entrée pour lesquels le prédicat retourne la valeur true. Second prototype public static int Count( this IEnumerable source, Func predicate);

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.

Linq.book Page 166 Mercredi, 18. février 2009 7:58 07

166

LINQ to Objects

Partie II

L’exception OverflowException est levée si le nombre d’éléments est supérieur à la valeur maximale autorisée par int.MaxValue. Exemples L’exemple du Listing 5.47 compte le nombre de présidents stockés dans la séquence d’entrée. Listing 5.47 : Exemple d’appel du premier prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; int count = presidents.Count(); Console.WriteLine(count);

Voici le résultat : 37

L’exemple du Listing 5.48 compte le nombre de présidents stockés dans la séquence d’entrée dont le nom commence par la lettre "J". Listing 5.48 : Exemple d’appel du second prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; int count = presidents.Count(s => s.StartsWith("J")); Console.WriteLine(count);

Voici le résultat : 3

Si le nombre d’éléments dépasse la capacité de int.MaxValue, vous utiliserez l’opérateur LongCount. Opérateur LongCount L’opérateur Count retourne le nombre d’éléments de la séquence d’entrée au format long.

Openmirrors.com

Linq.book Page 167 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

167

Prototypes Deux prototypes de l’opérateur LongCount seront étudiés dans ce livre. Premier prototype public static long LongCount( this IEnumerable source);

Le premier prototype énumère la séquence d’entrée et retourne le nombre d’éléments comptés. Le second prototype renvoie le nombre d’éléments de la séquence d’entrée pour lesquels le prédicat retourne la valeur true. Second prototype public static long LongCount( this IEnumerable source, Func predicate);

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Exemples Dans l’exemple du Listing 5.49, nous utilisons un opérateur de requête standard pour générer une séquence pour laquelle l’opérateur Count produirait une exception de type OverflowException. Au chapitre précédent, nous avons étudié l’opérateur Range, qui permettait de créer une séquence en spécifiant son nombre d’éléments sous la forme d’un int. Nous allons concaténer deux de ces séquences pour dépasser les capacités du type int, et cela va donc nécessiter l’utilisation de l’opérateur LongCount. Listing 5.49 : Exemple d’appel du premier prototype. long count = Enumerable.Range(0, int.MaxValue). Concat(Enumerable.Range(0, int.MaxValue)).LongCount(); Console.WriteLine(count);

L’opérateur Range est appelé à deux reprises pour générer deux séquences contenant chacune le nombre maximal d’éléments du type int. Ces deux séquences sont alors concaténées à l’aide de l’opérateur Concat. ATTENTION L’exécution de cet exemple est assez longue. Sur ma machine, un Pentium 4 doté de 1 Go de RAM, il a fallu attendre deux minutes et demie !

Ne soyez pas surpris si cet exemple est très long à s’exécuter : il génère en effet deux séquences de 2 147 483 647 éléments !

Linq.book Page 168 Mercredi, 18. février 2009 7:58 07

168

LINQ to Objects

Partie II

Voici le résultat : 4294967294

Si vous essayez d’exécuter cet exemple en utilisant l’opérateur Count, une exception OverflowException sera levée. Pour illustrer le second prototype, nous reprendrons le même code que dans l’exemple précédent, mais nous limiterons l’énumération aux entiers supérieurs à 1 et inférieurs à 4. Seuls les éléments 2 et 3 seront donc sélectionnés. Étant donné que le code définit deux séquences, l’énumération devrait donc compter quatre éléments (voir Listing 5.50). Listing 5.50 : Exemple d’appel du second prototype. long count = Enumerable.Range(0, int.MaxValue). Concat(Enumerable.Range(0, int.MaxValue)).LongCount(n => n > 1 && n < 4); Console.WriteLine(count);

À l’exception du prédicat, ce code est très proche du précédent. Il est également très long à exécuter, et même plus long que celui de l’exemple précédent. Voici le résultat affiché dans la console : 4

Opérateur Sum L’opérateur Sum retourne la somme des valeurs numériques contenues dans les éléments de la séquence d’entrée.

Prototypes Deux prototypes de l’opérateur Sum seront étudiés dans ce livre. Premier prototype public static Numeric Sum( this IEnumerable source);

Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un de leurs équivalents nullables, int?, long?, double? ou decimal?.

Le premier prototype retourne la somme de tous les éléments de la séquence d’entrée source. Si la séquence d’entrée est vide, la valeur retournée est 0. Les valeurs null des types nullables ne sont pas incluses dans la somme. Le second prototype est semblable au premier, mais les valeurs additionnées sont sélectionnées par l’intermédiaire d’un délégué. Openmirrors.com

Linq.book Page 169 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

169

Second prototype public static Numeric Sum( this IEnumerable source, Func selector);

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Si la somme des éléments dépasse la capacité du type Numeric : m

une valeur –infini ou +infini est retournée si Numeric est de type decimal ou decimal? ;

m

une exception OverflowException est levée si Numeric est d’un autre type.

Exemples L’exemple du Listing 5.51 génère une séquence d’entiers avec l’opérateur Range et calcule leur somme en utilisant le premier prototype de l’opérateur Sum. Listing 5.51 : Exemple d’appel du premier prototype. IEnumerable ints = Enumerable.Range(1, 10); foreach (int i in ints) Console.WriteLine(i); Console.WriteLine("--"); int sum = ints.Sum(); Console.WriteLine(sum);

Voici les résultats : 1 2 3 4 5 6 7 8 9 10 -55

Le Listing 5.52 illustre le second prototype. Ici, le calcul porte sur la somme des options des employés de la classe commune EmployeeOptionEntry. Listing 5.52 : Exemple d’appel du second prototype. IEnumerable options = EmployeeOptionEntry.GetEmployeeOptionEntries(); long optionsSum = options.Sum(o => o.optionsCount); Console.WriteLine("Somme des options des employés : {0}", optionsSum);

Linq.book Page 170 Mercredi, 18. février 2009 7:58 07

170

LINQ to Objects

Partie II

Plutôt que calculer la somme de tous les membres des éléments, nous utilisons ici le sélecteur du second prototype pour limiter la somme au membre OptionsCount. Voici le résultat : Somme des options des employés : 51504

Opérateur Min L’opérateur Min retourne la plus petite valeur de la séquence d’entrée.

Prototypes Quatre prototypes de l’opérateur Min seront étudiés dans ce livre. Premier prototype public static Numeric Min( this IEnumerable source);

Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un de leurs équivalents nullables, int?, long?, double? ou decimal?.

Le premier prototype retourne la plus petite valeur de la séquence d’entrée. Si les éléments implémentent l’interface IComparable, cette interface est utilisée pour comparer les éléments. Dans le cas contraire, c’est l’interface non générique IComparable qui est utilisée. La valeur null est retournée si la séquence est vide ou uniquement composée de valeurs null. Le deuxième prototype de l’opérateur Min se comporte comme le premier, mais il s’applique aux types non numériques. Deuxième prototype public static T Min( this IEnumerable source);

Le troisième prototype est dédié aux types numériques. Il implémente une méthode de sélection qui permet de limiter la comparaison à un seul membre de chaque élément. Troisième prototype public static Numeric Min( this IEnumerable source, Func selector);

Le quatrième prototype est dédié aux types non numériques. Tout comme le précédent, il implémente une méthode de sélection qui permet de limiter la comparaison à un seul membre de chaque élément. Quatrième prototype public static S Min( this IEnumerable source, Func selector);

Openmirrors.com

Linq.book Page 171 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

171

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Si le type T est nullable (int?, long?, double? ou decimal?), les premier et troisième prototypes retournent la valeur null si la source est vide. Si le type T n’est pas nullable (int, long, double ou decimal), les premier et troisième prototypes lèvent une exception InvalidOperationException si la séquence source est vide. Exemples Dans l’exemple du Listing 5.53, la plus petite valeur stockée dans un tableau d’entiers est retournée par le premier prototype de l’opérateur Min. Listing 5.53 : Exemple d’appel du premier prototype. int[] myInts = new int[] { 974, 2, 7, 1374, 27, 54 }; int minInt = myInts.Min(); Console.WriteLine(minInt);

Voici le résultat retourné : 2

Pour illustrer le deuxième prototype, nous appliquerons l’opérateur Min sur le tableau presidents. La valeur retournée sera la "plus petite", alphabétiquement parlant. Listing 5.54 : Exemple d’appel du deuxième prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string minName = presidents.Min(); Console.WriteLine(minName);

Voici le résultat : Adams

Le résultat est le même que celui qui aurait été renvoyé par l’opérateur First. Mais ceci est un cas particulier : si les éléments du tableau presidents avaient été classés dans un autre ordre ou de façon aléatoire, le résultat de la fonction Min resterait "Adams". Pour illustrer le troisième prototype, nous rechercherons la date de naissance la plus ancienne dans la classe Actor (voir Listing 5.55).

Linq.book Page 172 Mercredi, 18. février 2009 7:58 07

172

LINQ to Objects

Partie II

Listing 5.55 : Exemple d’appel du troisième prototype. int oldestActorAge = Actor.GetActors().Min(a => a.birthYear); Console.WriteLine(oldestActorAge);

Voici le résultat : 1960

Pour illustrer le quatrième prototype, nous allons rechercher le "premier" nom d’acteur (alphabétiquement parlant) dans la classe Actor (voir Listing 5.56). Listing 5.56 : Exemple d’appel du quatrième prototype. string firstAlphabetically = Actor.GetActors().Min(a => a.lastName); Console.WriteLine(firstAlphabetically);

Voici le résultat : Bullock

Opérateur Max L’opérateur Max retourne la plus grande valeur de la séquence d’entrée.

Prototypes Quatre prototypes de l’opérateur Max seront étudiés dans ce livre. Premier prototype public static Numeric Max( this IEnumerable source);

Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un de leurs équivalents nullables, int?, long?, double? ou decimal?.

Le premier prototype retourne la plus grande valeur de la séquence d’entrée. Si les éléments implémentent l’interface IComparable, cette interface est utilisée pour comparer les éléments. Dans le cas contraire, c’est l’interface non générique IComparable qui est utilisée. La valeur null est retournée si la séquence est vide ou uniquement composée de valeurs null. Le deuxième prototype de l’opérateur Max se comporte comme le premier, mais il s’applique aux types non numériques. Deuxième prototype public static T Max( this IEnumerable source);

Le troisième prototype est dédié aux types numériques. Il implémente une méthode de sélection qui permet de limiter la comparaison à un seul membre de chaque élément. Openmirrors.com

Linq.book Page 173 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

173

Troisième prototype public static Numeric Max( this IEnumerable source, Func selector);

Le quatrième prototype est dédié aux types non numériques. Tout comme le précédent, il implémente une méthode de sélection qui permet de limiter la comparaison à un seul membre de chaque élément. Quatrième prototype public static S Max( this IEnumerable source, Func selector);

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. Si le type T est nullable (int?, long?, double? ou decimal?), les premier et troisième prototypes retournent la valeur null si la source est vide. Si le type T n’est pas nullable (int, long, double ou decimal), les premier et troisième prototypes lèvent une exception InvalidOperationException si la séquence source est vide. Exemples Dans l’exemple du Listing 5.57, la plus grande valeur stockée dans un tableau d’entiers est retournée par le premier prototype de l’opérateur Max. Listing 5.57 : Exemple d’appel du premier prototype. int[] myInts = new int[] { 974, 2, 7, 1374, 27, 54 }; int minInt = myInts.Max(); Console.WriteLine(minInt);

Voici le résultat retourné : 1374

Pour illustrer le deuxième prototype (voir Listing 5.58), nous appliquerons l’opérateur Max sur le tableau presidents. La valeur retournée sera la "plus grande", alphabétiquement parlant. Listing 5.58 : Exemple d’appel du deuxième prototype. string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"}; string minName = presidents.Max(); Console.WriteLine(minName);

Linq.book Page 174 Mercredi, 18. février 2009 7:58 07

174

LINQ to Objects

Partie II

Voici le résultat : Wilson

Le résultat est le même que celui qui aurait été renvoyé par l’opérateur Last. Mais ceci est un cas particulier : si les éléments du tableau presidents avaient été classés dans un autre ordre ou de façon aléatoire, le résultat de la fonction Max resterait "Wilson". Pour illustrer le troisième prototype, nous rechercherons la date de naissance la plus récente dans la classe Actor (voir Listing 5.59). Listing 5.59 : Exemple d’appel du troisième prototype. int oldestActorAge = Actor.GetActors().Max(a => a.birthYear); Console.WriteLine(oldestActorAge);

Voici le résultat : 1968

Pour illustrer le quatrième prototype, nous allons rechercher le "dernier" nom d’acteur (alphabétiquement parlant) dans la classe Actor (voir Listing 5.60). Listing 5.60 : Exemple d’appel du quatrième prototype. string firstAlphabetically = Actor.GetActors().Max(a => a.lastName); Console.WriteLine(firstAlphabetically);

Voici le résultat : Wilson

Opérateur Average L’opérateur Average retourne la moyenne des valeurs numériques contenues dans la séquence d’entrée.

Prototypes Deux prototypes de l’opérateur Average seront étudiés dans ce livre. Premier prototype public static Result Average( this IEnumerable source);

Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un de leurs équivalents nullables, int?, long?, double? ou decimal?. Si Numeric est de type int ou long, Result sera de type double. Si Numeric est de type int? ou long?, Result sera de type double?. Dans tous les autres cas, Result sera du même type que Numeric.

Openmirrors.com

Linq.book Page 175 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

175

Le premier prototype énumère la séquence d’entrée source et calcule la moyenne des éléments de type Numeric. Le second prototype énumère la séquence d’entrée source et calcule la moyenne des éléments de type Numeric désignés par la méthode selector. Second prototype public static Result Average( this IEnumerable source, Func selector);

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception OverflowException est levée si la somme des valeurs dont on calcule la moyenne dépasse la capacité du type long lorsque Numeric a un type int, int?, long ou long?. Exemples Afin d’illustrer le premier prototype, nous allons utiliser l’opérateur Range pour créer une séquence d’entiers dont nous calculerons la moyenne (voir Listing 5.61). Listing 5.61 : Exemple d’appel du premier prototype. IEnumerable intSequence = Enumerable.Range(1, 10); Console.WriteLine("Séquence d’entiers :"); foreach (int i in intSequence) Console.WriteLine(i); double average = intSequence.Average(); Console.WriteLine("Moyenne : {0}", average);

Voici les résultats : Séquence d’entiers : 1 2 3 4 5 6 7 8 9 10 Moyenne : 5.5

Pour illustrer le second prototype, nous travaillerons avec la classe EmployeeOptionEntry (voir Listing 5.62).

Linq.book Page 176 Mercredi, 18. février 2009 7:58 07

176

LINQ to Objects

Partie II

Listing 5.62 : Exemple d’appel du second prototype. IEnumerable options = EmployeeOptionEntry.GetEmployeeOptionEntries(); Console.WriteLine("Identifiants et options des employés :"); foreach (EmployeeOptionEntry eo in options) Console.WriteLine("Identifiant employé : {0}, Options: {1}", eo.id, ➥eo.optionsCount); // Calcul de la moyenne des options double optionAverage = options.Average(o => o.optionsCount); Console.WriteLine("La moyenne des options des employés est : {0}", optionAverage);

Dans un premier temps, l’objet options est défini et initialisé avec la méthode GetOptionEntries(). Les identifiants et options des employés sont ensuite affichés à l’aide d’une boucle foreach. Enfin, la moyenne des options est calculée avec le second prototype de l’opérateur Average, en ne travaillant que sur le membre optionsCount des éléments. Voici les résultats : Identifiants et options des employés : Identifiant employé : 1, Options : 2 Identifiant employé : 2, Options : 10000 Identifiant employé : 2, Options : 10000 Identifiant employé : 3, Options : 5000 Identifiant employé : 2, Options : 10000 Identifiant employé : 3, Options : 7500 Identifiant employé : 3, Options : 7500 Identifiant employé : 4, Options : 1500 Identifiant employé : 101, Options : 2 La moyenne des options des employés est : 5722.66666666667

Opérateur Aggregate L’opérateur Aggregate exécute une fonction spécifiée par l’utilisateur sur chacun des éléments de la séquence d’entrée. Il passe la valeur retournée par la fonction au rang précédent et retourne la valeur calculée pour le dernier élément.

Prototypes Deux prototypes de l’opérateur Average seront étudiés dans ce livre. Premier prototype public static T Aggregate( this IEnumerable source, Func func);

Dans cette version du prototype, l’opérateur Aggregate énumère les éléments de la séquence d’entrée source. Le délégué func est appelé sur chaque élément. Deux arguments lui sont passés : la valeur retournée par la fonction à l’élément précédent et l’élément lui-même. La valeur retournée par func est mémorisée dans une mémoire interne, afin d’être passée au prochain élément. C’est le premier élément qui est passé lors de la première invocation de la méthode func. Le second prototype est identique au premier mais, ici, la valeur à passer lors de la première invocation de la méthode func est spécifiée. Openmirrors.com

Linq.book Page 177 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

177

Second prototype public static U Aggregate( this IEnumerable source, U seed, Func func);

Exceptions L’exception ArgumentNullExpression est levée si un des arguments a pour valeur null. L’exception InvalidOperationException est levée dans le premier prototype si la séquence d’entrée est vide. Exemples Le Listing 5.63 illustre le premier prototype. Ici, nous calculons la valeur 5! (factorielle 5). Ce résultat est obtenu en multipliant entre eux tous les entiers positifs inférieurs ou égaux à 5. La valeur 5! est donc égale à 1 × 2 × 3 × 4 × 5. Listing 5.63 : Exemple d’appel du premier prototype. int N = 5; IEnumerable intSequence = Enumerable.Range(1, N); // Liste des éléments de la séquence foreach (int item in intSequence) Console.WriteLine(item); // Calcul et affichage de la factorielle // av == valeur de l’agrégat, e == élément int agg = intSequence.Aggregate((av, e) => av * e); Console.WriteLine("{0}! = {1}", N, agg);

Ce code génère une séquence d’entiers compris entre 1 et 5 en utilisant l’opérateur Range. Après avoir affiché ces éléments, l’opérateur Aggregate est appelé, en lui fournissant une expression lambda qui multiplie l’agrégat par l’élément. Voici les résultats : 1 2 3 4 5 5! = 120

ATTENTION Lorsque vous utilisez le premier prototype de l’opérateur Aggregate, vous devez faire attention à ce que le premier élément ne soit pas traité à deux reprises par la méthode func. Dans l’exemple précédent, les paramètres 1 et 1 sont transmis en entrée de la méthode func. Cela n’affecte en rien le résultat final, puisque les valeurs sont multipliées entre elles. Le résultat aurait en revanche été faussé si les valeurs avaient été additionnées.

Linq.book Page 178 Mercredi, 18. février 2009 7:58 07

178

LINQ to Objects

Partie II

Pour illustrer le second prototype, nous allons utiliser un opérateur Sum "fait maison" (voir Listing 5.64). Listing 5.64 : Exemple d’appel du second prototype. IEnumerable intSequence = Enumerable.Range(1, 10); // Affichage des éléments de la séquence foreach (int item in intSequence) Console.WriteLine(item); Console.WriteLine("--"); // Calcul et affichage de la somme int sum = intSequence.Aggregate(0, (s, i) => s + i); Console.WriteLine(sum);

La valeur "0" a été définie comme premier argument de l’opérateur Aggregate afin que le premier appel de la méthode func n’altère pas le résultat final. Voici les résultats affichés dans la console : 1 2 3 4 5 6 7 8 9 10 -55

Comme vous pouvez le voir, le résultat est le même que celui obtenu pour illustrer l’opérateur Sum, dans le Listing 5.51.

Résumé Ce chapitre et le précédent vous semblent peut-être quelque peu indigestes. Ils contiennent cependant les bases de LINQ. J’espère avoir couvert tous les opérateurs qui vous seront utiles. Pour que LINQ révèle toute sa puissance, vous devez bien comprendre ces opérateurs et savoir comment les utiliser. Il n’est pas nécessaire de retenir le détail de chaque opérateur. Sachez juste qu’ils existent et quels services ils peuvent vous rendre. En se fondant sur ce qui a été vu jusqu’ici à propos de LINQ to Objects et des opérateurs de requête standard, vous avez pu voir à quel point LINQ s’est révélé puissant et pratique pour interroger des données de tout type stockées dans des collections en mémoire. En utilisant les quelque 50 opérateurs de LINQ to Objects, vos requêtes seront plus cohérentes, plus fiables et plus rapides à écrire. Openmirrors.com

Linq.book Page 179 Mercredi, 18. février 2009 7:58 07

Chapitre 5

Les opérateurs non différés

179

Je n’insisterai jamais assez sur le fait que la plupart des opérateurs de requête standard travaillent sur des collections qui implémentent l’interface IEnumerable. Aucune des collections C# héritées (celles de l’espace de noms System.Collection) n’implémentent cette interface ; elles sont donc exclues. Je sais pourtant que certains lecteurs essayeront (sans succès !) d’appliquer des requêtes LINQ à des ArrayList provenant de code hérité. Si vous vous trouvez dans une telle situation, jetez un œil aux opérateurs Cast et OfType. Au chapitre suivant, nous allons nous intéresser à la génération et à l’interrogation de séquences XML. Cette partie de LINQ a pour nom "LINQ to XML".

Linq.book Page 180 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 181 Mercredi, 18. février 2009 7:58 07

III LINQ to XML

Linq.book Page 182 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 183 Mercredi, 18. février 2009 7:58 07

6 Introduction à LINQ to XML Ce chapitre aborde la facette LINQ to XML du langage LINQ. En préambule, le Listing 6.1 montre comment créer une hiérarchie XML en utilisant l’API Microsoft DOM (Document Object Model) W3C DOM XML. Il n’est pas nécessaire d’aller bien loin dans le code pour se rendre compte à quel point le processus est douloureux ! Listing 6.1 : Un exemple XML basique. using System.Xml; // Déclaration de variables XmlElement xmlBookParticipant; XmlAttribute xmlParticipantType; XmlElement xmlFirstName; XmlElement xmlLastName; // Instanciation d’un objet XmlDocument XmlDocument xmlDoc = new XmlDocument(); // Création de l’élément parent et ajout au document XmlElement xmlBookParticipants = xmlDoc.CreateElement("BookParticipants"); xmlDoc.AppendChild(xmlBookParticipants); // Création d’un participant et ajout à la liste des participants xmlBookParticipant = xmlDoc.CreateElement("BookParticipant"); xmlParticipantType = xmlDoc.CreateAttribute("type"); xmlParticipantType.InnerText = "Author"; xmlBookParticipant.Attributes.Append(xmlParticipantType); xmlFirstName = xmlDoc.CreateElement("FirstName"); xmlFirstName.InnerText = "Joe"; xmlBookParticipant.AppendChild(xmlFirstName); xmlLastName = xmlDoc.CreateElement("LastName"); xmlLastName.InnerText = "Rattz"; xmlBookParticipant.AppendChild(xmlLastName); xmlBookParticipants.AppendChild(xmlBookParticipant); // Création d’un participant autre et ajout à la liste des participants xmlBookParticipant = xmlDoc.CreateElement("BookParticipant"); xmlParticipantType = xmlDoc.CreateAttribute("type"); xmlParticipantType.InnerText = "Editor";

Linq.book Page 184 Mercredi, 18. février 2009 7:58 07

184

LINQ to XML

Partie III

xmlBookParticipant.Attributes.Append(xmlParticipantType); xmlFirstName = xmlDoc.CreateElement("FirstName"); xmlFirstName.InnerText = "Ewan"; xmlBookParticipant.AppendChild(xmlFirstName); xmlLastName = xmlDoc.CreateElement("LastName"); xmlLastName.InnerText = "Buckingham"; xmlBookParticipant.AppendChild(xmlLastName); xmlBookParticipants.AppendChild(xmlBookParticipant); // Recherche des auteurs et affichage de leur nom XmlNodeList authorsList = xmlDoc.SelectNodes("BookParticipants/BookParticipant[@type=\"Author\"]"); foreach (XmlNode node in authorsList) { XmlNode firstName = node.SelectSingleNode("FirstName"); XmlNode lastName = node.SelectSingleNode("LastName"); Console.WriteLine("{0} {1}", firstName, lastName); }

Ce code construit la hiérarchie XML et affiche le nom de chaque participant. La structure XML désirée

Joe Rattz

Ewan Buckingham

L’écriture, la compréhension et la maintenance de ce code sont un vrai cauchemar ! Par ailleurs, il ne suffit pas d’observer son contenu pour en déduire la structure XML générée. Si la méthode DOM est si lourde, c’est en partie parce qu’il n’est pas possible de créer un élément, de l’initialiser et de l’attacher à la hiérarchie en utilisant une seule et même instruction. Au lieu de cela, trois étapes sont nécessaires : chaque élément doit être créé, son membre InnerText, initialisé à la valeur souhaitée puis l’élément ajouté à un nœud de l’arborescence. Cette technique génère beaucoup de code. Sans compter qu’il faut également créer un document XML : sans lui, impossible de créer un simple élément ! Observez le listing et son résultat. Ne trouvez-vous pas la quantité de code disproportionnée ? Appuyez sur Ctrl+F5 pour exécuter ce programme. Voici le résultat affiché dans la console : System.Xml.XmlElement System.Xml.XmlElement

Les noms et prénoms des participants n’apparaissent pas. Nous allons tenter de modifier la ligne Console.WriteLine pour obtenir les données souhaitées : Console.WriteLine("{0} {1}", firstName.ToString(), lastName.ToString());

Openmirrors.com

Linq.book Page 185 Mercredi, 18. février 2009 7:58 07

Chapitre 6

Introduction à LINQ to XML

185

Un nouveau Ctrl+F5 produit… le même résultat : System.Xml.XmlElement System.Xml.XmlElement

La tentative a échoué !

Introduction Microsoft aurait pu se contenter de fournir une API LINQ de requêtage XML. Heureusement, les développeurs sont allés beaucoup plus loin. Après plusieurs années d’utilisation de l’API W3C DOM XML, il est apparu clairement qu’une amélioration s’imposait. Plutôt qu’utiliser l’artillerie DOM, n’avez-vous jamais créé directement des éléments XML en passant par des chaînes ? Ou été tenté de le faire ? Plusieurs déficiences de l’API W3C DOM XML ont été examinées et un nouveau modèle d’objet a été créé. Il en a résulté une méthode bien plus simple et élégante pour créer des arbres XML : la "construction fonctionnelle". Et, croyez-moi, cette technique vaut son pesant d’or ! Bien entendu, la nouvelle API se devait de supporter les requêtes LINQ, sans quoi elle n’aurait pas pu faire partie du langage LINQ. Par l’intermédiaire de méthodes d’extension, des opérateurs de requêtes spécifiques XML ont ainsi été implémentés. En combinant ces opérateurs et les opérateurs de requête standard de LINQ to Objects (voir Chapitre 2), vous aurez à votre disposition tout ce qu’il faut pour manipuler des données XML de façon élégante et efficace.

Se passer de l’API W3C DOM XML Nous allons raisonner sur un cas pratique : le projet sur lequel j’ai personnellement travaillé dans la division IT d’une grande entreprise. J’ai dû mettre au point une classe permettant de pister toutes les actions des utilisateurs dans une application ASP.NET. C’est ainsi qu’est née la classe logging. Cette classe avait deux buts : identifier tout utilisateur qui abuserait du système et être prévenu par e-mail si une exception avait été levée. Ce second point se justifiait par le fait que les utilisateurs qui avaient provoqué une exception n’étaient jamais en mesure de m’indiquer clairement dans quelles conditions s’était produit l’incident. Je voulais donc un procédé capable de traquer les moindres mouvements des utilisateurs côté serveur. Toutes les actions entreprises par l’utilisateur (une demande de facture ou la soumission d’une commande, par exemple) devaient être considérées comme un événement. Chaque événement était mémorisé dans les champs d’une base de données : références de l’utilisateur, date, heure, type de l’événement, etc. Malheureusement, ces informations ne me permettaient pas de connaître le détail de chaque

Linq.book Page 186 Mercredi, 18. février 2009 7:58 07

186

LINQ to XML

Partie III

action. Par exemple, pour une commande, j’aurais voulu connaître son numéro et les différents articles commandés. En fait, j’avais besoin de toutes les informations qui me permettraient de réitérer la situation qui avait déclenché une exception. Chaque événement manipulait des données différentes, mais je ne voulais pas que ces données soient stockées dans des tableaux différents. La solution XML s’imposait d’elle-même. Par exemple, pour une demande de facture, les données XML pouvaient avoir l’allure suivante : 10/2/2006 10/9/2006 False

Et, pour une commande : 4754611903 12 Atlanta USPS First Class

Étant donné que les données étaient liées au type des événements, il était impossible de les valider. L’utilisation de l’API XML DOM était donc avantageuse. Ce gestionnaire d’événements est devenu un outil très utile. Il a permis d’identifier et de résoudre plus facilement les bugs. Il est assez amusant d’appeler un client et de l’informer que l’erreur survenue sur la commande 32728 qu’il a passée la veille est désormais réparée. Le trouble qui résulte lorsque le client prend conscience qu’il est possible de connaître le détail de ses actions est une vraie récompense en soi. Si vous connaissez déjà le XML, vous avez certainement remarqué que ces données n’ont aucun nœud parent. Cela constitue un problème si vous utilisez l’API W3C DOM. Mais, dans mon cas, j’ai utilisé l’API String.Format XML, qui vous est peut-être également familière. Voici le code utilisé : string xmlData = string.Format( "{0}{1}{2}", Date.ToShortDateString(), endDate.ToShortDateString(), includePaid.ToString());

Je sais que ce n’est pas la meilleure des façons de définir des données XML et qu’il est facile de se tromper dans son écriture. Pour faciliter la saisie, j’ai donc créé une méthode à laquelle je passe en paramètres une liste d’éléments et les données correspondantes : string xmlData = XMLHelper( "StartDate", startDate.ToShortDateString(), "EndDate", endDate.ToShortDateString(), "IncPaid", includePaid.ToString());

La méthode XMLHelper crée également un nœud parent. Les améliorations ne sont pas flagrantes. Comme vous pouvez le voir, je n’ai rien fait pour encoder mes données dans cet appel. Openmirrors.com

Linq.book Page 187 Mercredi, 18. février 2009 7:58 07

Chapitre 6

Introduction à LINQ to XML

187

Bien que l’utilisation de la méthode String.Format (ou une autre technique externe à l’API XML DOM) ne soit pas une très bonne alternative, DOM se révèle trop complexe lorsqu’il s’agit de manipuler quelques lignes de XML. Si vous pensez que mon approche est un peu trop personnelle, sachez que, récemment, lors d’un séminaire Microsoft, l’intervenant a présenté un code qui construisait une structure XML… en concaténant plusieurs chaînes !

Résumé La plupart des développeurs associent LINQ au requêtage de données, et en particulier de données provenant de bases de données. En tournant les pages de cet ouvrage, vous verrez que LINQ to XML apporte également une vraie réponse quant à la manipulation et à l’interrogation de données XML. Dans ce chapitre, je vous ai montré à quel point il était douloureux de manipuler du XML via l’API W3C DOM XML. Vous avez également vu qu’il était possible de se passer de cette API. Au chapitre suivant, nous nous intéresserons à l’API LINQ to XML. Par son intermédiaire, vous apprendrez à créer des hiérarchies XML en quelques lignes. À titre indicatif, si la hiérarchie créée dans le Listing 6.1 demandait 29 lignes de code, elle sera réduite à 10 lignes seulement en passant par LINQ to XML. Après avoir lu les deux prochains chapitres, vous serez certainement convaincu de l’avancée révolutionnaire de LINQ, tant au niveau de la manipulation du XML qu’à celui de l’interrogation des bases de données.

Linq.book Page 188 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 189 Mercredi, 18. février 2009 7:58 07

7 L’API LINQ to XML Au chapitre précédent, vous avez vu à quel point il était difficile de créer un document XML en utilisant l’API W3C DOM XML. Vous avez également appris à vous passer de cette API pour alléger le code. En outre, vous avez pu constater que LINQ sait faire autre chose qu’interroger des collections : il peut également manipuler des hiérarchies XML, à travers l’API LINQ to XML. Dans ce chapitre, vous allez découvrir comment utiliser LINQ to XML pour créer, parcourir, manipuler et interroger des documents XML, et effectuer des recherches dans des objets XML. Pour illustrer ce chapitre, nous utiliserons une application console. Afin de pouvoir tirer parti de LINQ to XML, vous devez y ajouter une référence vers l’assembly System.Xml.Linq, si celle-ci n’est pas déjà présente.

Espaces de noms référencés Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq, System.Xml.Linq et System.Collections.Generic. Si elles ne sont pas déjà présentes, vous devez donc ajouter les directives using suivantes dans votre code : using System.Linq; using System.Xml.Linq; using System.Collections.Generic;

Si vous parcourez le code source (www.pearson.fr), vous verrez qu’une directive using a également été ajoutée sur l’espace de noms System.Diagnostic. Cette directive n’est pas nécessaire si vous saisissez directement les exemples de ce chapitre. Elle n’est là que pour les besoins propres au code source.

Linq.book Page 190 Mercredi, 18. février 2009 7:58 07

190

LINQ to XML

Partie III

Améliorations de l’API Après avoir expérimenté l’API Microsoft W3C XML DOM pendant plusieurs années, des points négatifs et des faiblesses se sont peu à peu dessinés. Pour y remédier, les points suivants ont été examinés par les équipes de développement de Microsoft : m

construction d’arbres XML ;

m

solutions "centrées-document" ;

m

espaces de noms et préfixes ;

m

extraction de valeurs de nœuds.

Non contents de grossir et parfois d’obscurcir le code, ces points sont une véritable gêne lorsque l’on travaille avec des données XML. Il était donc important de les examiner de près afin que LINQ to XML fonctionne d’une manière irréprochable. Un exemple : supposons que vous vouliez utiliser une projection, afin qu’une requête LINQ retourne du code XML. L’API XML existante ne permettant pas d’instancier un nouvel élément avec une déclaration new, il fallait corriger cette limitation pour que LINQ to XML manipule des données XML aussi simplement que possible. Dans les pages suivantes, nous allons passer en revue chacune de ces problématiques et voir comment LINQ to XML les solutionne. La construction fonctionnelle simplifie la création d’arbres XML Si vous vous reportez au Listing 6.1, au chapitre précédent, vous verrez qu’il est très difficile d’en tirer un schéma XML. Vous constaterez également que le code est très "verbeux". Après avoir instancié un nouveau document XML, plusieurs nœuds doivent être définis. À titre d’exemple, pour ajouter un élément il est nécessaire de le définir, de l’initialiser et de le lier avec un élément parent. Ces étapes doivent être répétées autant de fois que nécessaire pour définir toute la structure XML. Un tel procédé rend difficilement perceptible le schéma XML et fait exagérément grossir le code. Cette API n’est malheureusement pas capable de créer un élément (ou un autre type de nœud) en le positionnant dans l’arbre XML et de l’initialiser par la même occasion. Cette technique est toujours utilisable dans l’API LINQ to XML, mais une autre, bien plus efficace, connue sous le nom de "construction fonctionnelle", a fait son apparition. Cette technique permet de définir le schéma XML pendant les phases de construction et d’initialisation des objets XML. Pour ce faire, la nouvelle API fournit des constructeurs d’objets XML qui acceptent un ou plusieurs objets, accompagnés de leurs valeurs. Le type de l’objet ou des objets étant spécifié, il détermine leur point d’appartenance. Voici le modèle général : XMLOBJECT o = new XMLOBJECT(OBJECTNAME, XMLOBJECT1, XMLOBJECT2, ... XMLOBJECTN);

Openmirrors.com

Linq.book Page 191 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

191

INFO Le code précédent n’a qu’un objet purement démonstratif. Aucune des classes référencées dans les arguments n’existe réellement. Elles ne sont là que pour matérialiser des classes XML purement abstraites.

Si vous utilisez la classe LINQ to XML XAttribute pour ajouter un attribut XML à un élément de type XElement, l’attribut devient un attribut de l’élément. Par exemple, dans le code précédent, si l’attribut XMLOBJECT1 est ajouté à l’élément XMLOBJECT o, si o est un XElement et XMLOBJECT1, un XAttribute, XMLOBJECT1 devient un attribut du XElement o. Si vous ajoutez un XElement à un XElement, l’élément ajouté devient un enfant de l’élément auquel il est ajouté. Par exemple, si XMLOBJECT1 et o sont deux éléments, XMLOBJECT1 devient l’enfant de l’élément o. Lorsqu’un XMLOBJECT est instancié, son contenu peut être défini par un ou plusieurs XMLOBJECT. Comme vous le verrez un peu plus loin, dans la section "Création de textes avec XText", il est également possible de spécifier son contenu en ajoutant une chaîne. Cette dernière sera automatiquement convertie en un XMLOBJECT. Le Listing 7.1 donne un exemple de création d’un schéma XML. Listing 7.1 : Utilisation de la construction fonctionnelle pour définir un schéma XML. XElement xBookParticipant = new XElement("BookParticipant", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")); Console.WriteLine(xBookParticipant.ToString());

Deux objets XElement ont été passés lors de la construction de l’élément BookParticipant. Chacun d’eux est donc un enfant de BookParticipant. Notez également que, lors de la construction des XElement FirstName et LastName, une valeur texte (et non deux objets enfants) a été passée. Voici les résultats de ce code :

Joe Rattz

Le schéma XML apparaît clairement dans le code. Remarquez également à quel point le code est concis. Le Listing 7.2 représente le code LINQ to XML équivalent au Listing 6.1. Listing 7.2 : Définition de l’arbre du Listing 6.1, avec un code bien moins important. XElement xBookParticipants = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"),

Linq.book Page 192 Mercredi, 18. février 2009 7:58 07

192

LINQ to XML

Partie III

new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))); Console.WriteLine(xBookParticipants.ToString());

Ce code est bien plus concis et facile à maintenir que celui du Listing 6.1. Par ailleurs, la structure des données peut être facilement devinée par simple lecture du code. Voici le résultat :

Joe Rattz

Ewan Buckingham

La nouvelle API a également un autre avantage : les données créées sont formatées comme un arbre XML traditionnel. Il en va tout autrement de l’arbre créé par le code du Listing 6.1 : Joe…

Au chapitre suivant, quand nous nous intéresserons aux requêtes LINQ qui produisent des sorties XML, vous verrez à quel point la construction fonctionnelle est importante. L’élément, point central d’un objet XML Avec l’API W3C DOM, il était impossible de définir un élément XML XmlElement sans le rattacher à un document XML XmlDocument. Si vous essayez d’instancier un XmlElement avec cette instruction : XmlElement xmlBookParticipant = new XmlElement("BookParticipant");

vous obtenez l’erreur de compilation ci-après : ’System.Xml.XmlElement’ ne contient pas un constructeur qui accepte des arguments ’1’

Avec l’API W3C DOM, la seule façon de créer un XmlElement consiste à appeler la méthode CreateElement d’un objet XmlDocument : XmlDocument xmlDoc = new XmlDocument(); XmlElement xmlBookParticipant = xmlDoc.CreateElement("BookParticipant");

Ce code fonctionne à la perfection, mais il n’est pas toujours pratique de devoir créer un document XML avant de pouvoir définir un élément XML. La nouvelle API LINQ to XML permet d’instancier un élément sans le rattacher nécessairement à un document XML. Openmirrors.com

Linq.book Page 193 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

193

XElement xeBookParticipant = new XElement("BookParticipant");

Les éléments XML ne sont pas les seuls nœuds affectés par cette restriction de l’API X3C DOM. Les attributs, les commentaires, les sections CData, les instructions de calcul et les références d’entités doivent tous être rattachés à un document XML. Avec LINQ to XML, tous ces objets pourront être instanciés à la volée, sans qu’un document XML n’ait été défini au préalable. Bien entendu, rien ne vous empêche de créer un document XML avec la nouvelle API. À titre d’exemple, le Listing 7.3 crée un document XML, y ajoute l’élément BookParticipants et insère un élément BookParticipant dans ce dernier. Listing 7.3 : Création d’un document XML et de sa structure avec l’API LINQ to XML. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(xDocument.ToString());

Voici le résultat affiché dans la console suite à l’appui sur Ctrl+F5 :

Joe Rattz

Le code XML issu du Listing 7.3 est très proche de celui en sortie du Listing 6.1, à ceci près qu’un seul participant a été ajouté au document. La construction fonctionnelle le rend cependant bien plus lisible, et il suffit d’observer le code pour en déduire le schéma correspondant. Étant donné qu’il n’est plus nécessaire de définir un document XML, il est encore possible de simplifier le code (voir Listing 7.4). Listing 7.4 : Le même exemple que le précédent, mais sans la définition du document XML. XElement xElement = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))); Console.WriteLine(xElement.ToString());

L’exécution de ce code produit le même résultat que précédemment :

Linq.book Page 194 Mercredi, 18. février 2009 7:58 07

194

LINQ to XML

Partie III

Joe Rattz

Là ne s’arrêtent pas les prouesses de LINQ to XML. Par son intermédiaire, vous pouvez également lire et écrire des données XML dans un fichier. Noms, espaces de noms et préfixes Les termes "noms", "espaces de noms" et "préfixes d’espaces de noms" sont souvent abscons, sinon difficiles à appréhender pour le programmeur XML. Pour éviter toute confusion, sachez que les préfixes des espaces de noms sont gérés à l’extérieur de l’API. Ils ne font que s’ajouter aux espaces de noms et n’ont aucune existence à l’intérieur de l’API. Les espaces de noms sont utilisés pour identifier de manière unique le schéma XML d’une portion d’arbre XML. Une URI peut donc être utilisée pour chaque espace de noms, puisqu’il est unique au sein d’une société. Dans plusieurs exemples, nous utiliserons l’arbre XML suivant :

Joe Rattz

Les codes écrits pour traiter ces données XML s’attendront à ce que le nœud BookParticipants contienne plusieurs nœuds BookParticipant, chacun d’entre eux ayant un attribut type et des nœuds FirstName et LastName. Que se passerait-il si ce code devait également traiter des données XML issues d’une autre source, contenant un nœud BookParticipants, mais dont le schéma diffère du précédent ? Eh bien, un espace de noms informerait le code sur la structure du schéma, et le traitement serait alors approprié. Dans XML, chaque élément a besoin d’un nom. Lorsqu’un élément est créé, si son nom est spécifié dans le constructeur, son type string est implicitement converti en un objet XName. Ce dernier consiste en un espace de noms XNameSpace, l’objet et son nom local (c’est-à-dire le nom que vous avez choisi). À titre d’exemple, l’élément BookParticipants pourrait être créé comme suit : XElement xBookParticipants = new XElement("BookParticipants");

Lorsque l’élément est créé, un objet XName contenant un espace de noms non référencé et le nom local BookParticipants est défini. Si vous utilisez le débogueur sur cette ligne de code et que vous examiniez la variable xBookParticipants dans la fenêtre Espion Express, vous verrez que son membre Name est initialisé à {BookParticipants}. Développez le membre Name. Vous verrez qu’il contient le membre LocalName initialisé à BookParticipants, et un membre NameSpace vide. Ici, l’espace de noms n’a pas été défini. Openmirrors.com

Linq.book Page 195 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

195

Pour spécifier un espace de noms, il vous suffit de créer un objet XNameSpace et de l’utiliser comme préfixe du nom local choisi : XNamespace nameSpace = "http://www.linqdev.com"; XElement xBookParticipants = new XElement(nameSpace + "BookParticipants");

Maintenant, lorsque vous visualisez l’élément xBookParticipants dans la fenêtre Espion Express du débogueur, le nom a pour valeur {{http://www.linqdev.com}BookParticipants}. Développez le membre Name. Le membre LocalName a toujours pour valeur BookParticipants mais, maintenant, le membre Namespace est initialisé à {http://www.linqdev.com}. Il n’est pas obligatoire d’utiliser un objet NameSpace pour spécifier l’espace de noms. Vous auriez tout aussi bien pu le spécifier dans l’implémentation du XElement : XElement xBookParticipants = new XElement("{http://www.linqdev.com}" +"BookParticipants");

Des accolades entourent l’espace de noms, afin d’indiquer au constructeur XElement qu’il s’agit d’un espace de noms et pas du nom de l’élément. Si vous examinez à nouveau le membre Name dans la fenêtre Espion Express du débogueur, vous verrez que le membre Name et ses enfants LocalName et NameSpace sont tous initialisés comme auparavant, lorsque l’élément avait été créé avec un objet XNamespace. Ayez bien en tête qu’il ne suffit pas de définir l’URI de votre société ou de votre domaine pour garantir l’unicité d’un espace de noms. Cela garantit simplement que vous n’entrerez pas en conflit avec d’autres sociétés qui utilisent également les règles inhérentes aux espaces de noms. Notez cependant qu’à l’intérieur de votre société des conflits entre départements pourraient se produire si la seule URI constitue l’espace de noms. C’est à ce point précis que vous devrez faire intervenir votre connaissance des divisions, départements et autres sous-structures de votre société. L’idéal serait que l’espace de noms s’étende sur tous les niveaux dont vous avez le contrôle. Supposons par exemple que vous travailliez chez LINQDev.com et que vous deviez créer un schéma relatif aux retraites pour le département des ressources humaines. L’espace de noms pourrait être le suivant : XNamespace nameSpace = "http://www.linqdev.com/ressourceshumaimes/retraites";

Pour terminer cette discussion sur le fonctionnement des espaces de noms, nous allons modifier le Listing 7.2 en y incluant un espace de noms (voir Listing 7.5). Listing 7.5 : Une version modifiée du Listing 7.2 incluant un espace de noms. XNamespace nameSpace = "http://www.linqdev.com"; XElement xBookParticipants = new XElement(nameSpace + "BookParticipants", new XElement(nameSpace + "BookParticipant", new XAttribute("type", "Author"), new XElement(nameSpace + "FirstName", "Joe"), new XElement(nameSpace + "LastName", "Rattz")), new XElement(nameSpace + "BookParticipant", new XAttribute("type", "Editor"),

Linq.book Page 196 Mercredi, 18. février 2009 7:58 07

196

LINQ to XML

Partie III

new XElement(nameSpace + "FirstName", "Ewan"), new XElement(nameSpace + "LastName", "Buckingham"))); Console.WriteLine(xBookParticipants.ToString());

Appuyez sur Ctrl+F5 pour exécuter ce code. Voici les résultats affichés dans la console :

Joe Rattz

Ewan Buckingham

Si un programme lit ce schéma, il saura qu’il a été émis par LINQDev.com. Pour isoler le préfixe de l’espace de noms, vous utiliserez l’objet XAttribute, comme dans le Listing 7.6. Listing 7.6 : Définition d’un préfixe dans un espace de noms. XNamespace nameSpace = "http://www.linqdev.com"; XElement xBookParticipants = new XElement(nameSpace + "BookParticipants", new XAttribute(XNamespace.Xmlns + "linqdev", nameSpace), new XElement(nameSpace + "BookParticipant")); Console.WriteLine(xBookParticipants.ToString());

Le préfixe utilisé dans ce code est "linqdev". Un objet XAttribute est utilisé pour inclure ce préfixe dans le schéma. Voici les résultats affichés dans la console :



Extraction de valeurs de nœuds Si vous avez parcouru le chapitre précédent, vous avez certainement été étonné par les résultats du Listing 6.1. L’obtention des valeurs issues d’un nœud est un vrai cassetête ! N’ayant pas travaillé sur du code XML DOM depuis un moment, j’ai inévitablement été confronté à une erreur, en oubliant qu’une étape supplémentaire était nécessaire pour extraire les données. L’API LINQ to XML solutionne ce problème d’une manière élégante. Tout d’abord, l’appel de la méthode ToString sur un élément produit la chaîne XML elle-même, et non le type de l’objet, comme le fait l’API W3C DOM. Ceci est très pratique lorsque vous voulez obtenir une portion de XML à partir d’un certain point dans l’arbre, et elle a bien plus de sens que de fournir le type de l’objet. Openmirrors.com

Linq.book Page 197 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

197

Listing 7.7 : La méthode ToString appliquée à un élément produit l’arbre XML correspondant. XElement name = new XElement("Name", "Joe"); Console.WriteLine(name.ToString());

Voici le résultat obtenu par un appui sur Ctrl+F5 : Joe

Quel changement ! Attendez un peu, la suite est encore plus étonnante. Bien entendu, les nœuds enfants sont inclus dans la sortie mais, étant donné qu’aucune surcharge de la méthode WriteLine n’a été définie pour traiter les XElement, la méthode ToString est automatiquement appelée, comme dans le Listing 7.8. Listing 7.8 : Appel implicite de la méthode ToString dans un Console.WriteLine pour obtenir l’arbre XML. XElement name = new XElement("Person", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")); Console.WriteLine(name);

Voici les résultats affichés dans la console :

Joe Rattz

Encore plus important : si vous utilisez un opérateur de casting sur un nœud pour le convertir dans un type compatible avec son contenu, vous obtenez la valeur du nœud. Le Listing 7.9 donne un exemple dans lequel la valeur du nœud Name est convertie en une chaîne de caractères. Listing 7.9 : Le casting d’un élément produit la donnée qui y est stockée. XElement name = new XElement("Name", "Joe"); Console.WriteLine(name); Console.WriteLine((string)name);

Voici les résultats de ce code : Joe Joe

String n’est pas le seul opérateur de casting. Les opérateurs suivants sont également à votre disposition : int, int?, uint, uint?, long, long?, ulong, ulong?, bool, bool?, float, float?, double, double?, decimal, decimal?, TimeSpan, TimeSpan?, DateTime, DateTime?, GUID et GUID?.

Linq.book Page 198 Mercredi, 18. février 2009 7:58 07

198

LINQ to XML

Partie III

Le Listing 7.10 donne un exemple des valeurs stockées dans plusieurs nœuds. Listing 7.10 : Valeurs stockées dans différents nœuds et récupérées par casting. XElement count = new XElement("Count", 12); Console.WriteLine(count); Console.WriteLine((int)count); XElement smoker = new XElement("Smoker", false); Console.WriteLine(smoker); Console.WriteLine((bool)smoker); XElement pi = new XElement("Pi", 3.1415926535); Console.WriteLine(pi); Console.WriteLine((double)pi);

Voici les résultats : 12 12 false False 3.1415926535 3.1415926535

Cette approche est simple et intuitive. En utilisant l’API LINQ to XML, les difficultés rencontrées dans le Listing 6.1 feront à tout jamais partie du passé ! Dans les exemples étudiés jusqu’ici, les éléments ont été convertis dans leurs types d’origine. Ceci n’est pas une obligation : il suffit que la conversion soit possible. Le Listing 7.11 donne un exemple de casting d’une chaîne de caractères en booléen. Listing 7.11 : Casting d’un nœud en utilisant un type différent du type d’origine. XElement smoker = new XElement("Smoker", "true"); Console.WriteLine(smoker); Console.WriteLine((bool)smoker);

Étant donné que l’élément a pour valeur la chaîne "true" et que cette chaîne peut être convertie en une valeur booléenne, le code s’exécute sans encombre. Voici les résultats : true True

Ce code ne laisse pas apparaître le nom de la méthode utilisée pour effectuer le casting. Le Listing 7.12 va vous montrer qu’il s’agit de la méthode System.Xml.XmlConvert. Listing 7.12 : Le casting booléen utilise la classe System.Xml.XmlConvert. try { XElement smoker = new XElement("Smoker", "Tue"); Console.WriteLine(smoker); Console.WriteLine((bool)smoker); }

Openmirrors.com

Linq.book Page 199 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

199

catch (Exception ex) { Console.WriteLine(ex); }

La valeur affectée à l’élément Smoker a été intentionnellement mal orthographiée afin d’obtenir le nom de la méthode utilisée pour effectuer le casting. Un appui sur Ctrl+F5 affiche les informations suivantes dans la console : Tue System.FormatException: The string ’tue’ is not a valid Boolean value. at System.Xml.XmlConvert.ToBoolean(String s) ...

Comme vous pouvez le voir, le casting a provoqué une exception lors de l’appel à la méthode System.Xml.XmlConvert.ToBoolean.

Le modèle d’objet LINQ to XML L’API LINQ to XML vient avec un nouveau modèle d’objet contenant plusieurs classes issues de l’espace de noms System.Xml.Linq. L’une d’entre elles est la classe statique qui héberge les méthodes d’extension (Extensions). Deux autres sont dédiées aux comparateurs (XNodeDocumentOrderComparer et XNodeEqualityComparer). Les autres classes sont utilisées pour construire les arbres XML (voir Figure 7.1). Le modèle d’objet LINQ to XML.

XDocument

XComment

XElement

XContainer

XDocumentType

XAttribute

XDeclaration

XName

XNamespace

XCData

XProcessingInstruction

XText

XNode

XObject

XStreamingElement

Derived

Figure 7.1 :

Quelques remarques intéressantes : 1. Les classes XObject, XContainer et XNode sont abstraites. Elles ne peuvent donc pas être construites. 2. Les attributs XAttribute ne sont pas dérivés de nœuds XNode. En fait, il s’agit d’un tout autre type de classe, constitué de paires nom/valeur. 3. Les éléments XStreamingElement n’héritent pas de XElement. 4. Les classes XDocument et XElement sont les seules à avoir des nœuds enfants dérivés de XNode. Vous utiliserez ces classes pour construire vos arbres XML. L’API LINQ to XML étant centrée sur les éléments, la classe XElement vous sera particulièrement utile.

Linq.book Page 200 Mercredi, 18. février 2009 7:58 07

200

LINQ to XML

Partie III

Exécution différée des requêtes, suppression de nœuds et bogue d’Halloween L’exécution de toutes requêtes LINQ est différée. Il peut parfois en découler des effets secondaires indésirables. Le "bogue d’Halloween" doit son nom à la première équipe qui en a débattu ouvertement. Ces spécialistes d’Halloween ont discuté des problèmes qui découlent du changement manuel d’un index dans une boucle. Cette situation a été détectée pour la première fois par des ingénieurs bases de données alors qu’ils mettaient au point un processus d’optimisation. Une de leurs requêtes de test a modifié la valeur d’une cellule utilisée comme index par le processus d’optimisation. Cela a engendré une boucle sans fin dont le processus d’optimisation ne pouvait se dégager. Vous avez peut-être déjà expérimenté ce problème sans connaître son nom. N’avezvous jamais effectué une boucle sur une collection dans laquelle la suppression d’un élément entraînait la fin ou le mauvais comportement de la boucle ? J’ai personnellement rencontré ce problème récemment, alors que je travaillais avec des contrôles serveur ASP.NET. J’ai été amené à supprimer les enregistrements sélectionnés par l’utilisateur dans un contrôle DataGrid. Pour ce faire, j’ai bouclé sur les enregistrements, du premier au dernier, en supprimant ceux qui étaient sélectionnés. Ce faisant, les pointeurs utilisés dans la boucle ont été désorganisés. Résultat : certains enregistrements ont été supprimés par erreur et d’autres qui auraient dû être supprimés ont été ignorés. Le concepteur des contrôles a trouvé une solution qui consistait à parcourir les enregistrements du dernier au premier. Avec LINQ to XML, vous tomberez forcément sur ce type de problème lorsque vous supprimerez des nœuds dans un arbre XML, mais peut-être également dans d’autres situations totalement différentes. Il est donc important d’avoir ce problème à l’esprit lorsque vous vous lancerez dans le codage. Listing 7.13 : Mise en évidence du bogue d’Halloween. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); foreach (XElement element in elements) { Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value);

Openmirrors.com

Linq.book Page 201 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

201

} foreach (XElement element in elements) { Console.WriteLine("Suppressionde l’élément {0} = {1} ...", element.Name, ➥element.Value); element.Remove(); } Console.WriteLine(xDocument);

La première ligne définit le document XML. Le bloc d’instructions suivant initialise ce document avec une séquence d’éléments BookParticipant. La première boucle foreach affiche les deux éléments de la séquence. La boucle suivante énumère à nouveau la séquence et supprime l’élément BookParticipant. Enfin, la dernière instruction affiche le document XML résultant. Si le bogue d’Halloween ne vous saute pas aux yeux, regardez de plus près le message de suppression. Normalement, les deux éléments BookParticipant devraient être supprimés et il devrait en résulter un document XML vide. Et, pourtant, voici le résultat : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Suppression de l’élément BookParticipant = JoeRattz ...

Ewan Buckingham

Sur les deux éléments SourceParticipant, seul le premier, JoeRattz, est effectivement supprimé. Le bogue d’Halloween a eu raison de la seconde énumération ! Dans certains cas, ce problème peut se manifester différemment : l’énumération peut se terminer prématurément ou une exception peut être levée. Vous vous demandez certainement quelle solution peut être apportée à ce problème. Eh bien, dans ce cas précis, la solution consiste à mettre les éléments dans une mémoire tampon et à énumérer cette mémoire plutôt que le document XML, pour lequel les pointeurs internes sont altérés par la suppression. Pour ce faire, nous allons utiliser un opérateur de requête standard spécialement conçu pour mettre des éléments dans une mémoire tampon, afin d’éviter les problèmes liés au côté différé de certaines requêtes. Listing 7.14 : Une solution au bogue d’Halloween. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"),

Linq.book Page 202 Mercredi, 18. février 2009 7:58 07

202

LINQ to XML

Partie III

new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } foreach (XElement element in elements.ToArray()) { Console.WriteLine("Suppression de l’élément {0} = {1} ...", element.Name, ➥element.Value); element.Remove(); } Console.WriteLine(xDocument);

Ce code est proche du précédent mais, ici, la suppression se fait en énumérant les éléments via l’opérateur ToArray. Voici le résultat : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Suppression de l’élément BookParticipant = JoeRattz ... Suppression de l’élément BookParticipant = EwanBuckingham ...

Cette fois-ci, deux messages de suppression sont affichés dans la console. Les deux éléments sont bien supprimés, et le bogue d’Halloween a été éradiqué.

Création XML Comme il a été dit précédemment, la construction fonctionnelle de l’API LINQ to XML facilite grandement la création d’un arbre XML. Cela vous sera confirmé tout au long de cette section, qui passe en revue la création des principales classes XML par l’intermédiaire de cette nouvelle API. Étant donné que les éléments sont le point central de l’API LINQ to XML et que vous travaillerez avec ce type d’objet dans la plupart des cas, nous allons nous intéresser dans un premier temps à la création d’éléments avec la classe XElement. Par la suite, les autres classes XML seront passées en revue par ordre alphabétique. Création d’éléments avec XElement La classe XElement est la plus utilisée dans cette nouvelle API. Nous allons examiner deux des constructeurs de cette classe : XElement.XElement(XName name, object content); XElement.XElement(XName name, params object[] content);

Le premier constructeur est le plus simple. Il correspond au cas où un élément a une valeur texte et aucun nœud enfant (voir Listing 7.15). Openmirrors.com

Linq.book Page 203 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

203

Listing 7.15 : Création d’un élément avec le premier prototype. XElement firstName = new XElement("FirstName", "Joe"); Console.WriteLine((string)firstName);

Le premier argument du constructeur est un objet XName. Comme il a été dit précédemment, cet objet sera créé en convertissant de façon implicite la chaîne passée en entrée en un XName. Le deuxième argument représente la valeur de l’élément ; dans cet exemple une chaîne initialisée à "Joe". L’API convertit automatiquement cette chaîne en un objet XText. La deuxième instruction utilise un opérateur de casting pour obtenir la valeur de l’élément FirstName. Voici le résultat : Joe

Le choix du type des objets est très flexible. C’est le type d’un objet qui contrôle les relations avec l’élément auquel il est ajouté. Le Tableau 7.1 dresse la liste de tous les types de contenus autorisés et indique comment ils sont gérés. Même si bon nombre d’éléments sont stockés sous la forme de chaînes (c’est par exemple le cas des entiers, qui font partie de la catégorie "autres types" du Tableau 7.1), vous pouvez les lire dans leur format d’origine en utilisant les opérateurs de casting appropriés. Par exemple, en appliquant l’opérateur de casting (int) à un élément, vous obtenez la valeur entière de cet élément. Tant que vous utilisez un opérateur de casting licite, le casting est la façon la plus simple d’obtenir la valeur d’un élément, exprimée dans son type d’origine. Le deuxième constructeur XElement est semblable au premier, mais il permet de spécifier un contenu composé de plusieurs objets. Reportez-vous aux Listings 7.1 ou 7.2 pour avoir un exemple du deuxième constructeur. Tableau 7.1 : Comportement de l’insertion d’objets LINQ to XML dans un objet parent

Type de l’objet

Gestion

String

Un objet string ou une chaîne littérale est automatiquement converti en un objet XText et considéré comme tel.

XText

Un tel objet peut avoir une valeur string ou XText. Il est ajouté comme nœud enfant de l’élément, mais considéré comme le contenu texte de l’élément.

XCData

Un tel objet peut avoir une valeur string ou XCData. Il est ajouté comme nœud enfant de l’élément, mais considéré comme le contenu CData de l’élément.

XElement

Cet objet est ajouté en tant qu’élément enfant.

XAttribute

Cet objet est ajouté en tant qu’attribut.

XProcessingInstruction Cet objet est ajouté en tant que contenu enfant.

Linq.book Page 204 Mercredi, 18. février 2009 7:58 07

204

LINQ to XML

Partie III

Tableau 7.1 : Comportement de l’insertion d’objets LINQ to XML dans un objet parent (suite)

Type de l’objet

Gestion

XComment

Cet objet est ajouté en tant que contenu enfant.

IEnumerable

Cet objet est énuméré et la manipulation des types est appliquée de façon récursive.

null

Cet objet est ignoré. Comme vous le verrez par la suite, ce type d’objet peut se révéler utile lors de transformations XML.

Autres types

La méthode ToString est appelée et la valeur résultante est traitée en tant qu’une chaîne de caractères.

Un peu plus tôt dans cette section, nous avons rappelé que la construction fonctionnelle allait être très utile pour définir des requêtes LINQ qui produisent des données XML. Pour illustrer ces propos, nous allons créer l’arbre XML BookParticipants. Plutôt qu’écrire "à la main" les valeurs des éléments, nous allons les récupérer en interrogeant une source de données compatible LINQ. Dans cet exemple, la source de données sera un tableau. Avant de commencer, nous avons besoin d’une classe pour stocker les données. Étant donné qu’il existe plusieurs types de BookParticipants, nous allons utiliser un enum pour les recenser. enum ParticipantTypes { Author = 0, Editor } class BookParticipant { public string FirstName; public string LastName; public ParticipantTypes ParticipantType; }

Nous allons maintenant définir et initialiser un tableau de BookParticipant. L’arbre XML sera alors généré en utilisant une requête LINQ qui extraira les données du tableau (voir Listing 7.16). Listing 7.16 : Création d’un arbre XML avec une requête LINQ. BookParticipant[] bookParticipants = new[] { new BookParticipant {FirstName = "Joe", LastName = "Rattz", ParticipantType = ParticipantTypes.Author}, new BookParticipant {FirstName = "Ewan", LastName = "Buckingham", ParticipantType = ParticipantTypes.Editor} }; XElement xBookParticipants = new XElement("BookParticipants", bookParticipants.Select(p =>

Openmirrors.com

Linq.book Page 205 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

205

new XElement("BookParticipant", new XAttribute("type", p.ParticipantType), new XElement("FirstName", p.FirstName), new XElement("LastName", p.LastName)))); Console.WriteLine(xBookParticipants);

Le premier bloc de code crée le tableau bookParticipants d’éléments BookParticipant. Le deuxième bloc interroge ce tableau en utilisant un opérateur select et génère des éléments BookParticipant à partir des éléments membres du tableau. Voici l’arbre XML généré :

Joe Rattz

Ewan Buckingham

Pour n’avoir aucun regret, reportez-vous au Listing 6.1 : ce code génère le même arbre en utilisant l’API W3C XML DOM ! Création d’attributs avec XAttribute Contrairement à ce qui se faisait dans l’API W3C XML DOM, les attributs n’héritent pas des nœuds. Implémentés avec la classe XAttribute, ils consistent en des paires nom/valeur stockées dans une collection d’objets XAttribute appartenant à un objet XElement. Grâce à la construction fonctionnelle, un attribut peut être créé et ajouté à un élément à la volée, comme dans le Listing 7.17. Listing 7.17 : Définition d’un attribut avec la construction fonctionnelle. XElement xBookParticipant = new XElement("BookParticipant", new XAttribute("type", "Author")); Console.WriteLine(xBookParticipant);

L’exécution de ce code donne le résultat suivant :

Parfois, il n’est pas possible de créer un attribut pendant la construction de l’élément. Comme le montre le Listing 7.18, ces deux actions peuvent tout aussi bien être séparées.

Linq.book Page 206 Mercredi, 18. février 2009 7:58 07

206

LINQ to XML

Partie III

Listing 7.18 : La définition de l’élément et l’ajout de son attribut sont dissociés. XElement xBookParticipant = new XElement("BookParticipant"); XAttribute xAttribute = new XAttribute("type", "Author"); xBookParticipant.Add(xAttribute); Console.WriteLine(xBookParticipant);

Le résultat est identique au précédent :

À nouveau, remarquez à quel point la méthode XElement.Add est flexible. Elle accepte tout type d’objet et applique les mêmes règles au contenu de l’élément que lors de l’instanciation du XElement. Création de commentaires avec XComment La création de commentaires avec LINQ to XML est vraiment simple. La classe utilisée est XComment. Vous pouvez créer un commentaire et le lier à un élément à la volée, en utilisant la construction fonctionnelle (voir Listing 7.19). Listing 7.19 : Définition d’un commentaire avec la création fonctionnelle. XElement xBookParticipant = new XElement("BookParticipant", new XComment("Cette personne est ➥retraitée")); Console.WriteLine(xBookParticipant);

Voici le résultat affiché dans la console :



Parfois, il n’est pas possible de définir un commentaire lors de la construction de l’élément. Si nécessaire, vous pouvez utiliser la méthode Add pour ajouter le commentaire après que l’élément eut été construit (voir Listing 7.20). Listing 7.20 : La définition de l’élément et l’ajout du commentaire sont dissociés. XElement xBookParticipant = new XElement("BookParticipant"); XComment xComment = new XComment("Cette personne est retraitée"); xBookParticipant.Add(xComment); Console.WriteLine(xBookParticipant);

Le résultat est identique au précédent :



Openmirrors.com

Linq.book Page 207 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

207

Création de conteneurs avec XContainer XContainer est une classe abstraite. Il n’est donc pas possible de l’instancier. En revanche, vous pouvez instancier une de ses sous-classes : XDocument ou XElement. La classe XContainer hérite de la classe XNode et peut contenir d’autres classes qui héritent de XNode.

Création de déclarations avec XDeclaration Grâce à la classe XDeclaration de l’API LINQ to XML, la définition de déclarations est un jeu d’enfant. Contrairement à la plupart des autres classes de l’API LINQ to XML, les déclarations s’ajoutent au document XML et non à un élément. Vous rappelez-vous à quel point le constructeur de la classe XElement était flexible ? Toutes les classes pour lesquelles il n’était pas spécialement destiné déclenchaient l’appel de la méthode ToString et le texte retourné était ajouté à l’élément sous une forme textuelle. Par inadvertance, il est donc possible d’ajouter une déclaration à un élément en utilisant la classe XDeclaration. Cependant, si cela est permis, le résultat ne sera pas celui escompté. ATTENTION Les déclarations XML s’appliquent au document XML. Cependant, il est possible de les appliquer à un XElement, sans toutefois obtenir l’effet recherché.

Il est possible de définir une déclaration à la volée et de l’ajouter à un document XML en utilisant la construction fonctionnelle (voir Listing 7.21). Listing 7.21 : Définition d’une déclaration avec la construction fonctionnelle. XDocument xDocument = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"), new XElement("BookParticipant")); Console.WriteLine(xDocument);

Voici le résultat :

Comme vous pouvez le voir, la déclaration n’apparaît pas dans la sortie console. Cependant, si vous déboguez le code et affichez la fenêtre Espion Express, vous verrez que la déclaration est bien là. Parfois, il n’est pas possible de définir la déclaration lors de la construction du document. Vous devez alors instancier la déclaration, puis l’affecter à la propriété Declaration du document (voir Listing 7.22).

Linq.book Page 208 Mercredi, 18. février 2009 7:58 07

208

LINQ to XML

Partie III

Listing 7.22 : Création d’une déclaration et affectation à la propriété Declaration du document. XDocument xDocument = new XDocument(new XElement("BookParticipant")); XDeclaration xDeclaration = new XDeclaration("1.0", "UTF-8", "yes"); xDocument.Declaration = xDeclaration; Console.WriteLine(xDocument);

Voici le résultat :

Tout comme dans l’exemple précédent, la déclaration n’apparaît pas dans la sortie console. Cependant, si vous déboguez le code et affichez la fenêtre Espion Express, vous verrez que la déclaration est bien là. Création de types de documents avec XDocumentType La classe XDocumentType de l’API LINQ to XML facilite grandement la création de types de documents (DTD). Contrairement à la plupart des autres classes de l’API LINQ to XML, les types de documents s’ajoutent au document XML et non à un élément. Vous rappelez-vous à quel point le constructeur de la classe XElement était flexible ? Toutes les classes pour lesquelles il n’était pas spécialement destiné déclenchaient l’appel de la méthode ToString et le texte retourné était ajouté à l’élément sous une forme textuelle. Par inadvertance, il est donc possible d’ajouter une déclaration à un élément en utilisant la classe XDeclaration. Cela est permis, mais ne donnera pas le résultat escompté. ATTENTION Les types de documents XML s’appliquent au document XML. Cependant, il est possible de les appliquer à un XElement, sans toutefois obtenir l’effet recherché.

Il est possible de définir un type de document à la volée et de l’ajouter à un document XML en utilisant la construction fonctionnelle (voir Listing 7.23). Listing 7.23 : Définition d’un type de document avec la construction fonctionnelle. XDocument xDocument = new XDocument(new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XElement("BookParticipant")); Console.WriteLine(xDocument);

Openmirrors.com

Linq.book Page 209 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

209

Voici le résultat :

Parfois, il n’est pas possible de définir le type de document lors de la construction du document. Vous devez alors instancier la définition, puis l’ajouter au document XML avec la méthode add (voir Listing 7.24). Listing 7.24 : Création d’un type de document et ajout au document. XDocument xDocument = new XDocument(); XDocumentType documentType = new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null); xDocument.Add(documentType, new XElement("BookParticipants")); Console.WriteLine(xDocument);

Voici le résultat :

Dans ce code, aucun élément n’a été ajouté avant de définir le type du document. Si vous tentez de définir le type du document après avoir ajouté un ou plusieurs éléments, l’exception suivante est levée : L’exception InvalidOperationException n’a pas été gérée. Cette opération créerait un document incorrectement structuré.

Si vous êtes amené à définir un type de document après l’instanciation du document, assurez-vous qu’aucun élément n’a été spécifié durant l’instanciation du document ou avant la déclaration DTD. Création de documents avec XDocument Comme il a été dit précédemment, il n’est pas nécessaire de définir un document XML pour être en mesure de créer un arbre ou un élément XML. Cependant, si vous êtes amené à le faire, LINQ to XML va vous simplifier grandement la tâche (voir Listing 7.25). Listing 7.25 : Création d’un document XML avec XDocument. XDocument xDocument = new XDocument(); Console.WriteLine(xDocument);

Ce code ne produit aucune sortie, puisque le document est vide.

Linq.book Page 210 Mercredi, 18. février 2009 7:58 07

210

LINQ to XML

Partie III

Cet exemple étant un peu trop simple, nous allons créer un nouveau document et y ajouter toutes les classes LINQ to XML spécifiquement conçues pour être ajoutées à un objet XDocument (voir Listing 7.26). Listing 7.26 : Un autre exemple légèrement plus complexe de création d’un document XML avec XDocument. XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), new XElement("BookParticipants")); Console.WriteLine(xDocument);

L’instruction de traitement et l’élément auraient pu être ajoutés au niveau élément. Ils ont été placés au niveau du document pour lui donner un peu de consistance. Voici le résultat :



Vous avez peut-être remarqué que la déclaration n’apparaît pas dans la sortie console. Comme indiqué dans les exemples de la section "Définition de déclarations avec XDeclarations", vous pouvez déboguer le code et afficher une fenêtre Espion Express pour constater que la déclaration est bien là. Création de noms avec XName Comme indiqué un peu plus tôt dans ce chapitre, il n’est pas possible de créer des noms en utilisant un objet XName. Cette classe n’a en effet aucun constructeur public. Vous ne pouvez donc pas l’instancier. Un objet XName sera défini à partir d’une chaîne, éventuellement complétée d’un espace de noms, lorsque le code le nécessite. Un objet XName est constitué d’un nom local (une chaîne) et d’un espace de noms (un XNamespace). Dans le Listing 7.27, le code appelle le constructeur XElement dont l’argument est un XName. Listing 7.27 : Dans cet exemple, un objet XName est automatiquement créé. XElement xBookParticipant = new XElement("BookParticipant"); Console.WriteLine(xBookParticipant);

Dans cet exemple, un objet XElement est instancié à partir de son nom au format chaîne. L’objet XName BookParticipant est automatiquement créé et affecté à la propriété Name de l’objet XElement. Ici, aucun espace de noms n’étant spécifié, le XName n’a donc aucun espace de noms. Openmirrors.com

Linq.book Page 211 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

211

Voici le résultat :

Le Listing 7.28 montre comment instancier un XElement en fournissant son nom et un espace de noms. Listing 7.28 : Dans cet exemple, un objet XName est automatiquement créé, accompagné d’un espace de noms. XNamespace ns = "http://www.linqdev.com/Books"; XElement xBookParticipant = new XElement(ns + "BookParticipant"); Console.WriteLine(xBookParticipant);

Ce code produit la sortie XML suivante :

Pour avoir de plus amples informations sur la création de noms en utilisant l’API LINQ to XML, reportez-vous à la section intitulée "Noms, espaces de noms et préfixes", un peu plus tôt dans ce chapitre. Création d’espaces de noms avec XNamespace Dans l’API LINQ to XML, les espaces de noms sont implémentés avec la classe XNamespace. Vous trouverez un exemple de création et d’utilisation d’un espace de noms dans le Listing 7.28. Pour avoir de plus amples informations sur la création de noms en utilisant l’API LINQ to XML, reportez-vous à la section intitulée "Noms, espaces de noms et préfixes", un peu plus tôt dans ce chapitre. Création de nœuds avec XNode XNode étant une classe abstraite, il n’est pas possible de l’instancier. Vous pouvez en revanche instancier une de ses sous-classes : XComment, XContainer, XDocumentType, XProcessingInstruction ou XText. Théoriquement, un XNode est une classe quelconque qui fonctionne comme un nœud dans un arbre XML.

Création d’instructions de traitement avec XProcessingInstruction La définition d’instructions de traitement n’a jamais été aussi simple qu’avec la classe XProcessingInstruction de l’API LINQ to XML. Vous pouvez définir des instructions de traitement au niveau document ou élément. Le Listing 7.29 illustre ces deux possibilités en utilisant la construction fonctionnelle.

Linq.book Page 212 Mercredi, 18. février 2009 7:58 07

212

LINQ to XML

Partie III

Listing 7.29 : Définition d’une instruction de traitement aux niveaux document et élément. XDocument xDocument = new XDocument( new XProcessingInstruction("BookCataloger", "out-of-print"), new XElement("BookParticipants", new XElement("BookParticipant", new XProcessingInstruction("ParticipantDeleter", "delete"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(xDocument);

Avant de donner le résultat de ce code, je veux insister sur la simplicité d’utilisation de la construction fonctionnelle. La comparaison de ce code avec celui du Listing 6.1 met clairement en évidence la supériorité de l’API LINQ to XML par rapport à l’ancienne API W3C XML. Voici les résultats :



Joe Rattz

Je suppose qu’il ne vous sera pas trop difficile d’imaginer le code permettant d’ajouter une instruction de traitement après la construction du document, puisqu’il s’apparente à celui permettant d’ajouter un autre type de nœud. Le Listing 7.30 donne un exemple plus complexe de création et d’ajout d’une instruction de traitement a fortiori. Listing 7.30 : Ajout d’instructions de traitement après la construction du document et de l’élément. XDocument xDocument = new XDocument(new XElement("BookParticipants", new XElement("BookParticipant", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); XProcessingInstruction xPI1 = new XProcessingInstruction("BookCataloger", "out-of-print"); xDocument.AddFirst(xPI1); XProcessingInstruction xPI2 = new XProcessingInstruction("ParticipantDeleter", "delete"); XElement outOfPrintParticipant = xDocument .Element("BookParticipants") .Elements("BookParticipant") .Where(e => ((string)((XElement)e).Element("FirstName")) == "Joe" && ((string)((XElement)e).Element("LastName")) == "Rattz") .Single(); outOfPrintParticipant.AddFirst(xPI2); Console.WriteLine(xDocument);

Openmirrors.com

Linq.book Page 213 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

213

Plusieurs passages de ce listing sont dignes d’intérêt. Comme vous pouvez le voir, le document et l’arbre XML ont été créés en utilisant la construction fonctionnelle. Une instruction de traitement a été ajoutée au document après sa construction. Ici, c’est la méthode XElement.AddFirst qui a été choisie pour créer le premier nœud enfant du document (cette méthode a été préférée à XElement.Add, qui ajoute un nœud à la fin des nœuds enfants du document. À cet emplacement, il pourrait être trop tard pour honorer une instruction de traitement). Pour ajouter une instruction de traitement à un des éléments, nous devons y faire référence. Nous aurions pu construire un objet XElement et mémoriser sa référence, mais j’ai pensé qu’il était temps d’introduire les possibilités des requêtes LINQ à venir. Comme vous pouvez le voir, la requête utilisée est plutôt complexe. Elle extrait du document l’élément BookParticipants en utilisant la méthode Element (voir "Déplacements XML", un peu plus loin dans cette section). La séquence d’objets XElement BookParticipant, pour laquelle les éléments FirstName et LastName ont respectivement pour valeur "Joe" et "Ratz", est alors récupérée. Les valeurs de FirstName et LastName ont été obtenues en utilisant l’opérateur de casting (string). L’opérateur Where retourne un IEnumerable, alors que nous avons besoin d’un XElement. La réponse retournée par la requête étant unique, nous pouvons utiliser l’opérateur de requête standard différé First de LINQ to Object. Une fois la référence à l’objet XElement obtenue, il est très simple d’ajouter l’instruction de traitement et d’afficher les résultats. Voici les résultats affichés dans la console :



Joe Rattz

Création d’éléments streaming avec XStreamingElement Dans la deuxième partie de cet ouvrage, nous avons vu que plusieurs des opérateurs de requête standard différaient leur exécution jusqu’à l’énumération des données retournées. Si vous utilisez de tels opérateurs tout en voulant obtenir une projection au format XML, il faudra choisir entre le côté différé des opérateurs de requête standard et l’exécution immédiate d’une requête de projection LINQ to XML. À titre d’exemple, dans le Listing 7.31, le quatrième élément du tableau names est modifié et, pourtant, lorsque nous affichons les valeurs de l’objet XElement, l’arbre XML contient les données originales. Ceci vient du fait que l’élément XNames a été entièrement créé avant que l’élément du tableau names n’ait été modifié.

Linq.book Page 214 Mercredi, 18. février 2009 7:58 07

214

LINQ to XML

Partie III

Listing 7.31 : Exécution immédiate de l’arbre XML. string[] names = { "John", "Paul", "George", "Pete" }; XElement xNames = new XElement("Beatles", from n in names select new XElement("Name", n)); names[3] = "Ringo"; Console.WriteLine(xNames);

Ce code produit l’arbre XML suivant :

John Paul George Pete

Comme vous le voyez, chaque objet XElement de la séquence devient un élément enfant de Beatles. L’élément name[3] a été initialisé à "Ringo" avant d’afficher l’arbre XML et, pourtant, le dernier élément de cet arbre contient toujours la valeur originale " Pete". Ceci vient du fait que la séquence names doit être énumérée pour pouvoir construire l’objet XElement. La requête est donc exécutée immédiatement. Si vous voulez que la construction de l’arbre XML soit différée, il faut utiliser des éléments streaming implémentés avec la classe XStreamingElement. Le Listing 7.32 représente le même exemple, mais cette fois-ci nous utilisons des objets XStreamingElement à la place des objets XElement. Listing 7.32 : Exécution différée de la construction de l’arbre XML avec la classe XStreamingElement. string[] names = { "John", "Paul", "George", "Pete" }; XStreamingElement xNames = new XStreamingElement("Beatles", from n in names select new XStreamingElement("Name", n)); names[3] = "Ringo"; Console.WriteLine(xNames);

Si ce code fonctionne, le dernier nœud Name devrait avoir la valeur "Ringo". Voici le résultat :

John Paul George Ringo

Openmirrors.com

Linq.book Page 215 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

215

Création de textes avec XText Comme le prouve le Listing 7.33, la définition de texte est très simple. Listing 7.33 : Création d’un élément et affectation d’une valeur chaîne. XElement xFirstName = new XElement("FirstName", "Joe"); Console.WriteLine(xFirstName);

Voici le résultat : Joe

Une chose n’apparaît pas dans ce listing : la chaîne "Joe" est transformée en un objet XText avant d’être ajoutée à l’objet XElement. En examinant l’objet xFirstName dans le débogueur, on se rend compte qu’il contient un seul nœud : un objet XText de valeur "Joe". Étant donné que cette conversion est automatique, dans la plupart des cas vous ne serez pas obligé de construire un objet texte. Cependant, si cela est nécessaire, il vous suffira d’instancier un objet XText, comme dans le Listing 7.34. Listing 7.34 : Création d’un nœud texte et utilisation dans l’initialisation d’un XElement. XText xName = new XText("Joe"); XElement xFirstName = new XElement("FirstName", xName); Console.WriteLine(xFirstName);

Ce code donne le même résultat que le précédent. Si vous utilisez le débogueur pour examiner l’état interne de l’objet xFirstName, vous verrez qu’il est identique à celui de l’objet créé dans l’exemple précédent : Joe

Définition d’un objet CData avec XCData Le Listing 7.35 donne un exemple de définition d’un objet CData. Listing 7.35 : Création d’un nœud CData puis initialisation d’un XElement. XElement xErrorMessage = new XElement("HTMLMessage", new XCData("Invalid user id or password.")); Console.WriteLine(xErrorMessage);

Voici le résultat : Invalid user id or password.]]>

Linq.book Page 216 Mercredi, 18. février 2009 7:58 07

216

LINQ to XML

Partie III

Sauvegarde de fichiers XML La création, la modification et la suppression de données XML n’auraient aucun intérêt s’il n’était pas possible de sauvegarder les données. Cette section va vous montrer plusieurs techniques de sauvegarde. Sauvegardes avec XDocument.Save() Vous pouvez sauvegarder vos données XML en utilisant un des prototypes de la méthode XDocument.Save : void void void void void

XDocument.Save(string filename); XDocument.Save(TextWriter textWriter); XDocument.Save(XmlWriter writer); XDocument.Save(string filename, SaveOptions options); XDocument.Save(TextWriter textWriter, SaveOptions options);

Le Listing 7.36 donne un exemple de sauvegarde du document XML dans le dossier du projet. Listing 7.36 : Sauvegarde d’un document avec la méthode XDocument.Save. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); xDocument.Save("bookparticipants.xml");

La méthode Save a été appelée sur un objet de type XDocument. Ceci est possible car les méthodes Save sont des méthodes d’instances. Comme vous le verrez un peu plus loin, les méthodes Load sont en revanche des méthodes statiques. Elles doivent être appelées sur des classes XDocument ou XElement. Voici le contenu du fichier bookparticipants.xml, ouvert dans un éditeur de texte tel que le Bloc-notes de Windows.

Joe Rattz

Ce document XML est facile à lire parce que la version de la méthode Save met en forme les données. Si, en revanche, nous appelions la méthode Save suivante : xDocument.Save("bookparticipants.xml", SaveOptions.DisableFormatting);

Openmirrors.com

Linq.book Page 217 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

217

les résultats seraient bien moins lisibles : Joe Rattz

Les données sont placées sur une seule et même ligne. Pour vous en assurer, ouvrez le fichier dans un éditeur de texte. Si vous l’ouvriez dans un navigateur Internet, elles seraient automatiquement mises en forme pour apparaître comme dans le résultat du Listing 7.36. INFO Passée en deuxième argument de la méthode Save, la valeur SaveOptions.None produit le même résultat que le Listing 7.36.

Sauvegarde avec XElement.Save Je l’ai répété plusieurs fois, avec l’API LINQ to XML, il n’est pas nécessaire de créer un document XML. Ceci reste d’actualité quant à la sauvegarde de données XML. La classe XElement propose plusieurs méthodes qui abondent dans ce sens : void void void void void

XElement.Save(string filename); XElement.Save(TextWriter textWriter); XElement.Save(XmlWriter writer); XElement.Save(string filename, SaveOptions options); XElement.Save(TextWriter textWriter, SaveOptions options);

Le Listing 7.37 est un exemple très proche du précédent mais, ici, aucun document XML n’est créé. Listing 7.37 : Sauvegarde d’un élément avec la méthode XElement. XElement bookParticipants = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))); bookParticipants.Save("bookparticipants.xml");

Le résultat est identique au précédent :

Joe Rattz

Linq.book Page 218 Mercredi, 18. février 2009 7:58 07

218

LINQ to XML

Partie III

Lecture de fichiers XML Cette section passe en revue quelques-unes des techniques qui permettent de lire des données stockées dans un fichier XML. Lecture avec XDocument.Load() Voici la liste des méthodes qui vous permettront de lire des données stockées dans un fichier XML : static static static static static static

XDocument XDocument XDocument XDocument XDocument XDocument

XDocument.Load(string uri); XDocument.Load(TextReader textReader); XDocument.Load(XmlReader reader); XDocument.Load(string uri, LoadOptions options); XDocument.Load(TextReader textReader, LoadOptions options); XDocument.Load(XmlReader reader, LoadOptions options);

Ces méthodes sont les parfaites répliques des méthodes XDocument.Save. Il existe cependant quelques différences qui valent la peine d’être signalées. Tout d’abord, les méthodes Save étant des méthodes d’instance, elles s’appliquent à un objet XDocument ou XElement. Les méthodes Load étant des méthodes statiques, vous devez appeler la classe XDocument elle-même. Par ailleurs, les méthodes Save dont le premier paramètre est de type string doivent spécifier le nom du fichier, alors que les méthodes Load dont le premier paramètre est de type string acceptent les URI. Le Tableau 7.2 dresse la liste des valeurs possibles du paramètre LoadOptions. Tableau 7.2 : Le paramètre LoadOptions.

Valeur

Description

LoadOptions.None

Aucune option de chargement.

LoadOptions.PreserveWhitespace

Conservation des sauts de ligne et autres espaces dans la source XML.

LoadOptions.SetLineInfo

Cette option permet d’obtenir la ligne et la position des objets hérités de XObject en utilisant l’interface IXmlLineInfo.

LoadOptions.SetBaseUri

Cette option permet d’obtenir l’URI des objets qui héritent de XObject.

Ces options peuvent être combinées en utilisant l’opérateur OR (|). Mais, attention, en fonction du contexte certaines options ne donneront pas les résultats escomptés. Par exemple, lorsqu’un élément ou un document est créé à partir d’une chaîne, aucune ligne d’information ni aucun URI ne sont disponibles. De même, lorsqu’un document est créé à partir d’un XmlReader, aucun URI n’est disponible. Le Listing 7.38 montre comment lire le document XML créé dans l’exemple précédent. Openmirrors.com

Linq.book Page 219 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

219

Listing 7.38 : Lecture d’un document avec la méthode XDocument.Load. XDocument xDocument = XDocument.Load("bookparticipants.xml", LoadOptions.SetBaseUri | LoadOptions.SetLineInfo); Console.WriteLine(xDocument); XElement firstName = xDocument.Descendants("FirstName").First(); Console.WriteLine("FirstName : ligne {0}, position{1}", ((IXmlLineInfo)firstName).LineNumber, ((IXmlLineInfo)firstName).LinePosition); Console.WriteLine("Adresse URI de l’élément FirstName

:{0}", firstName.BaseUri);

INFO Pour que le type IXmlLineInfo puisse être utilisé, vous devez ajouter une directive using System.xml; ou faire référence à l’espace de noms correspondant.

Ce code charge le fichier XML créé dans l’exemple précédent. Après le chargement et l’affichage du document, nous définissons une référence pour l’élément FirstName et affichons sa ligne et sa position dans le document XML source. Le code se termine par l’affichage de l’adresse URI de l’élément FirstName. Voici les résultats :

Joe Rattz

FirstName : ligne 4, position 6 Adresse URI de l’élément FirstName : file:///C:/Documents and Settings/…/Projects/ LINQChapter7/LINQChapter7/bin/Debug/bookparticipants.xml

Le document a bien l’allure souhaitée. Cependant, la ligne de l’élément FirstName n’a pas l’air de correspondre. Un rapide coup d’œil au résultat du Listing 7.37 aura tôt fait de vous convaincre que cette information est correcte. En effet, la première ligne est réservée à la déclaration du document, et cette ligne n’apparaît pas dans l’affichage du document :

Lecture avec XElement.Load() Virtuellement, la lecture d’un élément ou d’un document ne présente aucune différence. Voici les méthodes permettant de lire des données stockées dans un XDocument ou un XElement : static XElement XElement.Load(string uri); static XElement XElement.LoadTextReader textReader); static XElement XElement.Load(XmlReader reader);

Linq.book Page 220 Mercredi, 18. février 2009 7:58 07

220

LINQ to XML

Partie III

static XElement XElement.Load(string uri, LoadOptions options); static XElement XElement.Load(TextReader textReader, LoadOptions options); static XElement XElement.Load(XmlReader reader, LoadOptions options);

Tout comme les méthodes XDocument.Save, ces méthodes sont statiques. Elles doivent donc être appelées à partir de la classe XElement. Le Listing 7.39 montre comment lire les données XML sauvegardées avec la méthode XElement.Save dans le Listing 7.37. Listing 7.39 : Lecture d’un document avec la méthode XElement.Load. XElement xElement = XElement.Load("bookparticipants.xml"); Console.WriteLine(xElement);

Les résultats sont bien conformes à nos attentes :

Joe Rattz

Tout comme pour XDocument.Load, il existe des surcharges de la méthode XElement.Load qui permettent d’utiliser le paramètre LoadOptions. Reportez-vous à la section intitulée "Lecture avec XDocument.Load()" pour avoir de plus amples informations à ce sujet. Extraction avec XDocument.Parse() ou XElement.Parse() Combien de fois avez-vous extrait des données XML en passant par des chaînes de caractères ? Il faut bien avouer que cette tâche n’est pas des plus agréables ! Mais, rassurezvous, l’API LINQ to XML va apporter une réponse élégante à cette problématique. La méthode statique Parse est accessible aux classes XDocument et XElement. Par son intermédiaire, il est possible d’extraire des données XML. Fort de ce qui a été vu dans ce chapitre, vous ne devez avoir aucune difficulté à imaginer que, si l’extraction de données est possible depuis la classe XDocument, elle l’est aussi depuis la classe XElement. Nous allons donc raisonner sur un seul exemple relatif à la classe XElement. Dans la section intitulée "Sauvegardes avec XDocument.Save", vous avez pu voir l’influence du paramètre LoadOptions lorsqu’il est initialisé à DisableFormatting : les données sont sauvegardées sur une seule et même ligne XML. Le Listing 7.40 utilise cette chaîne XML (en ayant pris le soin d’échapper les guillemets), l’extrait dans un élément et affiche le résultat. Listing 7.40 : Extraction d’une chaîne XML dans un élément. string xml = "" + "JoeRattz" + "";

Openmirrors.com

Linq.book Page 221 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

221

XElement xElement = XElement.Parse(xml); Console.WriteLine(xElement);

Voici le résultat :

Joe Rattz

Impressionnant, n’est-ce pas ? Rappelez-vous les vieux jours où vous deviez créer un document en utilisant la classe XmlDocument de l’API W3C XML DOM. Le document n’étant plus l’élément de référence, un simple appel à la méthode Parse suffit désormais pour transformer une chaîne XML en un arbre XML !

Déplacements XML Les déplacements XML sont effectués par l’intermédiaire de 4 propriétés et de 11 méthodes. Dans cette section, nous allons nous efforcer d’utiliser le même code pour chacune des propriétés et des méthodes, en modifiant un simple argument chaque fois que cela sera possible. Le Listing 7.41 est un exemple de construction d’un document XML complet. Listing 7.41 : Le code dont seront dérivés les prochains exemples. // Définition d’une référence vers un des éléments de l’arbre XML XElement firstParticipant; XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(xDocument);

La première ligne crée une référence au premier élément BookParticipant. Ceci afin d’avoir un élément par rapport auquel effectuer le déplacement (la variable firstParticipant n’est pas utilisée dans ce premier exemple, mais elle le sera dans les suivants).

Linq.book Page 222 Mercredi, 18. février 2009 7:58 07

222

LINQ to XML

Partie III

Le document est passé en argument de la méthode Console.WriteLine. Tout le contenu du document XML sera donc affiché. Dans les prochains exemples, nous choisirons un autre argument pour illustrer les différents types de déplacements. Voici le résultat :



Joe Rattz

Ewan Buckingham

Propriétés de déplacement Nous commencerons par les propriétés de déplacement primaires. Lorsqu’une direction (up, down, etc.) est spécifiée, elle est relative à l’élément sur lequel la méthode est appelée. Dans les exemples suivants, la référence au premier élément BookParticipant sera prise comme élément de base pour le déplacement. Nœud suivant avec XNode.NextNode La propriété NextNode obtient le nœud frère du nœud courant (voir Listing 7.42). Listing 7.42 : Obtention du nœud frère suivant d’un objet XElement avec la propriété NextNode. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.NextNode);

Openmirrors.com

Linq.book Page 223 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

223

L’élément de base étant le premier élément BookParticipant, la propriété NextNode devrait renvoyer vers le deuxième élément BookParticipant. Voici le résultat :

Ewan Buckingham

Nœud précédent avec XNode.PreviousNode La propriété PreviousNode donne accès au nœud frère précédent. Pour illustrer cette propriété, nous allons partir du nœud FirstParticipant. Nous lui appliquerons la propriété NextNode pour obtenir le nœud frère suivant puis la propriété PreviousNode pour obtenir le nœud frère précédent, c’est-à-dire… le nœud de départ (voir Listing 7.43). Listing 7.43 : Obtention du nœud frère précédent d’un objet XElement avec la propriété PreviousNode. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.NextNode.PreviousNode);

C’est bien le premier élément qui est affiché dans la console :

Joe Rattz

Remonter au niveau du document avec XObject.Document Pour remonter au niveau du document à partir d’un XElement quelconque, il suffit d’utiliser la propriété Document (voir Listing 7.44, et en particulier l’appel à la méthode WriteLine).

Linq.book Page 224 Mercredi, 18. février 2009 7:58 07

224

LINQ to XML

Partie III

Listing 7.44 : Accès au document à partir d’un objet XElement en utilisant la propriété Document. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.Document);

Tout comme pour le Listing 7.41, ce code affiche la totalité du document :



Joe Rattz

Ewan Buckingham

Remonter d’un niveau avec XObject.Parent Pour obtenir l’élément parent d’un objet XElement, il vous suffit d’utiliser la propriété Parent (voir Listing 7.45, et en particulier l’appel à la méthode WriteLine). Listing 7.45 : Accès au parent de l’objet firstParticipant en utilisant la propriété Parent. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")),

Openmirrors.com

Linq.book Page 225 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

225

new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.Parent);

Voici le résultat :

Joe Rattz

Ewan Buckingham

Ne vous laissez pas abuser : il s’agit non pas du document complet, mais du parent de l’objet firstParticipant. Remarquez l’absence du DTD et de l’instruction de transformation. Méthodes de déplacement Étant donné que les méthodes de déplacement retournent des séquences composées de plusieurs nœuds, l’instruction Console.WriteLine va être remplacée par une boucle foreach qui permettra d’afficher les différents nœuds : foreach(XNode node in firstParticipant.Nodes()) { Console.WriteLine(node); }

Dans les différents exemples, seule différera la méthode appliquée à l’objet firstParticipant dans la boucle foreach. Nœuds enfants avec XContainer.Nodes() La méthode Nodes() retourne une collection de nœuds enfants XNode de l’élément spécifié (voir Listing 7.46). À toutes fins utiles, nous rappelons qu’une séquence est un objet IEnumerable. Listing 7.46 : Accès aux enfants de l’objet firstParticipant en utilisant la propriété Nodes. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant",

Linq.book Page 226 Mercredi, 18. février 2009 7:58 07

226

LINQ to XML

Partie III

new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.Nodes());

Voici le résultat : Joe Rattz

Cette méthode retourne les éléments enfants (XElement), mais également les autres types de nœuds : commentaires (XComment), texte (XText), instructions de traitement (XProcessingInstruction), type de document (XDocumentType). En revanche, elle ne retourne pas les attributs puisque ces derniers ne sont pas des nœuds. Pour mieux illustrer la méthode Nodes(), plusieurs nœuds enfants ont été ajoutés à l’élément firstParticipant dans le Listing 7.47. Listing 7.47 : Accès aux différents types d’enfants de l’objet firstParticipant en utilisant la propriété Nodes. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XNode node in firstParticipant.Nodes()) { Console.WriteLine(node); }

Cet exemple est différent du précédent. Ici, l’élément firstParticipant a également un enfant de type XComment et un autre de type XProcessingInstruction. Voici le résultat affiché après l’appui sur Ctrl+F5 :

Que diriez-vous d’utiliser l’opérateur OfType pour limiter la sortie aux attributs ? Eh bien, ceci est tout bonnement impossible puisque, selon l’API LINQ to XML, les attributs ne sont pas des nœuds de l’arbre XML. Ils consistent en une séquence de paires nom/valeur attachée à un élément. Pour obtenir les attributs de l’objet firstParticipant, le code doit être modifié comme dans le Listing 7.50. Listing 7.50 : Accès aux attributs d’un élément avec la méthode Attributes. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Openmirrors.com

Linq.book Page 229 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

229

foreach (XAttribute attr in firstParticipant.Attributes()) { Console.WriteLine(attr); }

Comme vous pouvez le voir, nous avons changé l’argument de la boucle foreach, mais également le type de la variable d’énumération, puisque XAttribute n’hérite pas de XNode. Voici le résultat : type="Author"

Nœuds enfants avec XContainer.Elements() L’API LINQ to XML étant centrée sur les éléments, Microsoft a défini la méthode Elements() pour retourner une collection constituée des éléments enfants d’un élément.

Le Listing 7.51 donne un exemple d’utilisation de cette méthode. Tout en utilisant une autre technique, il est cependant équivalent au Listing 7.48. Listing 7.51 : Accès aux éléments enfants d’un élément avec la méthode Elements. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XNode node in firstParticipant.Elements()) { Console.WriteLine(node); }

Ce code affiche le même résultat que le Listing 7.48 : Joe Rattz

Il existe également une version surchargée de la méthode Elements qui permet de passer le nom de l’élément recherché (voir Listing 7.52).

Linq.book Page 230 Mercredi, 18. février 2009 7:58 07

230

LINQ to XML

Partie III

Listing 7.52 : Accès aux éléments enfants d’un élément nommé avec la méthode XContainer.Elements. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XContainer.Elements(FirstName) { Console.WriteLine(node); }

Voici le résultat : Joe

Premier nœud enfant avec XContainer.Element() La méthode Element retourne le premier élément enfant de l’élément passé en argument. Contrairement à la méthode précédende, c’est non pas une séquence qui est retournée, mais un élément unique (voir Listing 7.53). Listing 7.53 : Accès au premier élément enfant d’un élément nommé avec la méthode Element. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"),

Openmirrors.com

Linq.book Page 231 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

231

new XElement("LastName", "Buckingham")))); Console.WriteLine(firstParticipant.Element("FirstName"));

Voici le résultat : Joe

Ancêtres d’un nœud avec XNode.Ancestors() La propriété Parent permet d’obtenir l’ancêtre direct (le parent) d’un nœud. Si vous désirez obtenir une séquence contenant tous les ancêtres d’un nœud, jusqu’au niveau hiérarchique le plus élevé, vous utiliserez la méthode Ancestors. Seuls les éléments (et non tous les nœuds) ancêtres sont retournés.

Pour mieux illustrer cette méthode, nous allons ajouter plusieurs nœuds enfants à l’élément FirstName du premier participant. Par ailleurs, plutôt qu’énumérer les ancêtres du premier participant, nous utiliserons la méthode Element pour nous déplacer de deux niveaux hiérarchiques vers le bas afin d’atteindre l’élément NickName. Le nombre d’ancêtres sera ainsi plus élevé, ce qui facilitera la compréhension de la méthode Ancestors (voir Listing 7.54). Listing 7.54 : Ancêtres d’un objet XElement avec la méthode Ancestors. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in firstParticipant. Element("FirstName").Element("NickName").Ancestors()) { Console.WriteLine(element.Name); }

Comme vous pouvez le voir, un objet XText initialisé à "Joe" et un XElement nommé NickName ont été ajoutés à l’élément FirstName. Le dernier bloc d’instructions recherche les ancêtres de l’élément NickName. La boucle foreach est exécutée au niveau

Linq.book Page 232 Mercredi, 18. février 2009 7:58 07

232

LINQ to XML

Partie III

XElement (et non XNode). Ainsi, l’instruction WriteLine peut accéder à la propriété Name des éléments retournés. Plutôt qu’afficher le code XML de chaque élément ancêtre, nous nous contenterons d’afficher leur nom. Ceci uniquement dans un souci de clarté.

Voici les résultats : FirstName BookParticipant BookParticipants

Ancêtres d’un nœud avec XElement.AncestorsAndSelf() Cette méthode est comparable à la méthode Ancestors, mais ses résultats incluent l’élément sur lequel s’effectue la recherche. Le Listing 7.55 est le même que le précédent, à ceci près que la méthode AncestorsAndSelf remplace la méthode Ancestors. Listing 7.55 : Ancêtres d’un objet XElement avec la méthode AncestorsAndSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in firstParticipant. Element("FirstName").Element("NickName").AncestorsAndSelf()) { Console.WriteLine(element.Name); }

Les résultats sont identiques à ceux du listing précédent mais, cette fois, ils incluent l’élément NickName : NickName FirstName BookParticipant BookParticipants

Openmirrors.com

Linq.book Page 233 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

233

Descendants d’un nœud avec XContainer.Descendants() Pour obtenir une séquence contenant tous les éléments descendant d’un nœud, vous utiliserez la méthode Descendants. Vous pouvez également utiliser la méthode DescendantNodes pour obtenir tous les nœuds descendant d’un autre nœud. Le Listing 7.56 est le même que le précédent mais, ici, c’est la méthode Descendants qui est appelée. Listing 7.56 : Descendants d’un objet XElement avec la méthode Descendants. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in firstParticipant.Descendants()) { Console.WriteLine(element.Name); }

Voici les résultats : FirstName NickName LastName

Tous les éléments qui descendent de l’élément firstParticipant, mais pas les autres types de nœuds, sont bien listés dans la console. Descendants d’un nœud avec XElement.DescendantsAndSelf() DescendantsAndSelf est le pendant de AncestorsAndSelf. Cette méthode renvoie les descendants de l’élément sur lequel porte la requête, en y incluant cet élément. Le Listing 7.57 est le même que le précédent, à ceci près que la méthode DescendantsAndSelf remplace la méthode Descendants. Listing 7.57 : Descendants d’un objet XElement avec la méthode DescendantsAndSelf. XElement firstParticipant; // Le document complet

Linq.book Page 234 Mercredi, 18. février 2009 7:58 07

234

LINQ to XML

Partie III

XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in firstParticipant.DescendantsAndSelf()) { Console.WriteLine(element.Name); }

Les résultats incluent désormais le nom de l’élément firstParticipant : BookParticipant FirstName NickName LastName

Nœuds frères suivants avec XNode.NodesAfterSelf() Pour illustrer cet exemple, deux commentaires ont été ajoutés à l’élément BookParticipants. Les commentaires XComment étant des nœuds et non des éléments, les résultats mettront en évidence que la méthode NodesAfterSelf retourne tous les types de nœuds frères du nœud ciblé (voir Listing 7.58). Listing 7.58 : Nœuds frères d’un objet XNode avec la méthode NodesAfterSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste")));

Openmirrors.com

Linq.book Page 235 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

235

foreach (XNode node in firstParticipant.NodesAfterSelf()) { Console.WriteLine(node); }

Les nœuds ajoutés sont tous deux frères des deux éléments BookParticipant. Cette modification du document XML concerne les exemples des méthodes NodesAfterSelf, ElementsAfterSelf, NodesBeforeSelf et ElementsBeforeSelf. Tous les nœuds frères situés après le premier nœud BookParticipant sont énumérés. Voici le résultat :

Ewan Buckingham

Comme vous le voyez, le dernier commentaire est inclus dans le résultat. C’est en effet un nœud frère du nœud situé après le premier BookParticipant. Il se trouve au même niveau hiérarchique que les éléments BookParticipant. Si les éléments FirstName et LastName sont affichés dans les résultats, c’est parce que la méthode ToString est appliquée au nœud BookParticipant. Cette méthode ne se limite pas aux éléments. Elle retourne également les autres types de nœuds. Si vous voulez filtrer les nœuds retournés à un certain type, utilisez l’opérateur TypeOf. Si ce ne sont que les éléments qui vous intéressent, utilisez la méthode ElementsAfterSelf (voir section suivante). Éléments frères suivants avec XNode.ElementsAfterSelf() Nous utiliserons le même document XML que dans l’exemple précédent. Pour ne retenir que les éléments frères qui suivent le nœud référencé, la méthode ElementsAfterSelf est appelée (voir Listing 7.59). Listing 7.59 : Éléments frères qui suivent le nœud référencé avec la méthode ElementsAfterSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Linq.book Page 236 Mercredi, 18. février 2009 7:58 07

236

LINQ to XML

Partie III

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste"))); foreach (XNode node in firstParticipant.ElementsAfterSelf()) { Console.WriteLine(node); }

Voici le résultat :

Ewan Buckingham

Cette fois-ci, étant donné que le commentaire n’est pas un élément, il est exclu du résultat. Nous rappelons que les éléments FirstName et LastName sont affichés dans les résultats car la méthode ToString est appliquée au nœud BookParticipant. Nœuds frères précédents avec XNode.NodesBeforeSelf() Cet exemple utilise le même document XML que le Listing 7.58. NodesBeforeSelf se comporte comme NodesAfterSelf, si ce n’est qu’elle retourne les nœuds frères qui précèdent le nœud référencé. Dans cet exemple, nous invoquons la méthode NextNode avant d’appeler NodesBeforeSelf pour que le résultat ne soit pas vide (voir Listing 7.60). Listing 7.60 : Nœuds frères qui précèdent le nœud référencé avec la méthode NodesBeforeSelf. XElement firstParticipant; // Le document complet XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste"))); foreach (XNode node in firstParticipant.NextNode.NodesBeforeSelf()) { Console.WriteLine(node); }

Openmirrors.com

Linq.book Page 237 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

237

La méthode NextNode donne accès au deuxième participant. En lui appliquant la méthode ElementsBeforeSelf, les éléments frères qui précèdent le deuxième participant sont listés. Ici, le premier participant. Voici le résultat :

Joey Rattz, Jr.

Les valeurs des nœuds ont bien été mises à jour. Les propriétés XDocumentType.Name, XDocumentType.PublicId, XDocumentType.SystemId et XDocumentType.InternalSubset Pour modifier les valeurs relatives à la définition de type de document (DTD), vous utiliserez quatre propriétés de la classe XDocumentType (voir Listing 7.71). Listing 7.71 : Modification de la définition de type de document. // Définition d’une référence sur le type de document pour un usage futur XDocumentType docType; XDocument xDocument = new XDocument( docType = new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XElement("BookParticipants")); Console.WriteLine("Avant la mise à jour du DTD"); Console.WriteLine(xDocument); docType.Name = "MyBookParticipants"; docType.SystemId = "http://www.somewhere.com/DTDs/MyBookParticipants.DTD"; docType.PublicId = "-//DTDs//TEXT Book Participants//EN";

Openmirrors.com

Linq.book Page 247 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

247

Console.WriteLine("Après la mise à jour du DTD"); Console.WriteLine(xDocument);

Voici les résultats : Avant la mise à jour du DTD

Après la mise à jour du DTD

XProcessingInstruction.Target sur les objets XProcessingInstruction Objects et XProcessingInstruction.Data sur les objets XProcessingInstruction Pour modifier la valeur d’une instruction de traitement, il suffit de modifier les propriétés Target et Data de l’objet XProcessingInstruction (voir Listing 7.72). Listing 7.72 : Mise à jour d’une instruction de traitement. // Définition d’une référence pour un usage futur XProcessingInstruction procInst; XDocument xDocument = new XDocument( new XElement("BookParticipants"), procInst = new XProcessingInstruction("BookCataloger", "out-of-print")); Console.WriteLine("Avant la modification de l’instruction de traitement"); Console.WriteLine(xDocument); procInst.Target = "BookParticipantContactManager"; procInst.Data = "update"; Console.WriteLine("Après la modification de l’instruction de traitement"); Console.WriteLine(xDocument);

Voici le résultat de ce code : Avant la modification de l’instruction de traitement

Après la modification de l’instruction de traitement

XElement.ReplaceAll() La méthode ReplaceAll permet de remplacer l’arbre XML relatif à un élément. Il est possible de passer une simple valeur – une chaîne ou un nombre, par exemple – ou, si une méthode surchargée accepte des objets multiples via le mot-clé params, une portion d’arbre. La méthode ReplaceAll remplace également les attributs. Le Listing 7.73 donne un exemple d’utilisation de cette méthode.

Linq.book Page 248 Mercredi, 18. février 2009 7:58 07

248

LINQ to XML

Partie III

Listing 7.73 : Utilisation de la méthode ReplaceAll pour modifier l’arbre relatif à un élément. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la modification"); Console.WriteLine(xDocument); firstParticipant.ReplaceAll( new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")); Console.WriteLine(System.Environment.NewLine + "Après la modification"); Console.WriteLine(xDocument);

Les instructions en gras modifient l’arbre de l’élément firstParticipant. Comme vous pouvez le voir, l’attribut type n’a pas été spécifié. Voici le résultat : Avant la modification

Joe Rattz

Après la modification

Ewan Buckingham

Bien que les attributs ne soient pas des nœuds enfants des éléments, la méthode ReplaceAll a été en mesure de supprimer l’attribut type de l’arbre XML.

XElement.SetElementValue() sur des objets enfants de XElement Cette méthode est très puissante. Elle permet d’ajouter, de modifier et de supprimer les éléments enfants de l’élément sur lequel elle est appelée. Cette méthode admet deux paramètres : le nom de l’élément enfant à atteindre et la valeur qui doit lui être affectée. Si un enfant portant ce nom est trouvé, et si la valeur passée est différente de null, l’enfant est mis à jour. Si la valeur passée vaut null, l’enfant est supprimé. Si aucun enfant portant ce nom n’est trouvé, il est créé et la valeur spécifiée lui est affectée. Openmirrors.com

Linq.book Page 249 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

249

La méthode SetElementValue n’affecte que le premier élément enfant portant le nom spécifié. Si un ou plusieurs autres éléments enfants portent le même nom, ils ne sont pas affectés. Le Listing 7.74 donne un exemple des trois possibilités de cette méthode. Listing 7.74 : Utilisation de SetElementValue pour mettre à jour, ajouter et supprimer des éléments enfants. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la mise à jour des éléments"); Console.WriteLine(xDocument); // Mise à jour de la valeur d’un élément // L’élément enfant FirstName étant trouvé, sa valeur sera initialisée à Joseph firstParticipant.SetElementValue("FirstName", "Joseph"); // Ajout d’un élément // L’élément enfant MiddleInitial n’étant pas trouvé, il est créé firstParticipant.SetElementValue("MiddleInitial", "C"); // Suppression d’un élément // La valeur de l’élément étant initialisée à null, l’élément est supprimé firstParticipant.SetElementValue("LastName", null); Console.WriteLine(System.Environment.NewLine + "Après la mise à jour des éléments"); Console.WriteLine(xDocument);

Dans un premier temps, la méthode SetElementValue est appelée sur l’élément enfant FirstName de l’élément firstParticipant. Comme un élément portant ce nom existe, sa valeur est mise à jour. Dans un deuxième temps, la méthode SetElementValue est appelée sur l’élément enfant MiddleInitial de l’élément firstParticipant. Comme aucun élément portant ce nom n’existe, il est créé. Enfin, dans un troisième temps, la méthode SetElementValue est appelée sur l’élément enfant LastName de l’élément firstParticipant. La valeur null étant passée dans le deuxième argument de la méthode, l’élément LastName est supprimé. Voici les résultats : Avant la mise à jour des éléments

Joe Rattz

Après la mise à jour des éléments

Linq.book Page 250 Mercredi, 18. février 2009 7:58 07

250

LINQ to XML

Partie III

Joseph C

L’élément FirstName a été mis à jour, l’élément MiddleInitial a été créé et l’élément LastName, supprimé. ATTENTION Lorsque la méthode SetElementValue est appelée avec un deuxième argument ayant pour valeur null, elle supprime l’élément spécifié dans le premier argument. Que ceci ne vous fasse pas croire qu’il suffise d’initialiser un élément avec la valeur null pour le supprimer d’un arbre XML. Si vous tentez de le faire en agissant sur sa propriété Value, une exception sera levée.

Attributs XML Lorsque l’on utilise l’API LINQ to XML, les attributs sont implémentés dans la classe XAttribute. Contrairement à ce qui avait cours dans l’API W3C XML DOM, ils n’héritent pas d’un nœud. Ils n’ont donc aucune relation d’héritage avec les éléments. Et, pourtant, grâce à l’API LINQ to XML, ils sont tout aussi simples à utiliser. Création d’un attribut Les attributs sont créés de la même manière que les éléments et que la plupart des autres classes LINQ to XML. Reportez-vous à la section "Création d’attributs avec XAttribute", au début de ce chapitre, pour en savoir plus à ce sujet. Déplacements dans un attribut Pour vous déplacer dans les attributs, vous utiliserez les propriétés XElement.FirstAttribute, XElement.LastAttribute, XAttribute.NextAttribute et XAttribute .PreviousAttribute et les méthodes XElement.Attribute et XElement.Attributes. Vous en saurez plus à leur sujet dans les prochaines pages. Premier attribut avec XElement.FirstAttribute Pour accéder au premier attribut d’un élément, vous pouvez utiliser la propriété FirstAttribute (voir Listing 7.75). Listing 7.75 : Accès au premier attribut d’un élément avec la propriété FirstAttribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant;

Openmirrors.com

Linq.book Page 251 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

251

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.FirstAttribute);

Ce code produit le résultat suivant dans la console : type="Author"

Attribut suivant avec XAttribute.NextAttribute Pour accéder à l’attribut suivant, il suffit d’utiliser la propriété NextAttribute (voir Listing 7.76). Listing 7.76 : Accès à l’attribut suivant avec la propriété NextAttribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.FirstAttribute.NextAttribute);

Avant d’utiliser la propriété NextAttribute, la propriété FirstAttribute a été appliquée à l’élément firstParticipant pour obtenir une référence au premier attribut de l’élément. Voici le résultat : experience="first-time"

Si la propriété NextAttribute d’un attribut a pour valeur null, cela signifie qu’il s’agit du dernier attribut de l’élément. Attribut précédent avec XAttribute.PreviousAttribute Pour accéder à l’attribut précédent, il suffit d’utiliser la propriété PreviousAttribute (voir Listing 7.77). Listing 7.77 : Accès à l’attribut précédent avec la propriété PreviousAttribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant;

Linq.book Page 252 Mercredi, 18. février 2009 7:58 07

252

LINQ to XML

Partie III

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.FirstAttribute.NextAttribute.PreviousAttribute);

Les propriétés FirstAttribute et NextAttribute ont été chaînées pour obtenir une référence au deuxième attribut de l’élément firstParticipant. En appliquant la propriété PreviousAttribute, l’attribut pointé est donc le premier. Voici le résultat : type="Author"

Si la propriété PreviousAttribute d’un attribut vaut null, cela signifie qu’il a été appliqué au premier attribut de l’élément. Dernier attribut avec XElement.LastAttribute Pour accéder au dernier attribut d’un élément, vous utiliserez la propriété LastAttribute (voir Listing 7.78). Listing 7.78 : Accès au dernier attribut avec la propriété LastAttribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage ➥futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.LastAttribute);

L’instruction Writeln affiche le dernier attribut du XElement firstParticipant : language="English"

XElement.Attribute() S’il existe, cette méthode retourne le premier attribut dont le nom est passé en argument (voir Listing 7.79). Listing 7.79 : Accès à un attribut avec la méthode Attribute. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant;

Openmirrors.com

Linq.book Page 253 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

253

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(firstParticipant.Attribute("type").Value);

La méthode Attribute donne accès à l’attribut type. La valeur de cet attribut est alors affichée en utilisant la propriété Value. Voici le résultat : Author

À titre d’information, sachez que la valeur de l’attribut aurait également pu être obtenue en appliquant un casting de type string à l’attribut.

XElement.Attributes() La méthode Attributes() retourne tous les attributs de l’élément sur lequel elle est appliquée. Les attributs sont retournés sous la forme d’une séquence d’objets XAttribute (voir Listing 7.80). Listing 7.80 : Accès à tous les attributs d’un élément avec la méthode Attributes. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); foreach(XAttribute attr in firstParticipant.Attributes()) { Console.WriteLine(attr); }

Voici le résultat : type="Author" experience="first-time"

Modification d’attributs Comme il a été dit précédemment, les API W3C XML DOM et LINQ to XML manipulent les attributs d’une façon bien différente. Avec l’API W3C, les attributs sont des nœuds enfants du nœud dont ils sont l’attribut. Avec l’API LINQ to XML, les attributs

Linq.book Page 254 Mercredi, 18. février 2009 7:58 07

254

LINQ to XML

Partie III

sont des paires nom/valeur. Ils sont accessibles via la méthode Attributes ou la propriété FirstAttribute de l’élément. Il est important d’avoir cela en mémoire. Les méthodes et propriétés des attributs sont très proches de celles qui ont déjà été étudiées pour les éléments. Vous pouvez utiliser les méthodes suivantes pour ajouter un attribut à un élément : m

XElement.Add() ;

m

XElement.AddFirst() ;

m

XElement.AddBeforeThis() ;

m

XElement.AddAfterThis().

Ces méthodes ont déjà été illustrées dans la section "Ajout de nœuds", un peu plus tôt dans ce chapitre. Reportez-vous aux exemples de cette section pour voir comment ajouter des attributs. Consultez également la section relative à la méthode XElement.SetAttributeValue, un peu plus loin dans ce chapitre. Suppression d’attributs Pour supprimer un attribut, vous utiliserez la méthode XAttribute.Remove. Pour supprimer une séquence d’attributs, vous utiliserez la méthode IEnumerable.Remove.

Vous consulterez également la section XElement.SetAttributeValue(), un peu plus loin dans ce chapitre. XAttribute.Remove() Vous vous rappelez certainement que la méthode Remove de la classe XNode permettait de supprimer un nœud. Quant à elle, la méthode Remove de la classe XAttribute permet de supprimer un attribut (voir Listing 7.81). Listing 7.81 : Suppression d’un attribut. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la suppression de l’attribut"); Console.WriteLine(xDocument); firstParticipant.Attribute("type").Remove(); Console.WriteLine(System.Environment.NewLine + "Après la suppression de l’attribut"); Console.WriteLine(xDocument);

Openmirrors.com

Linq.book Page 255 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

255

Dans cet exemple, nous utilisons la méthode Attribute pour obtenir la référence de l’attribut à supprimer. La méthode Remove est alors appliquée à cette référence. Voici le résultat : Avant la suppression de l’attribut

Joe Rattz

Après la suppression de l’attribut

Joe Rattz

L’attribut type a bien été supprimé. IEnumerable.Remove() où T est un XNode Tout comme la méthode IEnumerable.Remove() de la classe XNode permet de supprimer une séquence de nœuds, la méthode IEnumerable.Remove() de la classe XAttribute permet de supprimer une séquence d’attributs (voir Listing 7.82). Listing 7.82 : Suppression de tous les attributs d’un élément. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la suppression des attributs"); Console.WriteLine(xDocument); firstParticipant.Attributes().Remove(); Console.WriteLine(System.Environment.NewLine + "Après la suppression des attributs"); Console.WriteLine(xDocument);

La méthode Attributes() retourne la séquence des attributs du XElement firstParticipant. La méthode Remove supprime cette séquence. Voici les résultats : Avant la suppression des attributs

Joe Rattz

Linq.book Page 256 Mercredi, 18. février 2009 7:58 07

256

LINQ to XML

Partie III

Après la suppression des attributs

Joe Rattz

Modification de la valeur des attributs Pour modifier la valeur d’un attribut, vous utiliserez la propriété XAttribute.Value (voir Listing 7.83). INFO Reportez-vous également à la section XElement.SetAttributeValue(), un peu plus loin dans ce chapitre. Listing 7.83 : Suppression de tous les attributs d’un élément. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la modification de la valeur de ➥l’attribut"); Console.WriteLine(xDocument); firstParticipant.Attribute("experience").Value = "beginner"; Console.WriteLine(System.Environment.NewLine + "Après la modification de la valeur

➥de l’attribut");

Console.WriteLine(xDocument);

La méthode Attribute a été utilisée pour obtenir une référence à l’attribut experience. La méthode Value a alors été appliquée à cette référence pour accéder à la valeur de l’attribut. Voici le résultat : Avant la modification de la valeur de l’attribut

Joe Rattz

Openmirrors.com

Linq.book Page 257 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

257

Après la modification de la valeur de l’attribut

Joe Rattz

L’attribut experience avait pour valeur "first-time" avant l’exécution du code. Il a désormais pour valeur "beginner".

XElement.SetAttributeValue() La méthode SetAttributeValue est le pendant pour les attributs de la méthode SetElementValue. Tout aussi complète, elle permet d’ajouter, de supprimer et de modifier la valeur d’un attribut. Si un nom d’attribut inexistant lui est passé, cet attribut est ajouté à l’élément. Si un nom d’attribut existant ayant une valeur différente de null lui est passé, l’attribut est mis à jour avec la valeur passée. Enfin, si un nom d’attribut existant ayant la valeur null lui est passé, il est supprimé (voir Listing 7.84). Listing 7.84 : Suppression de tous les attributs d’un élément. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")))); Console.WriteLine(System.Environment.NewLine + "Avant la modification des attributs"); Console.WriteLine(xDocument); // L’attribut "type" existe et le deuxième argument est différent de "null". // L’attribut "type" est donc mis à jour. firstParticipant.SetAttributeValue("type", "beginner"); // L’attribut "language" n’existe pas. Il est donc ajouté à l’élément. firstParticipant.SetAttributeValue("language", "English"); // L’attribut "experience" existe et le deuxième argument a pour valeur "null" // L’attribut "experience" est donc supprimé. firstParticipant.SetAttributeValue("experience", null); Console.WriteLine(System.Environment.NewLine + "Après la modification des attributs"); Console.WriteLine(xDocument);

Linq.book Page 258 Mercredi, 18. février 2009 7:58 07

258

LINQ to XML

Partie III

Ce code met à jour la valeur d’un attribut, définit un nouvel attribut et supprime un attribut existant. Voici les résultats : Avant la modification des attributs

Joe Rattz

Après la modification des attributs

Joe Rattz

Annotations XML En utilisant les annotations de l’API LINQ to XML, il est possible d’associer une donnée utilisateur à une classe quelconque qui hérite de la classe XObject. Il est ainsi possible d’affecter une donnée quelconque (une clé supplémentaire, un objet qui parse les valeurs d’un élément) à un élément, à un document ou à un autre objet dont la classe est dérivée de XObject. Ajout d’annotations avec XObject.AddAnnotation() Voici le prototype de la méthode AddAnnotation() : void XObject.AddAnnotation(object annotation);

Accès aux annotations avec XObject.Annotation() ou

XObject.Annotations() Voici les prototypes de ces deux méthodes : object XObject.Annotation(Type type); T XObject.Annotation(); IEnumerable XObject.Annotations(Type type); IEnumerable XObject.Annotations();

ATTENTION Lorsque vous accédez à des annotations, veillez à passer le type actuel de l’objet, et non sa classe de base ou son interface. Sans quoi l’annotation ne serait pas trouvée.

Suppression d’annotations avec XObject.RemoveAnnotations() Voici les deux prototypes de la méthode RemoveAnnotations() : void XObject.RemoveAnnotations(Type type); void XObject.RemoveAnnotations();

Openmirrors.com

Linq.book Page 259 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

259

Exemples d’annotations À titre d’exemple, nous allons définir un code qui ajoute, retrouve et supprime des annotations. Ici, nous utiliserons l’arbre XML désormais traditionnel BookParticipants. Nous allons associer un handler à chaque élément BookParticipant, en se basant sur son attribut type. Dans cet exemple, le handler affichera l’élément dans un format qui dépend de l’attribut type : un format pour les auteurs, un autre pour les éditeurs. Voici les classes handler utilisées (une pour les auteurs et une pour les éditeurs) : public class AuthorHandler { public void Display(XElement element) { Console.WriteLine("BIOGRAPHIE DE L’AUTEUR"); Console.WriteLine("--------------------------"); Console.WriteLine("Nom : {0} {1}", (string)element.Element("FirstName"), (string)element.Element("LastName")); Console.WriteLine("Langue : {0}", (string)element.Attribute("language")); Console.WriteLine("Expérience : {0}", (string)element.Attribute("experience")); Console.WriteLine("==========================" + System.Environment.NewLine); } } public class EditorHandler { public void Display(XElement element) { Console.WriteLine("BIOGRAPHIE DE L’EDITEUR"); Console.WriteLine("--------------------------"); Console.WriteLine("Nom: {0}", (string)element.Element("FirstName")); Console.WriteLine(" {0}", (string)element.Element("LastName")); Console.WriteLine("==========================" + System.Environment.NewLine); } }

Ce code définit deux classes au comportement distinct. Dans cet exemple, les données de l’élément sont affichées différemment. Bien entendu, le traitement pourrait être tout autre. Les annotations pourraient même ne pas être des handlers… Cet exemple étant plus complexe que les précédents, nous avons divisé le code en plusieurs sections. Chacune d’entre elles sera suivie d’explications (voir Listing 7.85). Listing 7.85 : Ajout, lecture et suppression d’annotations. // Définition d’une référence vers un des éléments de l’arbre XML pour un usage futur XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")),

Linq.book Page 260 Mercredi, 18. février 2009 7:58 07

260

LINQ to XML

Partie III

new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); // Affichage du document Console.WriteLine(xDocument + System.Environment.NewLine);

Ces quelques lignes de code définissent le document XML et l’affichent. Le bloc de code suivant énumère les participants. Pour chacun d’entre eux, un handler est instancié en fonction de l’attribut type et une annotation faisant référence à ce handler est ajoutée à l’élément. // Ajout d’annotations en fonction de la valeur de l’attribut type foreach(XElement e in xDocument.Element("BookParticipants").Elements()) { if((string)e.Attribute("type") == "Author") { AuthorHandler aHandler = new AuthorHandler(); e.AddAnnotation(aHandler); } else if((string)e.Attribute("type") == "Editor") { EditorHandler eHandler = new EditorHandler(); e.AddAnnotation(eHandler); } }

Après l’exécution de ce code, chaque élément BookParticipant possède une annotation qui référence un handler dont le code dépend de la valeur de l’attribut type. Nous allons maintenant énumérer les éléments BookParticipant, retrouver les annotations et exécuter les handlers associés. AuthorHandler aHandler2; EditorHandler eHandler2; foreach(XElement e in xDocument.Element("BookParticipants").Elements()) { if((string)e.Attribute("type") == "Author") { aHandler2 = e.GetAnnotation(); if(aHandler2 != null) { aHandler2.Display(e); } } else if((string)e.Attribute("type") == "Editor") { eHandler2 = e.GetAnnotation(); if(eHandler2 != null) { eHandler2.Display(e); } } }

Ce code exécute la méthode Display du handler associé à chaque élément. Le bloc de code suivant va supprimer les annotations de chaque élément : foreach(XElement e in xDocument.Element("BookParticipants").Elements()) {

Openmirrors.com

Linq.book Page 261 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

261

if((string)e.Attribute("type") == "Author") { e.RemoveAnnotation(); } else if((string)e.Attribute("type") == "Editor") { e.RemoveAnnotation(); } }

Ce code est plus long que les précédents. Il est composé de quatre sections principales. La première section crée l’arbre XML et l’affiche. Ceci n’a rien d’exceptionnel, puisque nous l’avons déjà fait fréquemment dans les autres exemples de cet ouvrage. La deuxième section énumère les éléments BookParticipant et ajoute un handler en fonction de la valeur de leur attribut type. La troisième section énumère les éléments BookParticipant. En fonction de la valeur de leur attribut type, la méthode Display du handler correspondant est exécutée. Enfin, la quatrième section énumère les éléments BookParticipant et supprime les annotations. Dans les sections 2, 3 et 4 du code, l’accès aux attributs s’est fait via un casting au format string. C’est ainsi qu’il a été possible de les comparer aux valeurs "Author" et "Editor". Voici les résultats :

Joe Rattz

Ewan Buckingham

BIOGRAPHIE AUTEUR -------------------------Nom : Joe Rattz Langue : English Expérience: first-time ========================== BIOGRAPHIE EDITEUR -------------------------Nom : Ewan Buckingham ==========================

Ce qu’il faut remarquer dans ces résultats, c’est que les deux handlers sont appelés en fonction de l’attribut type et via les annotations. Retenez également que les annotations peuvent être constituées d’objets quelconques, et pas seulement de handlers.

Linq.book Page 262 Mercredi, 18. février 2009 7:58 07

262

LINQ to XML

Partie III

Événements XML Grâce à l’API LINQ to XML, vous pouvez demander à être informé à tout moment de la modification des objets qui héritent de la classe XObject. Lorsque vous faites une telle demande auprès d’un objet, un événement sera levé si cet objet, ou un de ses descendants, est modifié. Cela signifie que si, par exemple, vous vous abonnez à un événement situé au niveau du document, toutes les modifications effectuées dans l’arbre provoqueront l’appel de la méthode à laquelle vous vous êtes abonné. C’est la raison pour laquelle vous ne devez faire aucune supposition sur le type de l’objet qui provoquera les événements. Lorsque la méthode de traitement est appelée, l’objet qui en est à l’origine est passé en tant qu’émetteur de l’événement. Son type est object. Faites attention lorsque vous lui appliquerez un opérateur de casting, lorsque vous accéderez à ses propriétés ou appellerez ses méthodes. Il se peut que son type ne corresponde pas à ce que vous attendez. Ceci sera illustré dans le Listing 7.86, où l’objet sera de type XText alors que l’on attend un type XElement. Sachez enfin que la construction d’un arbre XML ne génère aucun événement. Comment cela serait-il possible, puisque aucun événement ne peut être enregistré avant la construction de l’arbre ! Seule la modification ou la suppression d’un élément XML peut engendrer un événement, et seulement à condition que cet événement ait été enregistré.

XObject.Changing Cet événement est levé lorsqu’un objet qui hérite de XObject est sur le point d’être modifié. Pour vous abonner à l’événement, vous devez ajouter un objet de type EventHandler à l’événement Changing de l’objet : myobject.Changing += new EventHandler(MyHandler);

Le délégué doit avoir la signature suivante : void MyHandler(object sender, XObjectChangeEventArgs cea)

L’objet sender est celui qui est sur le point d’être modifié et qui provoque la levée de l’événement. La propriété ObjectChange de type XObjectChange de l’objet cea (Change Event Arguments) indique le type de changement qui est sur le point de survenir : XObjectChange.Add, XObjectChange.Name, XObjectChange.Remove ou XObjectChange.Value.

XObject.Changed Cet événement est levé lorsqu’un objet qui hérite de XObject a été modifié. Pour vous abonner à l’événement, vous devez ajouter un objet de type EventHandler à l’événement Changed de l’objet : myobject.Changed += new EventHandler(MyHandler);

Openmirrors.com

Linq.book Page 263 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

263

Le délégué doit avoir la signature suivante : void MyHandler(object sender, XObjectChangeEventArgs cea)

L’objet sender est celui qui a été modifié et qui provoque la levée de l’événement. La propriété ObjectChange de type XObjectChange de l’objet cea (Change Event Arguments) indique le type de changement qui est sur le point de survenir : XObjectChange.Add, XObjectChange.Name, XObjectChange.Remove ou XObjectChange.Value. Quelques exemples d’événements Un exemple va vous aider à bien comprendre toute la logique mise en œuvre pour gérer les événements XObject. Avant d’entrer dans le vif du sujet, nous allons présenter les gestionnaires d’événements utilisés. Cette méthode est exécutée lorsque l’événement Changing d’un élément est levé. Elle permet d’être prévenu lorsqu’un élément est sur le point d’être modifié. public static void MyChangingEventHandler(object sender, XObjectChangeEventArgs cea) { Console.WriteLine("Type de l’objet qui va être modifié : {0}, Type du changement : {1}", sender.GetType().Name, cea.ObjectChange); }

Voici le gestionnaire utilisé pour générer un événement juste après qu’un élément a été modifié. Elle permet d’être prévenu lorsqu’un élément a été modifié. Cette méthode est exécutée lorsque l’événement Changed d’un élément est levé : public static void MyChangedEventHandler(object sender, XObjectChangeEventArgs cea) { Console.WriteLine("Type de l’objet qui a été modifié : {0}, Type du changement : {1}", sender.GetType().Name, cea.ObjectChange); }

Un peu plus tôt, j’ai indiqué qu’un événement serait levé si un descendant d’un objet auquel vous êtes abonné est modifié. Pour illustrer ce fait, nous allons définir une autre méthode que nous enregistrerons une fois le document modifié. Son unique but est de montrer que le document reçoit également un événement Changed, même s’il s’agit d’un descendant situé à plusieurs niveaux hiérarchiques de celui qui a été modifié. Cette méthode est exécutée lorsque l’événement Changed du document XML est levé : public static void DocumentChangedHandler(object sender, XObjectChangeEventArgs cea) { Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : {1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine); }

La seule différence entre les méthodes DocumentChangedHandler et MyChangedEventHandler se situe dans le début de l’affichage : l’affichage effectué dans DocumentChangedHandler débute par le terme "Doc:", afin de signaler que le gestionnaire est appelé par l’événement Changed du document, et non de l’élément. Examinons le code du Listing 7.86.

Linq.book Page 264 Mercredi, 18. février 2009 7:58 07

264

LINQ to XML

Partie III

Listing 7.86 : Le gestionnaire d’événements XObject. XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);

Rien de nouveau pour l’instant. Comme il a été fait à de nombreuses reprises dans les pages précédentes, un document XML a été créé en utilisant la construction fonctionnelle, puis affiché dans la console. Remarquez également qu’une référence au premier élément BookParticipant a été mémorisée. Les événements seront déclenchés par rapport à cet élément : firstParticipant.Changing += new EventHandler(MyChangingEventHandler); firstParticipant.Changed += new EventHandler(MyChangedEventHandler); xDocument.Changed += new EventHandler(DocumentChangedHandler);

Après l’exécution de ces lignes de code, un événement sera généré : m

juste avant (Changing) le changement du premier élément BookParticipant ;

m

juste après (Changed) le changement du premier élément BookParticipant ;

m

juste après (Changed) la modification du document.

Le dernier type d’événement a été mis en place pour prouver que des événements sont générés lorsqu’un objet decendant est modifié. Il ne reste plus qu’à effectuer une modification dans l’élément firstParticipant. firstParticipant.Element("FirstName").Value = "Seph"; Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);

La première ligne change la valeur de l’élément FirstName du premier élément BookParticipant. La deuxième ligne affiche le document XML résultant. Voici les résultats :

Joe Rattz

Ewan Buckingham

Openmirrors.com

Linq.book Page 265 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

265

Type de l’objet qui va être modifié : XText, Type du changement : Suppression Type de l’objet qui a été changé : XText, Type du changement : Suppression Doc: Type de l’objet qui a été changé : XText, Type du changement : Suppression Type de l’objet qui va être modifié : XText, Type du changement : Add Type de l’objet qui a été modifié : XText, Type du changement : Add Doc: Type de l’objet qui a été modifié : XText, Type du changement : Add

Seph Rattz

Ewan Buckingham

Cette sortie console montre le document avant et après l’utilisation du gestionnaire d’événements. Comme vous pouvez le constater, l’élément FirstName du premier BookParticipant a été modifié. Les lignes situées entre les deux affichages de l’arbre XML correspondent aux messages affichés par les gestionnaires d’événements. L’objet modifié est de type XText. Pour ma part, je m’attendais à ce qu’il soit de type XElement. Il est facile d’oublier que, lorsque vous affectez une chaîne à la valeur d’un élément, un objet XText est automatiquement créé, de façon transparente. En regardant d’un peu plus près le texte affiché par les gestionnaires d’événements, on comprend mieux ce qu’il se passe lorsqu’un élément est modifié : dans le premier bloc de trois lignes, la valeur XText est sur le point d’être supprimée, puis elle est supprimée. L’événement Changed du document est alors levé. Cela montre que les événements se propagent du niveau le plus bas au niveau le plus haut. Dans le deuxième bloc de trois lignes, la même suite d’événements est générée mais, ici, un objet XText est ajouté à l’arbre XML. Vous savez maintenant que, lorsque vous modifiez la valeur d’un élément, un objet XText est supprimé puis restauré. Dans cet exemple, nous avons utilisé des méthodes nommées. Cette démarche n’est nullement obligatoire : il est également possible d’utiliser des méthodes anonymes ou des expressions lambda. Le Listing 7.87 est identique au précédent mais, au lieu d’utiliser les gestionnaires d’événements déjà implémentés, nous définissons des expressions lambda pour définir à la volée le code appelé par les événements. Listing 7.87 : Gestion d’un événement XObject avec des expressions lambda. XElement firstParticipant; XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"),

Linq.book Page 266 Mercredi, 18. février 2009 7:58 07

266

LINQ to XML

Partie III

new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine); firstParticipant.Changing += new EventHandler( (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Type de l’objet qui va être modifié : {0}, Type of change: {1}", sender.GetType().Name, cea.ObjectChange)); firstParticipant.Changed += (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Type de l’objet qui a été modifié : {0}, Type du changement : {1}", sender.GetType().Name, cea.ObjectChange); xDocument.Changed += (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : ➥{1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine); xDocument.Changed += new XObjectChangeEventHandler((sender, cea) => Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : ➥{1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine)); firstParticipant.Element("FirstName").Value = "Seph"; Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);

Ce code se suffit à lui-même. Il ne dépend d’aucun des gestionnaires d’événements précédemment écrits. Voici les résultats :

Joe Rattz

Ewan Buckingham

Type of object changing: XText, Type of change: Remove Type of object changed: XText, Type of change: Remove Doc: Type of object changed: XText, Type of change: Remove Type de l’objet qui va être modifié : XText, Type du changement : Add Type de l’objet qui a été modifié : XText, Type du changement : Add Doc: Type de l’objet qui a été modifié : XText, Type du changement : Add

Seph Rattz

Openmirrors.com

Linq.book Page 267 Mercredi, 18. février 2009 7:58 07

Chapitre 7

L’API LINQ to XML

267

Ewan Buckingham

Les résultats sont identiques à ceux du listing précédent. Avouez que les expressions lambda sont vraiment pratiques et efficaces. Les développeurs qui donnent leurs premières impressions sur LINQ disent souvent qu’ils n’apprécient pas les expressions lambda. Peut-être est-ce parce qu’elles sont nouvelles et très différentes. Mais avouez que cet exemple a de quoi les réconcilier avec ce nouvel outil. Le bogue d’Halloween Vous rappelez-vous du "bogue d’Halloween", introduit au début de ce chapitre ? De grâce, résistez à l’envie qui vous poussera certainement à intervenir sur la portion d’arbre XML dans laquelle vous capturez des événements. Le contenu de l’arbre XML et les événements générés pourraient en effet prendre une tournure incontrôlable.

Résumé Dans ce chapitre, nous avons vu comment utiliser LINQ to XML pour créer, modifier et parcourir des documents XML, ainsi que pour interroger des objets XML à l’aide de requêtes. Vous avez pu voir que la nouvelle API apporte une grande flexibilité : elle permet de créer un élément XML à la volée, de l’initialiser et de le placer dans un arbre XML en une seule instruction. L’API W3C DOM XML en est totalement incapable. C’est la raison pour laquelle l’API LINQ to XML a été conçue. Ce chapitre vous a montré comment appliquer une requête LINQ sur un objet XML unique. Les requêtes portaient par exemple sur les descendants ou les ancêtres d’un élément. À travers de nouveaux opérateurs XML, le chapitre suivant va vous montrer comment appliquer une requête LINQ sur une séquence d’éléments (les descendants d’une séquence, par exemple).

Linq.book Page 268 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 269 Mercredi, 18. février 2009 7:58 07

8 Les opérateurs LINQ to XML Les requêtes prises en exemple au chapitre précédent se contentaient de retourner tous les éléments enfants ou tous les ancêtres d’un nœud. Vous rappelez-vous des exemples qui faisaient appel à la méthode XContainer.Elements ? Dans l’affirmative, vous savez ce qu’est une requête XML. C’est là une autre preuve de l’intégration parfaite des requêtes LINQ dans le langage : il est parfois facile d’oublier que l’on est en train d’effectuer une requête. Comme beaucoup des méthodes examinées jusqu’ici retournent une séquence d’objets XML, c’est-à-dire des IEnumerable (où T est une classe de l’API LINQ to XML), il est possible d’appeler les opérateurs de requête standard sur la séquence retournée, ce qui procure encore plus de puissance et de flexibilité. Il est donc possible d’obtenir une séquence d’objets XML à partir d’un objet XML unique (les descendants ou les ancêtres d’un objet, par exemple) mais, ce qui manque, ce sont des opérateurs qui pourraient s’appliquer sur chacun des éléments de ces séquences. À titre d’exemple, il n’existe aucune façon simple d’obtenir une séquence d’éléments et d’effectuer une autre opération XML spécifique sur chacun des éléments de la séquence retournée, comme connaître les éléments enfants de chacun des éléments de la séquence. Pour dire les choses autrement, vous pouvez obtenir une séquence des éléments enfants d’un élément en appelant la méthode Elements de cet élément, mais vous ne pouvez pas obtenir une séquence des éléments enfants des éléments enfants d’un élément. Ceci parce que la méthode Elements doit être appelée sur un XContainer (XElement ou XDocument, par exemple), mais pas sur une séquence d’objets XContainer. C’est à ce point précis que les opérateurs LINQ to XML vont vous venir en aide.

Linq.book Page 270 Mercredi, 18. février 2009 7:58 07

270

LINQ to XML

Partie III

Introduction aux opérateurs LINQ to XML L’API LINQ to XML étend les opérateurs de requête standard de LINQ to Objects en y ajoutant des opérateurs spécifiques au XML. Ces opérateurs sont des méthodes d’extension définies dans la classe System.Xml.Linq.Extensions, qui joue le rôle d’une classe conteneur. Chacun de ces opérateurs est appelé sur une séquence d’un type de donnée LINQ to XML et effectue une action sur chacune des entrées de cette séquence. Il retourne par exemple les ancêtres ou les descendants des différentes entrées. Virtuellement, chacun des opérateurs XML décrits dans ce chapitre a un équivalent dans le chapitre précédent. Cependant, les méthodes du chapitre précédent ne s’appliquent qu’à un objet unique, alors que les opérateurs de ce chapitre s’appliquent à une séquence d’objets. À titre d’exemple, au chapitre précédent, nous avons parlé de la méthode XContainer.Elements, dont voici le prototype : IEnumerable XContainer.Elements()

Dans ce chapitre, nous aborderons l’opérateur Extensions.Elements, dont voici le prototype : IEnumerable Elements (this IEnumerable source) where T : XContainer

Il existe une différence de taille entre ces deux méthodes : le premier prototype est appelé sur un objet unique dérivé de XContainer, alors que le second est appelé sur une séquence d’objets dont chacun est dérivé de XContainer. Pour bien différencier les méthodes du chapitre précédent des méthodes d’extensions de ce chapitre, nous qualifierons les secondes du terme "opérateurs". Et, maintenant, il est temps d’entrer dans le vif du sujet.

Opérateur Ancestors L’opérateur Ancestors est appelé sur une séquence de nœuds. Il retourne une séquence qui contient les éléments ancêtres de chacun des nœuds sources. Prototypes L’opérateur Ancestors a deux prototypes. Premier prototype public static IEnumerable Ancestors ( this IEnumerable source ) where T : XNode

Cette version de l’opérateur peut être appelée sur une séquence de nœuds ou d’objets dérivés de XNode. Elle retourne une séquence d’éléments contenant les ancêtres de chacun des nœuds de la séquence source. Openmirrors.com

Linq.book Page 271 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

271

Second prototype public static IEnumerable Ancestors ( this IEnumerable source, XName name ) where T : XNode

Ce prototype est identique au précédent mais, ici, un nom est passé dans les arguments. Seuls les ancêtres qui correspondent à ce nom sont retournés dans la séquence de sortie. Exemples Le Listing 8.1 donne un exemple d’appel du premier prototype. Listing 8.1 : Un exemple d’appel du premier prototype de l’opérateur Ancestors. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Element source : {0} : Valeur = {1}", element.Name, element.Value); } // Affichage des éléments ancêtres des éléments sources foreach (XElement element in elements.Ancestors()) { Console.WriteLine("Elément ancêtre : {0}", element.Name); }

Les premières lignes de ce code définissent un document XML. Une séquence d’éléments FirstName est alors générée (rappelez-vous, la méthode Ancestors est appelée sur une séquence de nœuds, et non sur un nœud unique. Il est donc nécessaire de créer une séquence). Pour faciliter l’identification des nœuds, nous allons afficher leurs noms. Étant donné que les éléments ont un nom, mais pas les nœuds, nous avons choisi de définir une séquence d’éléments, et non de nœuds. Le dernier bloc de code énumère les éléments retournés par la méthode Ancestors et les affiche. Voici les résultats : Élément Élément Élément Élément Élément Élément

source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : BookParticipant ancêtre : BookParticipants ancêtre : BookParticipant ancêtre : BookParticipants

Linq.book Page 272 Mercredi, 18. février 2009 7:58 07

272

LINQ to XML

Partie III

Comme vous pouvez le voir, ces résultats affichent les deux éléments de la séquence source, puis les ancêtres de ces éléments. L’opérateur Ancestors retourne tous les éléments ancêtres de chaque nœud sous la forme d’une séquence de nœuds. Dans cet exemple, la séquence utilisée est composée d’éléments, mais cela ne pose pas de problème, puisque les éléments sont dérivés de XNode. Assurez-vous que vous faites bien la différence entre l’opérateur Ancestors, appelé sur une séquence de nœuds, et la méthode Ancestors, étudiée au chapitre précédent. Cet exemple n’est pas aussi impressionnant qu’il peut le paraître. Le code a en effet été étendu à des fins démonstratives. Nous avons ainsi utilisé quelques lignes de code pour énumérer les éléments de la séquence FirstName (appel à la méthode Descendants et bloc foreach suivant). La seconde boucle foreach appelle l’opérateur Ancestors et affiche les ancêtres. Dans cette deuxième boucle, il aurait été possible d’appeler la méthode Ancestors du chapitre précédent sur chacun des éléments de la séquence d’éléments FirstName. Cette technique est illustrée dans le Listing 8.2. Listing 8.2 : Même résultat que le listing précédent, mais sans appeler l’opérateur Ancestors. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value); } foreach (XElement element in elements) { // Appel de la méthode Ancestors sur chaque élément foreach(XElement e in element.Ancestors()) // Affichage des ancêtres de chaque élément source Console.WriteLine("Elément ancêtre : {0}", e.Name); }

Cet exemple est différent du précédent : ici, au lieu d’appeler l’opérateur Ancestors sur les éléments de la séquence dans la boucle foreach, la boucle applique la méthode Openmirrors.com

Linq.book Page 273 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

273

Ancestors du chapitre précédent à chacun des éléments de la séquence. Le résultat est le même que celui du listing précédent : Elément Elément Elément Elément Elément Elément

source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : BookParticipant ancêtre : BookParticipants ancêtre : BookParticipant ancêtre : BookParticipants

Grâce à l’opérateur Ancestors et à la concision de LINQ, cette requête peut être résumée à une déclaration bien plus réduite (voir Listing 8.3). Listing 8.3 : Un exemple concis d’appel de l’opérateur Ancestors. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); foreach (XElement element in xDocument.Element("BookParticipants").Descendants("FirstName").Ancestors()) { Console.WriteLine("Elément ancêtre : {0}", element.Name); }

Dans cet exemple, l’opérateur Ancestors est directement appelé sur la séquence d’éléments retournés par la méthode Descendants. Cette dernière retourne une séquence d’éléments, et l’opérateur Ancestors retourne une autre séquence d’éléments qui contient tous les ancêtres de chacun des éléments de la première séquence. Contrairement aux deux listings précédents, les éléments FirstName ne sont pas affichés. Mais, bien évidemment, les ancêtres sont les mêmes : Elément Elément Elément Elément

ancêtre ancêtre ancêtre ancêtre

: : : :

BookParticipant BookParticipants BookParticipant BookParticipants

En production, vous opterez certainement pour un code concis, semblable à celui présenté dans le Listing 8.3. Cependant, dans la suite de ce chapitre, nous utiliserons un code plus verbeux, comparable à celui du Listing 8.1. Pour illustrer le second prototype de l’opérateur Ancestors, nous utiliserons le même code que dans le Listing 8.1, mais nous changerons l’appel à l’opérateur Ancestors, de sorte qu’il limite la sortie aux ancêtres ayant pour valeur BookParticipant (voir Listing 8.4).

Linq.book Page 274 Mercredi, 18. février 2009 7:58 07

274

LINQ to XML

Partie III

Listing 8.4 : Appel du second prototype de l’opérateur Ancestors. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des ancêtres de chaque élément source foreach (XElement element in elements.Ancestors("BookParticipant")) { Console.WriteLine("Elément ancêtre : {0}", element.Name); }

Les résultats sont semblables à ceux du Listing 8.1 mais, cette fois-ci, les ancêtres BookParticipants ne sont pas affichés : Elément Elément Elément Elément

source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : BookParticipant ancêtre : BookParticipant

Opérateur AncestorsAndSelf L’opérateur AncestorsAndSelf est appelé sur une séquence d’éléments. Il retourne une séquence qui contient les éléments ancêtres de chacun des éléments sources, ainsi que l’élément source. Cet opérateur est assez proche de l’opérateur Ancestors, si ce n’est qu’il ne peut être appelé que sur des éléments et qu’il inclut l’élément source dans la séquence de sortie. Prototypes L’opérateur AncestorsAndSelf a deux prototypes. Premier prototype public static IEnumerable AncestorsAndSelf ( this IEnumerable source )

Openmirrors.com

Linq.book Page 275 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

275

Ce prototype de l’opérateur AncestorsAndSelf est appelé sur une séquence d’éléments. Il retourne une séquence d’éléments composée des éléments sources et de leurs éléments ancêtres. Second prototype public static IEnumerable AncestorsAndSelf ( this IEnumerable source, XName name )

Ce prototype est identique au précédent mais, ici, un nom est passé dans les arguments. Seuls les éléments sources et les ancêtres qui correspondent à ce nom sont retournés dans la séquence de sortie. Exemples Pour illustrer le premier prototype de l’opérateur AncestorsAndSelf, nous utiliserons le même exemple que dans le Listing 8.1 mais, ici, nous appellerons l’opérateur AncestorsAndSelf et non l’opérateur Ancestors (voir Listing 8.5). Listing 8.5 : Appel du premier prototype de l’opérateur AncestorsAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des éléments sources et de leurs ancêtres foreach (XElement element in elements.AncestorsAndSelf()) { Console.WriteLine("Elément ancêtre : {0}", element.Name); }

Le premier bloc de code crée le document XML. Une séquence d’éléments FirstName est ensuite générée (la méthode AncestorsAndSelf étant appelée sur une séquence d’éléments, et non sur un élément unique, il est donc nécessaire de créer une séquence). Les éléments de la séquence source sont ensuite énumérés et affichés. Enfin, la séquence retournée par AncestorsAndSelf est énumérée et les éléments résultants, affichés.

Linq.book Page 276 Mercredi, 18. février 2009 7:58 07

276

LINQ to XML

Partie III

Si tout fonctionne comme prévu, les résultats devraient être identiques à ceux affichés par le premier exemple du prototype Ancestors mais, ici, les éléments de la séquence FirstName devraient également être inclus. Elément Elément Elément Elément Elément Elément Elément Elément

source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : FirstName ancêtre : BookParticipant ancêtre : BookParticipants ancêtre : FirstName ancêtre : BookParticipant ancêtre : BookParticipants

Pour illustrer le second prototype de l’opérateur AncestorsAndSelf, nous utiliserons le même code que dans l’exemple du second prototype de l’opérateur Ancestors. Mais ici, bien entendu, nous utiliserons l’opérateur AncestorsAndSelf et non l’opérateur Ancestors (voir Listing 8.6). Listing 8.6 : Appel du second prototype de l’opérateur AncestorsAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Descendants("FirstName"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value); } // Affichage des ancêtres de chaque élément source foreach (XElement element in elements.AncestorsAndSelf("BookParticipant")) { Console.WriteLine("Elément ancêtre: {0}", element.Name); }

Voici les résultats. Les ancêtres FirstName et BookParticipants ont été éliminés, car ils ne correspondent pas au paramètre passé à l’opérateur AncestorsAndSelf : Elément Elément Elément Elément

Openmirrors.com

source : FirstName : valeur = Joe source : FirstName : valeur = Ewan ancêtre : BookParticipant ancêtre : BookParticipant

Linq.book Page 277 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

277

Le second prototype de cet opérateur semble avoir peu d’intérêt. En effet, pensez-vous que deux niveaux d’éléments ou plus portant le même nom puissent cohabiter dans un arbre XML ?

Opérateur Attributes L’opérateur Attributes est appelé sur une séquence d’éléments. Il retourne une séquence contenant les attributs de chacun des éléments sources. Prototypes L’opérateur Attributes a deux prototypes. Premier prototype public static IEnumerable Attributes ( this IEnumerable source )

Ce premier prototype est appelé sur une séquence d’éléments. Il retourne une séquence contenant tous les attributs des éléments sources. Second prototype public static IEnumerable Attributes ( this IEnumerable source, XName name )

Ce prototype est identique au précédent mais, ici, seuls les attributs qui correspondent au nom passé en argument sont retournés dans la séquence de sortie. Exemples Pour illustrer le premier prototype, nous allons ajouter des attributs à l’arbre XML utilisé dans les exemples précédents. Nous travaillerons donc avec une séquence d’éléments BookParticipant (voir Listing 8.7). Listing 8.7 : Appel du premier prototype Attributes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources

Linq.book Page 278 Mercredi, 18. février 2009 7:58 07

278

LINQ to XML

Partie III

foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des attributs des éléments sources foreach (XAttribute attribute in elements.Attributes()) { Console.WriteLine("Attribut : {0} : valeur = {1}", attribute.Name, attribute.Value); }

La séquence d’éléments BookParticipant est générée puis affichée. L’opérateur Attributes est alors appelé sur cette séquence et les attributs des éléments sont affichés à l’aide d’une boucle foreach. Voici les résultats : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Attribut : type : valeur = Author Attribut : type : valeur = Editor

Pour illustrer le second prototype, nous utiliserons le même code que dans l’exemple précédent, mais nous passerons un nom à l’opérateur Attributes. Seuls les attributs portant ce nom seront inclus dans la séquence de sortie (voir Listing 8.8). Listing 8.8 : Appel du second prototype Attributes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des attributs des éléments sources foreach (XAttribute attribute in elements.Attributes("type")) { Console.WriteLine("Attribut : {0} : valeur = {1}", attribute.Name, attribute.Value); }

Openmirrors.com

Linq.book Page 279 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

279

Seuls les attributs portant le nom "type" sont retournés dans la séquence de sortie. Voici les résultats obtenus suite à l’appui sur Ctrl+F5 : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Attribut : type : valeur = Author Attribut : type : valeur = Editor

Si nous avions passé le paramètre "type" à l’opérateur Attributes, les deux attributs n’auraient pas été affichés. Cet opérateur est donc sensible à la casse, ce qui n’a rien de surprenant, puisque XML est un langage sensible à la casse.

Opérateur DescendantNodes L’opérateur DescendantNodes est appelé sur une séquence d’éléments ou de documents. Il retourne une séquence contenant les nœuds descendants de chacun des éléments ou documents sources. Prototype L’opérateur DescendantNodes a un seul prototype : public static IEnumerable DescendantNodes ( this IEnumerable source ) where T : XContainer

Cet opérateur est différent de la méthode XContainer.DescendantNodes. Le premier est appelé sur une séquence d’éléments ou de documents, la deuxième, sur un élément ou un document unique. Exemple Nous utiliserons le même arbre XML que dans les exemples précédents mais, ici, nous ajouterons un commentaire dans le premier élément BookParticipant. Ceci afin que l’opérateur DescendantNodes retourne au moins un nœud qui n’est pas un élément. Les éléments BookParticipant ayant plusieurs descendants, nous leur appliquerons l’opérateur DescendantNodes (voir Listing 8.9). Listing 8.9 : Appel du prototype de l’opérateur DescendantNodes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Linq.book Page 280 Mercredi, 18. février 2009 7:58 07

280

LINQ to XML

Partie III

IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.DescendantNodes()) { Console.WriteLine("Nœud descendant : {0}", node); }

Les premières lignes définissent l’arbre XML. Une séquence d’éléments BookParticipant est alors définie. Les éléments de cette séquence sont affichés, puis l’opérateur DescendantNodes lui est appliqué. Voici les résultats : Elément source : Elément source : Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant Noeud descendant

BookParticipant : valeur = JoeRattz BookParticipant : valeur = EwanBuckingham : : Joe : Joe : Rattz : Rattz : Ewan : Ewan : Buckingham : Buckingham

Comme vous pouvez le voir, l’opérateur DescendantNodes renvoie tous les nœuds descendants de la séquence BookParticipant : les éléments, mais également le commentaire. Remarquez aussi que chacun des éléments descendants donne lieu à deux nœuds. Par exemple, Joe et Joe sont les deux nœuds descendants relatifs à l’élément Joe. Le premier est l’élément lui-même et le deuxième, sa valeur XText. Je suis sûr que vous aviez oublié que des objets XText sont automatiquement créés pour chaque élément…

Opérateur DescendantNodesAndSelf L’opérateur DescendantNodesAndSelf est appelé sur une séquence d’éléments. Il retourne une séquence contenant les éléments sources et leurs nœuds descendants. Prototype L’opérateur DescendantNodesAndSelf a un seul prototype : public static IEnumerable DescendantNodesAndSelf ( this IEnumerable source )

Openmirrors.com

Linq.book Page 281 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

281

Exemple Nous utiliserons le même code que pour illustrer l’opérateur DescendantNodes mais, ici, nous appellerons l’opérateur DescendantNodesAndSelf (voir Listing 8.10). Listing 8.10 : Appel du prototype de l’opérateur DescendantNodesAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des noeuds descendants des éléments sources foreach (XNode node in elements.DescendantNodesAndSelf()) { Console.WriteLine("Noeud descendant : {0}", node); }

Voici le résultat : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Noeud descendant :

Joe Rattz

Noeud descendant : Noeud descendant : Joe Noeud descendant : Joe Noeud descendant : Rattz Noeud descendant : Rattz Noeud descendant : Ewan Buckingham

Noeud descendant : Ewan Noeud descendant : Ewan Noeud descendant : Buckingham Noeud descendant : Buckingham

L’opérateur DescendantNodesAndSelf a retourné les éléments de la séquence d’entrée et leurs nœuds descendants, y compris le commentaire du premier élément BookParticipant.

Linq.book Page 282 Mercredi, 18. février 2009 7:58 07

282

LINQ to XML

Partie III

Comme vous avez pu le voir dans l’exemple précédent, l’opérateur DescendantNodes "oublie" le commentaire dans la séquence de sortie. Cette différence sera étudiée un peu plus loin dans ce chapitre.

Opérateur Descendants L’opérateur Descendants peut être appelé sur une séquence d’éléments ou de documents. Il retourne une séquence qui contient tous les éléments descendants des éléments ou documents sources. Prototypes L’opérateur Descendants a deux prototypes. Premier prototype public static IEnumerable Descendants ( this IEnumerable source ) where T : XContainer

Cet opérateur est différent de la méthode XContainer.Descendants. Le premier est appelé sur une séquence d’éléments ou de documents, la deuxième, sur un élément ou un document unique. Second prototype public static IEnumerable Descendants ( this IEnumerable source, XName name ) where T : XContainer

Ce prototype est identique au précédent mais, ici, seuls les descendants des éléments sources dont le nom correspond au paramètre sont retournés dans la séquence de sortie. Exemples Pour illustrer le premier prototype, nous allons utiliser le même code que pour l’opérateur DescendantNodes, mais nous allons appeler l’opérateur Descendants. Les résultats devraient être les mêmes, à ceci près que seuls les éléments devraient être retournés dans la séquence de sortie. Le Listing 8.11 représente le code utilisé pour illustrer ce prototype. Listing 8.11 : Appel du premier prototype de l’opérateur Descendants. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Openmirrors.com

Linq.book Page 283 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

283

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.Descendants()) { Console.WriteLine("Nœud descendant : {0}", node); }

Seuls les éléments descendants des deux éléments BookParticipant sont inclus dans la séquence de sortie : Elément Elément Elément Elément Elément Elément

source : BookParticipant : valeur = JoeRattz source : BookParticipant : valeur = EwanBuckingham descendant : Joe descendant : Rattz descendant : Ewan descendant : Buckingham

En comparant ces résultats à ceux de l’opérateur DescendantNodes, nous pouvons noter plusieurs différences : m

les descendants apparaissent en tant qu’éléments et non en tant que nœuds ;

m

le commentaire n’est pas inclus dans la séquence de sortie ;

m

les nœuds descendants (Joe et Ratz, par exemple) sont exclus de la séquence de sortie, puisqu’ils sont de type XText et non XElement.

Nous illustrerons le second prototype avec le même code mais, ici, nous passerons un nom dans l’argument de l’opérateur. Seuls les descendants correspondants seront inclus dans la séquence de sortie (voir Listing 8.12). Listing 8.12 : Appel du second prototype de l’opérateur Descendants. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements =

Linq.book Page 284 Mercredi, 18. février 2009 7:58 07

284

LINQ to XML

Partie III

xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.Descendants("LastName")) { Console.WriteLine("Nœud descendant : {0}", node); }

Voici les résultats. Comme on pouvait s’y attendre, seul le descendant LastName est inclus dans la séquence de sortie : Elément Elément Elément Elément

source : BookParticipant : valeur = JoeRattz source : BookParticipant : valeur = EwanBuckingham descendant : Rattz descendant : Buckingham

Opérateur DescendantsAndSelf L’opérateur DescendantsAndSelf est appelé sur une séquence d’éléments. Il retourne une séquence qui contient tous les éléments descendants des éléments sources. Prototypes L’opérateur DescendantsAndSelf a deux prototypes. Premier prototype public static IEnumerable DescendantsAndSelf ( this IEnumerable source )

Ce prototype est appelé sur une séquence d’éléments. Il retourne une séquence qui contient tous les éléments de la séquence et leurs descendants. Second prototype public static IEnumerable DescendantsAndSelf ( this IEnumerable source, XName name )

Le second prototype est semblable au premier, mais seuls les éléments qui correspondent au paramètre sont retournés dans la séquence de sortie. Exemples Pour illustrer le premier prototype, nous utiliserons le même code que dans le premier exemple de l’opérateur Descendants mais, ici, nous appellerons l’opérateur DescendantAndSelf (voir Listing 8.13). Openmirrors.com

Linq.book Page 285 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

285

Listing 8.13 : Appel du premier prototype de l’opérateur DescendantsAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.DescendantsAndSelf()) { Console.WriteLine("Nœud descendant : {0}", node); }

Voici les résultats : Elément source : BookParticipant : valeur = JoeRattz Elément source : BookParticipant : valeur = EwanBuckingham Elément descendant :

Joe Rattz

Elément descendant : Joe Elément descendant : Rattz Elément descendant : Ewan Buckingham

Elément descendant : Ewan Elément descendant : Buckingham

Les résultats sont identiques à ceux du premier prototype de l’opérateur Descendants, à ceci près qu’ils incluent également les éléments sources eux-mêmes, c’est-à-dire les éléments BookParticipant. Ne soyez pas trompé par la présence du commentaire dans les résultats. Cet objet est non pas un résultat retourné par l’opérateur, mais bel et bien une partie de la séquence

Linq.book Page 286 Mercredi, 18. février 2009 7:58 07

286

LINQ to XML

Partie III

d’entrée incluse dans les résultats (c’est la partie Self de l’opérateur DescendantsAndSelf). Pour illustrer le second prototype, nous utiliserons le même code, mais nous passerons un paramètre à l’opérateur pour limiter la sortie (voir Listing 8.14). Listing 8.14 : Appel du second prototype de l’opérateur DescendantsAndSelf. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XNode node in elements.DescendantsAndSelf("LastName")) { Console.WriteLine("Nœud descendant : {0}", node); }

Voici les résultats : Elément Elément Elément Elément

source : BookParticipant : valeur = JoeRattz source : BookParticipant : valeur = EwanBuckingham descendant : Rattz descendant : Buckingham

La sortie est bien plus limitée que dans l’exemple précédent. Il est même difficile de faire la différence entre les opérateurs Descendants et DescendantsAndSelf. Ceci vient du fait que les éléments sources n’ont pas été retournés, car ils ne correspondaient pas au paramètre passé à l’opérateur. Il est peu probable que vous ayez à utiliser la version "AndSelf" du second prototype de l’opérateur Descendants. En effet, les arbres XML que vous manipulerez n’ont que peu de chances d’avoir des éléments portant le même nom sur plusieurs niveaux hiérarchiques. Openmirrors.com

Linq.book Page 287 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

287

Opérateur Elements L’opérateur Elements peut être appelé sur une séquence d’éléments ou de documents. Il retourne une séquence d’éléments qui contient tous les éléments enfants des éléments ou documents sources. Les opérateurs Elements et Descendants sont différents. En effet, l’opérateur Elements ne retourne que les éléments enfants de premier niveau, alors que l’opérateur Descendants retourne tous les enfants de la séquence d’entrée, en parcourant récursivement tous les niveaux hiérarchiques de l’arborescence. Prototypes L’opérateur Elements a deux prototypes. Premier prototype public static IEnumerable Elements ( this IEnumerable source ) where T : XContainer

Ce premier prototype est appelé sur une séquence d’éléments ou de documents. Il retourne une séquence d’éléments qui contient tous les éléments enfants des éléments ou documents sources. Cet opérateur est différent de la méthode XContainer.Elements. Le premier est appelé sur une séquence d’éléments ou de documents, la deuxième, sur un élément ou un document unique. Second prototype public static IEnumerable Elements ( this IEnumerable source, XName name ) where T : XContainer

Ce prototype est identique au premier mais, ici, seuls les éléments correspondant au paramètre passé à l’opérateur sont retournés dans la séquence de sortie. Exemples Nous utiliserons le même code que dans l’exemple du premier prototype de l’opérateur DescendantsAndSelf mais, ici, nous invoquerons l’opérateur Elements (voir Listing 8.15). Listing 8.15 : Appel du premier prototype de l’opérateur Elements. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Linq.book Page 288 Mercredi, 18. février 2009 7:58 07

288

LINQ to XML

Partie III

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XElement element in elements.Elements()) { Console.WriteLine("Elément enfant : {0}", element); }

Voici les résultats : Elément Elément Elément Elément Elément Elément

source source enfant enfant enfant enfant

: : : : : :

BookParticipant : valeur = JoeRattz BookParticipant : valeur = EwanBuckingham Joe Rattz Ewan Buckingham

Cet exemple retourne tous les éléments enfants de la séquence d’entrée. Pour limiter la séquence de sortie aux seuls éléments dont le nom est spécifié, nous utiliserons le second prototype de l’opérateur Elements (voir Listing 8.16). Listing 8.16 : Appel du second prototype de l’opérateur Elements. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant"); // Affichage des éléments sources foreach (XElement element in elements) { Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value); } // Affichage des nœuds descendants des éléments sources foreach (XElement element in elements.Elements("LastName")) { Console.WriteLine("Elément enfant : {0}", element); }

Openmirrors.com

Linq.book Page 289 Mercredi, 18. février 2009 7:58 07

Chapitre 8

Les opérateurs LINQ to XML

289

Voici les résultats : Elément Elément Elément Elément

source source enfant enfant

: : : :

BookParticipant : valeur = JoeRattz BookParticipant : valeur = EwanBuckingham Rattz Buckingham

Opérateur InDocumentOrder L’opérateur InDocumentOrder est appelé sur une séquence de nœuds. Il retourne une séquence composée des nœuds enfants des nœuds sources, dans l’ordre du document. Prototype L’opérateur InDocumentOrder a un seul prototype : public static IEnumerable InDocumentOrder ( this IEnumerable source ) where T : XNode

Cet opérateur doit être appelé sur une séquence composée de nœuds ou d’objets dérivés. Il retourne une séquence du même type composée des nœuds enfants des nœuds sources, dans l’ordre du document. Exemple Pour illustrer cet opérateur, nous avons besoin d’une séquence de nœuds, éléments et non éléments. Pour ce faire, nous utiliserons la séquence des nœuds enfants des éléments BookParticipant. L’un des nœuds est un commentaire, pas un élément. Nous verrons ainsi comment l’opérateur InDocumentOrder se comporte sur ce type de nœud (voir Listing 8.17). Listing 8.17 : Appel du prototype de l’opérateur InDocumentOrder. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable nodes = xDocument.Element("BookParticipants").Elements("BookParticipant"). Nodes().Reverse();

Linq.book Page 290 Mercredi, 18. février 2009 7:58 07

290

LINQ to XML

Partie III

// Affichage des nœuds sources foreach (XNode node in nodes) { Console.WriteLine("Noeud source : {0}", node); } // Affichage des noeuds enfants des noeuds sources foreach (XNode node in nodes.InDocumentOrder()) { Console.WriteLine("Noeud ordonné : {0}", node); }

Après avoir construit l’arbre XML, les nœuds enfants des éléments BookParticipants sont obtenus en invoquant l’opérateur Nodes. L’opérateur Reverse est appliqué au résultat de l’opérateur Nodes pour inverser l’ordre de la séquence (si nécessaire, reportez-vous à la section relative à l’opérateur LINQ to SQL Reverse, dans la deuxième partie de l’ouvrage, pour avoir des informations complémentaires). La séquence utilisée en entrée de l’opérateur InDocumentOrder est donc composée des nœuds des éléments BookParticipant, disposés dans l’ordre inverse de celui du document. Voici le résultat : Noeud Noeud Noeud Noeud Noeud Noeud Noeud Noeud Noeud Noeud

source : Buckingham source : Ewan source : Rattz source : Joe source : ordonné : Joe Rattz

Ewan Buckingham

Nous allons maintenant illustrer le second prototype. Plutôt que nous contenter d’obtenir puis de supprimer une séquence de nœuds, nous allons envisager quelque chose de plus intéressant : extraire la séquence de commentaires de certains éléments et supprimer uniquement ces objets (voir Listing 8.20). Listing 8.20 : Appel du second prototype. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable comments = xDocument.Element("BookParticipants").Elements("BookParticipant"). Nodes().OfType(); // Affichage des commentaires sources foreach (XComment comment in comments) { Console.WriteLine("Source comment: {0}", comment); }

Linq.book Page 294 Mercredi, 18. février 2009 7:58 07

294

LINQ to XML

Partie III

comments.Remove(); // Affichage du document XML Console.WriteLine(xDocument);

Après avoir construit la séquence source, les nœuds enfants (Nodes) de type XComment (OfType) sont placés dans la séquence comments. Reportez-vous si nécessaire à la deuxième partie de ce livre pour en savoir plus sur l’opérateur de requête standard OfType. La méthode Remove est alors appliquée à la séquence comments. Après l’exécution de cet opérateur, l’arbre XML est privé de tout commentaire dans les éléments BookParticipant. Voici le résultat : Source comment:

Joe Rattz

Ewan Buckingham

L’opérateur OfType est très pratique et il s’intègre parfaitement dans une requête LINQ to XML. Il pourrait se révéler très utile en situation réelle.

Résumé Au chapitre précédent, nous avons introduit l’API LINQ to XML et montré comment l’utiliser pour créer, modifier, sauvegarder et lire des arbres XML. Nous avons intentionnellement utilisé le mot "arbre" et non le mot "document", car avec LINQ to XML il n’est plus nécessaire de manipuler des documents. Nous avons également montré comment effectuer une requête sur un nœud/un élément pour atteindre les nœuds/éléments qui lui sont hiérarchiquement liés. Dans ce chapitre, vous avez également appris à interroger des séquences de nœuds ou d’éléments en utilisant les opérateurs de LINQ to XML. Arrivé à ce point dans la lecture du livre, vous devriez être en mesure d’effectuer des requêtes élémentaires sur des arbres XML en utilisant les opérateurs LINQ to XML. Cette nouvelle API devrait se révéler très utile pour interroger des données… en particulier si vous lui adjoignez des opérateurs de requête standard. Vous connaissez maintenant toutes les techniques de base permettant de définir des requêtes LINQ to SQL. Au chapitre suivant, nous aborderons des requêtes légèrement plus complexes et nous nous intéresserons à d’autres domaines d’action de LINQ to XML tels que la validation et la transformation.

Openmirrors.com

Linq.book Page 295 Mercredi, 18. février 2009 7:58 07

9 Les autres possibilités de XML Dans les deux chapitres précédents, vous avez appris à créer, à modifier et à parcourir des données XML en utilisant l’API LINQ to XML. Nous avons également vu comment utiliser des blocs de construction pour créer des requêtes XML très puissantes. Je pense que, dès à présent, vous serez d’accord pour affirmer que LINQ to XML peut couvrir 90 % de vos besoins en matière de XML. Mais qu’en est-il des 10 % restants ? Voyons si nous pouvons diminuer ce pourcentage. Si Microsoft avait ajouté la validation de schéma, les transformations et les requêtes XPath, quel serait le pourcentage selon vous ? Nous avons vu les bases de l’API LINQ to XML et comment effectuer les requêtes élémentaires. Nous allons maintenant nous intéresser à des requêtes plus complexes et aussi plus proches du monde réel. Dans ce chapitre, nous allons passer en revue quelques exemples qui, je l’espère, rendront à vos yeux les requêtes XML des plus triviales lorsqu’elles seront effectuées via l’API LINQ to XML. Pour décrire plus complètement cette API, nous aborderons des fonctionnalités complémentaires (essentiellement la transformation et la validation) et vous donnerons diverses informations bonnes à connaître en LINQ to XML. D’une façon plus spécifique, nous verrons comment effectuer des transformations avec et sans XSLT, comment valider un document XML par rapport à un schéma et donnerons un exemple de requête utilisant le style XPath.

Espaces de noms référencés Outre les espaces de noms LINQ et LINQ to XML désormais traditionnels, System.Linq et System.Xml.Linq, les exemples de ce chapitre utilisent également les espaces de noms System.Xml, System.Xml.Schema, System.Xml.Xsl et System.Xml.XPath.

Linq.book Page 296 Mercredi, 18. février 2009 7:58 07

296

LINQ to XML

Partie III

À moins qu’elles ne soient déjà présentes dans votre code, vous devrez donc ajouter les directives using suivantes : using using using using using using

System.Linq; System.Xml; System.Xml.Linq; System.Xml.Schema; System.Xml.XPath; System.Xml.Xsl;

Requêtes Dans le chapitre précédent, nous avons vu les principes de base permettant d’exécuter des requêtes XML via LINQ to XML. La plupart des exemples avaient pour but l’illustration d’un opérateur ou d’une propriété. Dans cette section, nous allons passer en revue plusieurs exemples "orientés solution" et, donc, plus proches de la réalité. La description du chemin n’est pas une obligation Dans les chapitres précédents, la plupart des exemples "plongeaient" dans la hiérarchie XML pour obtenir une référence sur un élément particulier en utilisant les opérateurs Element ou Elements de façon récursive, jusqu’à ce que l’élément visé soit atteint. Ainsi, beaucoup d’exemples contenaient ce type d’instruction : IEnumerable elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

Cet exemple accède à l’élément enfant BookParticipants du document, puis aux éléments enfants BookParticipant de l’élément BookParticipants. Cette technique n’est pas toujours nécessaire. Vous pouvez en effet utiliser un code comparable au Listing 9.1. Listing 9.1 : Accès à des éléments sans décrire leur chemin. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument.Descendants("BookParticipant"); foreach (XElement element in elements) { Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value); }

Openmirrors.com

Linq.book Page 297 Mercredi, 18. février 2009 7:58 07

Chapitre 9

Les autres possibilités de XML

297

Dans cet exemple, l’instruction en gras obtient les descendants BookParticipant du document. Étant donné que l’accès ne se fait pas dans une branche particulière de l’arbre XML, il est nécessaire de connaître le schéma, car il serait possible d’accéder par erreur à certaines branches de l’arbre. Cependant, cette technique fonctionne dans de nombreux cas. Voici les résultats : Elément: BookParticipant : valeur = JoeRattz Elément: BookParticipant : valeur = EwanBuckingham

Si tous les éléments BookParticipant ne sont pas utiles, vous pouvez restreindre la requête. Le Listing 9.2, par exemple, ne retourne que les éléments dont l’élément FirstName a pour valeur "Ewan". Listing 9.2 : Accès restreint à des éléments sans décrire le chemin. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = xDocument .Descendants("BookParticipant") .Where(e => ((string)e.Element("FirstName")) == "Ewan"); foreach (XElement element in elements) { Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value); }

Cette fois-ci, nous avons appliqué l’opérateur Where en suffixe dans la définition de l’objet elements. Remarquez l’utilisation de l’opérateur de casting (string) pour comparer la valeur de l’élément avec la chaîne "Ewan". Voici les résultats : Elément: BookParticipant : valeur = EwanBuckingham

Il est parfois nécessaire de contrôler l’ordre des résultats. Dans le Listing 9.3, nous allons modifier l’expression lambda de l’opérateur Where pour que deux éléments soient retournés. La requête portera sur l’attribut type. Listing 9.3 : Accès restreint à des éléments sans décrire le chemin, en définissant l’ordre et en utilisant la syntaxe d’interrogation des requêtes. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"),

Linq.book Page 298 Mercredi, 18. février 2009 7:58 07

298

LINQ to XML

Partie III

new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); IEnumerable elements = from e in xDocument.Descendants("BookParticipant") where ((string)e.Attribute("type")) != "Illustrator" orderby ((string)e.Element("LastName")) select e; foreach (XElement element in elements) { Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value); }

La requête porte toujours sur les éléments BookParticipant du document mais, ici, seuls les éléments dont l’attribut type a une valeur différente de "Illustrator" sont sélectionnés. Dans cet arbre, cela correspond à tous les éléments BookParticipant. Les résultats sont alors classés par éléments LastName croissants. Remarquez l’utilisation d’opérateurs de casting pour obtenir la valeur de l’attribut type et de l’élément LastName. Voici les résultats : Elément: BookParticipant : valeur = EwanBuckingham Elément: BookParticipant : valeur = JoeRattz

Une requête complexe Jusqu’ici, toutes les requêtes passées en revue étaient simplistes. Avant de passer à un autre sujet, nous allons étudier une requête complexe. Nous utiliserons des données mises à disposition par le W3C à des fins de tests. L’exemple du Listing 9.4 contient des données issues de trois documents XML différents. Ces documents sont obtenus en divisant une représentation texte des documents XML suggérés par le W3C. Nous allons expliquer de façon détaillée chacune des parties du code. La première étape va consister à définir les documents en utilisant du code XML. Listing 9.4 : Une requête complexe qui effectue une jointure sur trois documents en utilisant la syntaxe d’expression de requête de LINQ. XDocument users = XDocument.Parse( @"

U01 Tom Jones B

U02 Mary Doe

Openmirrors.com

Linq.book Page 299 Mercredi, 18. février 2009 7:58 07

Chapitre 9

A

U03 Dee Linquent D

U04 Roger Smith C

U05 Jack Sprat B

U06 Rip Van Winkle B

"); XDocument items = XDocument.Parse( @"

1001 Red Bicycle U01 1999-01-05 1999-01-20 40

1002 Motorcycle U02 1999-02-11 1999-03-15 500

1003 Old Bicycle U02 1999-01-10 1999-02-20 25

1004 Tricycle U01 1999-02-25 1999-03-08 15

1005 Tennis Racket U03 1999-03-19 1999-04-30

Les autres possibilités de XML

299

Linq.book Page 300 Mercredi, 18. février 2009 7:58 07

300

LINQ to XML

20

1006 Helicopter U03 1999-05-05 1999-05-25 50000

1007 Racing Bicycle U04 1999-01-20 1999-02-20 200

1008 Broken Bicycle U01 1999-02-05 1999-03-06 25

"); XDocument bids = XDocument.Parse( @"

U02 1001 35 1999-01-07

U04 1001 40 1999-01-08

U02 1001 45 1999-01-11

U04 1001 50 1999-01-13

U02 1001 55 1999-01-15

U01 1002 400

Openmirrors.com

Partie III

Linq.book Page 301 Mercredi, 18. février 2009 7:58 07

Chapitre 9

1999-02-14

U02 1002 600 1999-02-16

U03 1002 800 1999-02-17

U04 1002 1000 1999-02-25

U02 1002 1200 1999-03-02

U04 1003 15 1999-01-22

U05 1003 20 1999-02-03

U01 1004 40 1999-03-05

U03 1007 175 1999-01-25

U05 1007 200 1999-02-08

U04 1007 225 1999-02-12

");

Les autres possibilités de XML

301

Linq.book Page 302 Mercredi, 18. février 2009 7:58 07

302

LINQ to XML

Partie III

Ces trois documents représentent les données (utilisateurs, objets vendus et enchères) manipulées sur un site web de vente aux enchères. Ils ont été créés en appelant la méthode XDocument.Parse sur des représentations chaînes des données. La requête va consister à extraire les enchères supérieures à 50 dollars. Les résultats doivent faire apparaître la date, le montant de l’enchère, le nom de la personne qui en est à l’origine, le numéro de l’objet et sa description. Voici la requête : var biddata = from b in bids.Descendants("bid_tuple") where ((double)b.Element("bid")) > 50 join u in users.Descendants("user_tuple") on ((string)b.Element("userid")) equals ((string)u.Element("userid")) join i in items.Descendants("item_tuple") on ((string)b.Element("itemno")) equals ((string)i.Element("itemno")) select new {Item = ((string)b.Element("itemno")), Description = ((string)i.Element("description")), User = ((string)u.Element("name")), Date = ((string)b.Element("bid_date")), Price = ((double)b.Element("bid"))};

La requête est plus complexe que celles étudiées jusqu’ici. La première ligne utilise la méthode Descendants pour accéder aux descendants bid_tuple du document bids. La ligne suivante utilise l’opérateur Where pour ne conserver que les enchères supérieures à 50 dollars. Il peut sembler inhabituel d’utiliser une clause Where si tôt dans la requête. Cette clause aurait tout aussi bien pu être spécifiée juste avant la clause select, mais cela aurait signifié que le Where aurait été appliqué sur la jointure entre les utilisateurs et les objets, y compris pour les enchères inférieures à 50 dollars. En ayant réduit le nombre de données avant la jointure, la charge de travail a ainsi été allégée pour la suite de la requête et les performances, améliorées. Une fois limitées aux seules enchères supérieures à 50 dollars, les données sont jointes au document XML users par l’intermédiaire de l’élément userid (lignes 3 à 5), afin d’obtenir le nom de chaque utilisateur. Arrivés à ce point dans la requête, nous avons joint les documents bids et users et limité les données aux enchères supérieures à 50 dollars. Les trois prochaines lignes (6 à 8) effectuent une jointure sur le document XML items par l’intermédiaire du champ itemno afin d’obtenir la description de l’objet. À ce point, les documents bids, users et items sont joints. Remarquez que différents opérateurs de casting ont été utilisés pour obtenir la valeur des éléments dans le type souhaité. Ainsi, par exemple, le montant de l’enchère a été obtenu avec un opérateur (double). Les enchères sont au format string mais, étant donné que leur contenu peut être converti en une valeur double, l’opérateur de casting a fait son travail. Openmirrors.com

Linq.book Page 303 Mercredi, 18. février 2009 7:58 07

Chapitre 9

Les autres possibilités de XML

303

La prochaine étape va consister à sélectionner une classe anonyme qui contient les éléments enfants des éléments issus de cette double jointure. Nous allons commencer par afficher un en-tête : Console.WriteLine("{0,-12} {1,-12} {2,-6} {3,-14} {4,10}", "Date", "User", "Item", "Description", "Price"); Console.WriteLine("===================================================");

Les instructions suivantes énumèrent la séquence et affichent les valeurs correspondantes : bid: foreach (var bd in biddata) { Console.WriteLine("{0,-12} {1,-12} {2,-6} {3,-14} {4,10:C}", bd.Date, bd.User, bd.Item, bd.Description, bd.Price); }

Cette portion de code est triviale. En fait, mis à part la requête elle-même, tout le reste du code est simplissime. Voici les résultats : Date

User

Item

Description

Price

=================================================================================== 1999-01-15 1999-02-14 1999-02-16 1999-02-17 1999-02-25 1999-03-02 1999-01-25 1999-02-08 1999-02-12

Mary Doe Tom Jones Mary Doe Dee Linquent Roger Smith Mary Doe Dee Linquent Jack Sprat Roger Smith

1001 1002 1002 1002 1002 1002 1007 1007 1007

Red Bicycle Motorcycle Motorcycle Motorcycle Motorcycle Motorcycle Racing Bicycle Racing Bicycle Racing Bicycle

$55.00 $400.00 $600.00 $800.00 $1,000.00 $1,200.00 $175.00 $200.00 $225.00

Quelques lignes de code ont suffi pour joindre trois documents XML ! Maintenant, je suis sûr que vous vous rendez compte de la puissance de LINQ to XML. Mais attendez un peu, d’autres possibilités très intéressantes vous attendent dans les pages suivantes…

Transformations LINQ to XML vous permet d’effectuer des transformations en utilisant deux approches diamétralement opposées. La première consiste à utiliser XSLT via les classes passerelles XmlReader et XmlWriter. La seconde approche consiste à utiliser LINQ to XML en construisant fonctionnellement le document XML cible et en incluant une requête LINQ to XML dans le document source XML.

Linq.book Page 304 Mercredi, 18. février 2009 7:58 07

304

LINQ to XML

Partie III

XSLT est une technologie XML standard. Des outils permettant d’écrire, de déboguer et de tester les transformations XSLT sont d’ores et déjà disponibles. Par ailleurs, il est possible que vous disposiez déjà de documents XSLT. Si tel est le cas, vous pouvez les utiliser dans vos nouvelles applications par l’intermédiaire de LINQ to XML. De nombreux documents XSLT sont disponibles. Vous n’avez qu’à choisir celui qui s’adapte le mieux à vos souhaits. De plus, l’utilisation de XSLT pour vos transformations se révèle plus dynamique. Contrairement à l’approche "construction fonctionnelle" de LINQ to XML, il n’est pas nécessaire de recompiler le code pour changer la transformation : le simple fait de modifier le document XSLT suffit pour changer la transformation à l’exécution. Enfin, la technologie XSLT est bien connue et bon nombre de développeurs experts dans ce domaine peuvent vous assister. Ce fait n’est bien entendu plus d’actualité si vous choisissez l’approche "construction fonctionnelle". L’approche "construction fonctionnelle" ne vous demandera pas un gros investissement. Les transformations XML seront en effet effectuées par l’intermédiaire de LINQ to XML. Si vous ne connaissez pas XSLT, et si vos besoins en matière de transformations sont modestes, cette approche peut vous convenir. Par ailleurs, bien que la construction fonctionnelle soit moins pratique que la modification d’un document XSLT, la nécessité d’avoir à recompiler le code pour modifier une transformation peut être considérée comme une sécurité supplémentaire : un tiers ne peut ainsi modifier un document externe pour changer le sens d’une transformation. Transformations avec XSLT Pour effectuer une transformation XML en utilisant XSLT, vous utiliserez les classes passerelles XmlWriter et XmlReader. Vous les obtiendrez à partir des méthodes CreateWriter et CreateReader des classes XDocument. L’exemple du Listing 9.5 demande quelques explications. Nous les donnerons au fur et à mesure, en séparant le code en plusieurs blocs fonctionnels. Listing 9.5 : Transformation d’un document XML avec XSLT. string xsl = @"

Book Participants



Role First Name Last Name


Openmirrors.com

Linq.book Page 305 Mercredi, 18. février 2009 7:58 07

Chapitre 9

Les autres possibilités de XML

305





";

Ce code se contente de définir quelques instructions XSL qui vont créer du code HTML afin d’afficher les données XML BookParticipant dans un tableau HTML. La prochaine étape va consister à créer le document XML avec les participants : XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Ce code a déjà été utilisé à de maintes reprises dans les pages précédentes. C’est à partir de maintenant que la magie va opérer. Nous allons créer un document XDocument pour la version transformée. À partir de ce document, nous définirons un XmlWriter, nous instancierons un objet XslCompiledTransform, nous chargerons l’objet transformé avec la feuille de style de transformation et nous transformerons le document XML d’entrée en la sortie XmlWriter : XDocument transformedDoc = new XDocument(); using (XmlWriter writer = transformedDoc.CreateWriter()) { XslCompiledTransform transform = new XslCompiledTransform(); transform.Load(XmlReader.Create(new StringReader(xsl))); transform.Transform(xDocument.CreateReader(), writer); } Console.WriteLine(transformedDoc);

Voici le résultat de la transformation. Comme vous pouvez le voir, nous utilisons les passerelles XmlWriter et XmlReader pour effectuer la transformation :

Book Participants

Linq.book Page 306 Mercredi, 18. février 2009 7:58 07

306

LINQ to XML

Partie III

Role First Name Last Name
Author Joe Rattz
Editor Ewan Buckingham


Transformations avec la construction fonctionnelle Cette section va vous montrer comment effectuer des transformations XSLT en utilisant l’API LINQ to XML. Logiquement parlant, une transformation peut être aussi simple que la combinaison d’un arbre XML défini par la construction fonctionnelle et d’une requête XML incorporée dans cet arbre. Nous allons expliquer les transformations XML à travers un exemple. Dans de nombreux autres exemples des chapitres dédiés à LINQ to XML, nous avons utilisé l’arbre XML suivant :

Joe Rattz

Ewan Buckingham

Supposons que nous devions transformer cet arbre XML comme suit :



Pour accomplir cette transformation, nous allons utiliser la construction fonctionnelle en incluant une requête dans l’arbre. Cette approche va consister à construire un nouveau document dont l’allure correspond à l’arbre XML cible en appliquant une requête LINQ to XML au document XML source pour y piocher les données. C’est la structure de l’arbre XML cible qui va guider la construction fonctionnelle et la logique de la requête. Étant donné que cette tâche est légèrement plus complexe que la plupart des exemples LINQ to XML précédents, nous donnerons des explications chaque fois que cela est nécessaire (voir Listing 9.6). Listing 9.6 : Transformation d’un document XML. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"),

Openmirrors.com

Linq.book Page 307 Mercredi, 18. février 2009 7:58 07

Chapitre 9

Les autres possibilités de XML

307

new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine("Document XML original :"); Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

Ce code définit le document XML source que nous allons transformer. La prochaine étape consiste à construire le nouveau document et l’élément racine : XDocument xTransDocument = new XDocument( new XElement("MediaParticipants",

Rappelez-vous que la structure de l’arbre XML de sortie guide la construction fonctionnelle. Arrivés à ce point, nous avons un document et l’élément racine, Mediaparticipants. Nous devons maintenant ajouter l’attribut type à l’élément racine : new XAttribute("type", "book"),

L’attribut type et sa valeur n’existent pas dans le document XML source. Ils ont donc été définis dans le code. Maintenant que l’attribut type est défini, nous allons générer un élément Participant pour chacun des éléments BookParticipant du document XML original. Pour ce faire, il va suffire d’exécuter la requête suivante : xDocument.Element("BookParticipants") .Elements("BookParticipant")

Ces deux lignes de code fournissent une séquence d’éléments BookParticipant. Nous allons maintenant générer et initialiser un élément Participant pour chaque élément BookParticipant. Pour ce faire, nous utiliserons l’opérateur de projection Select : .Select(e => new XElement("Participant",

Nous allons maintenant construire les attributs Role et Name de l’élément Participant en piochant leurs valeurs dans l’élément BookParticipant : new XAttribute("Role", (string)e.Attribute("type")), new XAttribute("Name", (string)e.Element("FirstName") + " " + (string)e.Element("LastName"))))));

Enfin, nous affichons le document XML transformé : Console.WriteLine("Document XML transformé :"); Console.WriteLine(xTransDocument);

Voici le résultat, tout à fait conforme aux attentes : Document XML original:

Joe Rattz

Ewan Buckingham

Linq.book Page 308 Mercredi, 18. février 2009 7:58 07

308

LINQ to XML

Partie III

Document XML transformé :













"; XmlSchemaSet schemaSet = new XmlSchemaSet(); schemaSet.Add("", XmlReader.Create(new StringReader(schema)));

Ce schéma est légèrement différent de celui utilisé dans les autres exemples. Ici, les guillemets de délimitation sont remplacés par des apostrophes et l’élément MiddleInitial est ajouté, entre les éléments FirstName et LastName. Remarquez également que l’attribut minOccurs de l’élément MiddleInitial a été initialisé à "0". Cet élément n’est donc pas obligatoire. Les deux dernières lignes créent un objet schemaSet en utilisant les données du schéma. La prochaine étape va consister à créer un document XML. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine("Le document XML source :"); Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

Openmirrors.com

Linq.book Page 325 Mercredi, 18. février 2009 7:58 07

Chapitre 9

Les autres possibilités de XML

325

Rien de nouveau dans ce code : le document XML habituel est créé et affiché. Nous allons maintenant le valider : bool valid = true; xDocument.Validate(schemaSet, (o, vea) => { Console.WriteLine("Une exception s’est produite pendant le traitement de l’objet {0}.", o.GetType().Name); Console.WriteLine(vea.Message); valid = false; }, true); Console.WriteLine("Le document {0}.{1}", valid ? "est valide" : "n’est pas valide", System.Environment.NewLine);

La validation est légèrement différente de celle utilisée dans les exemples précédents. Une variable booléenne indiquant si le document est valide est initialisée à la valeur true. À l’intérieur du gestionnaire de validation, elle est initialisée à la valeur false. Ainsi, si une erreur de validation se produit, valid aura pour valeur false. La valeur de la variable est testée pour déterminer si le document est valide et un message correspondant est affiché. Arrivé à ce point dans l’exécution du code, le document est valide. Imaginons maintenant que nous autorisons un utilisateur à éditer les éléments des différents participants. Ici, par exemple, l’utilisateur édite le participant dont le prénom est "Joe". Le code obtient une référence de cet élément, le met à jour et effectue une validation après la mise à jour. XElement bookParticipant = xDocument.Descendants("BookParticipant"). Where(e => ((string)e.Element("FirstName")).Equals("Joe")).First(); bookParticipant.Element("FirstName"). AddAfterSelf(new XElement("MiddleInitial", "C")); valid = true; bookParticipant.Validate(bookParticipant.GetSchemaInfo().SchemaElement, schemaSet, (o, vea) => { Console.WriteLine("An exception occurred processing object type {0}.", o.GetType().Name); Console.WriteLine(vea.Message); valid = false; }, true); Console.WriteLine("L’élément {0}.{1}", valid ? "est valide" : "n’est pas valide", System.Environment.NewLine);

La variable valid est initialisée à true, puis la méthode Validate est appelée sur l’élément BookParticipant (et non sur le document complet). À l’intérieur du gestionnaire

Linq.book Page 326 Mercredi, 18. février 2009 7:58 07

326

LINQ to XML

Partie III

d’événement de validation, valid est initialisée à true. Après l’étape de validation du participant, la validité de l’élément est affichée. Voici les résultats : Le document XML source

Joe Rattz

Ewan Buckingham

Le document est valide L’élément est valide

Dans cet exemple, l’élément a été considéré comme valide. Dans notre dernier exemple, nous allons utiliser le même code mais, ici, pendant la mise à jour de l’élément BookParticipant, nous allons créer un élément MiddleName, et non MiddleInitial. L’élément sera donc considéré comme invalide (voir Listing 9.19). Listing 9.19 : Échec de validation d’un élément XML. string schema = @"











"; XmlSchemaSet schemaSet = new XmlSchemaSet(); schemaSet.Add("", XmlReader.Create(new StringReader(schema))); XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Openmirrors.com

Linq.book Page 327 Mercredi, 18. février 2009 7:58 07

Chapitre 9

Les autres possibilités de XML

327

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); Console.WriteLine("Document XML source :"); Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine); bool valid = true; xDocument.Validate(schemaSet, (o, vea) => { Console.WriteLine("Une exception s’est produite pendant la validation d’un objet ➥de type {0}.", o.GetType().Name); Console.WriteLine(vea.Message); valid = false; }, true); Console.WriteLine("Le document {0}.{1}", valid ? "est valide" : "n’est pas valide", System.Environment.NewLine); XElement bookParticipant = xDocument.Descendants("BookParticipant"). Where(e => ((string)e.Element("FirstName")).Equals("Joe")).First(); bookParticipant.Element("FirstName"). AddAfterSelf(new XElement("MiddleName", "Carson")); valid = true; bookParticipant.Validate(bookParticipant.GetSchemaInfo().SchemaElement, schemaSet, (o, vea) => { Console.WriteLine("Une exception s’est produite pendant la validation d’un objet ➥de type {0}.", o.GetType().Name); Console.WriteLine(vea.Message); valid = false; }, true); Console.WriteLine("L’élément {0}.{1}", valid ? "est valide" : "n’est pas valide", System.Environment.NewLine);

Ce code est identique au précédent mais, ici, au lieu d’ajouter un élément MiddleInitial, nous ajoutons un élément MiddleName. Voici les résultats : Document XML source :

Joe Rattz

Ewan Buckingham

Le document est valide

Linq.book Page 328 Mercredi, 18. février 2009 7:58 07

328

LINQ to XML

Partie III

Une exception s’est produite pendant la validation d’un objet de type XElement. L’élément ’BookParticipant’ a un enfant non valide : ’MiddleName’. Éléments attendus : ’MiddleInitial, LastName’. L’élément n’est pas valide

Comme on s’y attendait, l’élément BookParticipant n’est pas valide. Cet exemple est quelque peu irréaliste. En effet, il est peu probable qu’un développeur définisse une interface pour que des utilisateurs puissent modifier un document XML. Mais imaginez que le document XML passe entre les mains d’un programmeur qui cherche personnellement à vous nuire (un hacker, par exemple). Dans ce cas, la revalidation des données prend tout son sens…

XPath Si vous utilisez couramment XPath, vous pouvez tirer avantage de la classe System.Xml.XPath.Extensions de l’espace de noms System.Xml.XPath.Extensions. Cette classe ajoute la possibilité de faire des recherches XPath par l’intermédiaire de méthodes d’extension. Prototypes Voici la liste des principaux System.Xml.XPath.Extensions :

prototypes

des

méthodes

de

la

classe

XPathNavigator Extensions.CreateNavigator(this XNode node); XPathNavigator Extensions.CreateNavigator(this XNode node, XmlNameTable nameTable); object Extensions.XPathEvaluate(this XNode node, string expression); object Extensions.XPathEvaluate(this XNode node, string expression, IXmlNamespaceResolver resolver); XElement Extensions.XPathSelectElement(this XNode node, string expression); XElement Extensions.XPathSelectElement(this XNode node, string expression, IXmlNamespaceResolver resolver); IEnumerable Extensions.XPathSelectElements(this XNode node, string expression); IEnumerable Extensions.XPathSelectElements(this XNode node, string expression, IXmlNamespaceResolver resolver);

En utilisant ces méthodes d’extension, vous pouvez appliquer une requête sur un document LINQ to XML en utilisant les expressions de recherche XPath (voir Listing 9.20). Listing 9.20 : Interrogation de données XML avec la syntaxe XPath. XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Openmirrors.com

Linq.book Page 329 Mercredi, 18. février 2009 7:58 07

Chapitre 9

Les autres possibilités de XML

329

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")))); XElement bookParticipant = xDocument.XPathSelectElement( "//BookParticipants/BookParticipant[FirstName=’Joe’]"); Console.WriteLine(bookParticipant);

Ces quelques lignes de code définissent le document XML conventionnel mais, contrairement à ce qui a été fait dans les exemples précédents, le document original n’est pas affiché. La méthode XPathSelectElement est appelée sur le document. Une expression de recherche XPath lui est passée en argument afin de trouver l’élément BookParticipant dont l’élément FirstName a pour valeur "Joe". Voici les résultats :

Joe Rattz

Les méthodes d’extension XPath donnent une référence sur un objet System.Xml.XPath.XPathNavigator. Par son intermédiaire, vous pouvez parcourir un document XML, exécuter une requête XPath pour obtenir un élément ou une séquence d’éléments ou évaluer une expression de requête XPath.

Résumé Arrivé à ce point dans la lecture de cet ouvrage, si vous n’aviez aucune expérience en XML, vous vous sentez peut-être dépassé. Si vous aviez une expérience en XML, mais pas en LINQ to XML, j’espère que vous avez pu comprendre tout ce qui a été dit. La puissance et la flexibilité de l’API LINQ to XML est vraiment grisante ! Pendant l’écriture de ce chapitre et des exemples qui le ponctuent, je me suis trouvé dans un tel état d’euphorie que je n’ai jamais eu envie de faire "machine arrière" et d’utiliser le langage XML traditionnel. Et ce malgré le fait que mon projet professionnel n’était pas encore en mesure d’utiliser LINQ to XML. Bien des fois, j’ai pensé "si seulement je pouvais utiliser la construction fonctionnelle pour définir un fragment XML", mais j’ai dû me replier sur la méthode String.Format de la librairie XML traditionnelle. Ne me jetez pas la pierre : comme je l’ai dit précédemment, un présentateur a utilisé les mêmes méthodes que moi lors d’un séminaire Microsoft ! Après avoir écrit les exemples des chapitres relatifs à LINQ to XML, je peux vous dire que je serais vraiment intéressé si je pouvais utiliser l’API LINQ to XML dans mon code de production. La création de documents XML est grandement facilitée, car elle est essentiellement basée sur les éléments (et non les documents) et qu’elle tire parti des énormes possibilités de la construction fonctionnelle. Le processus peut même se révéler amusant : combinez la facilité de création, le parcours et la modification intuitifs de

Linq.book Page 330 Mercredi, 18. février 2009 7:58 07

330

LINQ to XML

Partie III

documents XML, et cela devient un vrai plaisir d’utiliser LINQ to XML… surtout si l’on considère les autres alternatives ! Ces facilités d’utilisation ainsi que la puissance et la flexibilité du langage d’interrogation font de LINQ to XML mon préféré dans le petit monde de LINQ. Si votre approche de XML est plutôt difficile, vous devriez vous intéresser à LINQ to XML. Il fera certainement sauter bien des barrières.

Openmirrors.com

Linq.book Page 331 Mercredi, 18. février 2009 7:58 07

IV LINQ to DataSet

Linq.book Page 332 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 333 Mercredi, 18. février 2009 7:58 07

10 LINQ to DataSet Bien que LINQ to SQL n’ait pas encore été abordé dans cet ouvrage, je voudrais signaler que, pour utiliser LINQ to SQL sur une base de données, les classes de code source doivent être générées et compilées spécifiquement pour cette base de données, ou qu’un fichier de mapping doit être créé. Cela signifie qu’il est impossible d’effectuer des requêtes LINQ to SQL sur une base de données inconnue jusqu’à l’exécution. Mais alors que doit faire le développeur ? Les opérateurs LINQ to DataSet permettent d’exécuter des requêtes LINQ sur des DataSet. Étant donné qu’un DataSet peut être récupéré par une requête SQL ADO.NET, LINQ to DataSet permet d’effectuer des requêtes sur toute base de données accessible via ADO.NET. Cela offre un dynamisme bien plus grand que si vous utilisiez LINQ to SQL. Vous pouvez vous demander dans quelles circonstances la base de données pourrait ne pas être connue jusqu’à l’exécution. Effectivement, dans les applications traditionnelles, la base de données est connue pendant le développement, et LINQ to DataSet n’est pas un passage obligé. Mais qu’en est-il si vous développez un utilitaire pour bases de données ? Considérons une application telle que SQL Server Enterprise Manager (l’interface graphique de SQL Server pour les tâches de création et d’administration des bases de données). Jusqu’à l’exécution, cette application ne connaît pas les bases de données qui ont été installées. Cependant, elle vous permet de connaître leur nom ainsi que celui des différentes tables accessibles dans chacune d’entre elles. Le développeur d’une telle application n’a aucun moyen de générer les classes LINQ to SQL nécessaires à l’interfaçage des différentes bases de données à l’exécution. LINQ to DataSet devient donc une nécessité. Bien que ce chapitre soit intitulé "LINQ to DataSet", vous verrez que les opérateurs passés en revue sont essentiellement relatifs aux objets DataTable, DataRow et DataColumn. Ne soyez pas surpris si ce chapitre ne fait pas souvent référence aux objets DataSets. Il est bien entendu qu’en circonstances réelles vos objets DataTable viendront

Linq.book Page 334 Mercredi, 18. février 2009 7:58 07

334

LINQ to DataSet

Partie IV

essentiellement d’objets DataSets. Cependant, pour des raisons d’indépendance, de concision et de clarté, la plupart des exemples de ce chapitre se basent sur de simples objets DataTable créés par programme. Les données traitées ne sont donc pas extraites d’une base de données existante. LINQ to DataSet donne accès à plusieurs opérateurs spécifiques issus de différents assemblies et espaces de noms. Ces opérateurs permettent au développeur d’effectuer les actions suivantes : m

définitions de séquences d’objets DataRows ;

m

recherche et modification de valeurs DataColumn ;

m

obtention de séquences LINQ standard IEnumerable à partir de DataTable afin de pouvoir leur appliquer des opérateurs de requête standard ;

m

copie de séquences de DataRow modifiées dans un DataTable.

Outre ces opérateurs LINQ to DataSet, une fois l’opérateur AsEnumerable appelé, vous pouvez utiliser les opérateurs de requête standard de LINQ to Objects sur la séquence DataRow retournée, ce qui ajoute encore plus de puissance et de flexibilité à LINQ to DataSet.

Référence des assemblies Pour exécuter les exemples de ce chapitre, vous devrez (si elles ne sont pas déjà présentes) ajouter des références aux assemblies System.Data.dll et System.Data.DataSetExtensions.dll.

Espaces de noms référencés Pour être en mesure d’utiliser les opérateurs LINQ to DataSet, vous devez ajouter (si elles ne sont pas déjà présentes) les deux directives using suivantes en tête de votre code : using System.Data; using System.Linq;

Code commun utilisé dans les exemples Tous les exemples de ce chapitre ont besoin d’un objet DataTable pour effectuer des requêtes LINQ to DataSet. Dans un code de production réel, ces objets sont typiquement obtenus en effectuant une requête sur une base de données. Dans plusieurs exemples de ce chapitre, cette configuration est inconfortable, voire insuffisante. À titre d’exemple, nous aurons besoin de deux enregistrements identiques pour illustrer la méthode Distinct. Plutôt que jongler avec une base de données pour obtenir les enregistrements Openmirrors.com

Linq.book Page 335 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

335

nécessaires, nous avons préféré créer par programme un objet DataTable qui contient les données nécessaires à chaque exemple. Pour faciliter la définition de l’objet DataTable, nous utiliserons un tableau d’objets contenu dans la classe prédéfinie Student. Une classe simpliste avec deux membres publics class Student { public int Id; public string Name; }

Vous n’avez qu’à imaginer que nous interrogeons la table Students, composée de deux colonnes (Id et Name) et dans laquelle chaque enregistrement représente un étudiant. Pour faciliter la création du DataTable et pour ne pas nuire aux détails de chaque exemple, nous utiliserons une méthode commune pour convertir un tableau d’objets Student en un objet DataTable. Ceci nous permettra de faire varier simplement les données d’un exemple à l’autre. Voici le code de cette méthode commune : Conversion d’un tableau d’objets Student en un DataTable static DataTable GetDataTable(Student[] students) { DataTable table = new DataTable(); table.Columns.Add("Id", typeof(Int32)); table.Columns.Add("Name", typeof(string)); foreach (Student student in students) { table.Rows.Add(student.Id, student.Name); } return (table); }

Cette méthode n’a rien de bien compliqué. Elle se contente d’instancier un objet DataTable, puis d’ajouter deux colonnes et une ligne pour chacun des éléments du tableau students passé en argument. Pour plusieurs des exemples de ce chapitre, il est nécessaire d’afficher un objet DataTable, pour s’assurer que les résultats sont conformes aux attentes. D’un exemple à l’autre, les données du DataTable peuvent varier, mais le code permettant d’afficher le contenu du DataTable reste le même. Plutôt que répéter ce code dans tous les exemples, nous avons créé une méthode commune que nous appellerons chaque fois que cela sera nécessaire : La méthode OutputDataTableHeader static void OutputDataTableHeader(DataTable dt, int columnWidth) { string format = string.Format("{0}0,-{1}{2}", "{", columnWidth, "}"); // Display the column headings.

Linq.book Page 336 Mercredi, 18. février 2009 7:58 07

336

LINQ to DataSet

Partie IV

foreach(DataColumn column in dt.Columns) { Console.Write(format, column.ColumnName); } Console.WriteLine(); foreach(DataColumn column in dt.Columns) { for(int i = 0; i < columnWidth; i++) { Console.Write("="); } } Console.WriteLine(); }

Cette méthode affiche l’en-tête d’un objet DataTable sous une forme tabulaire.

Opérateurs dédiés aux DataRow Vous vous souvenez certainement que l’API LINQ to Objects contient un ensemble d’opérateurs de requête standard très utiles lorsqu’il s’agit d’initialiser et/ou de comparer des séquences. Je fais référence aux opérateurs Distinct, Except, Intersect, Union et SequenceEqual, qui définissent une séquence en fonction d’une autre. Chacun de ces opérateurs doit être en mesure de tester l’égalité des éléments d’une séquence pour effectuer l’opération pour laquelle il a été conçu. Le test d’égalité se fait en appliquant les méthodes GetHashCode et Equals aux éléments. En ce qui concerne les DataRow, ces deux méthodes provoquent la comparaison des références des éléments, ce qui n’est pas le comportement souhaité. Mais, rassurez-vous, ces opérateurs possèdent un autre prototype dont nous n’avons pas parlé dans les chapitres relatifs à LINQ to Objects. Ce prototype permet de passer un argument complémentaire : IEqualityComparer. Par commodité, un objet comparateur a été spécialement défini pour ces versions des opérateurs : System.Data.DataRowComparer.Default. Cette classe se trouve dans l’espace de noms System.Data et l’assembly System.Data.Entity.dll. L’égalité est déterminée en comparant le nombre de colonnes et le type de donnée statique de chaque colonne et en utilisant l’interface IComparable sur le type de donnée dynamique de la colonne si celui-ci l’implémente. Dans le cas contraire, la méthode System.Object Equals est appelée. Ces prototypes sont définis dans la même classe statique que les autres : System.Linq.Enumerable. Dans cette section, nous allons donner quelques exemples pour illustrer la mauvaise et, bien entendu, la bonne façon d’effectuer des comparaisons sur des objets DataSet. Opérateur Distinct L’opérateur Distinct supprime les lignes en double dans une séquence d’objets. Il retourne un objet dont l’énumération renvoie la séquence source privée des doublons. Openmirrors.com

Linq.book Page 337 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

337

Cet opérateur devrait pouvoir déterminer l’égalité entre les différentes lignes en appelant les méthodes GetHashCode et Equals sur chacun des éléments. Cependant, pour des objets de type DataRow, cette technique ne donne pas le résultat recherché. Pour obtenir le résultat escompté, nous appellerons un nouveau prototype de cet opérateur et nous lui passerons le comparateur System.Data.DataRowComparer.Default dans son deuxième argument. La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur le type de donnée statique de chaque colonne. L’interface IComparable est utilisée sur les colonnes qui implémentent cette interface. La méthode statique System.Object.Equals est utilisée sur les autres. Prototype Un seul prototype de l’opérateur Distinct sera étudié dans ce chapitre : public static IEnumerable Distinct ( this IEnumerable source, IEqualityComparer comparer);

Exemples Dans le premier exemple, l’objet DataTable sera créé en appliquant la méthode commune GetDataTable à un tableau d’objet Student. À dessein, ce tableau comprendra deux fois la même ligne : celle dont le champ Id vaut 1. Pour mettre en évidence la ligne en double dans le DataTable, le tableau sera affiché. La ligne en double sera ensuite enlevée à l’aide de l’opérateur Distinct, et l’objet DataTable sera à nouveau affiché, pour montrer que le doublon a été supprimé. Le code utilisé apparaît dans le Listing 10.1. Listing 10.1 : L’opérateur Distinct associé à un comparateur d’égalité. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = = = = = =

{ 1, Name = "Joe Rattz" }, 6, Name = "Ulyses Hutchens" }, 19, Name = "Bob Tanko" }, 45, Name = "Erin Doutensal" }, 1, Name = "Joe Rattz" }, 12, Name = "Bob Mapplethorpe" }, 17, Name = "Anthony Adams" }, 32, Name = "Dignan Stephens" }

DataTable dt = GetDataTable(students); Console.WriteLine("{0}Avant l’appel à Distinct(){0}", System.Environment.NewLine); OutputDataTableHeader(dt, 15);

Linq.book Page 338 Mercredi, 18. février 2009 7:58 07

338

LINQ to DataSet

Partie IV

foreach (DataRow dataRow in dt.Rows) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); } IEnumerable distinct = dt.AsEnumerable().Distinct(DataRowComparer.Default); Console.WriteLine("{0}Après l’appel à Distinct(){0}", System.Environment.NewLine); OutputDataTableHeader(dt, 15); foreach (DataRow dataRow in distinct) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); }

L’opérateur AsEnumerable a été utilisé pour obtenir la séquence d’objets DataRow à partir du DataTable. Ceci afin d’assurer la compatibilité avec l’opérateur Distinct. Remarquez également que, dans le tableau students, la ligne dont le champ Id vaut "1" apparaît en double. Vous avez sans doute noté que la méthode Field a été appelée sur l’objet DataRow. Pour l’instant, tout ce que vous devez en savoir, c’est qu’il s’agit d’une méthode qui facilite l’obtention des valeurs des objets DataColumn à partir d’un DataRow. L’opérateur Field sera étudié en détail un peu plus loin dans ce chapitre, dans la section "Opérateurs dédiés aux champs". Voici les résultats : Avant l’appel à Distinct() Id Name ============================== 1 Joe Rattz 6 Ulyses Hutchens 19 Bob Tanko 45 Erin Doutensal 1 Joe Rattz 12 Bob Mapplethorpe 17 Anthony Adams 32 Dignan Stephens Après l’appel à Distinct() Id Name ============================== 1 Joe Rattz 6 aUlyses Hutchens 19 Bob Tanko 45 Erin Doutensal 12 Bob Mapplethorpe 17 Anthony Adams 32 Dignan Stephens

Openmirrors.com

Linq.book Page 339 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

339

Comme vous le voyez, la ligne dont le champ Id vaut 1 apparaît en double avant l’appel à l’opérateur Distinct. Elle n’apparaît plus qu’une seule fois lorsque cet opérateur a été appelé. Dans notre deuxième exemple, nous allons voir ce qui se passerait si l’opérateur Distinct avait été appelé sans spécifier l’objet comparer (voir Listing 10.2). Listing 10.2 : L’opérateur Distinct appelé sans comparateur d’égalité. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = = = = = =

{ 1, Name = "Joe Rattz" }, 6, Name = "Ulyses Hutchens" }, 19, Name = "Bob Tanko" }, 45, Name = "Erin Doutensal" }, 1, Name = "Joe Rattz" }, 12, Name = "Bob Mapplethorpe" }, 17, Name = "Anthony Adams" }, 32, Name = "Dignan Stephens" }

DataTable dt = GetDataTable(students); Console.WriteLine("{0}Avant l’appel à Distinct(){0}", System.Environment.NewLine); OutputDataTableHeader(dt, 15); foreach (DataRow dataRow in dt.Rows) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); } IEnumerable distinct = dt.AsEnumerable().Distinct(); Console.WriteLine("{0}Après l’appel à Distinct(){0}", System.Environment.NewLine); OutputDataTableHeader(dt, 15); foreach (DataRow dataRow in distinct) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); }

La seule différence entre ce code et le précédent se situe au niveau de l’opérateur Distinct : dans le premier cas, on utilise un comparateur d’égalité, dans le second cas, non. Cette deuxième technique va-t-elle supprimer le doublon ? Jetons un œil aux résultats : Avant l’appel à Distinct() Id Name ============================== 1 Joe Rattz 6 Ulyses Hutchens 19 Bob Tanko 45 Erin Doutensal

Linq.book Page 340 Mercredi, 18. février 2009 7:58 07

340

LINQ to DataSet

1 12 17 32

Joe Rattz Bob Mapplethorpe Anthony Adams Dignan Stephens

Partie IV

Après l’appel à Distinct() Id Name ============================== 1 Joe Rattz 6 Ulyses Hutchens 19 Bob Tanko 45 Erin Doutensal 1 Joe Rattz 12 Bob Mapplethorpe 17 Anthony Adams 32 Dignan Stephens

Ces résultats ne sont pas concluants. Comme vous le voyez, la deuxième technique de comparaison est inefficace. Opérateur Except L’opérateur Except renvoie une séquence composée des objets DataRow de la première séquence qui n’appartiennent pas à la seconde. Les éléments de la séquence de sortie apparaissent dans l’ordre original de la séquence d’entrée. Pour déterminer quels éléments sont uniques, l’opérateur Except doit être en mesure de déterminer si deux éléments sont égaux. Pour ce faire, il devrait suffire d’utiliser les méthodes GetHashCode et Equals. Cependant, étant donné que les objets comparés sont des DataRow, un prototype spécifique doit être utilisé. Le deuxième argument de ce prototype désigne le comparateur (System.Data.DataRowComparer.Default) à utiliser. La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur le type de donnée statique de chaque colonne. L’interface IComparable est utilisée sur les colonnes qui l’implémentent. La méthode statique System.Object.Equals est utilisée sur les autres. Prototype Nous nous intéresserons à un seul prototype de cet opérateur : public static IEnumerable Except ( this IEnumerable first, IEnumerable second, IEqualityComparer comparer);

Exemple Dans cet exemple, nous appellerons l’opérateur Except à deux reprises. Dans le premier appel, le comparateur passé sera System.Data.DataRowComparer.Default. Les résultats de la comparaison devraient donc être conformes aux attentes. Dans le

Openmirrors.com

Linq.book Page 341 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

341

second appel, aucun comparateur ne sera passé au prototype. Comme vous le verrez, la comparaison ne fonctionnera pas (voir Listing 10.3). Listing 10.3 : Appel de l’opérateur Except avec et sans comparateur. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

Student[] students2 = { new Student { Id = 5, Name = "Abe Henry" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 29, Name = "Future Man" }, new Student { Id = 72, Name = "Dignan Stephens" } }; DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); DataTable dt2 = GetDataTable(students2); IEnumerable seq2 = dt2.AsEnumerable(); IEnumerable except = seq1.Except(seq2, System.Data.DataRowComparer.Default); Console.WriteLine("{0}Résultats de l’opérateur Except() avec le comparateur{0}", System.Environment.NewLine); OutputDataTableHeader(dt1, 15); foreach (DataRow dataRow in except) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); } except = seq1.Except(seq2); Console.WriteLine("{0}Résultats de l’opérateur Except() sans le comparateur{0}", System.Environment.NewLine); OutputDataTableHeader(dt1, 15); foreach (DataRow dataRow in except) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); }

Cet exemple crée deux objets DataTable et les remplit avec les données stockées dans les tableaux Student. La méthode AsEnumerable est alors appelée pour transformer les deux objets DataTable en séquences. Enfin, l’opérateur Except est appelé sur les deux séquences et les résultats sont affichés. Comme vous pouvez le voir, le premier appel à l’opérateur Except transmet le comparateur System.Data.DataRowComparer.Default dans le deuxième argument. Le second appel ne transmet aucun comparateur.

Linq.book Page 342 Mercredi, 18. février 2009 7:58 07

342

LINQ to DataSet

Partie IV

Voici les résultats affichés lors de l’appui sur Ctrl+F5 : Résultats de l’opérateur Except() avec le comparateur Id Name ============================== 1 Joe Rattz 13 Stacy Sinclair Résultats de l’opérateur Except() sans le comparateur Id Name ============================== 1 Joe Rattz 7 Anthony Adams 13 Stacy Sinclair 72 Dignan Stephens

Comme vous pouvez le voir, seul le premier appel à l’opérateur Except a été en mesure de comparer de façon correcte les données des deux séquences. Opérateur Intersect L’opérateur Intersect renvoie une séquence d’objets DataRow qui représente l’intersection des deux séquences DataRow passées en entrée. La séquence de sortie contient les éléments uniques des deux séquences d’entrée, listés dans leur ordre d’apparition original. Pour déterminer quels éléments sont uniques, l’opérateur Intersect doit être en mesure de déterminer si deux éléments sont égaux. Pour ce faire, il devrait lui suffire d’utiliser les méthodes GetHashCode et Equals. Cependant, étant donné que les objets comparés sont de type DataRow, un prototype spécifique doit être utilisé. Le deuxième argument de ce prototype désigne le comparateur (System.Data.DataRowComparer.Default) à utiliser. La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur le type de donnée statique de chaque colonne. L’interface IComparable est utilisée sur les colonnes qui implémentent cette interface. La méthode statique System.Object.Equals est utilisée sur les autres colonnes. Prototype Nous nous intéresserons à un seul prototype de cet opérateur : public static IEnumerable Intersect ( this IEnumerable first, IEnumerable second, IEqualityComparer comparer);

Exemple Nous utiliserons le même code que dans l’exemple de l’opérateur Except mais, ici, c’est l’opérateur Intersect qui sera appelé (voir Listing 10.4).

Openmirrors.com

Linq.book Page 343 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

343

Listing 10.4 : Appel de l’opérateur Intersect avec et sans comparateur. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

Student[] students2 = { new Student { Id = 5, Name = "Abe Henry" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 29, Name = "Future Man" }, new Student { Id = 72, Name = "Dignan Stephens" } }; DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); DataTable dt2 = GetDataTable(students2); IEnumerable seq2 = dt2.AsEnumerable(); IEnumerable intersect = seq1.Intersect(seq2, System.Data.DataRowComparer.Default); Console.WriteLine("{0}Résultats de l’opérateur Intersect() avec le comparateur{0}", System.Environment.NewLine); OutputDataTableHeader(dt1, 15); foreach (DataRow dataRow in intersect) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); } intersect = seq1.Intersect(seq2); Console.WriteLine("{0}Résultats de l’opérateur Intersect() sans le comparateur{0}", System.Environment.NewLine); OutputDataTableHeader(dt1, 15); foreach (DataRow dataRow in intersect) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); }

Rien de nouveau dans ce code : deux objets DataTable sont créés et initialisés avec les données des tableaux Student. Ils sont ensuite convertis en séquences, puis l’opérateur Intersect leur est appliqué, avec puis sans comparateur. Les résultats sont affichés après chaque appel à l’opérateur Intersect.

Linq.book Page 344 Mercredi, 18. février 2009 7:58 07

344

LINQ to DataSet

Partie IV

Voici les informations affichées suite à l’appui sur Ctrl+F5 : Résultats de l’opérateur Intersect() avec le comparateur Id Name ============================== 7 Anthony Adams 72 Dignan Stephens Résultats de l’opérateur Intersect() sans le comparateur Id Name ==============================

Comme vous pouvez le voir, seul le premier appel à l’opérateur Intersect a été en mesure de comparer de façon correcte les données des deux séquences. Opérateur Union L’opérateur Union renvoie une séquence d’objets DataRow qui représente la réunion des deux séquences DataRow passées en entrée. La séquence de sortie contient les éléments de la première séquence suivis des éléments de la seconde séquence qui n’ont pas déjà été cités. Pour déterminer quels éléments ont déjà été sélectionnés dans la première séquence, l’opérateur Union doit être en mesure de déterminer si deux éléments sont égaux. Pour ce faire, il devrait lui suffire d’utiliser les méthodes GetHashCode et Equals. Cependant, étant donné que les objets comparés sont de type DataRow, un prototype spécifique doit être utilisé. Le deuxième argument de ce prototype désigne le comparateur (System.Data.DataRowComparer.Default) à utiliser. La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur le type de donnée statique de chaque colonne. L’interface IComparable est utilisée sur les colonnes qui implémentent cette interface. La méthode statique System.Object.Equals est utilisée sur les autres colonnes. Prototype Nous nous intéresserons à un seul prototype de cet opérateur : public static IEnumerable Union ( this IEnumerable first, IEnumerable second, IEqualityComparer comparer);

Exemple Nous utiliserons le même code que dans l’exemple de l’opérateur Intersect mais, ici, c’est l’opérateur Union qui sera appelé (voir Listing 10.5).

Openmirrors.com

Linq.book Page 345 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

345

Listing 10.5 : Appel de l’opérateur Union avec et sans comparateur. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

Student[] students2 = { new Student { Id = 5, Name = "Abe Henry" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 29, Name = "Future Man" }, new Student { Id = 72, Name = "Dignan Stephens" } }; DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); DataTable dt2 = GetDataTable(students2); IEnumerable seq2 = dt2.AsEnumerable(); IEnumerable union = seq1.Union(seq2, System.Data.DataRowComparer.Default); Console.WriteLine("{0}Résultats de l’opérateur Union() avec le comparateur{0}", System.Environment.NewLine); OutputDataTableHeader(dt1, 15); foreach (DataRow dataRow in union) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); } union = seq1.Union(seq2); Console.WriteLine("{0}Résultats de l’opérateur Union() sans le comparateur{0}", System.Environment.NewLine); OutputDataTableHeader(dt1, 15); foreach (DataRow dataRow in union) { Console.WriteLine("{0,-15}{1,-15}", dataRow.Field(0), dataRow.Field(1)); }

Ici encore, rien de nouveau dans ce code : deux objets DataTable sont créés et initialisés avec les données des tableaux Student. Ils sont ensuite convertis en séquences, puis l’opérateur Union leur est appliqué, avec puis sans comparateur. Les résultats sont affichés après chaque appel à l’opérateur Union.

Linq.book Page 346 Mercredi, 18. février 2009 7:58 07

346

LINQ to DataSet

Partie IV

Voici les informations affichées suite à l’appui sur Ctrl+F5 : Résultats de l’opérateur Union() avec le comparateur Id Name ============================== 1 Joe Rattz 7 Anthony Adams 13 Stacy Sinclair 72 Dignan Stephens 5 Abe Henry 29 Future Man Résultats de l’opérateur Union() sans le comparateur Id Name ============================== 1 Joe Rattz 7 Anthony Adams 13 Stacy Sinclair 72 Dignan Stephens 5 Abe Henry 7 Anthony Adams 29 Future Man 72 Dignan Stephens

Comme vous pouvez le voir, seul le premier appel à l’opérateur Union a donné les résultats escomptés. Opérateur SequencialEqual L’opérateur SequencialEqual compare deux séquences d’objets DataRow et détermine leur égalité. Pour ce faire, les deux séquences sources sont énumérées et leurs objets DataRow, comparés. Si les deux séquences sources ont le même nombre de lignes, et si tous les objets DataRow sont égaux, l’opérateur retourne la valeur true. Dans le cas contraire, il retourne la valeur false. Cet opérateur doit être en mesure de déterminer si deux éléments sont égaux. Pour ce faire, il devrait lui suffire d’utiliser les méthodes GetHashCode et Equals. Cependant, étant donné que les objets comparés sont de type DataRow, un prototype spécifique doit être utilisé. Le deuxième argument de ce prototype désigne le comparateur (System.Data.DataRowComparer.Default) à utiliser. La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur le type de donnée statique de chaque colonne. L’interface IComparable est utilisée sur les colonnes qui l’implémentent. La méthode statique System.Object.Equals est utilisée sur les autres colonnes. Prototype Nous nous intéresserons à un seul prototype de cet opérateur : public static bool SequenceEqual ( this IEnumerable first, IEnumerable second, IEqualityComparer comparer);

Openmirrors.com

Linq.book Page 347 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

347

Exemple Dans cet exemple, nous allons construire deux séquences identiques d’objets DataRow et les comparer avec l’opérateur SequencialEqual. Deux comparaisons seront effectuées. La première utilisera un comparateur et la seconde, non (voir Listing 10.6). Listing 10.6 : Appel de l’opérateur SequenceEqual avec et sans comparateur. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); DataTable dt2 = GetDataTable(students); IEnumerable seq2 = dt2.AsEnumerable(); bool equal = seq1.SequenceEqual(seq2, System.Data.DataRowComparer.Default); Console.WriteLine("Appel de SequenceEqual() avec comparateur : {0}", equal); equal = seq1.SequenceEqual(seq2); Console.WriteLine("Appel de SequenceEqual() sans le comparateur : {0}", equal);

Comme on pouvait s’y attendre, le premier appel à l’opérateur SequenceEqual indique que les deux séquences sont égales, alors que le second indique qu’elles sont différentes : Appel de SequenceEqual() avec comparateur : True Appel de SequenceEqual() sans comparateur : False

Opérateurs dédiés aux champs Ces opérateurs viennent compléter ceux passés en revue dans la section précédente. Ils sont définis dans l’assembly System.Data.DataSetExtensions.dll, dans la classe statique System.Data.DataRowExtensions. Vous avez certainement remarqué que, dans la plupart des exemples précédents, nous avons utilisé l’opérateur Field pour extraire d’un DataRow la valeur d’un objet DataColumn. Cet opérateur a deux intérêts : grâce à lui, la comparaison de données est possible, et il gère la valeur null. La manipulation des objets DataRow présente un problème : les DataColumn, de type "valeur" (à opposer au type "référence"), ne peuvent pas être comparés correctement. En effet, ils peuvent contenir une donnée de type quelconque : un entier, une chaîne ou un autre type de donnée. Si, par exemple, un DataColumn contient une valeur de type int, il doit être converti en une référence de type Object. Cette conversion est connue sous le nom "boxing" dans l’environnement de développement .NET. L’opération inverse (la transformation d’un type référence en un type valeur) est appelée "unboxing". Le problème se situe au niveau du boxing.

Linq.book Page 348 Mercredi, 18. février 2009 7:58 07

348

LINQ to DataSet

Partie IV

Pour mieux comprendre ce problème, nous allons raisonner sur quelques exemples. Dans le Listing 10.7, nous comparons deux entiers littéraux de même valeur. Listing 10.7 : Comparaison des valeurs 3 et 3. Console.WriteLine("(3 == 3) vaut {0}.", (3 == 3));

Voici le résultat : (3 == 3) vaut True.

Aucune surprise dans ce résultat. Mais que devient la comparaison si les entiers à comparer ont subi un boxing ? Examinons le code du Listing 10.8. Listing 10.8 : Comparaison des valeurs 3 et 3 après leur casting dans des Object. Console.WriteLine("((Object)3 == (Object)3) vaut {0}.", ((Object)3 == (Object)3));

Voici le résultat : ((Object)3 == (Object)3) vaut False.

Que s’est-il passé ? L’opérateur de casting (Object) convertit chacune des valeurs en un objet de type Object. Dans cet exemple, la comparaison porte non pas sur la valeur de ces objets mais sur leur référence, c’est-à-dire leur adresse. Bien entendu, les deux adresses ne sont pas identiques. Lorsque vous accédez aux objets DataColumn en indexant un objet DataRow, si une colonne a un type valeur, elle subit un boxing, et sa comparaison ne donne pas le résultat escompté. Pour mettre en évidence ce problème, nous allons raisonner sur un exemple plus complexe qui utilise des objets DataColumn. Ici, nous utiliserons deux tableaux de classe différente. Le premier est le tableau Student, utilisé dans les exemples précédents. Le deuxième a pour classe designations. Il contient des données étrangères au tableau Student. Voici la classe StudentClass : Une classe élémentaire contenant deux propriétés publiques class StudentClass { public int Id; public string Class; }

Pour convertir un tableau de classe StudentClass en un objet DataTable, nous utiliserons la méthode suivante : static DataTable GetDataTable2(StudentClass[] studentClasses) { DataTable table = new DataTable(); table.Columns.Add("Id", typeof(Int32)); table.Columns.Add("Class", typeof(string)); foreach (StudentClass studentClass in studentClasses)

Openmirrors.com

Linq.book Page 349 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

349

{ table.Rows.Add(studentClass.Id, studentClass.Class); } return (table); }

Cette méthode est une copie de la méthode commune GetTableData, modifiée pour fonctionner avec des tableaux d’objets StudentClass. Si vous êtes amené à travailler avec des tableaux dans un code de production réel, vous ne définirez pas une méthode spécifique pour chacune des classes qui utilise des objets DataTable. Vous vous tournerez plutôt vers une méthode d’extension générique. Comme il a été dit il y a quelques pages, dans un environnement réel les données seront généralement obtenues en appliquant des requêtes LINQ to DataSet à une base de données… À titre d’exemple, nous convertirons les tableaux en séquences d’objets DataRow et nous tenterons d’effectuer une jointure sur le champ Id. Ce champ sera obtenu en utilisant le nom des colonnes pour indexer les DataRow (voir Listing 10.9). Listing 10.9 : Réalisation d’une jointure en indexant le DataRow. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

StudentClass[] classDesignations = { new StudentClass { Id = 1, Class = "Sophmore" }, new StudentClass { Id = 7, Class = "Freshman" }, new StudentClass { Id = 13, Class = "Graduate" }, new StudentClass { Id = 72, Class = "Senior" } }; DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); DataTable dt2 = GetDataTable2(classDesignations); IEnumerable seq2 = dt2.AsEnumerable(); string anthonysClass = (from s in seq1 where s.Field("Name") == "Anthony Adams" from c in seq2 where c["Id"] == s["Id"] select (string)c["Class"]). SingleOrDefault(); Console.WriteLine("La classe d’Anthony est : {0}", anthonysClass != null ? anthonysClass : "null");

Quelques éléments dans la requête précédente méritent des explications. La ligne en gras réalise une jointure sur les deux tableaux. Pour ce faire, elle indexe les deux objets DataRow afin d’accéder aux valeurs du champ Id. Ces valeurs étant de type string, elles subissent un boxing. Il sera donc impossible de déterminer leur égalité en utilisant des moyens conventionnels. Deux lignes plus haut, nous utilisons l’opérateur Field pour comparer la valeur du champ Name et la valeur littérale "Anthony Adams". Cet

Linq.book Page 350 Mercredi, 18. février 2009 7:58 07

350

LINQ to DataSet

Partie IV

opérateur est appelé pour éliminer le problème de boxing qui va être mis en évidence sur le champ Id. Remarquez également que la requête mélange la syntaxe d’interrogation de requête de LINQ et la notation à point classique. Voici les résultats : La classe d’Anthony est : Null

Ce problème vient du fait que la ligne en gras n’a pas été en mesure de réaliser la jointure. Le boxing du champ Id en est évidemment la cause. Pour corriger ce problème, nous allons modifier la ligne : where c["Id"] == s["Id"]

en : where (int)c["Id"] == (int)s["Id"]

Le code devient donc celui du Listing 10.10. Listing 10.10 : Utilisation d’un opérateur de casting pour pouvoir tester l’égalité des champs Id. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

StudentClass[] classDesignations = { new StudentClass { Id = 1, Class = "Sophmore" }, new StudentClass { Id = 7, Class = "Freshman" }, new StudentClass { Id = 13, Class = "Graduate" }, new StudentClass { Id = 72, Class = "Senior" } }; DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); DataTable dt2 = GetDataTable2(classDesignations); IEnumerable seq2 = dt2.AsEnumerable(); string anthonysClass = (from s in seq1 where s.Field("Name") == "Anthony Adams" from c in seq2 where (int)c["Id"] == (int)s["Id"] select (string)c["Class"]). SingleOrDefault(); Console.WriteLine("La classe d’Anthony est : {0}", anthonysClass != null ? anthonysClass : "null");

L’exécution de ce code produit le résultat suivant : La classe d’Anthony est : Freshman

Le problème lié au boxing du champ Id a été évité. Cependant, un autre problème est toujours présent : lorsque vous tentez d’obtenir une valeur dans une colonne en indexant un objet DataRow, l’objet retourné est de type Object. Pour le comparer à une Openmirrors.com

Linq.book Page 351 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

351

valeur littérale ou l’affecter à une variable, vous devrez utiliser un opérateur de casting. Dans cet exemple, nous utilisons l’opérateur (int). Étant donné que les objets DataSet utilisent la valeur DBNull.Value pour représenter une valeur null, si une colonne contient la valeur null son casting au format int produira une exception. Heureusement, les opérateurs LINQ to DataSet Field et SetField éliminent ces deux problèmes. Le Listing 10.11 représente l’exemple précédent, dans lequel l’opérateur de casting (int) a été remplacé par un Field. Listing 10.11 : Utilisation de l’opérateur Field. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

StudentClass[] classDesignations = { new StudentClass { Id = 1, Class = "Sophmore" }, new StudentClass { Id = 7, Class = "Freshman" }, new StudentClass { Id = 13, Class = "Graduate" }, new StudentClass { Id = 72, Class = "Senior" } }; DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); DataTable dt2 = GetDataTable2(classDesignations); IEnumerable seq2 = dt2.AsEnumerable(); string anthonysClass = (from s in seq1 where s.Field("Name") == "Anthony Adams" from c in seq2 where c.Field("Id") == s.Field("Id") select (string)c["Class"]). SingleOrDefault(); Console.WriteLine("La classe d’Anthony est : {0}", anthonysClass != null ? anthonysClass : "null");

Ce code étant équivalent au précédent, à ceci près que l’opérateur Field remplace l’opérateur de casting (int), il produit le même résultat : La classe d’Anthony est : Freshman

Opérateur Field Comme nous venons de le montrer dans le Listing 10.11, l’opérateur Field permet d’obtenir la valeur d’une colonne dans un objet DataRow. Par ailleurs, il évite les problèmes liés au boxing et aux valeurs null. Prototypes Six prototypes de cet opérateur seront étudiés dans cette section.

Linq.book Page 352 Mercredi, 18. février 2009 7:58 07

352

LINQ to DataSet

Partie IV

Le premier prototype retourne la valeur de la colonne pour le DataColumn et la version spécifiés : Le premier prototype public static T Field ( this DataRow first, System.Data.DataColumn column, System.Data.DataRowVersion version);

Le deuxième prototype retourne la valeur de la colonne pour la colonne dont le nom et la version sont spécifiés : Le deuxième prototype public static T Field ( this DataRow first, string columnName, System.Data.DataRowVersion version);

Le troisième prototype retourne la valeur de la colonne pour la colonne dont l’ordinal et la version sont spécifiés : Le troisième prototype public static T Field ( this DataRow first, int ordinal, System.Data.DataRowVersion version);

Le quatrième prototype retourne la valeur de la colonne dont le DataColumn est spécifié : Le quatrième prototype public static T Field ( this DataRow first, System.Data.DataColumn column);

Le cinquième prototype retourne la valeur de la colonne pour la colonne dont le nom est spécifié : Le cinquième prototype public static T Field ( this DataRow first, string columnName);

Le sixième prototype retourne la valeur de la colonne pour la colonne dont l’ordinal est spécifié : Le sixième prototype public static T Field ( this DataRow first, int ordinal);

Vous l’avez certainement remarqué, les trois premiers prototypes permettent de choisir la version (DataRowVersion) de l’objet DataColumn à obtenir. Openmirrors.com

Linq.book Page 353 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

353

Exemples Lorsque vous êtes arrivé à ce point dans la lecture de l’ouvrage, plusieurs exemples vous ont montré comment utiliser l’opérateur Field. Le Listing 10.12 va aller plus loin en vous montrant les six facettes de cet opérateur. Listing 10.12 : Un exemple des six prototypes de l’opérateur Field. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); int id; // Utilisation du premier prototype id = (from s in seq1 where s.Field("Name") == "Anthony Adams" select s.Field(dt1.Columns[0], DataRowVersion.Current)). Single(); Console.WriteLine("Le champ Id d’Anthony, obtenu avec le premier prototype, a pour ➥valeur {0}", id); // Utilisation du deuxième prototype id = (from s in seq1 where s.Field("Name") == "Anthony Adams" select s.Field("Id", DataRowVersion.Current)). Single(); Console.WriteLine("Le champ Id d’Anthony, obtenu avec le deuxième prototype, a pour ➥valeur {0}", id); // Utilisation du troisième prototype id = (from s in seq1 where s.Field("Name") == "Anthony Adams" select s.Field(0, DataRowVersion.Current)). Single(); Console.WriteLine("Le champ Id d’Anthony, obtenu avec le troisième prototype, a pour ➥valeur {0}", id); // Utilisation du quatrième prototype id = (from s in seq1 where s.Field("Name") == "Anthony Adams" select s.Field(dt1.Columns[0])). Single(); Console.WriteLine("Le champ Id d’Anthony, obtenu avec le quatrième prototype, a pour ➥valeur {0}", id); // Utilisation du cinquième prototype id = (from s in seq1 where s.Field("Name") == "Anthony Adams" select s.Field("Id")). Single(); Console.WriteLine("Le champ Id d’Anthony, obtenu avec le cinquième prototype, a pour ➥valeur {0}", id);

Linq.book Page 354 Mercredi, 18. février 2009 7:58 07

354

LINQ to DataSet

Partie IV

// Utilisation du sixième prototype id = (from s in seq1 where s.Field("Name") == "Anthony Adams" select s.Field(0)). Single(); Console.WriteLine("Le champ Id d’Anthony, obtenu avec le sixième prototype, a pour ➥valeur {0}", id);

Les premières lignes du code définissent le tableau Students, initialisent un objet DataTable avec son contenu et le convertissent en une séquence. La suite du code applique tour à tour les six prototypes de l’opérateur Field à la séquence pour obtenir la valeur du champ Id. Notez que l’opérateur Field est également utilisé dans la partie Where de la requête. Voici les résultats : Le Le Le Le Le Le

champ champ champ champ champ champ

Id Id Id Id Id Id

d’Anthony, d’Anthony, d’Anthony, d’Anthony, d’Anthony, d’Anthony,

obtenu obtenu obtenu obtenu obtenu obtenu

avec avec avec avec avec avec

le le le le le le

premier prototype, a pour valeur 7 deuxième prototype, a pour valeur 7 troisième prototype, a pour valeur 7 quatrième prototype, a pour valeur 7 cinquième prototype, a pour valeur 7 sixième prototype, a pour valeur 7

Pour illustrer l’utilisation de l’argument DataRowVersion, nous avons modifié une valeur DataColumn en utilisant l’opérateur SetField. Cet opérateur n’a pas encore été étudié. Pour l’instant, ignorez le code qui l’utilise. Vous en apprendrez plus à son sujet dans la section suivante. Ce chapitre étant consacré aux opérateurs LINQ to DataSet et non au fonctionnement détaillé de la classe DataSet, nous n’aborderons ce sujet que très brièvement, à travers les deux méthodes DataSet utilisées dans l’exemple du Listing 10.13. Listing 10.13 : Démonstration de l’argument DataRowVersion de l’opérateur Field. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); DataRow row = (from s in seq1 where s.Field("Name") == "Anthony Adams" select s).Single(); row.AcceptChanges(); row.SetField("Name", "George Oscar Bluth"); Console.WriteLine("Valeur originale = {0} : Valeur actuelle = {1}", row.Field("Name", DataRowVersion.Original), row.Field("Name", DataRowVersion.Current));

Openmirrors.com

Linq.book Page 355 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

355

row.AcceptChanges(); Console.WriteLine("Valeur originale = {0} : Valeur actuelle = {1}", row.Field("Name", DataRowVersion.Original), row.Field("Name", DataRowVersion.Current));

Cet exemple définit une séquence à partir des données du tableau students. Une requête est alors lancée pour obtenir un objet DataRow unique. Le premier code digne d’intérêt est la méthode AcceptChanges, appelée juste après avoir obtenu l’objet DataRow. Cette méthode est appelée pour que l’objet DataRow considère ses valeurs actuelles comme étant des valeurs originales. Si cette méthode n’avait pas été appelée, les valeurs originales de l’objet DataRow ne seraient pas définies, et une exception se produirait si on essayait de les afficher. Une fois la méthode AcceptChanges exécutée, l’objet DataRow est prêt pour pister les changements dans ses valeurs DataColumn. Le premier appel de la méthode AcceptChanges est suivi de la modification du champ Name avec l’opérateur SetField. Le bloc d’instructions suivant affiche la valeur originale ("Anthony Adams") et la valeur actuelle ("George Oscar Bluth") du DataColumn Name. La méthode AcceptChanges est appelée une deuxième fois, puis les valeurs originale et actuelle du DataColumn Name sont à nouveau affichées. Cette fois-ci, les deux valeurs devraient être identiques et égales à "George Oscar Bluth", puisque la méthode AcceptChanges a été appelée. Examinons les résultats : Valeur originale = Anthony Adams : Valeur actuelle = George Oscar Bluth Valeur originale = George Oscar Bluth : Valeur actuelle = George Oscar Bluth

Si vous ne deviez retenir qu’une chose de cet exemple, que ce soit l’utilisation de la méthode AcceptChanges. Cette méthode permet de mémoriser la valeur originale et d’affecter une autre valeur à un objet DataColumn. Comme il a été dit précédemment, l’opérateur Field sait également éviter le problème lié aux champs vides (null). Dans le Listing 10.14, nous allons voir ce qui se passe lorsqu’un nom d’étudiant n’est pas initialisé et que l’opérateur Field n’est pas utilisé. Listing 10.14 : Un exemple de champ null sans utiliser l’opérateur Field. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = null }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); string name = seq1.Where(student => student.Field("Id") == 7) .Select(student => (string)student["Name"]) .Single(); Console.WriteLine("Student’s name is ’{0}’", name);

Linq.book Page 356 Mercredi, 18. février 2009 7:58 07

356

LINQ to DataSet

Partie IV

Deux passages apparaissent en gras pour attirer votre attention. Dans le premier, le nom de l’étudiant dont la colonne id vaut 7 est initialisé à null. Dans le second, l’opérateur Field est remplacé par un simple casting (string). Voici les résultats : Une exception non gérée s’est produite : System.InvalidCastException : Impossible d’effectuer un cast d’un objet de type ’System.DBNull’ en type ’System.String’. …

Que s’est-il passé ? La valeur de l’objet DataColumn dont la colonne id vaut 7 étant null, il est impossible de lui appliquer un casting (string). Il existe des solutions plus verbeuses pour éviter ce problème, mais le plus simple consiste à utiliser l’opérateur Field (voir Listing 10.15). Listing 10.15 : Un exemple de champ null avec l’opérateur Field. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = null }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); string name = seq1.Where(student => student.Field("Id") == 7) .Select(student => student.Field["Name"]) .Single(); Console.WriteLine("Le nom de l’étudiant est ’{0}’", name);

Ce code est identique au précédent mais, ici, le casting (string) est remplacé par un appel à l’opérateur Field. Voici le résultat : Le nom de l’étudiant est ’’

Opérateur SetField La valeur null affecte également l’initialisation des objets DataColumn. Pour éviter tout problème, vous utiliserez l’opérateur SetField. Par son intermédiaire, il est en effet possible d’affecter à un DataColumn une donnée de type nullable dont la valeur est null. Prototypes Nous nous intéresserons à trois prototypes de cet opérateur dans ce chapitre.

Le premier prototype vous permet de définir la valeur de la colonne spécifiée : Le premier prototype public static void SetField ( this DataRow first,

Openmirrors.com

Linq.book Page 357 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

357

System.Data.DataColumn column, T value);

Le deuxième prototype vous permet de définir la valeur de la colonne dont le nom est spécifié : Le deuxième prototype public static void SetField ( this DataRow first, string columnName, T value);

Le troisième prototype vous permet de définir la valeur de la colonne dont l’ordinal est spécifié : Le troisième prototype public static void SetField ( this DataRow first, int ordinal, T value);

Exemples Dans le Listing 10.16, nous définissons puis affichons la séquence de DataRow avec laquelle nous allons travailler. Le DataRow d’un des étudiants est alors obtenu en effectuant une requête, puis le nom de l’étudiant est modifié avec l’opérateur SetField. Enfin, la séquence de DataRow ainsi modifiée est à nouveau affichée. Ce processus est répété pour chacun des prototypes de l’opérateur SetField. Listing 10.16 : Un exemple d’utilisation des prototypes de l’opérateur SetField. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); IEnumerable seq1 = dt1.AsEnumerable(); Console.WriteLine("{0}Résultats avant d’appeler les prototypes :", System.Environment.NewLine); foreach (DataRow dataRow in seq1) { Console.WriteLine("Student Id = {0} is {1}", dataRow.Field("Id"), dataRow.Field("Name")); } // Utilisation du premier prototype (from s in seq1 where s.Field("Name") == "Anthony Adams" select s).Single().SetField(dt1.Columns[1], "George Oscar Bluth"); Console.WriteLine("{0}Résultats après l’appel du premier prototype :", System.Environment.NewLine);

Linq.book Page 358 Mercredi, 18. février 2009 7:58 07

358

LINQ to DataSet

Partie IV

foreach (DataRow dataRow in seq1) { Console.WriteLine("Student Id = {0} is {1}", dataRow.Field("Id"), dataRow.Field("Name")); } // Utilisation du deuxième prototype (from s in seq1 where s.Field("Name") == "George Oscar Bluth" select s).Single().SetField("Name", "Michael Bluth"); Console.WriteLine("{0}Résultats après l’appel du deuxième prototype :", System.Environment.NewLine); foreach (DataRow dataRow in seq1) { Console.WriteLine("Student Id = {0} is {1}", dataRow.Field("Id"), dataRow.Field("Name")); } // Utilisation du troisième prototype (from s in seq1 where s.Field("Name") == "Michael Bluth" select s).Single().SetField("Name", "Tony Wonder"); Console.WriteLine("{0}Résultats après l’appel du troisième prototype :", System.Environment.NewLine); foreach (DataRow dataRow in seq1) { Console.WriteLine("L’étudiant dont l’Id = {0} est {1}", dataRow.Field("Id"), dataRow.Field("Name")); }

Le code n’est pas aussi difficile qu’il en a l’air. Après avoir obtenu et affiché la séquence d’étudiants, un même bloc de code est répété à trois reprises (une pour chaque prototype). Chacun des blocs contient une requête LINQ qui récupère le champ Name, modifie sa valeur et affiche dans la console une ligne d’en-tête suivie des éléments de la séquence. Nous allons nous attarder sur plusieurs passages dans ce listing. Dans chacune des requêtes LINQ portant sur le champ Name du DataRow, nous mélangeons la syntaxe de requête propre à LINQ et la traditionnelle syntaxe à point. Remarquez également que nous utilisons l’opérateur Field pour accéder à l’enregistrement sur lequel nous allons appliquer l’opérateur SetField. Chaque requête extrait un DataRow de la séquence par son champ Name (modifié à l’étape précédente, sauf pour le premier prototype) et le modifie avec l’opérateur SetField. Par exemple, dans le code relatif au premier prototype la requête extrait le DataRow dont le champ Name vaut "Anthony Adams" et modifie ce nom en "George Oscar Bluth". Dans le code du deuxième prototype, la requête extrait le DataRow dont le Name est "George Oscar Bluth" et le modifie en une valeur qui sera utilisée comme critère de sélection dans le troisième prototype. Pour chaque prototype, une boucle foreach affiche les éléments de la séquence ainsi modifiés, afin que vous puissiez vérifier que la modification a effectivement été effectuée. Openmirrors.com

Linq.book Page 359 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

359

Avez-vous remarqué que l’interrogation de la séquence et la mise à jour du DataRow se font dans une seule et même instruction ? Il n’y a rien de magique là-dedans : nous utilisons simplement la puissance de LINQ. Voici les résultats : Résultats avant L’étudiant dont L’étudiant dont L’étudiant dont L’étudiant dont

d’appeler les prototypes : l’Id = 1 est Joe Rattz l’Id = 7 est Anthony Adams l’Id = 13 est Stacy Sinclair l’Id = 72 est Dignan Stephens

Résultats après L’étudiant dont L’étudiant dont L’étudiant dont L’étudiant dont

l’appel du premier prototype : l’Id = 1 est Joe Rattz l’Id = 7 est George Oscar Bluth l’Id = 13 est Stacy Sinclair l’Id = 72 est Dignan Stephens

Résultats après L’étudiant dont L’étudiant dont L’étudiant dont L’étudiant dont

l’appel du deuxième prototype : l’Id = 1 est Joe Rattz l’Id = 7 est Michael Bluth l’Id = 13 est Stacy Sinclair l’Id = 72 est Dignan Stephens

Résultats après L’étudiant dont L’étudiant dont L’étudiant dont L’étudiant dont

l’appel du troisième prototype : l’Id = 1 est Joe Rattz l’Id = 7 est Tony Wonder l’Id = 13 est Stacy Sinclair l’Id = 72 est Dignan Stephens

Opérateurs dédiés aux DataTable Dans la classe DataRowExtensions, en complément des opérateurs dédiés aux DataRow, plusieurs opérateurs spécifiques DataTable ont été définis. Nous les avons regroupés dans cette section. Ces opérateurs sont définis dans l’assembly System.Data.Entity.dll, dans la classe statique System.Data.DataTableExtensions. Opérateur AsEnumerable Vous devez certainement être surpris de trouver un opérateur AsEnumerable dédié à la classe DataTable qui retourne une séquence d’objets DataRow. Si tel est le cas, cela signifie que vous ne vous êtes pas demandé pourquoi nous n’en avions pas déjà parlé… alors que nous l’avons utilisé dans pratiquement tous les exemples ! Si vous jetez un œil à la classe statique System.Data.DataTableExtensions, vous trouverez effectivement un opérateur AsEnumerable. Cet opérateur retourne une séquence de type IEnumerable à partir d’un objet DataTable. Prototype Un seul prototype de cet opérateur sera traité dans cette section : public static IEnumerable AsEnumerable ( this DataTable source );

Linq.book Page 360 Mercredi, 18. février 2009 7:58 07

360

LINQ to DataSet

Partie IV

Cet opérateur est appliqué à un objet DataTable. Il retourne une séquence d’objets DataRow. C’est traditionnellement la première étape lors de l’exécution d’une requête LINQ to DataSet sur un DataTable d’un objet DataSet. Cet opérateur retourne une séquence IEnumerable, où T est un DataRow. Après son appel, vous pouvez donc utiliser les nombreux opérateurs de LINQ qui sont appelés sur une séquence de type IEnumerable. Exemples Étant donné que l’opérateur AsEnumerable est la première étape permettant d’effectuer une requête LINQ to DataSet, la plupart des exemples de ce chapitre utilisent cet opérateur.

Opérateur CopyToDataTable Vous savez maintenant comment effectuer une requête et modifier les valeurs DataColumn d’un DataRow. L’opérateur CopyToDataTable va vous permettre de placer cette séquence d’objets DataRow ainsi modifiée dans un DataTable. Prototypes Deux prototypes de cet opérateur seront examinés dans ce chapitre.

Le premier prototype est appelé sur un IEnumerable et il retourne un DataTable. Vous l’utiliserez donc pour créer un nouvel objet DataTable à partir d’une séquence d’objets DataRow : Le premier prototype public static DataTable CopyToDataTable ( this IEnumerable source ) where T : DataRow;

Ce premier prototype crée automatiquement les versions originales de chaque champ sans qu’il soit nécessaire d’appeler la méthode AcceptChange. Le second prototype est appelé sur un IEnumerable d’une table source DataTable. Il met à jour cette table en se basant sur la valeur LoadOption spécifiée dans l’argument. Le second prototype public static void CopyToDataTable ( this IEnumerable source, DataTable table, LoadOption options ) where T : DataRow;

La valeur de l’argument LoadOption indique à l’opérateur si les valeurs originales et/ou les valeurs actuelles des colonnes doivent être modifiées. Voici les valeurs possibles pour cet argument : m

OverwriteChanges. Les valeurs originale et actuelle de chaque colonne sont modifiées.

Openmirrors.com

Linq.book Page 361 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

m

PreserveChanges. Seule la valeur originale de chaque colonne est modifiée.

m

Upsert. Seule la valeur actuelle de chaque colonne est modifiée.

361

L’argument LoadOption induit un nouveau problème : comment l’opérateur CopyToDataTable peut-il savoir quel enregistrement de la DataTable de destination correspond à l’enregistrement de la DataTable source ? L’enregistrement source doit-il être ajouté au tableau de destination, ou un des enregistrements déjà présents doit-il être mis à jour ? Impossible de répondre à ces deux questions, à moins que l’opérateur n’utilise des clés primaires. Pour que le second prototype de l’opérateur CopyToDataTable fonctionne, l’objet DataTable de destination doit donc être pourvu de champs appropriés spécifiés en tant que clés primaires. Dans le cas contraire, les enregistrements sources seront ajoutés au tableau de destination. Une autre complication est inhérente au second prototype : les champs n’ont aucune valeur originale, à moins que vous n’appeliez la méthode AcceptChanges pour les créer. Si vous essayez d’accéder à la version originale d’un champ qui en est dénué, une exception se produit. Notez cependant que vous pouvez appeler la méthode HasVersion sur chacun des objets DataRow pour savoir s’ils possèdent une version originale, et ainsi éviter qu’une exception ne soit générée. Exemples Pour illustrer le premier prototype, nous allons modifier un champ dans un DataTable, créer un nouveau DataTable à partir du DataTable modifié en invoquant l’opérateur CopyToDataTable et afficher le contenu du nouveau DataTable (voir Listing 10.17). Listing 10.17 : Appel du premier prototype de l’opérateur CopyToDataTable. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); Console.WriteLine("Le DataTable original :"); foreach (DataRow dataRow in dt1.AsEnumerable()) { Console.WriteLine("Student Id = {0} is {1}", dataRow.Field("Id"), dataRow.Field("Name")); } (from s in dt1.AsEnumerable() where s.Field("Name") == "Anthony Adams" select s).Single().SetField("Name", "George Oscar Bluth"); DataTable newTable = dt1.AsEnumerable().CopyToDataTable();

Linq.book Page 362 Mercredi, 18. février 2009 7:58 07

362

LINQ to DataSet

Partie IV

Console.WriteLine("{0}Le nouveau DataTable :", System.Environment.NewLine); foreach (DataRow dataRow in newTable.AsEnumerable()) { Console.WriteLine("L’étudiant d’Id = {0} est {1}", dataRow.Field("Id"), dataRow.Field("Name")); }

Les premières lignes définissent un objet DataTable à partir du tableau students. Le bloc d’instructions suivant affiche le contenu du DataTable dans la fenêtre console. Le bloc d’instructions suivant modifie le champ Name d’un des objets DataRow. Un nouveau DataTable est alors créé à partir des données modifiées en invoquant l’opérateur CopyToDataTable. Le dernier bloc d’instructions affiche le contenu du nouveau DataTable. Voici le résultat : Le DataTable original : L’étudiant d’Id = 1 est Joe Rattz L’étudiant d’Id = 7 est Anthony Adams L’étudiant d’Id = 13 est Stacy Sinclair L’étudiant d’Id = 72 est Dignan Stephens Le nouveau L’étudiant L’étudiant L’étudiant L’étudiant

DataTable : d’Id = 1 est Joe Rattz d’Id = 7 est George Oscar Bluth d’Id = 13 est Stacy Sinclair d’Id = 72 est Dignan Stephens

Comme vous pouvez le voir, le nouveau DataTable contient la version modifiée du DataTable original. Nous allons maintenant illustrer le deuxième prototype de l’opérateur CopyToDataTable. Comme il a été dit précédemment, il est nécessaire de définir une clé primaire dans le DataSet de destination pour que l’argument LoadOption produise l’effet escompté. À des fins démonstratives, nous ne définirons aucune clé primaire (voir Listing 10.18). Cet exemple étant plus complexe que le précédent, nous donnerons des explications à chaque fois que cela sera nécessaire. Listing 10.18 : Appel du second prototype sans définir une clé primaire dans le DataSet de destination. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); DataTable newTable = dt1.AsEnumerable().CopyToDataTable();

Jusqu’ici, rien de nouveau : le DataTable source est créé à partir du tableau students et le DataTable de destination, en appelant l’opérateur CopyToDataTable sur l’objet DataTable source. Étant donné que nous avons utilisé le premier prototype de l’opérateur Openmirrors.com

Linq.book Page 363 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

363

CopyToDataTable, il n’est pas nécessaire d’utiliser la méthode AcceptChanges sur le DataTable de destination. Il est important de le signaler car, dans le prochain bloc de code, la version originale du champ Name sera invoquée. Si la version originale de cet opérateur n’existait pas, une exception serait levée. Console.WriteLine("Avant la mise à jour du DataTable :"); foreach (DataRow dataRow in newTable.AsEnumerable()) { Console.WriteLine("Student Id = {0} : original {1} : current {2}", dataRow.Field("Id"), dataRow.Field("Name", DataRowVersion.Original), dataRow.Field("Name", DataRowVersion.Current)); }

Rien d’exceptionnel dans ce code, si ce n’est que la version originale du champ Name est utilisée. Aucune exception ne sera levée, puisqu’une version originale a été créée de façon transparente par le premier prototype de l’opérateur CopyToDataTable. (from s in dt1.AsEnumerable() where s.Field("Name") == "Anthony Adams" select s).Single().SetField("Name", "George Oscar Bluth"); dt1.AsEnumerable().CopyToDataTable(newTable, LoadOption.Upsert);

Ce bloc de code est le plus intéressant de cet exemple. Comme vous pouvez le voir, la valeur du champ Name d’un des enregistrements de l’objet DataTable source est modifiée avec l’opérateur SetField. Cette modification effectuée, l’opérateur CopyToDataTable est appelé en spécifiant qu’une copie de type LoadOption.Upsert (limitée à la valeur actuelle de chaque colonne) doit être effectuée. Ce deuxième opérateur CopyToDataTable pose un problème : la méthode AcceptChange n’ayant pas été appelée au préalable, la valeur initiale des colonnes n’a pas été définie. Si nous essayons d’accéder à ces valeurs initiales, une exception sera générée. Pour éviter ce problème, il est nécessaire d’utiliser la méthode HasVersion. Comme aucune clé primaire n’a été définie, tous les enregistrements sources seront ajoutés dans le tableau de destination. Console.WriteLine("{0}Après la mise à jour du DataTable:", System.Environment.NewLine); foreach (DataRow dataRow in newTable.AsEnumerable()) { Console.WriteLine("Etudiant d’Id = {0} : valeur originale {1}, valeur ➥actuelle{2}", dataRow.Field("Id"), dataRow.HasVersion(DataRowVersion.Original) ? dataRow.Field("Name", DataRowVersion.Original) : "-inexistante-", dataRow.Field("Name", DataRowVersion.Current)); }

Ce bloc de code se contente d’afficher le contenu de l’objet DataTable dans la console. Étant donné qu’aucune clé primaire n’a été définie dans le tableau de destination, aucune égalité ne sera établie entre les enregistrements lors de la copie du tableau. Tous les enregistrements sources seront donc ajoutés à la fin du DataTable de destination.

Linq.book Page 364 Mercredi, 18. février 2009 7:58 07

364

LINQ to DataSet

Partie IV

Remarquez également que seuls les champs Name dont la valeur initiale existe, c’est-àdire pour lesquels dataRow.HasVersion vaut true, sont affichés. Voici les résultats : Avant la mise à L’étudiant d’Id L’étudiant d’Id L’étudiant d’Id ➥Sinclair L’étudiant d’Id ➥Stephens

jour du DataTable : = 1 : valeur originale Joe Rattz, valeur actuelle Joe Rattz = 7 : valeur originale Anthony Adams, valeur actuelle Anthony Adams = 13 : valeur originale Stacy Sinclair, valeur actuelle Stacy

Après la mise à L’étudiant d’Id L’étudiant d’Id L’étudiant d’Id ➥Sinclair L’étudiant d’Id ➥Stephens L’étudiant d’Id L’étudiant d’Id ➥Bluth L’étudiant d’Id ➥Sinclair L’étudiant d’Id ➥Stephens

jour du DataTable : = 1 : valeur originale Joe Rattz, valeur actuelle Joe Rattz = 7 : valeur originale Anthony Adams, valeur actuelle Anthony Adams = 13 : valeur originale Stacy Sinclair, valeur actuelle Stacy

= 72 : valeur originale Dignan Stephens, valeur actuelle Dignan

= 72 : valeur originale Dignan Stephens, valeur actuelle Dignan = 1 : valeur originale -inexistante-, valeur actuelle Joe Rattz = 7 : valeur originale -inexistante-, valeur actuelle George Oscar = 13 : valeur originale -inexistante-, valeur actuelle Stacy = 72 : valeur originale -inexistante-, valeur actuelle Dignan

Comme vous pouvez le voir, plusieurs enregistrements apparaissent en double. Ceci est dû au fait qu’aucune clé primaire n’a été définie dans le DataTable de destination. L’enregistrement mis à jour apparaît également en double. La méthode AcceptChanges n’ayant pas été automatiquement appelée par le second prototype de l’opérateur CopyToDataTable, nous avons testé l’existence des valeurs initiales avec la méthode HasVersion. Vous vous demandez peut-être pourquoi nous n’avons pas simplement appelé la méthode AcceptChanges. Si nous l’avions fait, toutes les valeurs actuelles des champs seraient devenues des valeurs originales, et il aurait été impossible de déterminer quel enregistrement avait été modifié. Pour solutionner ce problème, il suffit de définir une clé primaire dans le DataTable de destination (voir Listing 10.19). Listing 10.19 : Appel du second prototype en définissant une clé primaire dans le DataTable de destination. Student[] students new Student { Id new Student { Id new Student { Id new Student { Id };

= = = = =

{ 1, Name = "Joe Rattz" }, 7, Name = "Anthony Adams" }, 13, Name = "Stacy Sinclair" }, 72, Name = "Dignan Stephens" }

DataTable dt1 = GetDataTable(students); DataTable newTable = dt1.AsEnumerable().CopyToDataTable(); newTable.PrimaryKey = new DataColumn[] { newTable.Columns[0] }; Console.WriteLine("Avant la mise à jour du DataTable :");

Openmirrors.com

Linq.book Page 365 Mercredi, 18. février 2009 7:58 07

Chapitre 10

LINQ to DataSet

365

foreach (DataRow dataRow in newTable.AsEnumerable()) { Console.WriteLine("Etudiant d’Id = {0} : valeur originale {1}, valeur actuelle ➥{2}", dataRow.Field("Id"), dataRow.Field("Name", DataRowVersion.Original), dataRow.Field("Name", DataRowVersion.Current)); } (from s in dt1.AsEnumerable() where s.Field("Name") == "Anthony Adams" select s).Single().SetField("Name", "George Oscar Bluth"); dt1.AsEnumerable().CopyToDataTable(newTable, LoadOption.Upsert); Console.WriteLine("{0}Après la mise à jour du DataTable :", System.Environment.NewLine); foreach (DataRow dataRow in newTable.AsEnumerable()) { Console.WriteLine("Etudiant d’Id = {0} : valeur originale {1}, valeur actuelle ➥{2}", dataRow.Field("Id"), dataRow.HasVersion(DataRowVersion.Original) ? dataRow.Field("Name", DataRowVersion.Original) : "-does not exist-", dataRow.Field("Name", DataRowVersion.Current)); }

La seule différence entre cet exemple et le précédent réside dans la définition d’une clé primaire dans le DataTable newTable. Voici les résultats : Avant la mise à L’étudiant d’Id L’étudiant d’Id L’étudiant d’Id Sinclair L’étudiant d’Id ➥Stephens

jour du DataTable : = 1 : valeur originale Joe Rattz, valeur actuelle Joe Rattz = 7 : valeur originale Anthony Adams, valeur actuelle Anthony Adams = 13 : valeur originale Stacy Sinclair, valeur actuelle Stacy

Après la mise à L’étudiant d’Id L’étudiant d’Id ➥Bluth L’étudiant d’Id ➥Sinclair L’étudiant d’Id ➥Stephens

jour du DataTable : = 1 : valeur originale Joe Rattz, valeur actuelle Joe Rattz = 7 : valeur originale Anthony Adams, valeur actuelle George Oscar

= 72 : valeur originale Dignan Stephens, valeur actuelle Dignan

= 13 : valeur originale Stacy Sinclair, valeur actuelle Stacy = 72 : valeur originale Dignan Stephens, valeur actuelle Dignan

Tout ceci est bien plus convenable : le champ Name de l’étudiant dont la colonne Id vaut 7 avait pour valeur "Anthony Adams", mais il est maintenant égal à "George Oscar Bluth", et les enregistrements ne sont pas dupliqués.

Résumé Ce chapitre vous a montré comment utiliser les opérateurs IEnumerable pour initialiser des objets DataRow et les opérateurs Field et SetField pour initialiser et lire les valeurs stockées dans des champs. Vous avez également vu qu’il était impératif d’utiliser les opérateurs spécifiques DataSet pour obtenir les résultats escomptés.

Linq.book Page 366 Mercredi, 18. février 2009 7:58 07

366

LINQ to DataSet

Partie IV

Enfin, vous avez vu qu’en les combinant avec les opérateurs de requête standard de LINQ to Objects vous pouviez définir des requêtes LINQ puissantes sur des objets DataSet. Dans le chapitre suivant, nous terminerons la partie dédiée à LINQ to DataSet en montrant comment effectuer des requêtes sur des DataSet typés. Vous découvrirez également un exemple de requête LINQ to DataSet portant sur une base de données réelle.

Openmirrors.com

Linq.book Page 367 Mercredi, 18. février 2009 7:58 07

11 Possibilités complémentaires des DataSet Le chapitre précédent a donné de nombreux exemples d’interrogation d’objets DataTable. Dans un environnement de développement réel, ces objets proviendront de DataSets. Cependant, dans un souci de simplicité, ils ont été créés par programme, en utilisant des tableaux statiques. N’ayez crainte, comme vous le verrez dans ce chapitre, cette technique n’est nullement limitative. Les exemples du chapitre précédent étaient tous basés sur des DataSets non typés. Il est parfois nécessaire d’exécuter une requête sur un DataSet typé en utilisant LINQ to DataSet. Dans ce chapitre, nous examinerons ces nouvelles possibilités et vous montrerons comment tirer le meilleur de LINQ to DataSet. Nous commencerons par l’interrogation de DataSets typés. Et nous poursuivrons par l’interrogation d’une base de données réelle.

Espaces de noms référencés Les exemples de ce chapitre utilisent les classes des espaces de noms System.Data, System.Data.SqlClient et System.Linq. Si les directives using correspondantes n’existent pas dans votre code, vous devez les définir comme suit : using System.Data; using System.Data.SqlClient; using System.Linq;

DataSets typés LINQ est en mesure d’exécuter des requêtes sur des DataSets non typés et typés. Dans le second cas, le code d’interrogation sera très simple à écrire et à lire. Étant donné qu’il

Linq.book Page 368 Mercredi, 18. février 2009 7:58 07

368

LINQ to DataSet

Partie IV

existe une classe dédiée aux DataSets, les requêtes peuvent accéder aux noms des tables et aux colonnes en utilisant les propriétés de classe des objets DataSet typés. Cela est plus pratique qu’indexer la collection de Tables ou utiliser les opérateurs Field et SetField. Plutôt qu’accéder à la table d’objets DataSet Students en utilisant cette instruction : DataTable Students = dataSet.Tables["Students"];

vous utiliserez l’instruction suivante : DataTable Students = dataSet.Students;

De la même manière, plutôt qu’obtenir la valeur d’un champ avec cette instruction : dataRow.Field("Name")

vous utiliserez l’instruction suivante : dataRow.Name

Ces facilités d’écriture rendent le code bien plus facile à lire et à maintenir. Avant de passer à la pratique, nous avons besoin de définir un DataSet typé. Voici comment procéder : 1. Cliquez du bouton droit sur l’entrée correspondant au nom de votre projet dans la fenêtre Explorateur de solutions. 2. Sélectionnez Ajouter/Nouvel élément dans le menu contextuel. 3. Si nécessaire, développez l’arbre des catégories et sélectionnez Données dans la liste. Sous Modèles Visual Studio installés, sélectionnez DataSet. Donnez le nom StudentsDataSet.xsd au fichier DataSet et cliquez sur le bouton Ajouter. 4. Quelques instants plus tard, l’espace de travail affiche un concepteur de DataSet. Placez le pointeur sur la Boîte à outils, cliquez et glissez-déposez un DataTable sur le concepteur de DataSet. 5. Cliquez du bouton droit sur la barre de titre du DataSet que vous venez d’ajouter et sélectionnez Propriétés dans le menu contextuel. 6. Dans la fenêtre Propriétés, donnez le nom Students au DataTable. 7. Cliquez du bouton droit sur la barre de titre du DataSet et sélectionnez Ajouter/ Colonne dans le menu contextuel. 8. Affectez la valeur "Id" à la propriété Name et la valeur "System.Int32" à la propriété DataType. 9. Cliquez du bouton droit sur la barre de titre du DataSet et sélectionnez Ajouter/ Colonne dans le menu contextuel. 10. Affectez la valeur "Name" à la propriété Caption de ce DataColumn. 11. Sauvegardez le fichier. Openmirrors.com

Linq.book Page 369 Mercredi, 18. février 2009 7:58 07

Chapitre 11

Possibilités complémentaires des DataSet

369

Vous venez de créer le DataSet typé StudentsDataSet. Ce DataSet contient le DataTable Student, qui contient lui-même deux colonnes de type DataColumn. La première a pour nom Id et pour type Int32. La seconde a pour nom Name et pour type string. Nous allons utiliser ce DataSet pour effectuer des requêtes LINQ. Étant donné que ce DataSet est typé, nous pourrons accéder aux champs DataRow directement (voir Listing 11.1). Listing 11.1 : Un exemple de requête sur un DataSet typé. StudentsDataSet studentsDataSet = new StudentsDataSet(); studentsDataSet.Students.AddStudentsRow(1, "Joe Rattz"); studentsDataSet.Students.AddStudentsRow(7, "Anthony Adams"); studentsDataSet.Students.AddStudentsRow(13, "Stacy Sinclair"); studentsDataSet.Students.AddStudentsRow(72, "Dignan Stephens"); string name = studentsDataSet.Students.Where(student => student.Id == 7).Single().Name; Console.WriteLine(name);

Dans cet exemple, un objet StudentsDataSet est instancié et quatre enregistrements Students y sont ajoutés. Tout comme dans les exemples du chapitre précédent, chaque enregistrement correspond à un étudiant. Dans la plupart des codes de production réels, cette étape ne sera pas nécessaire, car les données proviendront d’une base de données. Une fois le DataSet typé initialisé, une requête LINQ lui est appliquée. Remarquez qu’on accède à la DataTable Students en tant que propriété de l’objet StudentsDataSet. Remarquez également que, dans l’expression lambda de la clause Where, on accède à la propriété Id directement à partir de l’élément. Ici, il est inutile d’appeler la propriété Field du DataRow. Cette facilité d’écriture vient du fait que le DataSet est typé. Notez enfin qu’il est possible d’accéder à la propriété Name du résultat renvoyé par l’opérateur Single. Une fois encore, cette facilité d’écriture vient du fait que le DataSet est typé. Voici le résultat : Anthony Adams

Tout ceci est bien agréable : la manipulation de DataSets typés s’apparente au travail avec des objets et propriétés de classes.

Un exemple plus proche de la réalité Les exemples du chapitre précédent ont été intentionnellement simplifiés pour faciliter l’apprentissage de l’API LINQ to DataSet. Nous avons fait en sorte qu’à travers les différents exemples vous vous concentriez essentiellement sur LINQ. En particulier, nous avons évité de présenter le code nécessaire à la connexion sur une base de données. Avant de terminer ce chapitre, je voudrais néanmoins vous donner un exemple

Linq.book Page 370 Mercredi, 18. février 2009 7:58 07

370

LINQ to DataSet

Partie IV

plus complet et plus proche de la réalité, dans lequel le DataSet est défini à partir d’une base de données. Je dois avouer que la mise au point d’un exemple de taille raisonnable qui lit des données dans une base de données et utilise l’API LINQ to DataSet pour les interroger est un peu tiré par les cheveux. En effet, nous allons exécuter une requête SQL sur les données d’une base de données en utilisant ADO.NET pour obtenir un DataSet. Après quoi nous interrogerons ce DataSet avec LINQ to DataSet pour obtenir les données recherchées. Pourquoi ne pas modifier la requête SQL pour obtenir directement les informations recherchées ? Eh bien tout simplement dans un but pédagogique ! Dans cet exemple, nous allons travailler avec la base de données de la société Northwind. Cette société utilise une application qui effectue des requêtes sur les commandes. Cette application effectue différentes analyses sur les relations entre employés et clients et sur les pays d’expédition des différentes commandes. Cette application place les employés, les clients et les pays de destination dans un DataSet. Notre tâche va consister à effectuer une analyse complémentaire sur ces données. À titre d’exemple, nous allons établir la liste de toutes les ventes à destination de l’Allemagne effectuées par chacun des employés. Dans cet exemple, nous instancions un SqlDataAdapter puis un DataSet et appelons la méthode Fill du SqlDataAdapter pour remplir le DataSet. Cette étape aurait déjà dû être faite par l’application dont nous venons de parler. Mais, étant donné que nous ne travaillons pas dans un environnement réel, elle sera effectuée dans notre code. Une fois l’objet DataSet initialisé par la requête SQL, nous lancerons une requête LINQ to DataSet et afficherons le résultat (voir Listing 11.2). Listing 11.2 : Un exemple plus proche de la réalité. string connectionString = @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;"; SqlDataAdapter dataAdapter = new SqlDataAdapter( @"SELECT O.EmployeeID, E.FirstName + ’ ’ + E.LastName as EmployeeName, O.CustomerID, C.CompanyName, O.ShipCountry FROM Orders O JOIN Employees E on O.EmployeeID = E.EmployeeID JOIN Customers C on O.CustomerID = C.CustomerID", connectionString); DataSet dataSet = new DataSet(); dataAdapter.Fill(dataSet, "EmpCustShip"); // Ici se termine le code hérité var ordersQuery = dataSet.Tables["EmpCustShip"].AsEnumerable() .Where(r => r.Field("ShipCountry").Equals("Germany")) .Distinct(System.Data.DataRowComparer.Default) .OrderBy(r => r.Field("EmployeeName")) .ThenBy(r => r.Field("CompanyName")); foreach(var dataRow in ordersQuery)

Openmirrors.com

Linq.book Page 371 Mercredi, 18. février 2009 7:58 07

Chapitre 11

Possibilités complémentaires des DataSet

371

{ Console.WriteLine("{0,-20} {1,-20}", dataRow.Field("EmployeeName"), dataRow.Field("CompanyName")); }

Les premières lignes établissent la connexion avec la base de données Northwind. Il se peut que vous ayez à modifier les paramètres de la connexion pour qu’ils s’adaptent à votre propre base de données. Dans la requête LINQ, nous utilisons les opérateurs AsEnumerable, Distinct et Field (voir chapitre précédent) et les opérateurs Where, OrderBy et ThenBy de l’API LINQ to Objects pour créer la requête appropriée à nos besoins. J’espère que vous appréciez à sa juste valeur la facilité avec laquelle tous ces opérateurs dialoguent entre eux. Si la requête fonctionne, nous devrions obtenir la liste de tous les employés qui ont effectué au moins une vente à une société allemande. Cette liste devrait être classée par ordre alphabétique sur les noms d’employés puis sur les sociétés et ne devrait comprendre aucun doublon. Voici les résultats : Andrew FullerDie Wandernde Kuh Andrew FullerKöniglich Essen Andrew FullerLehmanns Marktstand Andrew FullerMorgenstern Gesundkost Andrew FullerOttilies Käseladen Andrew FullerQUICK-Stop Andrew FullerToms Spezialitäten Anne DodsworthBlauer See Delikatessen Anne DodsworthKöniglich Essen Anne DodsworthLehmanns Marktstand Anne DodsworthQUICK-Stop … Steven BuchananFrankenversand Steven BuchananMorgenstern Gesundkost Steven BuchananQUICK-Stop

Vous pouvez remarquer que les résultats ne comprennent aucun doublon. Cela montre une fois de plus l’intérêt des opérateurs d’initialisation de l’API LINQ to DataSet. À titre d’information, si vous supprimez l’argument DataRowComparer.Default dans l’opérateur Distinct, vous verrez que plusieurs doublons apparaissent dans les résultats. Le Listing 11.3 donne un exemple utilisant la syntaxe d’expression de requête. Listing 11.3 : Un exemple plus proche de la réalité utilisant la syntaxe d’expression de requête. string connectionString = @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;"; SqlDataAdapter dataAdapter = new SqlDataAdapter( @"SELECT O.EmployeeID, E.FirstName + ’ ’ + E.LastName as EmployeeName, O.CustomerID, C.CompanyName, O.ShipCountry FROM Orders O JOIN Employees E on O.EmployeeID = E.EmployeeID JOIN Customers C on O.CustomerID = C.CustomerID", connectionString);

Linq.book Page 372 Mercredi, 18. février 2009 7:58 07

372

LINQ to DataSet

Partie IV

DataSet dataSet = new DataSet(); dataAdapter.Fill(dataSet, "EmpCustShip"); // All code prior to this comment is legacy code. var ordersQuery = (from r in dataSet.Tables["EmpCustShip"].AsEnumerable() where r.Field("ShipCountry").Equals("Germany") orderby r.Field("EmployeeName"), r.Field("CompanyName") select r) .Distinct(System.Data.DataRowComparer.Default); *foreach (var dataRow in ordersQuery) { Console.WriteLine("{0,-20} {1,-20}", dataRow.Field("EmployeeName"), dataRow.Field("CompanyName")); }

Cette fois-ci, la requête utilise la syntaxe d’expression de requête. Nous avons essayé de lui donner la même allure que dans l’exemple précédent, mais cela n’a pas été possible. Remarquez la position de l’opérateur Distinct, en fin de la requête. Rappelez-vous que le compilateur n’est en mesure de traduire que les opérateurs les plus courants d’une requête exprimée avec la syntaxe d’expression de requête. Dans cet exemple, il ne sait pas comment traduire l’opérateur Distinct. C’est la raison pour laquelle cet opérateur ne peut pas être utilisé dans la portion "syntaxe d’expression de requête" de la requête et a été déporté à la fin de la requête. Les résultats finaux des requêtes des Listings 11.2 et 11.3 sont identiques, mais ils présentent une différence au niveau des performances. Dans le Listing 11.2, l’opérateur Distinct est appelé juste après l’opérateur Where. Les doublons sont donc éliminés avant d’effectuer le classement. Dans le Listing 11.3, l’opérateur Distinct est appelé à la fin de la requête. Les doublons ont donc été pris en compte pendant l’étape de classement. Cela engendre une charge supplémentaire qui se révèle toutefois indispensable si vous voulez utiliser la syntaxe d’expression de requête.

Résumé Ce chapitre vous a montré que vous pouviez effectuer des requêtes LINQ to DataSet sur des DataSets typés. Sur ce type de DataSets, le code d’interrogation est plus simple à maintenir et plus lisible. Vous avez également pu découvrir un exemple d’interrogation LINQ to DataSets plus réaliste, fondé sur la base de données Northwind. L’API LINQ to DataSet ajoute un autre domaine d’utilisation aux requêtes LINQ. Par son intermédiaire, l’interrogation de DataSets n’a jamais été aussi simple, et les nombreux codes qui utilisent ces objets ont tout intérêt à être remis au goût du jour. L’API LINQ to DataSet a un avantage par rapport à l’API LINQ to SQL : aucun code de classe de base de données ne doit être généré et compilé avant de pouvoir effectuer des requêtes. Ceci rend LINQ to DataSet plus dynamique et mieux adapté aux programmes qui ne connaissent la base de données qu’ils vont utiliser qu’au moment de l’exécution (les utilitaires de bases de données, par exemple). Openmirrors.com

Linq.book Page 373 Mercredi, 18. février 2009 7:58 07

Chapitre 11

Possibilités complémentaires des DataSet

373

Grâce à l’opérateur AsEnumerable, qui permet de créer des séquences à partir d’objets DataTable, les opérateurs de requête standard LINQ to Objects viennent compléter l’arsenal de LINQ to DataSet, augmentant encore ses possibilités déjà immenses. Des opérateurs ont été ajoutés dans les classes clés de LINQ to DataSet : DataTable, DataRow et DataColumn. N’oubliez pas que de nouveaux prototypes ont été ajoutés aux opérateurs Distinct, Union, Intersect, Except et SequentialEqual. Ils sont indispensables pour éliminer le problème lié à la comparaison des DataRows. Chaque fois que vous travaillerez avec des DataSets, DataTables et DataRows, utilisez les prototypes LINQ to DataSet des opérateurs d’initialisation Distinct, Union, Intersect, Except et SequentialEqual dans lesquels un comparateur d’égalité est spécifié en argument. Lorsque vous travaillez avec des valeurs de colonnes, prenez le soin d’utiliser les opérateurs Field et SetField pour éviter les problèmes liés à la comparaison et aux valeurs null. En travaillant avec LINQ to DataSet, je me suis rendu compte que j’avais totalement sous-estimé la puissance et l’utilité des DataSets. Ils offrent des caractéristiques très intéressantes en matière de manipulation et de stockage de données. Leurs possibilités de recherche quelque peu limitées disparaissent totalement lorsqu’ils sont épaulés par LINQ. Désormais, vous pourrez donc compter avec LINQ pour effectuer des requêtes sur vos DataSets. Vous verrez, le codage sera bien plus simple qu’avant…

Linq.book Page 374 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 375 Mercredi, 18. février 2009 7:58 07

V LINQ to SQL

Linq.book Page 376 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 377 Mercredi, 18. février 2009 7:58 07

12 Introduction à LINQ to SQL Listing 12.1 : Un exemple élémentaire de mise à jour du champ ContactName d’un client dans la base de données Northwind. // Création d’un DataContext Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // Accès au client LAZYK Customer cust = (from c in db.Customers where c.CustomerID == "LAZYK" select c).Single(); // Mise à jour du nom du contact cust.ContactName = "Ned Plimpton"; try { // Sauvegarde des modifications db.SubmitChanges(); } // Détection des conflits d’accès concurrentiels catch (ChangeConflictException) { // Résolution des conflits db.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges); }

INFO Cet exemple nécessite la génération de classes d’entités. Vous trouverez tous les renseignements nécessaires un peu plus loin dans ce chapitre.

Le Listing 12.1 travaille sur la table Customers de la base de données Northwind. Il utilise une requête LINQ to SQL pour obtenir l’enregistrement dont le champ CustomerID vaut "LAZYK" et pour retourner un objet Customer qui représente cet enregistrement. La propriété ContactName de l’objet Customer est alors mise à jour et l’enregistrement

Linq.book Page 378 Mercredi, 18. février 2009 7:58 07

378

LINQ to SQL

Partie V

est sauvegardé dans la base de données par l’intermédiaire de la méthode SubmitChanges. Ce code n’est pas très long si l’on considère qu’il détecte les éventuels conflits et, le cas échéant, les résout. Appuyez sur Ctrl+F5 pour exécuter ce code. Vous n’obtenez aucune sortie console mais, si vous vérifiez le contenu de la base de données, vous verrez que le champ ContactName du client LAZYK vaut maintenant "Ned Plimpton". INFO Cet exemple modifie la base de données mais ne rétablit pas les données originales. Pour que les exemples donnés dans ce chapitre fonctionnent correctement, vous devez affecter la valeur "John Steel" au champ ContactName du client LAZYK. Cette modification peut se faire "à la main" ou en ajustant le code du Listing 12.1 en conséquence.

INFO Cet ouvrage utilise une version étendue de la base de données Northwind. Reportez-vous à la section "Comment obtenir la version appropriée de la base de données Northwind" pour avoir toutes les informations nécessaires à ce sujet.

Introduction à LINQ to SQL Arrivé à ce point dans la lecture de cet ouvrage, vous savez comment utiliser LINQ sur des collections de données et des tableaux en mémoire, des fichiers XML et des DataSets. Nous allons nous intéresser à ce que beaucoup considèrent comme la partie la plus importante de LINQ : LINQ to SQL. Je dis cela parce que la plupart des billets relatifs à LINQ dans le forum MSDN s’intéressent essentiellement à LINQ to XML. Je pense que beaucoup de développeurs ne sont pas pleinement conscients que le langage de requête LINQ peut "jouer dans plusieurs cours". J’espère que les chapitres précédents vous ont convaincu de son éclectisme. L’API (Application Programming Interface) LINQ to SQL est faite pour interfacer les bases de données SQL Server. Dans le monde des langages de programmation orientés objets, l’interfaçage d’une base de données est souvent considéré comme le point le plus épineux. Lorsque nous écrivons une application, nous modelons les classes pour représenter des objets du monde réel : des clients, des comptes, des stocks, etc. Nous avons besoin de rendre ces objets persistants, de telle sorte que, lorsque l’application est lancée une nouvelle fois, ces objets et leurs valeurs ne sont pas perdus. La plupart des bases de données utilisées en production sont toujours relationnelles. Elles stockent les données dans des tables, en tant qu’enregistrements et non en tant qu’objets. Une classe client peut ainsi contenir des adresses et des téléphones stockés dans des collections qui sont des propriétés enfants de cette classe. Une fois rendues persistantes, ces données seront certainement stockées dans différentes tables : une table de clients, une table d’adresses et une table de téléphones. Openmirrors.com

Linq.book Page 379 Mercredi, 18. février 2009 7:58 07

Chapitre 12

Introduction à LINQ to SQL

379

Par ailleurs, les types de données supportés par le langage de programmation diffèrent souvent de ceux de la base de données. Les développeurs doivent alors écrire du code qui sait comment initialiser un objet client à partir des tables appropriées et comment sauvegarder un objet Customer dans ces mêmes tables, en effectuant les conversions de types nécessaires. Cette étape est souvent ennuyeuse et propice aux erreurs. Pour contrer ce problème, lié au mappage des données relationnelles, de nombreux logiciels ORM (Object-Relational Mapping) ont été écrits. LINQ to SQL est l’ORM d’entrée de gamme compatible LINQ de Microsoft dédié aux bases de données SQL Server. Les autres fabricants de bases de données sont déjà (ou vont se mettre) au travail pour implémenter leur propre API LINQ. Personnellement, j’aimerais bien qu’une API LINQ to DB2 voie le jour. Je suis sûr que beaucoup d’entre vous apprécieraient des API LINQ to Oracle, LINQ to MySQL, LINQ to Sybase, etc. INFO LINQ to SQL ne peut être utilisé qu’avec SQL Server et SQL Express. Pour utiliser LINQ avec d’autres types de bases de données, vous devez utiliser des API additionnelles, mises au point par les différents fabricants des bases de données. Si ces API n’existent pas encore, vous pouvez toujours utiliser LINQ to DataSet.

Quelques lignes auparavant, j’ai dit que LINQ to SQL était une implémentation ORM d’entrée de gamme. Si sa puissance et/ou sa flexibilité ne vous suffisent pas, vous pouvez vous tourner vers LINQ to Entities. Cette partie de LINQ ne sera pas traitée dans cet ouvrage. Si elle procure plus de puissance et de flexibilité, elle complexifie également l’écriture. Par ailleurs, elle n’est pas aussi mature que LINQ to SQL… La plupart des outils ORM limitent la manipulation des bases de données à celle des objets métier (entités) correspondants. Cette limitation interdit l’utilisation de requêtes SQL, pourtant si importantes dans les bases de données relationnelles. LINQ to SQL se différencie de beaucoup de ses contemporains : il sait en effet tirer parti des objets mappés à la base de données et offre un langage de requête semblable au SQL. INFO LINQ to SQL est un ORM d’entrée de gamme qui permet l’utilisation de requêtes SQL puissantes.

Étant donné que les requêtes LINQ to SQL retournent des objets entité (et non de simples champs, des classes de nonentités nommées ou des classes anonymes), vous avez accès à toute la puissance de LINQ. Par ailleurs, LINQ to SQL vous permet également de rechercher les modifications effectuées sur les enregistrements et de mettre à jour la base de données, tout en détectant et en résolvant d’éventuels conflits d’accès concurrentiels et en assurant l’intégrité transactionnelle.

Linq.book Page 380 Mercredi, 18. février 2009 7:58 07

380

LINQ to SQL

Partie V

Les premières lignes du Listing 12.1 ont défini une instance de la classe Northwind. Cette classe est dérivée de la classe DataContext (reportez-vous au Chapitre 16 pour avoir de plus amples informations). Pour l’instant, considérez cette instance comme une connexion surchargée à la base de données. La mise à jour de la base de données est également supportée via la méthode SubmitChanges. Quelques lignes plus bas, un des clients de la base de données Northwind a été placé dans un objet Customer. Cet objet a été obtenu en instanciant la classe d’entité Customer. Cette dernière doit être écrite ou générée. Dans cet exemple, la classe Customer (tout comme la classe Northwind) a été générée par l’utilitaire SQLMetal. Après avoir récupéré le client, la propriété ContactName de l’objet Customer a été mise à jour, et la méthode SubmitChanges a été appelée pour stocker la modification dans la base de données, et ainsi la rendre persistante. L’appel à la méthode SubmitChanges a été placé dans un bloc try/catch et nous avons écrit un code de traitement pour l’exception ChangeConflictException. Cette exception se produit lorsqu’un conflit d’accès concurrentiel est détecté. Vous en saurez plus à ce sujet en consultant le Chapitre 17. Avant de pouvoir exécuter cet exemple ou un des autres de ce chapitre, vous devez créer des classes d’entité pour la base de données Northwind. Reportez-vous à la section intitulée "Prérequis pour exécuter les exemples" de ce chapitre pour savoir comment procéder. LINQ to SQL est un sujet complexe. Pour mettre au point un exemple, de nombreux éléments LINQ to SQL sont nécessaires. Dans le premier exemple, au début de ce chapitre, nous utilisons une classe dérivée de DataContext (Northwind) et une classe d’entités (Customer). La détection et la résolution de conflits de concurrence ainsi que la mise à jour de la base de données sont effectuées via la méthode SubmitChanges. Avant de pouvoir expliquer ces différents concepts, nous allons vous inculquer quelques connaissances de base qui vous permettront de comprendre les fondements de LINQ to SQL. Rassurez-vous, tous ces concepts seront traités en détail dans les chapitres suivants. La classe DataContext DataContext est la classe qui permet d’établir une connexion avec la base de données. Elle fournit également plusieurs services annexes, tels que le contrôle d’identité, la détection de modifications et le processus de sauvegarde des modifications. Tout ceci sera traité en détail au Chapitre 16. Pour l’instant, il vous suffit de savoir que c’est la classe DataContext qui établit la connexion avec la base de données, qui contrôle les modifications et met à jour la base de données lorsque la méthode SubmitChanges est appelée.

L’utilisation d’une classe dérivée de DataContext est très classique en LINQ to SQL. Le nom de la classe dérivée est généralement le même que celui de la base de données Openmirrors.com

Linq.book Page 381 Mercredi, 18. février 2009 7:58 07

Chapitre 12

Introduction à LINQ to SQL

381

à laquelle elle est reliée. Nous y ferons parfois référence sous la forme [Your]DataContext, car son nom est lié à celui de la base de données pour laquelle elle a été créée. Dans les exemples de ce chapitre, la classe dérivée de DataContext appelle "Northwind". Cette classe a en effet été générée avec l’outil en ligne de commande SQLMetal, qui donne automatiquement le nom de la base de données à la classe DataContext dérivée. La classe [Your]DataContext, dérivée de DataContext, aura généralement une propriété publique Table pour chaque table mappée à la base de données (où T est le type de la classe d’entité instanciée pour chaque enregistrement obtenu à partir de cette table, et Table est une collection spécialisée). Par exemple, étant donné que la base Northwind contient une table Customers, la classe Northwind, dérivée de la classe DataContext, aura un Table nommé Customers. Il est donc possible d’accéder aux enregistrements de la base de données Customers par l’intermédiaire des propriétés Customers de type Table dans la classe Northwind. Le Listing 12.1 en est un exemple : le raccourci d’écriture db.Customers donne accès aux enregistrements de la table Customers de la base de données Northwind. Classes d’entités LINQ to SQL utilise des classes d’entités. Chaque classe d’entité est généralement liée à une seule table de la base de données. Cependant, il est possible, sous certaines circonstances spécifiques, de mapper toute la hiérarchie d’une classe dans une simple table. Vous en apprendrez plus à ce sujet en vous reportant au Chapitre 18. Nous avons donc des classes d’entités liées aux tables d’une base de données, et les propriétés des classes d’entité liées aux colonnes des tables. Ces relations classe/table et propriété/colonne sont l’essence même de LINQ to SQL. INFO Le fondement de LINQ to SQL consiste à relier les classes d’entité aux tables d’une base de données et les propriétés des classes d’entité aux colonnes des tables de la base de données.

Ces liaisons peuvent se faire directement dans les fichiers de la classe source, en utilisant les bons attributs, ou dans un fichier de mappage XML externe. En utilisant un fichier de mappage externe, les éléments spécifiques à LINQ to SQL peuvent être maintenus à l’extérieur du code source. Ceci est très pratique si vous n’avez pas accès au code source ou si vous voulez le séparer de LINQ to SQL. Dans la plupart des exemples de ce chapitre, nous utiliserons des classes d’entité générées par l’outil en ligne de commande SQLMetal. Dans ces classes d’entité générées, le mappage LINQ to SQL est intégré dans le module source, sous la forme d’attributs et de propriétés.

Linq.book Page 382 Mercredi, 18. février 2009 7:58 07

382

LINQ to SQL

Partie V

Vous détecterez sans peine les classes d’entité dans les exemples : vous verrez des classes ou des objets dont le nom est le singulier d’un nom de table de la base de données Northwind. À titre d’exemple, dans le Listing 12.1 nous utilisons la classe Customer. Ce nom étant le singulier de Customers, et la base de données Northwind ayant une table nommée Customers, nous pouvons en déduire que la classe Customer est une classe d’entité de la table Customers de la base de données Northwind. L’option /pluralize de l’outil en ligne de commande SQLMetal est le responsable de cette "singularisation" des classes d’entité. Si cette option n’avait pas été spécifiée lors de la génération des classes d’entité, la classe d’entité de la table Customers aurait été nommée Customers (et non Customer). Cette distinction est importante, pour le cas où vous vous sentiriez confus en lisant d’autres écrits relatifs à LINQ to SQL : en fonction de la façon dont l’outil SQLMetal a été utilisé, les noms des classes d’entité peuvent être au pluriel ou au singulier. Associations Le terme "association" désigne la relation entre une clé primaire et une clé étrangère, utilisées pour relier deux classes d’entité. Dans une relation un-à-plusieurs, par exemple, une association consiste en une classe parent dotée d’une clé primaire et une collection de classes enfants contenant des clés étrangères. Cette collection est stockée dans une variable membre privée de type EntitySet, où T est le type de la classe d’entité enfant. À titre d’exemple, dans la classe d’entité Customer, générée par l’outil en ligne de commande SQLMetal pour la base de données Northwind, le membre privé _Orders, de type EntitySet, contient tous les objets Order pour un objet Customer spécifique : private EntitySet _Orders;

SQLMetal génère également la propriété publique Orders, afin d’accéder à la collection privée _Orders. De l’autre côté de la relation, la classe enfant (celle dans laquelle se trouve la clé étrangère) contient une référence vers la classe parent, puisqu’il s’agit d’une relation un-àplusieurs. Cette référence est mémorisée dans une variable membre privée de type EntityRef, où T est le type de la classe parent. La classe d’entité Order contient la variable membre privée _Customer de type EntityRef : private EntityRef _Customer;

Ici encore, l’outil SQLMetal a généré la propriété Customer pour donner accès au parent.

Openmirrors.com

Linq.book Page 383 Mercredi, 18. février 2009 7:58 07

Chapitre 12

Introduction à LINQ to SQL

383

L’association entre les clés primaire et étrangère ainsi que la direction de la relation sont définies par des attributs et des propriétés d’attributs dans le module source des classes d’entité générées. Cette association permet d’accéder aux classes enfants du parent – et donc aux enregistrements de la base de données – aussi simplement que s’il s’agissait de propriétés de la classe parent. De la même façon, l’accès à la classe parent d’un enfant est aussi simple qu’accéder à une propriété d’une classe enfant. Détection de conflit d’accès concurrentiel Un des services appréciables du DataContext est le traitement associé aux modifications : lorsque vous essayez de mettre à jour votre base de données en appelant la méthode SubmitChanges de l’objet DataContext, une détection de conflit d’accès concurrentiels est automatiquement lancée. Si un conflit est détecté, une exception ChangeConflictException est levée. Chaque fois que vous appelez la méthode SubmitChanges, vous devez donc l’inclure dans un bloc try/catch afin de traiter une éventuelle exception ChangeConflictException. Reportez-vous au Listing 12.2 pour avoir un exemple de détection de conflit. Nous entrerons bien plus dans les détails sur la détection et la résolution des conflits au Chapitre 17. Dans un but de concision et de clarté, la plupart des exemples des chapitres dédiés à LINQ to SQL n’incluront aucun code de détection et de résolution d’erreur. Cependant, dans un code de production réel, ce code devrait être systématiquement mis en place… Résolution de conflit d’accès concurrentiel Une fois qu’un conflit a été détecté, vous devez le résoudre. Plusieurs techniques peuvent être utilisées. Le Listing 12.1 utilise la technique la plus élémentaire. Ici, nous nous contentons d’appeler la méthode ResolveAll (collection ChangeConflicts de la classe dérivée de DataContext) lorsqu’une exception ChangeConflictException est levée. Rappelons une fois encore que, dans un but de concision et de clarté, la plupart des exemples des chapitres dédiés à LINQ to SQL n’incluront aucun code de détection et de résolution d’erreur. Cependant, dans un code de production réel, ce code devrait être systématiquement mis en place. Le cas échéant, reportez-vous au Chapitre 17 pour avoir de plus amples détails sur la résolution de conflits.

Prérequis pour exécuter les exemples La plupart des exemples des chapitres dédiés à LINQ to SQL utilisant la base de données Northwind, fournie en exemple par Microsoft, nous avons besoin de classes d’entité et de fichiers de mappage pour cette base de données.

Linq.book Page 384 Mercredi, 18. février 2009 7:58 07

384

LINQ to SQL

Partie V

Obtenir la version appropriée de la base de données Northwind Plusieurs petites choses manquent dans la version originale de la base de données Northwind de Microsoft (les fonctions table-valued et scalar-valued, par exemple) pour que nous puissions montrer toutes les facettes de LINQ to SQL. Nous allons donc utiliser une version étendue de cette base de données. Vous pouvez télécharger la version appropriée de la base de données Northwind dans la section "Book Extras" de la page suivante, sur le site d’Apress : http://www.apress.com/book/bookDisplay.html?bID=10241

Vous pouvez également vous rendre sur le site LINQDev.com et lancer le téléchargement depuis la section "Obtain the Northwind Database" : http://www.linqdev.com

Si vous téléchargez la base de données depuis LINQDev.com, assurez-vous que vous téléchargez la version étendue et non la version originale de la base de données. Génération des classes d’entité de la base de données Northwind La génération de classes d’entité n’a pas encore été étudiée. Je vais donc vous dire comment procéder, sans toutefois entrer dans les détails. Reportez-vous au Chapitre 13 pour en savoir plus à ce sujet. Pour commencer, assurez-vous que vous avez téléchargé la version étendue de la base de données Northwind. Ouvrez une fenêtre Invite de commandes de Visual Studio. Pour ce faire, cliquez successivement sur le bouton Démarrer, Tous les programmes, Microsoft Visual Studio 2008, Visual Studio Tools puis Invite de commandes de Visual Studio 2008. Déplacezvous dans le dossier où les classes d’entité et le fichier de mappage doivent être générés. Nous allons par exemple nous déplacer dans la racine du disque C en tapant : cd \

Si vous voulez générer les classes d’entité de la base de données Northwind sans les attacher au préalable à la base, utilisez la commande suivante : sqlmetal /namespace:nwind /code:Northwind.cs /pluralize /functions /sprocs /views

ATTENTION Faites particulièrement attention au nom et à la casse du fichier MDF spécifié dans la ligne de commande. Le nom et la casse de la classe générée par SQLMetal seront en effet identiques à ceux passés dans la ligne de commande. Si vous choisissez un autre nom que [chemin]\Northwind.mdf ([chemin]\northwind.mdf ou [chemin]\NorthWind.mdf, par exemple), aucun des exemples ne fonctionnera !

Openmirrors.com

Linq.book Page 385 Mercredi, 18. février 2009 7:58 07

Chapitre 12

Introduction à LINQ to SQL

385

Pour créer des classes d’entité à partir du fichier Northwind.mdf, situé dans la racine du disque C, entrez la commande suivante : sqlmetal /namespace:nwind /code:Northwind.cs /pluralize /functions /sprocs /views "C:\Northwind.mdf"

L’exécution de cette commande fabriquera le module de classe d’entité Northwind.cs dans le dossier courant. Si vous voulez générer les classes d’entité de la base de données Northwind, déjà attachée à SQL Server, utilisez la commande suivante : sqlmetal /server: /user: /password: /database:Northwind /namespace:nwind /code:Northwind.cs /pluralize /functions /sprocs /views

Pour créer les classes d’entité de la base de données Northwind attachée à SQLExpress, utilisez la commande suivante : sqlmetal /server:.\SQLExpress /database:Northwind /namespace:nwind /code:Northwind.cs /pluralize /functions /sprocs /views

INFO En fonction de votre environnement de travail, il se peut que vous deviez spécifier un nom d’utilisateur (option /user:[username]) et un mot de passe (option password:[password]) dans la ligne de commande. Reportez-vous à la section intitulée "SQLMetal" du Chapitre 13 pour avoir plus de détails à ce sujet.

Après avoir tapé une de ces commandes, SQLMetal génère le code source dans le dossier courant, dans un fichier nommé Northwind.cs. Toutes les options de ce programme seront commentées au chapitre suivant. Insérez le fichier Northwind.cs ainsi généré dans votre projet en l’ajoutant en tant que "nouvel élément". Vous pouvez maintenant vous servir de LINQ to SQL sur la base de données Northwind en utilisant les classes d’entité du fichier Northwind.cs. ASTUCE Vous pouvez faire des modifications dans le fichier d’entité, mais sachez qu’elles seront perdues si vous devez le générer une nouvelle fois. Vous pourriez par exemple vouloir ajouter une logique métier en définissant de nouvelles méthodes dans les classes d’entité. Mais, plutôt que modifier le fichier généré, pensez à tirer profit des classes partielles de C# 2.0 en plaçant les nouvelles propriétés et méthodes dans un module source annexe.

Génération du fichier de mappage XML de la base de données Northwind Certains exemples ont également besoin d’un fichier de mappage. Ici encore, nous allons utiliser SQLMetal. Dans la même fenêtre Invite de commandes et à partir du même dossier, exécutez la commande suivante : sqlmetal /map:northwindmap.xml "C:\Northwind.mdf" /pluralize /functions /sprocs / views /namespace:nwind

Linq.book Page 386 Mercredi, 18. février 2009 7:58 07

386

LINQ to SQL

Partie V

Comme précédemment, faites bien attention à la casse du fichier MDF. Cette commande génère le fichier northwindmap.xml dans le dossier courant. INFO Cette commande affiche sur l’écran le code inséré dans le fichier de mappage XML. Toutes ces lignes de code affichées sur votre écran sont donc tout à fait normales.

Utilisation de l’API LINQ to SQL Pour pouvoir utiliser l’API LINQ to SQL, vous devez ajouter l’assembly System.Data.Linq.Dll dans votre projet, si elle ne s’y trouve pas déjà. De même, si les directives using suivantes ne sont pas déjà présentes, vous devez les ajouter dans votre module source : using System.Data.Linq; using System.Linq;

Enfin, vous devez ajouter une clause using concernant l’espace de noms dans lequel les classes d’entité ont été générées : using nwind;

IQueryable Dans la plupart des exemples des chapitres dédiés à LINQ to SQL, nous travaillerons avec des séquences de type IQueryable, où T est le type d’une classe d’entité. Ces séquences sont généralement retournées par les requêtes LINQ to SQL. Elles fonctionnent souvent comme les séquences IEnumerable, et…, cela n’est pas une coïncidence : l’interface IQueryable implémente l’interface IEnumerable. Voici la définition de l’interface IQueryable : interface IQueryable : IEnumerable, IQueryable

Grâce à cet héritage, les séquences IQueryable peuvent être traitées comme des séquences IEnumerable.

Quelques méthodes communes Un grand nombre d’exemples des chapitres dédiés à LINQ to SQL ont tendance à devenir rapidement complexes. Pour démontrer un conflit, il est nécessaire d’effectuer des modifications dans la base de données en dehors de LINQ to SQL. Parfois, il est également nécessaire d’extraire des données sans utiliser LINQ to SQL. Pour mettre en valeur le code LINQ to SQL et ne pas être gêné par des détails annexes – sans pour autant s’écarter de la réalité –, nous avons défini quelques méthodes communes qui seront utilisées dans les exemples. Openmirrors.com

Linq.book Page 387 Mercredi, 18. février 2009 7:58 07

Chapitre 12

Introduction à LINQ to SQL

387

Assurez-vous que ces méthodes ont été ajoutées à vos modules sources lorsque vous testerez les exemples des chapitres LINQ to SQL. La méthode GetStringFromDb() Cette méthode se révélera bien pratique par la suite. Elle permet d’extraire une chaîne d’une base de données en utilisant ADO.NET. Cela nous permettra d’examiner ce qui se trouve dans la base de données et de le comparer à ce que LINQ to SQL affiche : La méthode GetStringFromDb permet d’extraire une chaîne en utilisant ADO.NET static private string GetStringFromDb( System.Data.SqlClient.SqlConnection sqlConnection, string sqlQuery) { if (sqlConnection.State != System.Data.ConnectionState.Open) { sqlConnection.Open(); } System.Data.SqlClient.SqlCommand sqlCommand = new System.Data.SqlClient.SqlCommand(sqlQuery, sqlConnection); System.Data.SqlClient.SqlDataReader sqlDataReader = sqlCommand.ExecuteReader(); string result = null; try { if (!sqlDataReader.Read()) { throw (new Exception( String.Format("Exception inattendue pendant l’exécution de la requête [{0}].", sqlQuery))); } else { if (!sqlDataReader.IsDBNull(0)) { result = sqlDataReader.GetString(0); } } } finally { // Toujours appeler Close quand la lecture est faite sqlDataReader.Close(); } return (result); }

La méthode GetStringFromDb demande deux arguments : un objet SqlConnection et une chaîne qui contient une requête SQL. La méthode vérifie que la connexion est ouverte. Dans le cas contraire, elle l’ouvre. Ensuite, un objet SqlCommand est créé en passant la requête et la connexion dans le constructeur. Un objet SqlDataReader est alors obtenu en appelant la méthode ExecuteReader sur l’objet SqlCommand. Le SqlDataReader est lu en appelant la méthode Read.

Linq.book Page 388 Mercredi, 18. février 2009 7:58 07

388

LINQ to SQL

Partie V

Si une donnée a été lue et si la première valeur de la colonne est différente de null, cette valeur est lue avec la méthode GetString. Enfin, le SqlDataReader est fermé et la première valeur de la colonne est retournée à l’appelant. La méthode ExecuteStatementInDb() De temps à autre, il sera nécessaire d’exécuter des commandes insert, update et delete en ADO.NET pour modifier l’état de la base de données sans utiliser LINQ to SQL. Pour ce faire, nous utiliserons la méthode ExecuteStatementInDb : La méthode ExecuteStatementInDb exécute des commandes Insert, Update et Delete en ADO.NET static private void ExecuteStatementInDb(string cmd) { string connection = @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;"; System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection(connection); System.Data.SqlClient.SqlCommand sqlComm = new System.Data.SqlClient.SqlCommand(cmd); sqlComm.Connection = sqlConn; try { sqlConn.Open(); Console.WriteLine("Exécution de la commande SQL sur la base de données avec ➥ADO.NET ..."); sqlComm.ExecuteNonQuery(); Console.WriteLine("Base de données mise à jour"); } finally { // Fermeture de la connexion sqlComm.Connection.Close(); } }

La méthode ExecuteStatementInDb demande un argument : une chaîne contenant une commande SQL. Un objet SqlConnection est créé, suivi par un objet SqlCommand. Le premier est affecté au second. L’objet SqlConnection est alors ouvert et la commande SQL, exécutée en appelant la méthode ExecuteNonQuery de l’objet SqlCommand. Enfin, l’objet SqlConnection est fermé.

Résumé Ce chapitre constitue une introduction à LINQ to SQL et à un certain nombre de termes qui y sont relatifs. Par exemple, les objets DataContext, les classes d’entité, les associations, la détection et la résolution des conflits d’accès concurrentiel. Openmirrors.com

Linq.book Page 389 Mercredi, 18. février 2009 7:58 07

Chapitre 12

Introduction à LINQ to SQL

389

Vous y avez également appris à générer les classes d’entité et le fichier de mappage pour la version étendue de la base de données Northwind. Les classes d’entité seront abondamment utilisées dans les exemples LINQ to SQL. Enfin, vous avez pu découvrir deux méthodes communes qui viendront en complément des instructions LINQ to SQL. Au chapitre suivant, vous allez découvrir quelques astuces et voir comment utiliser des outils dédiés à LINQ to SQL.

Linq.book Page 390 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 391 Mercredi, 18. février 2009 7:58 07

13 Astuces et outils pour LINQ to SQL Le chapitre précédent a introduit LINQ to SQL et la terminologie qui lui est propre. Vous y avez appris à générer les classes d’entités nécessaires à la plupart des exemples relatifs à LINQ to SQL. Vous avez également découvert plusieurs méthodes communes qui seront utiles à de nombreux exemples des Chapitres 12 à 17. Dans ce chapitre, vous allez découvrir des astuces qui, je l’espère, vous seront utiles lorsque vous utiliserez LINQ to SQL. Vous ferez également connaissance avec quelquesuns des outils qui rendent LINQ to SQL si agréable à utiliser.

Introduction aux astuces et aux outils pour LINQ to SQL Je tiens à rappeler ici que, pour pouvoir exécuter les exemples de ce chapitre, vous devez au préalable satisfaire les conditions exposées dans la section "Prérequis pour exécuter les exemples" du chapitre précédent. En particulier, vous devez avoir téléchargé la version étendue de la base de données Northwind et avoir généré les classes d’entité correspondantes. Dans ce chapitre, étant donné que nous allons mettre en œuvre du code qui utilise les classes d’entité générées par SQLMetal et par le Concepteur Objet/Relationnel, nous n’indiquerons pas la directive using nwind dans le code des exemples. Cet espace de noms sera spécifié explicitement à chaque fois que cela se révélera nécessaire. Cette démarche est nécessaire, car nous voulons contrôler quelle classe d’entité Customer est référencée dans chacun des exemples. Par défaut, le Concepteur Objet/Relationnel définit une classe qui porte le nom du projet. Étant donné que les exemples existent déjà dans l’espace de noms du projet, il ne sera pas nécessaire de le spécifier à nouveau. En revanche, ceci n’est plus vrai lorsqu’un exemple utilise les classes d’entité générées par SQLMetal.

Linq.book Page 392 Mercredi, 18. février 2009 7:58 07

392

LINQ to SQL

Partie V

INFO Dans les exemples de ce chapitre, il ne sera pas nécessaire de déclarer une directive using nwind;.

Astuces Pour ne pas déroger à ce qui a été fait dans les chapitres précédents, nous allons vous présenter quelques astuces qui mettent en œuvre des concepts qui n’ont pas encore été abordés. Vous devez en effet connaître ces astuces avant d’en avoir besoin, et pas après avoir décortiqué les théories qui les animent. La propriété DataContext.Log Nous allons rappeler quelques-unes des astuces relatives à LINQ to SQL présentées au Chapitre 1. Une de ces astuces a été présentée dans la section "Utiliser le Log du DataContext". Elle vous a montré comment utiliser la propriété Log d’un objet DataContext pour avoir un aperçu des requêtes traduites en SQL. Ceci peut être très utile, non seulement à des fins de débogage, mais également pour analyser les performances. Vous pouvez par exemple découvrir que vos requêtes LINQ to SQL vont être traduites en des requêtes SQL peu efficaces. Ou encore qu’en raison du chargement différé des classes d’entité associées vous effectuez bien plus de requêtes SQL que le strict nécessaire. Le cas échéant, la propriété DataContext.Log vous révélera ce type d’information. Pour pouvoir tirer parti de cette fonctionnalité, il vous suffit d’affecter la propriété DataContext.Log à un objet System.IO.TextWriter : Console.Out, par exemple (voir Listing 13.1). Listing 13.1 : Un exemple d’utilisation de la propriété DataContext.Log. nwind.Northwind db = new nwind.Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; var custs = from c in db.Customers where c.Region == "WA" select new { Id = c.CustomerID, Name = c.ContactName }; foreach (var cust in custs) { Console.WriteLine("{0} - {1}", cust.Id, cust.Name); }

Étant donné que nous utiliserons des classes d’entité fabriquées par SQLMetal et par le Concepteur Objet/Relationnel, nous aurons affaire à deux classes Customer différentes. Comme il a été dit précédemment, aucune directive using ne sera ajoutée dans les exemples de ce chapitre, afin d’ôter toute ambiguïté en ce qui concerne les classes Openmirrors.com

Linq.book Page 393 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

393

d’entité utilisées. Dans le cas du Listing 13.1, nous devons cependant spécifier l’espace de noms nwind de la classe Northwind, car nous utiliserons le code de la classe d’entité générée par SQLMetal. Comme vous avez pu le voir, le Listing 13.1 se contente d’affecter l’objet Console.Out à la propriété Log de l’objet NorthwindDataContext. Voici les résultats de ce code : SELECT [t0].[CustomerID], [t0].[ContactName] FROM [dbo].[Customers] AS [t0] WHERE [t0].[Region] = @p0 -- @p0: Input String (Size = 2; Prec = 0; Scale = 0) [WA] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 LAZYK - John Steel TRAIH - Helvetius Nagy WHITC - Karl Jablonski

Ces résultats contiennent le code SQL généré par la requête LINQ to SQL. Remarquez que ce code utilise des paramètres. En utilisant LINQ to SQL, vous êtes donc automatiquement protégé des attaques de type "injection de commandes SQL". ATTENTION Si vos résultats laissent apparaître que le nom associé au client LAZYK est "Ned Plimpton" et non "John Steel", vous avez certainement exécuté le code du Listing 12.1 sans restaurer la donnée qui a été affectée. Vous devriez régler ce problème avant d’exécuter les exemples suivants. Reportez-vous si nécessaire au Chapitre 12 pour savoir comment procéder.

Dans les chapitres suivants, vous verrez comment utiliser le Log du DataContext pour détecter et résoudre des problèmes de performances potentiels. La méthode GetChangeSet() La méthode GetChangeSet de l’objet DataContext permet de connaître tous les objets entité qui ont été modifiés et qui doivent être mémorisés dans la base de données lorsque la méthode SubmitChanges est appelée. Cette méthode est utile en ce qui concerne le Log du DataContext et le débogage. Vous en saurez plus à son sujet en vous reportant au Chapitre 16. Utilisation de classes partielles ou de fichiers de mappage Lors de l’utilisation d’un outil ORM, une des principales difficultés consiste en la gestion des modifications dans la base de données. Si vous conservez la logique de vos classes métier et de LINQ to SQL dans les mêmes modules, vous aurez beaucoup de mal à les maintenir lorsque la base de données est modifiée. Pensez à placer votre logique métier dans un module différent de celui des classes d’entité. En utilisant des classes partielles pour séparer vos attributs de base de données LINQ to SQL de votre

Linq.book Page 394 Mercredi, 18. février 2009 7:58 07

394

LINQ to SQL

Partie V

logique métier, vous minimiserez la nécessité d’ajouter du code dans les classes d’entité. Une autre solution consisterait à utiliser des fichiers de mappage XML externes pour découpler les classes métier et le mappage LINQ to SQL. Ce fichier XML relierait les objets métier à la base de données sans compter sur les attributs LINQ to SQL. Vous en saurez plus au sujet des fichiers de mappage dans la section intitulée "Schéma de fichier de mappage externe XML" du Chapitre 15 et dans la section "La classe DataContext" du Chapitre 16. Utilisation de méthodes partielles Si les méthodes partielles sont apparues assez tardivement dans le langage C#, vous ne devez pas pour autant les ignorer. Vous les utiliserez pour traiter certains événements qui ont lieu dans les classes d’entité. Si vous n’implémentez aucune méthode partielle (et c’est là toute leur "beauté"), le compilateur n’émet aucun code pour les activer. Reportez-vous à la section "Appel des méthodes partielles appropriées" du Chapitre 15 pour en savoir plus sur l’utilisation des méthodes partielles dans les classes d’entité.

Outils Cette section va vous présenter plusieurs outils qui vous faciliteront la vie et accéléreront votre adoption de LINQ to SQL. Bien qu’un peu prématurée, cette étape me semble nécessaire, tout au moins pour que vous sachiez que ces outils existent, même si vous ne les utilisez pas encore. SQLMetal Si vous n’avez pas encore de classes métier, la façon la plus simple de créer les classes d’entité d’une base de données consiste à utiliser l’outil SQLMetal. Vous le trouverez dans le dossier %windir%\Microsoft.NET\Framework\v3.5. Il suffit d’indiquer le nom d’une base de données à SQLMetal pour qu’il génère toutes les classes d’entité nécessaires à LINQ to SQL. SQLMetal fonctionne en ligne de commande et ne dispose d’aucune interface utilisateur. Pour avoir une idée des options utilisables, commencez par ouvrir une fenêtre Invite de commandes Visual Studio. Pour ce faire, cliquez successivement sur Démarrer, Tous les programmes, Microsoft Visual Studio 2008, Visual Studio Tools puis Invite de commandes de Visual Studio 2008. Dans la fenêtre Invite de commandes, tapez sqlmetal et appuyez sur la touche Entrée du clavier : sqlmetal

Openmirrors.com

Linq.book Page 395 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

395

Cette commande provoque l’affichage suivant dans la fenêtre Invite de commandes : Microsoft (R) Database Mappage Generator 2008 version 1.00.21022 pour Microsoft (R) .NET Framework version 3.5 Copyright (C) Microsoft Corporation.Tous droits réservés. SqlMetal [options] [] Génère un code et un mappage pour le composant LINQ to SQL du .NET Framework. SqlMetal peut effectuer les opérations suivantes : – Générer des attributs de code source et de mappage ou un fichier de mappage à partir d’une base de données. – Générer un fichier dbml intermédiaire pour le personnaliser à partir de la base de données. – Générer des attributs de code et de mappage ou un fichier de mappage à partir d’un fichier dbml. Options : /server: /database: /user: /password: /conn:

/timeout:

Nom du serveur de base de données. Catalogue de bases de données sur le serveur. ID utilisateur de connexion (valeur par défaut : utilisation de l’authentification Windows). Mot de passe de connexion (par défaut : utilisation de l’authentification Windows). Chaîne de connexion de base de données. Ne peut pas être utilisée avec les options /server, /database, /user et /password. Valeur de délai d’attente à utiliser lorsque SqlMetal accède à la base de données (valeur par défaut : 0, soit à l’infini).

/views /functions /sprocs

Extraire des vues de base de données. Extraire des fonctions de base de données. Extraire des procédures stockées.

/dbml[:file]

Sortie en dbml. Ne peut être utilisé avec l’option /map. Sortie en tant que code source. Ne peut être utilisée avec l’option /dbml. Générer un fichier de mappage mais pas des attributs. Ne peut être utilisé avec l’option /dbml.

/code[:file] /map[:file]

/language: /namespace: /context: /entitybase:

/pluralize

/serialization: /provider:

Langage du code source : VB ou C# (provient par défaut de l’extension du nom de fichier du code). Espace de noms du code généré (valeur par défaut : aucun espace de noms). Nom de la classe du contexte de données (provient par défaut du nom de la base de données). Classe de base des classes d’entité dans le code généré (valeur par défaut : les entités n’ont aucune classe de base). Mettez automatiquement au pluriel ou au singulier des noms de membres et de classes d’après des règles de langue anglaise. Générer des classes sérialisables : None ou Unidirectional (valeur par défaut : None). Type de fournisseur : SQLCompact, SQL2000 ou SQL2005. (valeur par défaut : le fournisseur est déterminé au moment de l’exécution). Peut être un fichier mdf SqlExpress, un fichier sdf SqlCE ou un fichier dbml intermédiaire.

Créer du code à partir de SqlServer : SqlMetal /server:myserver /database:northwind /code:nwind.cs /namespace:nwind

Linq.book Page 396 Mercredi, 18. février 2009 7:58 07

396

LINQ to SQL

Partie V

Générer un fichier dbml intermédiaire à partir de SqlServer : SqlMetal /server:myserver /database:northwind /dbml:northwind.dbml /namespace:nwind Générer du code avec du mappage externe à partir d’un fichier dbml : SqlMetal /code:nwind.cs /map:nwind.map northwind.dbml Générer un fichier dbml à partir d’un fichier sdf SqlCE : SqlMetal /dbml:northwind.dbml northwind.sdf Générer un fichier dbml à partir d’un serveur local SqlExpress : SqlMetal /server:.\sqlexpress /database:northwind /dbml:northwind.dbml Générer un fichier dbml à l’aide d’une chaîne de connexion dans la ligne de commande : SqlMetal /conn:"server=’myserver’; database=’northwind’" /dbml:northwind.dbml

Comme vous pouvez le voir, plusieurs exemples sont donnés dans la fenêtre Invite de commandes. La plupart des options se comprennent d’elles-mêmes. Le Tableau 13.1 donne quelques explications complémentaires pour les options les plus complexes. Tableau 13.1 : Les options de l’outil SQLMetal

Option/Exemple

Description

/server: /server:.\SQLExpress

Nom du serveur sur lequel se trouve la base de données à utiliser. Si cette option n’est pas présente, SQLMetal utilise la valeur localhost/sqlexpress par défaut. Pour que SQLMetal génère les classes d’entité à partir d’un fichier MDF, omettez cette option ainsi que / database, et spécifiez le nom complet du fichier MDF à la fin de la commande.

/database: /database:Northwind

Nom de la base de données pour lequel vous voulez générer les classes d’entité. Pour que SQLMetal génère les classes d’entité à partir d’un fichier MDF, omettez cette option ainsi que / server, et spécifiez le nom complet du fichier MDF à la fin de la commande.

/user: /user:sa

Nom d’utilisateur permettant de se connecter à la base de données.

/password: /password:1590597893

Mot de passe permettant de se connecter à la base de données.

/conn: /conn:"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;"

Chaîne de connexion à la base de données. Vous pouvez utiliser cette option pour regrouper le nom du serveur, le nom de la base de données, le nom d’utilisateur et le mot de passe.

Openmirrors.com

Linq.book Page 397 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

397

Tableau 13.1 : Les options de l’outil SQLMetal (suite)

Option/Exemple

Description

/timeout: /timeout:120

Délai d’attente en secondes pour accéder à la base de données. Si cette option n’est pas spécifiée, aucun délai d’attente n’est utilisé. Alors que nous écrivons ces lignes, cette option n’est pas encore supportée. Quoi qu’il en soit, le code généré ne dépend aucunement de cette option. Vous pouvez vérifier si elle fonctionne dans votre Visual Studio 2008. Dans la négative, vous pouvez toujours utiliser la propriété CommandTimeout de la classe DataContext, ou encore appeler la méthode DataContext.GetCommand et définir un délai d’attente pour une requête particulière. Consultez le Listing 16.29, au Chapitre 16, pour en savoir plus à ce sujet.

/views /views

Spécifiez cette option si vous voulez que SQLMetal génère le code nécessaire afin que les propriétés Table et les classes d’entité supportent les vues de la base de données.

/functions /functions

Spécifiez cette option pour que SQLMetal génère des méthodes qui permettront d’appeler les fonctions de base de données définies par l’utilisateur.

/sprocs /sprocs

Spécifiez cette option pour que SQLMetal génère des méthodes qui permettront d’appeler les procédures stockées.

/dbml[:file] /dbml:Northwind.dbml

Cette option spécifie le nom d’un fichier intermédiaire DBML. Par son intermédiaire, vous pourrez contrôler les noms des classes et des propriétés des classes d’entité générées. Si vous utilisez cette option, éditez le fichier DBML généré, faites les modifications nécessaires et demandez la définition du module de code source en appelant SQLMetal sur le fichier DBML modifié et en spécifiant l’option /code. Vous pouvez également ouvrir le fichier DBML généré dans le Concepteur Objet/Relationnel, faire les modifications nécessaires par son intermédiaire et demander au Concepteur de générer le code source correspondant. Cette option ne doit pas être utilisée conjointement avec /map.

Linq.book Page 398 Mercredi, 18. février 2009 7:58 07

398

LINQ to SQL

Partie V

Tableau 13.1 : Les options de l’outil SQLMetal (suite)

Option/Exemple

Description

/code[:file] /code:Northwind.cs

Nom du fichier créé par SQLMetal et contenant le DataContext dérivé et les classes d’entité dans le langage de programmation spécifié. Cette option ne peut être utilisée conjointement à /dbml. Si vous spécifiez les options /code et /map dans une même invocation à SQLMetal, le code généré ne contiendra pas les attributs LINQ to SQL. Bien entendu, vous utiliserez le fichier de mappage et le fichier de code générés pour être en mesure d’utiliser LINQ to SQL.

/map[:file] /map:northwindmap.xml

Demande la génération d’un fichier de mappage XML (à opposer à l’option /code, qui demande la création d’un fichier de code). Ce fichier de mappage XML externe peut être chargé lors de l’instanciation du DataContext. Cela permet d’utiliser LINQ to SQL sans qu’aucun code source LINQ to SQL ne doive être compilé avec votre code. Si vous spécifiez les options /code et /map dans une même invocation à SQLMetal, le code généré ne contiendra pas les attributs LINQ to SQL. Bien entendu, vous utiliserez le fichier de mappage et le fichier de code générés pour être en mesure d’utiliser LINQ to SQL.

/language: /language:C#

Cette option définit le langage dans lequel SQLMetal doit écrire le code source. Les valeurs possibles sont csharp, C# et VB. Si vous omettez cette option, SQLMetal déduira le langage de l’extension du fichier de code source.

/namespace: /namespace:nwind

Indique l’espace de noms duquel la classe dérivée de DataContext et les classes d’entité dépendront.

/context: /context:Northwind

Nom de la classe générée, dérivée de la classe DataContext. Si cette option est omise, le nom de la classe sera le même que celui de la base de données.

/entitybase: Nom de la classe de base pour toutes les classes d’entité /entitybase:MyEntityClassBase générées. Si cette option est omise, les classes d’entité n’auront aucune classe de base.

Openmirrors.com

Linq.book Page 399 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

399

Tableau 13.1 : Les options de l’outil SQLMetal (suite)

Option/Exemple

Description

/pluralize /pluralize

Lorsque cette option est spécifiée, SQLMetal met le nom des tables au pluriel et le nom des classes d’entité mappées au singulier. Par exemple, pour une table Customers, la classe d’entité générée aura pour nom Customer, et un Table nommé Customers sera généré. Ainsi, la table Customers contiendra des objets Customer, ce qui, grammaticalement parlant, est tout à fait correct. Si cette option n’est pas spécifiée, la classe d’entité et le Table porteront le nom Customers. La table Customers contiendra des objets Customers, ce qui, grammaticalement parlant, est incorrect.

/serialization: /serialization:none

Indique si les attributs de sérialisation doivent être générés dans les classes. Les valeurs possibles sont None et Unidirectional. Si cette option n’est pas spécifiée, les attributs de sérialisation ne seront pas inclus.

/provider: /provider:SQL2005

Indique la classe du fournisseur de la base de données. Les valeurs possibles sont SQLCompact, SQL2000 et SQL2005. SQLMetal génère un attribut Provider qui indique la valeur spécifiée dans cette option. Les classes des fournisseurs se trouvent dans l’espace de noms System.Data.Linq.SqlClient. N’oubliez pas de spécifier cet espace de noms si vous utilisez cette option.

À titre d’information, sachez que les options /dbml, /code et /map peuvent être spécifiées sans aucun nom de fichier. Dans ce cas, le code ou XML généré sera affiché dans la console. Fichier de mappage XML ou fichier intermédiaire DBML ? SQLMetal vous permet de spécifier deux différents types de fichiers XML…, ce qui peut se révéler assez déroutant. Le premier correspond à l’option /map et le second, à l’option /dbml.

L’option /map crée un fichier de mappage externe XML destiné à être chargé à l’instanciation du DataContext. Cette option est une alternative à la génération ou à l’écriture manuelle d’un module source qui contient les attributs LINQ to SQL à compiler. Avec cette approche, le code source ne comprend et ne fait référence à aucun code LINQ to SQL spécifique à la base de données. Cela autorise une consommation "quelque peu dynamique" de la base de données, puisque vous n’avez besoin d’aucun code prégénéré

Linq.book Page 400 Mercredi, 18. février 2009 7:58 07

400

LINQ to SQL

Partie V

et compilé. J’ai bien dit "quelque peu dynamique". En effet, le code doit connaître le nom des tables et des champs, sans quoi il ne serait pas en mesure d’effectuer des requêtes. Le fichier de mappage externe indique à LINQ to SQL le nom des tables, colonnes et procédures stockées aves lesquelles il peut interagir, ainsi que le nom des classes, des propriétés et des méthodes auxquelles elles sont mappées. L’option /dbml crée un fichier intermédiaire DBML (XML). Vous pouvez éditer ce fichier afin de choisir le nom des classes et propriétés pour les classes d’entité à générer. Vous devez alors exécuter une nouvelle fois SQLMetal en lui indiquant non pas le nom de la base de données, mais le nom du fichier DBML, et en utilisant l’option /code. Vous pouvez également ouvrir le fichier DBML généré dans le Concepteur Objet/Relationnel, faire les modifications nécessaires par son intermédiaire et demander au Concepteur de générer le code source correspondant. Pour ajouter à la confusion, les schémas des deux types de fichiers XML générés par SQLMetal sont assez proches. Si nécessaire, reportez-vous au Chapitre 15 pour avoir des informations complémentaires sur les fichiers de mappage XML. Travailler avec des fichiers intermédiaires DBML Comme il a été dit dans la section précédente, le fichier intermédiaire DBML permet de contrôler le nom des classes et des propriétés, en intervenant manuellement entre l’extraction du schéma de la base de données et la génération de la classe d’entité. Si vous n’avez que faire de cette possibilité, les fichiers DBML ne sont pas pour vous. Dans la suite, nous allons supposer que vous avez besoin de choisir le nom des classes et des propriétés des classes d’entité.

Supposons que vous ayez attaché la base de données étendue Northwind à votre serveur SQL. Vous définirez le fichier intermédiaire DBML avec la commande suivante : sqlmetal /server:.\SQLExpress /database:Northwind /pluralize /sprocs /functions / views /dbml:Northwind.dbml

INFO L’utilisation des options /server et /database dans la commande sqlmetal nécessite que la base de données soit attachée au serveur SQL.

Il se peut également que vous ayez à spécifier les options /user et /password pour que SQLMetal soit en mesure de se connecter à la base de données. Si vous préférez, le fichier intermédiaire DBML peut être généré à partir d’un fichier MDF : sqlmetal /pluralize /sprocs /functions /views /dbml:Northwind.dbml "C:\Northwind.mdf"

Openmirrors.com

Linq.book Page 401 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

401

INFO La génération du fichier intermédiaire DBMF à partir d’un fichier MDF peut engendrer l’attachement du fichier MDF au serveur SQL sous le nom C:\NORTHWIND.MDF, ou quelque chose de similaire. Le cas échéant, vous devriez donner le nom Northwind à la base de données dans SQL Server Enterprise Manager ou SQL Server Management Studio pour que les exemples de ce chapitre soient en mesure de fonctionner.

Ces deux approches devraient produire le même fichier DBML. Dans les deux lignes de commande précédentes, seules les options nécessaires à la lecture de la base de données et à la création du fichier DBML ont été spécifiées. Des options telles que /language et /code n’ont d’intérêt que pour la génération d’un module de code source. Une fois le fichier XML intermédiaire modifié, vous obtiendrez le module de code source en exécutant cette commande : sqlmetal /namespace:nwind /code:Northwind.cs Northwind.dbml

Les options spécifiées dans cette commande sont appropriées à la génération de code source. Schéma de fichier intermédiaire DBML Si vous choisissez de créer un fichier intermédiaire DBML, de l’éditer et de générer des classes d’entité par cet intermédiaire, vous devez savoir ce qu’est un schéma et ce que signifient les noms d’éléments et d’attributs. Le fonctionnement des schémas étant sujet à modifications, consultez la documentation Microsoft pour prendre connaissance des dernières informations à leur sujet. Une fois le concept de schéma compris, vous pourrez choisir d’éditer manuellement le fichier DBML pour contrôler les noms des classes d’entité et des propriétés, puis générer le code source de la classe d’entité en indiquant à SQLMetal le nom du fichier DBML modifié. Encore mieux, vous pourrez ouvrir le fichier DBML généré dans le Concepteur Objet/ Relationnel et y faire les modifications nécessaires. En utilisant son interface graphique, et même si vous ne connaissez ni ne comprenez ce qu’est un schéma, vous pourrez modifier le mappage relationnel. Le Concepteur Objet/Relationnel Outre SQLMetal, il existe également un outil graphique permettant de générer des classes d’entité. Cet outil fait partie intégrante de Visual Studio. Il est connu sous les noms "Concepteur Objet/Relationnel", "Concepteur LINQ to SQL", "Concepteur O/R" (Object-to-Relational) ou encore "Concepteur DLinq". L’outil en ligne de commande SQLMetal a été conçu pour fabriquer des classes d’entité pour toutes les tables de la base de données. Vous avez cependant la possibilité de limiter son action à certaines tables de la base en créant un fichier DBML, en le modifiant et en générant des

Linq.book Page 402 Mercredi, 18. février 2009 7:58 07

402

LINQ to SQL

Partie V

classes d’entité par son intermédiaire. Le Concepteur Objet/Relationnel permet une approche plus sélective et… entièrement graphique. Dans la suite de ce chapitre, nous utiliserons le terme "Concepteur" pour désigner le Concepteur Objet/Relationnel. Le Concepteur permet de modifier la classe d’entité par de simples glisser-déposer. N’ayez crainte, le Concepteur fait toute la partie ingrate du travail. Votre part ne consiste qu’à sélectionner les tables à modeler et, si vous le souhaitez, à modifier les noms et les propriétés des classes d’entité. Il est bien entendu toujours possible de créer le modèle à la main dans le Concepteur pour avoir un contrôle total… Création du fichier des classes LINQ to SQL La première chose à faire concernant le Concepteur consiste à créer les classes LINQ to SQL en cliquant du bouton droit sur le projet et en sélectionnant Ajouter/Nouvel élément dans le menu contextuel. Cette action provoque l’affichage de la boîte de dialogue Ajouter un nouvel élément. Sélectionnez le modèle Classes LINQ to SQL dans la liste. Choisissez un nom pour la nouvelle classe : le nom de la base de données est un bon choix, et l’extension est .dbml. Dans cet exemple, nous utiliserons le nom Northwind.dbml. ATTENTION Si vous avez déjà défini un fichier Northwind.dbml dans un autre projet créé à partir des exemples de cet ouvrage, faites attention à ne pas écraser le code existant.

Cliquez sur Ajouter. Après quelques instants, un espace blanc occupe le centre de la fenêtre. Il s’agit de l’espace de travail (aussi appelé canevas) du Concepteur. La Figure 13.1 donne un aperçu du canevas. Figure 13.1 : Le canevas du Concepteur Objet/Relationnel.

Openmirrors.com

Linq.book Page 403 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

403

Cliquez du bouton droit sur le canevas et sélectionnez Propriétés dans le menu. Les propriétés apparaissent dans la partie droite de la fenêtre. La propriété Name représente le nom de la classe DataContext générée. Le fichier de classes LINQ to SQL ayant pour nom "Northwind.dbml", le nom par défaut de la classe DataContext sera NorthwindDataContext. Ce nom peut être modifié mais, ici, nous allons le laisser intact. Examinez l’Explorateur de solutions. Vous verrez que le fichier Northwind.designer.cs a été inséré dans le dossier Northwind.dbml. Si vous ouvrez ce fichier, vous verrez qu’il ne contient que très peu de code à ce niveau. En fait, il contient les constructeurs de la nouvelle classe DataContext NorthwindDataContext dont il est dérivé. Connexion du DataContext à la base de données La prochaine étape va consister à ajouter (si elle n’existe pas déjà) une connexion au serveur de base de données contenant la base de données Northwind dans la fenêtre Explorateur de serveurs. ASTUCE Si la fenêtre Explorateur de serveurs n’est pas accessible, lancez la commande Explorateur de serveurs dans le menu Affichage de Visual Studio.

Pour ajouter une connexion à la base de données, cliquez du bouton droit sur l’icône Connexion de données dans la fenêtre Explorateur de serveurs et choisissez Ajouter une connexion dans le menu contextuel. Cette action ouvre la boîte de dialogue Ajouter une connexion (voir Figure 13.2). La zone de texte Source de données devrait laisser apparaître Microsoft SQL Server (SqlClient). Si ce n’est pas le cas, cliquez sur Modifier et choisissez l’entrée correspondante. Figure 13.2 : La boîte de dialogue Ajouter une connexion.

Linq.book Page 404 Mercredi, 18. février 2009 7:58 07

404

LINQ to SQL

Partie V

Configurez les paramètres nécessaires à l’accès de la base de données Northwind dans la boîte de dialogue Ajouter une connexion. Pour vous assurer que la connexion est bien configurée, cliquez sur Tester la connexion. Une fois que la connexion est configurée, cliquez sur OK. Vous devriez avoir une entrée qui représente la connexion avec la base de données Northwind sous le libellé Connexions de données, dans l’Explorateur de serveurs. Vous êtes maintenant en mesure d’accéder à la base de données Northwind dans le Concepteur. Avant de commencer, assurez-vous que vous êtes en train de visualiser le fichier Northwind.dbml dans l’éditeur de Visual Studio. Ajout d’une classe d’entité Dans la fenêtre Explorateur de serveurs, sous Connexions de données, cliquez sur le signe "+" qui précède l’entrée Northwind.mdf, puis sur le signe "+" qui précède l’entrée "Tables" pour afficher la liste des tables de la base de données Northwind. Les classes d’entité sont créées en déposant des tables de la fenêtre Explorateur de serveurs sur le canevas du Concepteur.

Déposez la table Customers sur le canevas du Concepteur. Cette simple opération demande au Concepteur de créer la classe d’entité Customer pour la table Customers. Le canevas devrait maintenant ressembler à la Figure 13.3. Figure 13.3 : Le Concepteur, après avoir déposé la table Customers sur le canevas.

Il se peut que vous ayez à opérer plusieurs redimensionnements pour que tout ce qui est affiché dans Visual Studio apparaisse clairement. En déposant la table Customers sur le canevas du Concepteur, le code source de l’entité Customer est ajouté au fichier source Openmirrors.com

Linq.book Page 405 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

405

Northwind.designer.cs. Une fois que vous aurez construit votre projet, vous pourrez commencer à utiliser la classe d’entité Customer pour accéder aux données et les mettre à jour dans la base de données Northwind. C’est aussi simple que cela !

Avant de construire le projet et d’écrire le code qui utilise les classes d’entité générées, nous allons créer d’autres petites choses nécessaires pour tirer parti de LINQ to SQL. Déposez la table Orders sur le canevas (après l’avoir déposée, il se peut que vous ayez à la déplacer pour lui donner une meilleure position). Ce faisant, vous avez demandé au Concepteur de créer la classe d’entité Order pour la table Orders. Votre canevas devrait maintenant avoir l’allure de la Figure 13.4. Figure 13.4 : Le Concepteur, après avoir déposé la table Orders sur le canevas.

Vous avez peut-être remarqué que le volet qui apparaissait à droite du canevas à la Figure 13.3 a maintenant disparu. Il s’agit du volet Méthodes. Pour le fermer, il suffit de le pointer, de cliquer à droite et de sélectionner Masquer le volet Méthodes dans le menu contextuel. Pour l’ouvrir à nouveau, cliquez du bouton droit sur le canevas et sélectionnez Afficher le volet Méthodes. Nous avons fermé ce volet pour laisser un plus grand espace dans le canevas. En observant le canevas, vous pouvez voir une ligne pointillée qui relie la classe Customer à la classe Order. Cette ligne représente la relation (connue sous le nom "association" dans LINQ to SQL) entre les tables Customers et Orders, telle qu’elle a été définie par la clé étrangère FK_Orders_Customers définie dans la base de données Northwind. Cette ligne pointillée indique également que le Concepteur définira une association entre les classes d’entité pour supporter la relation qui lie les deux tables.

Linq.book Page 406 Mercredi, 18. février 2009 7:58 07

406

LINQ to SQL

Partie V

C’est par cette association que vous pourrez obtenir une référence vers une collection de commandes des clients en référençant une propriété d’un objet Customer. De même, vous pourrez obtenir une référence vers une commande d’un client en référençant une propriété dans l’objet Order. Si vous ne voulez pas conserver l’association, il vous suffit de cliquer sur la ligne pointillée qui relie les deux tables et d’appuyer sur la touche Suppr du clavier. Vous pouvez également cliquer du bouton droit sur la ligne pointillée et sélectionner Supprimer dans le menu contextuel. Utilisation des classes d’entité générées par le Concepteur Vous êtes maintenant prêt pour utiliser les classes d’entité générées par le Concepteur. À titre d’exemple, le Listing 13.2 effectue une requête sur la base de données Northwind pour connaître les clients qui habitent à Londres. Listing 13.2 : Un exemple d’utilisation des classes d’entité générées par le Concepteur. NorthwindDataContext db = new NorthwindDataContext(); IQueryable custs = from c in db.Customers where c.City == "London" select c; foreach(Customer c in custs) { Console.WriteLine("{0} a passé {1} commandes.", c.CompanyName, c.Orders.Count); }

Ce code est assez proche de celui des autres exemples. Mais remarquez qu’aucune information de connexion n’a été spécifiée lors de l’instanciation de l’objet NorthwindDataContext. Ceci vient du fait que le Concepteur a généré la classe NorthwindDataContext avec un constructeur qui n’a pas besoin de paramètres, car les informations de connexion proviennent du fichier de configuration du projet : app.config. Ce fichier contient le code suivant : Le constructeur DataContext généré par le Concepteur public NorthwindDataContext() : base(global::LINQChapter13.Properties.Settings.Default.NorthwindConnectionString, mappingSource) { OnCreated(); }

ATTENTION Si vous avez téléchargé le code source d’accompagnement de cet ouvrage, assurez-vous que vous avez mis à jour la chaîne de connexion connectionString dans le fichier app.config. En particulier, le code source doit contenir le nom de votre ordinateur, qui n’a que peu de chances d’être identique au mien.

Openmirrors.com

Linq.book Page 407 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

407

Dans le code précédent, remarquez qu’il nous a été possible d’accéder aux commandes des clients en faisant référence à la propriété Orders d’un objet Customer. Ceci vient du fait que le Concepteur a automatiquement défini une association entre ces deux tables. Voici les résultats de cette requête : Around the Horn a passé 13 commandes. B’s Beverages a passé 10 commandes. Consolidated Holdings a passé 3 commandes. Eastern Connection a passé 8 commandes. North/South a passé 3 commandes. Seven Seas Imports a passé 9 commandes.

Édition du modèle de classe d’entité Il se peut que vous désiriez modifier les noms des classes d’entité, les propriétés des classes d’entité (dans la fenêtre des propriétés), le nom des propriétés (des membres) des classes d’entité, les propriétés d’une propriété d’une classe d’entité (d’un membre d’une classe d’entité). Merci Microsoft ! Vous auriez certainement pu mieux faire au niveau du choix des termes. Pourquoi les membres des classes ont-ils été appelés "propriétés", alors que Visual Studio utilise également ce terme pour parler des différents réglages accessibles dans la fenêtre des propriétés ?

Si le Concepteur est si intéressant, c’est certainement à cause de sa flexibilité et de la facilité avec laquelle il est possible de contrôler le nom des classes d’entité et de leurs propriétés : de simples glisser-déposer, pointer et cliquer suffisent ! Modification du nom d’une classe d’entité Pour modifier le nom d’une classe d’entité, il suffit de double-cliquer sur ce nom dans le canevas. Vous pouvez également cliquer sur la classe d’entité dans le canevas et modifier la propriété Name dans la fenêtre des propriétés. Modification des propriétés d’une classe d’entité Pour modifier les propriétés d’une classe d’entité, il suffit de cliquer sur cette classe dans le canevas et de faire la modification souhaitée dans la fenêtre des propriétés. Vous pouvez ainsi modifier le nom de la table dans laquelle les entités sont stockées, le nom des méthodes surchargées Delete, Insert et Update, ainsi que d’autres propriétés. Modification du nom d’une propriété d’une classe d’entité Pour modifier le nom d’une propriété d’une classe d’entité, il suffit de triplecliquer dessus dans le canevas. Vous pouvez également sélectionner la propriété en cliquant dessus dans le canevas et modifier la propriété Name dans la fenêtre des propriétés.

Linq.book Page 408 Mercredi, 18. février 2009 7:58 07

408

LINQ to SQL

Partie V

Modification des propriétés d’une propriété d’une classe d’entité Pour modifier les propriétés d’une propriété d’une classe d’entité, sélectionnez la propriété dans le canevas et modifiez les propriétés souhaitées dans la fenêtre des propriétés : Name et UpdateCheck, par exemple. Nous discuterons en détail des attributs des classes d’entité au Chapitre 15. Ajout d’objets dans le modèle de classe d’entité Glisser-déposer une classe dans le canevas est une technique simple… à condition que la table correspondante se trouve dans la fenêtre Explorateur de serveurs. Il existe plusieurs cas dans lesquels ce luxe ne vous sera pas permis. Par exemple, si vous définissez la classe d’entité en premier et prévoyez de générer la base de données en appelant la méthode CreateDatabase sur le DataContext. Ou encore si vous voulez vous servir de l’héritage d’une classe d’entité et qu’il n’existe aucune table pour la mapper.

Ajout de nouvelles classes d’entité Pour ajouter de nouvelles classes d’entité dans le modèle de classes d’entité, une solution consiste à les déplacer depuis les tables de la base de données (fenêtre Explorateur de serveurs) sur le canevas du Concepteur, comme cela a été fait dans la section précédente. Une autre technique consiste à déplacer l’objet Classe du Concepteur Objet/ Relationnel de la Boîte à outils sur le canevas. Modifiez le nom et les propriétés de la classe d’entité comme il a été exposé dans la section précédente. Ajout de nouvelles propriétés (membres) dans une classe d’entité Pour ajouter une nouvelle propriété dans une classe d’entité, cliquez du bouton droit sur la classe d’entité, dans le Concepteur, pointez Ajouter et sélectionnez Propriété dans le menu contextuel. Lorsque la propriété a été ajoutée, procédez comme indiqué dans la section "Modification des propriétés d’une propriété d’une classe d’entité" précédente pour modifier les propriétés de cette propriété. Ajout d’une nouvelle association Plutôt qu’utiliser un glisser-déposer pour créer une association, cliquez sur l’objet Association dans la Boîte à outils, sur la classe d’entité parent (le côté "un" d’une relation "un-à-plusieurs"), puis sur la classe d’entité enfant (le côté "plusieurs" d’une relation "un-à-plusieurs"). Avant de pouvoir définir une association, chacune des deux classes doit posséder une propriété appropriée, de telle sorte que vous puissiez définir la clé primaire du côté "un" et la clé étrangère du côté "plusieurs". Après avoir cliqué sur la deuxième classe (celle qui correspond au côté "plusieurs" de l’association), la boîte de dialogue Editeur d’associations est affichée. Il ne vous reste plus qu’à l’utiliser pour relier une propriété du côté "un" à une autre du côté "plusieurs". Openmirrors.com

Linq.book Page 409 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

409

Cette étape terminée, cliquez sur OK. Une flèche pointillée reliera la classe d’entité parent à la classe d’entité enfant. Sélectionnez l’association en cliquant sur la ligne pointillée et définissez les propriétés de l’association dans la fenêtre des propriétés. Si nécessaire, reportez-vous au Chapitre 15 pour avoir plus d’informations sur les propriétés d’une association. Ajout d’un nouvel héritage Vous pouvez également utiliser le Concepteur Objet/Relationnel pour modeler les relations d’héritage : ajouter une relation d’héritage revient à ajouter une nouvelle association. Sélectionnez l’objet Héritage dans la Boîte à outils de Visual Studio, cliquez sur la classe d’entité qui sera la classe dérivée, puis sur la classe d’entité qui sera la classe de base. Assurez-vous que vous avez défini correctement les propriétés de la classe d’entité, comme spécifié dans les attributs de classe d’entité InheritanceMapping et Column. Si nécessaire, reportez-vous au Chapitre 15 pour en savoir plus à ce sujet. Ajout de procédures stockées et de fonctions définies par l’utilisateur Au Chapitre 14, vous apprendrez à surcharger les méthodes insert, update et delete utilisées par LINQ to SQL suite à des modifications effectuées dans une classe d’entité. Pour surcharger les méthodes par défaut, il suffit d’ajouter des méthodes spécifiques dans une classe d’entité. Si vous adoptez cette approche, assurez-vous que vous utilisez des classes partielles. Ainsi, vous ne modifierez aucun code généré. Nous étudierons cette technique en détail au Chapitre 14.

Sachez cependant que les méthodes insert, update et delete peuvent être facilement surchargées en utilisant le Concepteur. Supposons que nous disposions de la procédure stockée InsertCustomer, qui insère un enregistrement d’un nouveau client dans la table Customer de la base de données Northwind. Voici le code de la procédure stockée : CREATE PROCEDURE dbo.InsertCustomer ( @CustomerID nchar(5), @CompanyName nvarchar(40), @ContactName nvarchar(30), @ContactTitle nvarchar(30), @Address nvarchar(60), @City nvarchar(15), @Region nvarchar(15), @PostalCode nvarchar(10), @Country nvarchar(15), @Phone nvarchar(24), @Fax nvarchar(24) ) AS INSERT INTO Customers ( CustomerID, CompanyName, ContactName, ContactTitle, Address,

Linq.book Page 410 Mercredi, 18. février 2009 7:58 07

410

LINQ to SQL

Partie V

City, Region, PostalCode, Country, Phone, Fax ) VALUES ( @CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, @Region, @PostalCode, @Country, @Phone, @Fax )

INFO La procédure stockée InsertCustomer ne fait pas partie de la version étendue de la base de données Northwind. Elle a été ajoutée manuellement pour cette démonstration.

Pour surcharger la méthode insert de la classe d’entité Customer, assurez-vous que le volet Méthodes est visible. Dans le cas contraire, cliquez du bouton droit sur le canevas et sélectionnez Ajouter le volet Méthodes dans le menu contextuel. Si la fenêtre Explorateur de serveurs n’est pas déjà ouverte, ouvrez-la. Développez le nœud Procédures stockées. La fenêtre de Visual Studio doit maintenant ressembler à la Figure 13.5. Figure 13.5 : La procédure stockée InsertCustomer.

m

Openmirrors.com

Linq.book Page 411 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

411

Faites glisser la procédure stockée InsertCustomer de l’Explorateur de serveurs dans le volet Méthodes. La fenêtre de Visual Studio devrait maintenant ressembler à la Figure 13.6. Figure 13.6 : Glisser-déposer de la procédure stockée dans le volet Méthodes.

Le simple fait de déposer une méthode stockée depuis l’Explorateur de serveurs dans le volet Méthodes demande au Concepteur de générer le code qui permettra d’appeler la procédure stockée depuis LINQ to SQL. Vous utiliserez la même méthode pour générer le code d’une fonction définie par l’utilisateur. Pour qu’une opération Delete, Insert ou Update appelle une procédure stockée (et non la méthode par défaut), la procédure doit être accessible depuis LINQ to SQL. Cette première étape effectuée, il suffit alors de surcharger l’opération. Cliquez sur la classe Customers dans le Concepteur et observez la fenêtre des propriétés. Les dernières lignes listent les méthodes utilisées par défaut pour les opérations Delete, Insert et Update. Cliquez sur l’entrée Insert. Un bouton contenant des points de suspension apparaît dans la partie droite de l’entrée (voir Figure 13.7). Cliquez sur le bouton pour afficher la boîte de dialogue Configurer le comportement. Sélectionnez l’option Personnaliser et la procédure stockée InsertCustomer dans la liste déroulante. Reliez les arguments de méthode (à gauche) aux propriétés de classe (à droite), comme illustré à la Figure 13.8. Comme vous pouvez le constater, tous les arguments de méthode sont déjà correctement reliés aux propriétés de classe. Cliquez sur OK pour fermer la boîte de dialogue Configurer le comportement. Vous êtes maintenant prêt à insérer des enregistrements Customer en utilisant la procédure stockée InsertCustomer (voir Listing 13.3).

Linq.book Page 412 Mercredi, 18. février 2009 7:58 07

412

LINQ to SQL

Partie V

Figure 13.7 : Sélection de la méthode Insert dans la catégorie Méthodes par défaut de la fenêtre des propriétés.

Figure 13.8 : Liaison des arguments de méthode aux propriétés de classe.

Listing 13.3 : Création d’un enregistrement Customer avec la surcharge de la méthode par défaut Insert. NorthwindDataContext db = new NorthwindDataContext(); db.Log = Console.Out; Customer cust = new Customer {

Openmirrors.com

Linq.book Page 413 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

413

CustomerID = "EWICH", CompanyName = "Every ’Wich Way", ContactName = "Vickey Rattz", ContactTitle = "Owner", Address = "105 Chip Morrow Dr.", City = "Alligator Point", Region = "FL", PostalCode = "32346", Country = "USA", Phone = "(800) EAT-WICH", Fax = "(800) FAX-WICH" }; db.Customers.InsertOnSubmit(cust); db.SubmitChanges(); Customer customer = db.Customers.Where(c => c.CustomerID == "EWICH").First(); Console.WriteLine("{0} - {1}", customer.CompanyName, customer.ContactName); // Restauration de la base de données db.Customers.DeleteOnSubmit(cust); db.SubmitChanges();

Comme vous pouvez le voir, aucun espace de noms n’est spécifié dans la classe Customer référencée. Nous utiliserons en effet la classe Customer générée par le Concepteur. Cette classe se trouve dans le même espace de noms que le projet. Le Listing 13.3 est assez simple à comprendre. Après avoir instancié le DataContext NorthwindDataContext, généré par le Concepteur, un objet Customer est créé et initialisé. Cet objet est inséré dans la propriété Customers Table. La méthode SubmitChanges est alors appelée pour reporter la modification dans la base de données. Une requête retrouve cet enregistrement dans la base de données, puis une instruction Console.WriteLine l’affiche dans la console, pour mettre en évidence que l’enregistrement a bien été inséré dans la base de données. Enfin, l’enregistrement est supprimé de la base de données avec la méthode DeleteOnSubmit, et la suppression est rendue permanente avec la méthode SubmitChanges. La base de données se trouve donc dans le même état après l’exécution du code (voir Listing 13.3). Les exemples suivants ne seront donc pas affectés par ce code, et ce qu’il soit exécuté une ou plusieurs fois. EXEC @RETURN_VALUE = [dbo].[InsertCustomer] @CustomerID = @p0, @CompanyName = @p1, @ContactName = @p2, @ContactTitle = @p3, @Address = @p4, @City = @p5, @Region = @p6, @PostalCode = @p7, @Country = @p8, @Phone = @p9, @Fax = @p10 -- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [EWICH] -- @p1: Input String (Size = 15; Prec = 0; Scale = 0) [Every ’Wich Way] -- @p2: Input String (Size = 12; Prec = 0; Scale = 0) [Vickey Rattz] -- @p3: Input String (Size = 5; Prec = 0; Scale = 0) [Owner] -- @p4: Input String (Size = 19; Prec = 0; Scale = 0) [105 Chip Morrow Dr.] -- @p5: Input String (Size = 15; Prec = 0; Scale = 0) [Alligator Point] -- @p6: Input String (Size = 2; Prec = 0; Scale = 0) [FL] -- @p7: Input String (Size = 5; Prec = 0; Scale = 0) [32346] -- @p8: Input String (Size = 3; Prec = 0; Scale = 0) [USA] -- @p9: Input String (Size = 14; Prec = 0; Scale = 0) [(800) EAT-WICH] -- @p10: Input String (Size = 14; Prec = 0; Scale = 0) [(800) FAX-WICH] -- @RETURN_VALUE: Output Int32 (Size = 0; Prec = 0; Scale = 0) [] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Linq.book Page 414 Mercredi, 18. février 2009 7:58 07

414

LINQ to SQL

Partie V

SELECT TOP 1 [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] WHERE [t0].[CustomerID] = @p0 -- @p0: Input String (Size = 5; Prec = 0; Scale = 0) [EWICH] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Every ’Wich Way - Vickey Rattz DELETE FROM [dbo].[Customers] WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) AND ([ContactTitle] = @p3) AND ([Address] = @p4) AND ([City] = @p5) AND ([Region] = @p6) AND ([PostalCode] = @p7) AND ([Country] = @p8) AND ([Phone] = @p9) AND ([Fax] = @p10) -- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [EWICH] -- @p1: Input String (Size = 15; Prec = 0; Scale = 0) [Every ’Wich Way] -- @p2: Input String (Size = 12; Prec = 0; Scale = 0) [Vickey Rattz] -- @p3: Input String (Size = 5; Prec = 0; Scale = 0) [Owner] -- @p4: Input String (Size = 19; Prec = 0; Scale = 0) [105 Chip Morrow Dr.] -- @p5: Input String (Size = 15; Prec = 0; Scale = 0) [Alligator Point] -- @p6: Input String (Size = 2; Prec = 0; Scale = 0) [FL] -- @p7: Input String (Size = 5; Prec = 0; Scale = 0) [32346] -- @p8: Input String (Size = 3; Prec = 0; Scale = 0) [USA] -- @p9: Input String (Size = 14; Prec = 0; Scale = 0) [(800) EAT-WICH] -- @p10: Input String (Size = 14; Prec = 0; Scale = 0) [(800) FAX-WICH] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Comme vous pouvez le voir, aucune déclaration SQL insert n’a été créée. En revanche, la procédure stockée InsertCustomer a été appelée (code en gras). Le Concepteur facilite grandement la surcharge des méthodes Insert, Update et Delete d’une classe d’entité.

Utiliser SQLMetal et le Concepteur O/R Étant donné que le fichier intermédiaire DBML issu de SQLMetal utilise le même schéma que le Concepteur Objet/Relationnel, il est tout à fait possible d’utiliser ces deux outils conjointement. Par exemple, vous pourriez très bien générer un fichier DBML intermédiaire en utilisant SQLMetal, puis ouvrir ce fichier dans le Concepteur afin de modifier le nom d’une classe d’entité ou d’une propriété d’une classe d’entité. Cette technique vous permet de créer simplement les classes d’entité d’une base de données et de modifier le ou les noms souhaités. La surcharge des opérations insert, delete et update est un autre exemple pour lequel cette interchangeabilité peut être mise à profit : vous pourriez générer un fichier intermédiaire DBML avec SQLMetal, le charger dans le Concepteur et modifier les méthodes insert, delete et update, comme indiqué dans la section "Le Concepteur Objet/ Relationnel" de ce chapitre. Openmirrors.com

Linq.book Page 415 Mercredi, 18. février 2009 7:58 07

Chapitre 13

Astuces et outils pour LINQ to SQL

415

Résumé Les classes d’entité et la classe DataContext n’ayant pas encore été étudiées en détail, la plupart des informations de ce chapitre peuvent vous sembler prématurées. Cependant, je me devais d’introduire les astuces et outils disponibles pour les développements LINQ to SQL. N’hésitez pas à revenir vers ce chapitre lorsque vous aurez toutes les connaissances nécessaires pour bien l’appréhender. Rappelez-vous que vous disposez de deux outils pour générer les classes d’entité d’une base de données : SQLMetal et le Concepteur Objet/Relationnel. SQLMetal est un outil qui fonctionne en ligne de commande. Il est approprié lorsqu’il s’agit de générer toutes les classes d’entité d’une base de données. Le Concepteur O/R (aussi connu sous le nom Concepteur LINQ to SQL) fait partie intégrante de Visual Studio. Il utilise la technique du glisser-déposer de Windows pour modéliser les classes d’entité. Très interactif, il s’adapte parfaitement aux nouveaux développements. Notez cependant que ces deux outils peuvent être utilisés conjointement. La meilleure technique consiste certainement à générer toutes les classes d’entité avec SQLMetal et à assurer leur maintenance avec le Concepteur O/R. Vous connaissez maintenant les outils LINQ to SQL et quelques astuces de développement, et vous avez appris à créer vos classes d’entité. Le chapitre suivant va vous montrer comment effectuer les opérations basiques de base de données, couramment utilisées lors de développements LINQ to SQL.

Linq.book Page 416 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 417 Mercredi, 18. février 2009 7:58 07

14 Opérations standard sur les bases de données Dans ce chapitre, vous allez apprendre à utiliser LINQ to SQL pour effectuer les opérations standard sur des bases de données. Nous nous intéresserons en particulier aux opérations suivantes : m

insertion d’enregistrements ;

m

requêtes ;

m

mises à jour ;

m

suppression d’enregistrements.

Après avoir introduit les opérations de base de données standard, vous verrez comment surcharger les méthodes insert, update et delete utilisées pour enregistrer des modifications dans une base de données. Le sujet suivant sera consacré à la traduction automatique des requêtes LINQ to SQL. Vous y apprendrez en outre comment "bien écrire" vos requêtes. Pour pouvoir discuter des opérations de base de données standard, nous devrons nous référer au DataContext et aux classes afférentes. Pour l’instant, nous n’avons donné que peu de détails sur leur fonctionnement. Vous en apprendrez plus à leur sujet dans les chapitres (respectivement) 16 et 15. Pour l’instant, il vous suffit de savoir que le DataContext gère la connexion à la base de données et les objets des classes d’entité (une classe d’entité représente la forme objet d’un enregistrement de base de données).

Prérequis pour exécuter les exemples Pour pouvoir exécuter les exemples de ce chapitre, vous devez avoir téléchargé la version étendue de la base de données exemple de Microsoft Northwind et avoir généré

Linq.book Page 418 Mercredi, 18. février 2009 7:58 07

418

LINQ to SQL

Partie V

les classes d’entité correspondantes. Reportez-vous à la section "Prérequis pour exécuter les exemples" du Chapitre 12 pour en savoir plus à ce sujet. Méthodes communes Vous aurez également besoin de quelques méthodes communes. Reportez-vous à la section "Méthodes communes" du Chapitre 12 pour en savoir plus à ce sujet. Utilisation de l’API LINQ to SQL Pour exécuter les exemples de ce chapitre, vous aurez peut-être besoin d’ajouter les références et directives using appropriées dans votre projet. Reportez-vous à la section "Utilisation de l’API LINQ to SQL" du Chapitre 12 pour en savoir plus à ce sujet.

Opérations standard de bases de données Les requêtes LINQ to SQL seront traitées en détail dans les prochains chapitres. Ici, nous nous contenterons de vous montrer comment effectuer les opérations de base. Les exemples de ce chapitre étant volontairement limités à des fins pédagogiques, nous n’y inclurons aucun code de recherche d’erreur ni de gestion des exceptions. Normalement, les opérations qui effectuent des modifications dans la base de données devraient inclure un code de détection et de traitement des conflits d’accès concurrentiel. Mais, dans un souci de simplification, cette portion de code sera omise. Si nécessaire, reportez-vous au Chapitre 17 pour savoir comment détecter et résoudre les conflits d’accès concurrentiels. Insertions Il ne suffit pas d’instancier une classe d’entité (la classe Customer, par exemple) pour insérer un enregistrement dans la base de données. Vous devez également insérer un objet entité dans une collection de tables de type Table (où T est le type de la classe d’entité stocké dans la table) ou l’ajouter à un EntitySet (où T est le type de la classe d’entité) sur un objet d’entité déjà présent dans le DataContext. Pour insérer un enregistrement dans la base de données, respectez les quatre étapes suivantes : 1. Définissez un DataContext (c’est également la première étape pour toute requête LINQ to SQL). 2. Instanciez un objet entité à partir de la classe d’entité. 3. Insérez l’objet entité dans la collection de table. 4. Appelez la méthode SubmitChanges sur le DataContext. Le Listing 14.1 donne un exemple d’insertion d’un enregistrement dans une base de données. Openmirrors.com

Linq.book Page 419 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

419

Listing 14.1 : Insertion d’un enregistrement en insérant un objet entité dans un Table. // 1. Création du DataContext Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // 2. Instanciation d’un objet entité Customer cust = new Customer { CustomerID = "LAWN", CompanyName = "Lawn Wranglers", ContactName = "Mr. Abe Henry", ContactTitle = "Owner", Address = "1017 Maple Leaf Way", City = "Ft. Worth", Region = "TX", PostalCode = "76104", Country = "USA", Phone = "(800) MOW-LAWN", Fax = "(800) MOW-LAWO" }; // 3. Ajout de l’objet entité à la table Customers db.Customers.InsertOnSubmit(cust); // 4. Appel de la méthode SubmitChanges db.SubmitChanges(); // 5. Requête sur l’enregistrement Customer customer = db.Customers.Where(c => c.CustomerID == "LAWN").First(); Console.WriteLine("{0} - {1}", customer.CompanyName, customer.ContactName); // Cette portion de code restaure la base de données dans son // état initial. Le code peut donc être exécuté à plusieurs reprises. Console.WriteLine("Suppression du client LAWN."); db.Customers.DeleteOnSubmit(cust); db.SubmitChanges();

Cet exemple est très simple. Le premier bloc instancie l’objet Northwind afin d’obtenir un objet DataContext pour la base de données Northwind. Le deuxième bloc de code instancie l’objet Customer et l’initialise en utilisant la technique d’initialisation d’objet apparue dans C# 3.0. Le troisième bloc insère l’objet Customer de type Tabledans la table Customers. Le quatrième bloc appelle la méthode SubmitChanges pour enregistrer l’objet Customer dans la base de données. Enfin, le cinquième bloc effectue une requête sur la base de données, afin de montrer que l’enregistrement a bien été inséré. INFO Si vous exécutez cet exemple, un nouvel enregistrement correspondant au client LAWN est temporairement ajouté à la table Customers de la base de données Northwind. Après avoir effectué une requête sur ce nouvel enregistrement et affiché les données correspondantes, les dernières lignes du code le suppriment. Cette précaution est nécessaire car, ainsi, l’exemple pourra être exécuté autant de fois que vous le souhaiterez sans que la base de données ne soit modifiée pour les exemples suivants. Cette portion de code sera utilisée pour tous les exemples qui modifient la base de données. Si, pour une raison ou pour une autre, le code ne s’exécute pas entièrement, vous devrez restaurer manuellement la base de données à son état initial.

Linq.book Page 420 Mercredi, 18. février 2009 7:58 07

420

LINQ to SQL

Partie V

Voici les résultats du Listing 14.1 : Lawn Wranglers - Mr. Abe Henry Suppression du client LAWN.

Comme vous pouvez le constater, le nouvel enregistrement a bien été trouvé dans la base de données. Pour insérer un nouvel enregistrement dans une base de données, vous pouvez également ajouter une nouvelle instance d’une classe d’entité à un objet entité déjà référencé dans l’objet DataContext (voir Listing 14.2). Listing 14.2 : Insertion d’un enregistrement dans la base de données Northwind en intervenant sur EntitySet. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // Requête pour retrouver l’enregistrement déjà référencé. Customer cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single(); // Définition du nouvel enregistrement. DateTime now = DateTime.Now; Order order = new Order { CustomerID = cust.CustomerID, EmployeeID = 4, OrderDate = now, RequiredDate = DateTime.Now.AddDays(7), ShipVia = 3, Freight = new Decimal(24.66), ShipName = cust.CompanyName, ShipAddress = cust.Address, ShipCity = cust.City, ShipRegion = cust.Region, ShipPostalCode = cust.PostalCode, ShipCountry = cust.Country }; cust.Orders.Add(order); db.SubmitChanges(); IEnumerable orders = db.Orders.Where(o => o.CustomerID == "LONEP" && o.OrderDate.Value == now); foreach (Order o in orders) { Console.WriteLine("{0} {1}", o.OrderDate, o.ShipName); } // Cette portion de code restaure la base de données dans son // état initial. Le code peut donc être exécuté à plusieurs reprises. db.Orders.DeleteOnSubmit(order); db.SubmitChanges();

INFO La méthode InsertOnSubmit est appelée dans le Listing 14.1, alors que c’est la méthode Add qui est appelée dans le Listing 14.2. Ces méthodes sont différentes, car elles sont appe-

Openmirrors.com

Linq.book Page 421 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

421

lées sur des objets différents. Dans le Listing 14.1, la méthode InsertOnSubmit est appelée sur un objet Table. Dans le Listing 14.2, la méthode Add est appelée sur un objet EntitySet.

Dans le Listing 14.2, nous avons créé le DataContext Northwind, effectué une requête sur le client LONEP et ajouté l’objet entité order fraîchement construit à l’EntitySet Orders de l’objet entité Customer. Le nouvel enregistrement a alors été récupéré à l’aide d’une requête et affiché dans la console. INFO Les dernières lignes du code utilisent la méthode DeleteOnSubmit pour supprimer l’enregistrement order, précédemment ajouté à la base de données. Si le code ne s’exécute pas entièrement, vous devrez supprimer manuellement l’enregistrement pour que la base de données ne soit pas affectée pour les autres exemples.

Si vous pensez que cet exemple est comparable au précédent, sachez que, dans le Listing 14.1, un objet Customer a été inséré dans une variable de type Table, alors que, dans le Listing 14.2, l’objet Order a été ajouté à une variable de type EntitySet. Voici les résultats du Listing 14.2 : 9/2/2007 6:02:16 PM Lonesome Pine Restaurant

Insertion d’objets entité liés Le DataContext détecte les objets de classe d’entité "liés". Ces objets seront également enregistrés lors de l’appel de la méthode SubmitChanges. Le terme "lié" se rapporte aux objets de classe d’entité qui possèdent une clé étrangère vers l’objet de classe entité inséré. Pour clarifier les choses, considérez l’exemple du Listing 14.3. Listing 14.3 : Ajout d’enregistrements liés. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = new Customer { CustomerID = "LAWN", CompanyName = "Lawn Wranglers", ContactName = "Mr. Abe Henry", ContactTitle = "Owner", Address = "1017 Maple Leaf Way", City = "Ft. Worth", Region = "TX", PostalCode = "76104", Country = "USA", Phone = "(800) MOW-LAWN", Fax = "(800) MOW-LAWO", Orders = { new Order {

Linq.book Page 422 Mercredi, 18. février 2009 7:58 07

422

LINQ to SQL

Partie V

CustomerID = "LAWN", EmployeeID = 4, OrderDate = DateTime.Now, RequiredDate = DateTime.Now.AddDays(7), ShipVia = 3, Freight = new Decimal(24.66), ShipName = "Lawn Wranglers", ShipAddress = "1017 Maple Leaf Way", ShipCity = "Ft. Worth", ShipRegion = "TX", ShipPostalCode = "76104", ShipCountry = "USA" } } }; db.Customers.InsertOnSubmit(cust); db.SubmitChanges(); Customer customer = db.Customers.Where(c => c.CustomerID == "LAWN").First(); Console.WriteLine("{0} - {1}", customer.CompanyName, customer.ContactName); foreach (Order order in customer.Orders) { Console.WriteLine("{0} - {1}", order.CustomerID, order.OrderDate); } // Cette portion de code restaure la base de données dans son // état initial. Le code peut donc être exécuté à plusieurs reprises. db.Orders.DeleteOnSubmit(cust.Orders.First()); db.Customers.DeleteObSubmit(cust); db.SubmitChanges();

Dans le Listing 14.3, un objet Customer a été créé puis initialisé avec des données contenant, entre autres, une collection Orders composée d’un objet Order. L’objet Order étant lié à l’objet Customer, il est automatiquement sauvegardé dans la base de données lors de l’appel de la méthode SubmitChanges. Nous allons consacrer quelques instants à un autre détail de ce listing. Le dernier bloc d’instructions restaure la base de données dans son état initial. Pour ce faire, il invoque la méthode DeleteOnSubmit à deux reprises : une pour l’enregistrement Order, une autre pour l’enregistrement Customer. Dans cet exemple, le premier enregistrement Order est supprimé. L’enregistrement Customer étant nouveau, nous pouvons être sûrs que l’enregistrement Order est unique. Cette double suppression est utile car, si l’insertion d’un enregistrement contenant des données liées provoque la mise à jour "en cascade" des tables correspondantes, il n’en est rien de la suppression : la suppression d’un objet entité parent n’entraîne pas la suppression automatique des objets entité enfant liés. Si l’enregistrement Order lié à l’enregistrement Customer n’avait pas été supprimé, une exception aurait été levée. Nous reparlerons de tout cela en détail dans la section "Suppressions" de ce chapitre. Voici le résultat affiché dans la console lorsque vous appuyez sur Ctrl+F5 : Lawn Wranglers - Mr. Abe Henry LAWN - 9/2/2007 6:05:07 PM

Openmirrors.com

Linq.book Page 423 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

423

Le nouveau client a bel et bien été inséré dans la base de données. Du moins temporairement, puisque les dernières lignes du code en ont effacé toute trace. Requêtes L’exécution de requêtes LINQ to SQL s’apparente à l’exécution d’autres requêtes LINQ, à quelques petits détails près. Nous allons parler brièvement de ces détails dans cette section. Pour exécuter une requête LINQ to SQL, il suffit de créer un objet DataContext, puis d’appliquer la requête sur cet objet (voir Listing 14.4). Listing 14.4 : Une requête LINQ to SQL élémentaire sur la base de données Northwind. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single();

Ce code stocke l’enregistrement du client dont le champ CustomerId vaut "LONEP" dans l’objet Customer cust. Comme il a été mentionné au Chapitre 5, si ce client n’existe pas, l’opérateur Single provoque une exception. Vous devez donc vous assurer de l’existence du client avant d’exécuter ce code. En réalité, vous pourriez utiliser l’opérateur SingleOrDefault à la place de l’opérateur Single, pour éviter la levée d’une exception. Plusieurs autres détails méritent quelques explications. Remarquez que la requête utilise la syntaxe C# pour tester la valeur du champ CustomerId. Ceci est facile à déduire, car la valeur LONEP est entourée de guillemets, et non d’apostrophes, comme l’exige la syntaxe de SQL. Un autre indice : l’opérateur "==" est utilisé à la place de l’opérateur SQL "=". La requête est incluse dans le langage. Heureusement, puisqu’elle est exprimée en LINQ et que cette abréviation signifie "langage d’interrogation intégré". Remarquez également que nous mélangeons la syntaxe d’expression de requête et la notation standard "à point" dans cette requête. La portion syntaxe d’expression de requête se trouve à l’intérieur des parenthèses. L’opérateur Single, quant à lui, est appliqué au résultat de la requête en utilisant la notation standard de C#. Dans les chapitres précédents, nous avons souvent parlé des opérateurs de requête différés. Pensez-vous que la requête du Listing 14.4 exécutera immédiatement la requête ? La réponse est oui. En effet, l’opérateur de requête standard Single provoquera l’exécution immédiate de la requête. Si cet opérateur avait été omis, la requête n’aurait pas été exécutée. Le Listing 14.4 ne provoque aucune sortie écran. Pour vérifier que le code extrait le bon client de la base de données, nous allons lui ajouter une instruction qui affichera le nom de la société et le nom du contact dans la console (voir Listing 14.5).

Linq.book Page 424 Mercredi, 18. février 2009 7:58 07

424

LINQ to SQL

Partie V

Listing 14.5 : La même requête avec une sortie dans la console. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single(); Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);

Voici la sortie console de ce listing : Lonesome Pine Restaurant - Fran Wilson

Quelques exceptions Un peu plus tôt, nous avons signalé que les requêtes LINQ to SQL se comportaient comme des requêtes LINQ, à quelques exceptions près. Cette section passe en revue ces exceptions.

Si une requête LINQ to SQL retourne un IQueryable Alors que les requêtes LINQ appliquées à des tableaux et à des collections retournent des séquences de type IEnumerable, la séquence retournée par une requête LINQ to SQL (le cas échéant) est de type IQueryable. Le Listing 14.6 donne un exemple d’une requête qui retourne une séquence de ce type. Listing 14.6 : Une requête LINQ to SQL basique qui retourne une séquence IQueryable. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable custs = from c in db.Customers where c.City == "London" select c; foreach(Customer cust in custs) { Console.WriteLine("Client : {0}", cust.CompanyName); }

Comme vous pouvez le voir, le type retourné par cette requête est IQueryable. Voici les résultats affichés dans la console : Client : Client : Client : Client : Client : Client :

Around the Horn B’s Beverages Consolidated Holdings Eastern Connection North/South Seven Seas Imports

Comme il a été dit dans le Chapitre 12, l’interface IQueryable étend l’interface IEnumerable. Vous pouvez donc traiter une séquence de type IQueryable comme s’il s’agissait d’une séquence IEnumerable. Si cela vous pose des difficultés, rappelez-vous l’opérateur AsEnumerable… Openmirrors.com

Linq.book Page 425 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

425

Si une requête LINQ to SQL est appliquée à un objet Table La plupart des requêtes LINQ traditionnelles sont appliquées à des tableaux ou à des collections qui implémentent l’interface IEnumerable ou IEnumerable. Les requêtes LINQ to SQL, en revanche, sont appliquées sur des classes qui implémentent IQueryable ; par exemple la classe Table. Cela signifie que les requêtes LINQ to SQL disposent d’opérateurs additionnels et qu’elles peuvent utiliser les opérateurs de requête standard, puisque IQueryable étend IEnumerable. Les requêtes LINQ to SQL sont traduites en SQL Comme indiqué au Chapitre 2, étant donné que les requêtes LINQ to SQL retournent des séquences de type IQueryable, elles ne sont pas compilées en langage intermédiaire .NET, contrairement aux requêtes LINQ traditionnelles : elles sont converties en arbres d’expression. Ainsi, elles sont traduites en requêtes SQL optimisées. Reportezvous à la section "Traduction SQL", à la fin de ce chapitre, pour en savoir plus au sujet de la traduction appliquée aux requêtes LINQ to SQL. Les requêtes LINQ to SQL sont exécutées dans la base de données Contrairement aux requêtes LINQ traditionnelles, qui sont exécutées dans la mémoire de l’ordinateur local, les requêtes LINQ to SQL sont traduites en appels SQL et exécutées sur l’ordinateur qui héberge la base de données. En corollaire, la façon dont les projections sont gérées implique que les traitements ne peuvent pas se faire dans la base de données : en effet, cette dernière ignore tout des classes d’entité ou des autres classes de ce type. Par ailleurs, étant donné que les requêtes s’exécutent dans la base de données et que cette dernière n’a pas accès au code de l’application, les actions effectuées dans une requête doivent être traduites, en respectant les possibilités du traducteur. Par exemple, si vous effectuez un appel à une méthode qui a été spécifiée dans une expression lambda, SQL Server sera incapable de traiter cet appel. C’est la raison pour laquelle il est bon de savoir ce qui peut être traduit, le résultat de la traduction et ce qui se passera si une expression ne peut pas être traduite. Associations Avec LINQ to SQL, l’interrogation d’une classe associée est aussi simple que l’accès à une variable membre d’une classe d’entité. Ceci est dû au fait qu’une classe associée est une variable membre de la classe d’entité liée ou qu’elle est stockée dans une collection de classes d’entité et que la collection est une variable membre de la classe d’entité liée. Si la classe associée se trouve du côté "plusieurs" (enfant) d’une relation "un-à-plusieurs", la classe "plusieurs" sera stockée dans une collection de classes "plusieurs". Le type de la collection sera EntitySet, où T est le type de la classe d’entité "plusieurs". Cette collection sera une variable membre de

Linq.book Page 426 Mercredi, 18. février 2009 7:58 07

426

LINQ to SQL

Partie V

la classe "un". Si la classe associée se trouve du côté "un" (parent) d’une relation "un-à-plusieurs", une référence à la classe "un" sera stockée dans une variable de type EntityRef, où T est le type de la classe "un". Cette référence sera une variable membre de la classe "plusieurs". À titre d’exemple, considérez le cas des classes d’entité Customer et Order, générées à partir de la base de données Northwind. Un client peut avoir passé plusieurs commandes, mais une commande ne peut être associée qu’à un seul client. Dans cet exemple, la classe Customer est le côté "un" de la relation "un-à-plusieurs" entre les classes d’entité Customer et Order. La classe Order est le côté "plusieurs" de cette relation. Par conséquent, les commandes d’un objet Customer peuvent être référencées par une variable membre, généralement appelée Orders, de type EntitySet dans la classe Customer. Un client d’un objet Order peut être référencé avec une variable membre, généralement nommée Customer, de type EntityRef dans la classe Order (voir Figure 14.1). Figure 14.1 : Une relation entre classes d’entité parent et enfant.

Customer (parent) EntitySetOrders

EntitySet Order (enfant) EntityRefCustomer Order (enfant) EntityRefCustomer Order (enfant) EntityRefCustomer

Si vous avez du mal à vous remémorer quel côté de la relation est stocké dans quel type de variable, rappelez-vous qu’un enfant a un parent et que, par conséquent, il est stocké dans une référence "un". L’enfant stocke le parent associé dans une variable de type EntityRef. Comme un parent peut avoir plusieurs enfants, il doit stocker les références à ses enfants dans une collection. Le parent stocke donc les références à ses enfants dans une variable de type EntitySet. Les classes sont associées en définissant leur attribut Association dans la propriété de classe qui contient la référence à la classe associée dans la définition de classe d’entité. Étant donné que le parent a une propriété de classe qui référence les enfants et inversement, l’attribut Association doit être spécifié dans les classes d’entité parent et enfant. Nous reviendrons plus en détail sur l’attribut Association au Chapitre 15. Le Listing 14.7 recherche certains clients, les affiche ainsi que chacune des commandes qu’ils ont passées. Openmirrors.com

Linq.book Page 427 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

427

Listing 14.7 : Utilisation d’une association pour accéder à des données liées. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable custs = from c in db.Customers where c.Country == "UK" && c.City == "London" orderby c.CustomerID select c; foreach (Customer cust in custs) { Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName); foreach (Order order in cust.Orders) { Console.WriteLine(" {0} {1}", order.OrderID, order.OrderDate); } }

Ce code sélectionne les clients dont le champ Country vaut "UK" et le champ City vaut "London". Ces clients sont affichés, ainsi que chacune des commandes qu’ils ont passées. Comme vous pouvez le voir, il n’a jamais été fait référence à la table Orders dans la requête. Voici une partie des résultats renvoyés par le code du Listing 14.7 : Around the Horn - Thomas Hardy 10355 11/15/1996 12:00:00 AM 10383 12/16/1996 12:00:00 AM 10453 2/21/1997 12:00:00 AM 10558 6/4/1997 12:00:00 AM 10707 10/16/1997 12:00:00 AM 10741 11/14/1997 12:00:00 AM 10743 11/17/1997 12:00:00 AM 10768 12/8/1997 12:00:00 AM 10793 12/24/1997 12:00:00 AM 10864 2/2/1998 12:00:00 AM 10920 3/3/1998 12:00:00 AM 10953 3/16/1998 12:00:00 AM 11016 4/10/1998 12:00:00 AM … Consolidated Holdings - Elizabeth Brown 10435 2/4/1997 12:00:00 AM 10462 3/3/1997 12:00:00 AM 10848 1/23/1998 12:00:00 AM …

Tout ceci fonctionne à la perfection. Les commandes sont bien listées, même si aucune requête n’a été explicitement définie pour les obtenir. Vous êtes donc en droit de vous demander si ce code n’est pas inefficace si l’on n’accède jamais aux commandes des clients. La réponse est non. En effet, les commandes ne sont pas extraites tant qu’elles ne sont pas référencées. Si la deuxième boucle foreach n’avait pas été écrite, les commandes n’auraient jamais été extraites de la base de données. Ce principe est connu sous le nom de "chargement différé". Ne le confondez pas avec les requêtes différées, qui ont été présentées dans les chapitres précédents.

Linq.book Page 428 Mercredi, 18. février 2009 7:58 07

428

LINQ to SQL

Partie V

Chargement différé On dit qu’il y a chargement différé quand les enregistrements ne sont chargés dans la base de données que lorsque cela est absolument nécessaire. C’est-à-dire à leur premier référencement. Dans le Listing 14.7, la variable membre Orders n’est jamais référencée. Les enregistrements correspondants ne sont donc jamais chargés depuis la base de données. Dans la plupart des situations, le chargement différé est une bonne chose. Il empêche les requêtes d’extraire des données dont elles n’ont pas besoin et limite la bande passante consommée sur le réseau. Cependant, le chargement différé peut provoquer certains problèmes. Le Listing 14.8 est identique au Listing 14.7, à ceci près que le Log du DataContext a été activé pour mettre le problème en évidence. Listing 14.8 : Illustration du chargement différé. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable custs = from c in db.Customers where c.Country == "UK" && c.City == "London" orderby c.CustomerID select c; // Activation du Logging db.Log = Console.Out; foreach (Customer cust in custs) { Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName); foreach (Order order in cust.Orders) { Console.WriteLine(" {0} {1}", order.OrderID, order.OrderDate); } }

Voici une partie des résultats affichée dans la console lors de l’appui sur Ctrl+F5 : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1) ORDER BY [t0].[CustomerID] •@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK] •@p1: Input String (Size = 6; Prec = 0; Scale = 0) [London] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Around the Horn - Thomas Hardy SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry] FROM [dbo].[Orders] AS [t0] WHERE [t0].[CustomerID] = @p0 •@p0: Input String (Size = 5; Prec = 0; Scale = 0) [AROUT] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Openmirrors.com

Linq.book Page 429 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

429

10355 11/15/1996 12:00:00 AM 10383 12/16/1996 12:00:00 AM 10453 2/21/1997 12:00:00 AM 10558 6/4/1997 12:00:00 AM 10707 10/16/1997 12:00:00 AM 10741 11/14/1997 12:00:00 AM 10743 11/17/1997 12:00:00 AM 10768 12/8/1997 12:00:00 AM 10793 12/24/1997 12:00:00 AM 10864 2/2/1998 12:00:00 AM 10920 3/3/1998 12:00:00 AM 10953 3/16/1998 12:00:00 AM 11016 4/10/1998 12:00:00 AM B’s Beverages - Victoria Ashworth SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry] FROM [dbo].[Orders] AS [t0] WHERE [t0].[CustomerID] = @p0 •@p0: Input String (Size = 5; Prec = 0; Scale = 0) [BSBEV] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 10289 8/26/1996 12:00:00 AM 10471 3/11/1997 12:00:00 AM 10484 3/24/1997 12:00:00 AM 10538 5/15/1997 12:00:00 AM 10539 5/16/1997 12:00:00 AM 10578 6/24/1997 12:00:00 AM 10599 7/15/1997 12:00:00 AM 10943 3/11/1998 12:00:00 AM 10947 3/13/1998 12:00:00 AM 11023 4/14/1998 12:00:00 AM Consolidated Holdings - Elizabeth Brown …

Les requêtes SQL apparaissent en gras, pour les différencier des données issues des tables Customer et Order. La première requête SQL interroge la table Customers. Elle ne fait aucunement référence à la table Orders. Après avoir affiché le nom de la société (du client) et le nom du contact de la première société, une deuxième requête est exécutée. Cette dernière interroge la table Orders. La clause where limite les réponses aux enregistrements dont le champ CustomerID est égal au client sélectionné dans la première requête. Les lignes suivantes affichent la liste des commandes de ce client et le nom du client suivant. La requête SQL suivante concerne une commande client spécifique. Comme vous le voyez, une requête différente est exécutée pour obtenir les commandes de chaque client. La table Orders n’est pas interrogée, et donc non chargée, jusqu’à ce que la variable EntityRef Orders ne soit référencée dans la deuxième boucle foreach ; c’est-à-dire juste après l’affichage des informations relatives au client. Les commandes n’étant pas chargées jusqu’à leur référencement, on dit que le chargement est différé.

Linq.book Page 430 Mercredi, 18. février 2009 7:58 07

430

LINQ to SQL

Partie V

Une requête est définie pour chaque client. Cela fait beaucoup d’allers-retours entre le programme et la base de données, et les performances ne sont pas optimales. Pour améliorer les choses, il faudrait que les commandes soient obtenues en même temps que les clients. Dans ce cas, on dit que le chargement est immédiat. Chargement immédiat avec la classe DataLoadOptions Par défaut, les classes associées utilisent un chargement différé. Si vous le souhaitez, vous pouvez forcer le chargement immédiat. Dans ce cas, les classes associées seront chargées avant d’être référencées. Dans certains cas, les performances en seront améliorées. Pour ce faire, vous utiliserez l’opérateur LoadWith de la classe DataLoadOptions pour demander au DataContext de charger immédiatement la classe associée, spécifiée dans l’expression lambda de l’opérateur LoadWith. Ainsi, à l’exécution de la requête, la classe primaire et ses classes associées seront chargées. Le Listing 14.9 utilise le même code que le Listing 14.8, mais, ici : m

Un objet DataLoadOptions est instancié.

m

L’opérateur LoadWith est appelé sur cet objet, en passant le membre Orders pour provoquer le chargement immédiat des commandes lorsqu’un objet Customer est chargé.

m

L’objet DataLoadOptions est affecté au DataContext Northwind.

m

Pour s’assurer que les classes associées (les commandes) sont chargées avant d’être référencées, le code d’énumération des commandes clients est omis.

Listing 14.9 : Illustration du chargement immédiat avec la classe DataLoadOptions. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); DataLoadOptions dlo = new DataLoadOptions(); dlo.LoadWith(c => c.Orders); db.LoadOptions = dlo; IQueryable custs = (from c in db.Customers where c.Country == "UK" && c.City == "London" orderby c.CustomerID select c); // Activation du Log db.Log = Console.Out; foreach (Customer cust in custs) { Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName); }

Rappelons que les différences avec le listing précédent portent sur : m

l’instanciation d’un objet DataLoadOptions ;

m

l’appel de l’opérateur LoadWith ;

Openmirrors.com

Linq.book Page 431 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

m

l’affectation de l’objet DataLoadOptions au DataContext Northwind ;

m

la suppression de toute référence aux commandes des utilisateurs.

431

Dans l’appel à l’opérateur LoadWith, nous demandons au DataLoadOptions de charger immédiatement les commandes chaque fois qu’un objet Customer est chargé. Voici les résultats de ce listing : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax], [t1].[OrderID], [t1].[CustomerID] AS [CustomerID2], [t1].[EmployeeID], [t1].[OrderDate], [t1].[RequiredDate], [t1].[ShippedDate], [t1].[ShipVia], [t1].[Freight], [t1].[ShipName], [t1].[ShipAddress], [t1].[ShipCity], [t1].[ShipRegion], [t1].[ShipPostalCode], [t1].[ShipCountry], ( SELECT COUNT(*) FROM [dbo].[Orders] AS [t2] WHERE [t2].[CustomerID] = [t0].[CustomerID] ) AS [count] FROM [dbo].[Customers] AS [t0] LEFT OUTER JOIN [dbo].[Orders] AS [t1] ON [t1].[CustomerID] = [t0].[CustomerID] WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1) ORDER BY [t0].[CustomerID], [t1].[OrderID] •@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK] •@p1: Input String (Size = 6; Prec = 0; Scale = 0) [London] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Around the Horn - Thomas Hardy B’s Beverages - Victoria Ashworth Consolidated Holdings - Elizabeth Brown Eastern Connection - Ann Devon North/South - Simon Crowther Seven Seas Imports - Hari Kumar

Tout comme dans les résultats du Listing 14.8, les requêtes SQL apparaissent en gras. Les données des clients importent peu : ici, nous ne nous intéressons qu’aux requêtes SQL. Comme vous le voyez, une seule requête suffit pour retrouver les clients qui répondent aux conditions de la clause where. Vous constatez également que, même si les commandes des clients n’ont jamais été référencées dans le code, la requête a permis d’extraire les données des clients et les commandes correspondantes. Les commandes ont été obtenues par un chargement immédiat. Par ailleurs, il n’est plus nécessaire d’avoir une requête pour la table des clients et autant de requêtes que de commandes : une seule et unique requête suffit. Si le nombre de clients est élevé, cela peut faire une grande différence. Avec la classe DataLoadOptions, vous n’êtes pas limité au chargement immédiat d’une seule classe associée ou d’une seule hiérarchie de classes associées. Dans tous les cas, quel que soit le nombre de classes associées, le chargement immédiat fonctionne selon le même principe.

Linq.book Page 432 Mercredi, 18. février 2009 7:58 07

432

LINQ to SQL

Partie V

Lorsque le chargement immédiat… n’est pas si immédiat Lorsque les classes ne sont pas chargées avant leur référencement, on parle de chargement différé. Lorsqu’elles sont chargées avant leur référencement, on parle de chargement immédiat. Dans certains cas, le chargement immédiat peut se révéler non aussi immédiat qu’il le devrait. Dans le code du Listing 14.9, nous avons vu qu’en spécifiant une classe associée en argument de la méthode LoadWith de la classe DataLoadOptions les commandes étaient chargées en même temps que les clients. Si la méthode LoadWith est appelée plusieurs fois pour que plusieurs classes soient chargées immédiatement, une seule d’entre elles sera liée à la classe d’origine et chargée en même temps qu’elle. Les autres ne seront chargées qu’au référencement de la classe d’origine. Lorsque cette situation se produit, étant donné que les classes non liées à la classe d’origine sont chargées avant d’être référencées, on parle toujours de chargement immédiat, mais une requête complémentaire est toujours nécessaire lorsque vous référencez chacune des classes d’origine. Le chargement des classes non liées est donc moins immédiat que celui des classes liées. C’est LINQ to SQL (et non la base de données) qui choisit quelles classes associées doivent être liées et quelles classes associées ne doivent pas l’être. Sa décision est fondée sur des principes généraux appliqués au modèle de classe d’entité. La liaison est effectuée sur l’association qui a le plus bas niveau hiérarchique dans les classes chargées immédiatement. Ceci sera plus facile à comprendre lorsque nous aborderons la section qui montre comment charger une hiérarchie de classes associées. Nous allons nous intéresser à deux approches : le chargement de plusieurs classes associées à la classe d’entité originale, et le chargement d’une hiérarchie de classes associées. Chargement immédiat de plusieurs classes associées En utilisant la classe DataLoadOptions, vous pouvez demander le chargement immédiat de plusieurs classes associées d’une classe d’entité. Dans le Listing 14.9, la requête SQL générée ne faisait aucune référence aux données démographiques des clients de la table associée Customers. Si nous avions fait référence à ces données, une nouvelle requête SQL aurait été exécutée pour chaque client dont les données démographiques étaient référencées. Dans le Listing 14.10, nous demandons au DataLoadOptions de charger immédiatement les données provenant de la table CustomerCustomerDemos ainsi que celles de la table Commandes. Listing 14.10 : Chargement immédiat de plusieurs EntitySets. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); DataLoadOptions dlo = new DataLoadOptions();

Openmirrors.com

Linq.book Page 433 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

433

dlo.LoadWith(c => c.Orders); dlo.LoadWith(c => c.CustomerCustomerDemos); db.LoadOptions = dlo; IQueryable custs = (from c in db.Customers where c.Country == "UK" && c.City == "London" orderby c.CustomerID select c); // Activation du logging db.Log = Console.Out; foreach (Customer cust in custs) { Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName); }

Ce listing demande au DataLoadOptions de charger immédiatement des données provenant de deux tables. Comme vous pouvez le voir, aucune d’elles n’est référencée en dehors de la requête. Le chargement est donc immédiat, et non différé. Ce qui va nous intéresser dans les résultats du code, ce sont non pas les données extraites mais bel et bien les requêtes SQL émises. Voici un sous-ensemble des résultats : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax], [t1].[CustomerID] AS [CustomerID2], [t1].[CustomerTypeID], ( SELECT COUNT(*) FROM [dbo].[CustomerCustomerDemo] AS [t2] WHERE [t2].[CustomerID] = [t0].[CustomerID] ) AS [count] FROM [dbo].[Customers] AS [t0] LEFT OUTER JOIN [dbo].[CustomerCustomerDemo] AS [t1] ON [t1].[CustomerID] = [t0].[CustomerID] WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1) ORDER BY [t0].[CustomerID], [t1].[CustomerTypeID] •@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK] •@p1: Input String (Size = 6; Prec = 0; Scale = 0) [London] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry] FROM [dbo].[Orders] AS [t0] WHERE [t0].[CustomerID] = @x1 •@x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [AROUT] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Around the Horn - Thomas Hardy SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry] FROM [dbo].[Orders] AS [t0] WHERE [t0].[CustomerID] = @x1 •@x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [BSBEV] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 B’s Beverages - Victoria Ashworth …

Linq.book Page 434 Mercredi, 18. février 2009 7:58 07

434

LINQ to SQL

Partie V

Comme vous pouvez le voir, la table des données démographiques a été jointe avec la table des clients dans la première requête. En revanche, une requête a été émise pour charger les commandes de chaque client. Les requêtes concernant les commandes sont exécutées au référencement de chaque client, à l’intérieur de la boucle foreach. Dans les résultats, remarquez que la requête permettant de retrouver les commandes d’un client est affichée avant que les informations du client ne soient affichées dans la console. Étant donné que les tables des données démographiques et des commandes n’ont pas été référencées dans le code, en dehors de l’appel à la méthode LoadWith, le chargement est donc immédiat et non différé. Cependant, le chargement des données démographiques est "un peu plus immédiat" que celui des commandes. Chargement immédiat d’une hiérarchie de classes associées La section précédente vous a montré comment charger immédiatement plusieurs classes d’entité associées. Dans cette section, vous allez apprendre à charger immédiatement une hiérarchie de classes d’entité associées. Dans le Listing 14.11, la requête ne se contente pas de charger immédiatement les différentes commandes : elle fait de même en ce qui concerne le détail des différentes commandes. Listing 14.11 : Chargement immédiat d’une hiérarchie de classes d’entité. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); DataLoadOptions dlo = new DataLoadOptions(); dlo.LoadWith(c => c.Orders); dlo.LoadWith(o => o.OrderDetails); db.LoadOptions = dlo; IQueryable custs = (from c in db.Customers where c.Country == "UK" && c.City == "London" orderby c.CustomerID select c); // Activation du logging db.Log = Console.Out; foreach (Customer cust in custs) { Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName); foreach (Order order in cust.Orders) { Console.WriteLine(" {0} {1}", order.OrderID, order.OrderDate); } }

Dans ce listing, les commandes et le détail des commandes sont chargés immédiatement. Openmirrors.com

Linq.book Page 435 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

435

Voici un extrait des informations affichées dans la console : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1) ORDER BY [t0].[CustomerID] •@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK] •@p1: Input String (Size = 6; Prec = 0; Scale = 0) [London] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry], [t1].[OrderID] AS [OrderID2], [t1].[ProductID], [t1].[UnitPrice], [t1].[Quantity], [t1].[Discount], ( SELECT COUNT(*) FROM [dbo].[Order Details] AS [t2] WHERE [t2].[OrderID] = [t0].[OrderID] ) AS [count] FROM [dbo].[Orders] AS [t0] LEFT OUTER JOIN [dbo].[Order Details] AS [t1] ON [t1].[OrderID] = [t0].[OrderID] WHERE [t0].[CustomerID] = @x1 ORDER BY [t0].[OrderID], [t1].[ProductID] •@x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [AROUT] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Around the Horn - Thomas Hardy SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry], [t1].[OrderID] AS [OrderID2], [t1].[ProductID], [t1].[UnitPrice], [t1].[Quantity], [t1].[Discount], ( SELECT COUNT(*) FROM [dbo].[Order Details] AS [t2] WHERE [t2].[OrderID] = [t0].[OrderID] ) AS [count] FROM [dbo].[Orders] AS [t0] LEFT OUTER JOIN [dbo].[Order Details] AS [t1] ON [t1].[OrderID] = [t0].[OrderID] WHERE [t0].[CustomerID] = @x1 ORDER BY [t0].[OrderID], [t1].[ProductID] •@x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [BSBEV] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 B’s Beverages - Victoria Ashworth ...

Ici encore, ce sont non pas les données mais les requêtes SQL qui sont intéressantes. Cette fois-ci, la requête sur les clients n’est pas liée à la table des commandes ni à la table des détails des commandes. Lors du référencement de chaque client, une requête SQL complémentaire est émise pour lier la table des commandes à la table des détails des commandes. Aucune de ces deux tables n’étant référencée, leur chargement est considéré comme immédiat. En observant le fonctionnement de cet exemple, vous voyez que LINQ to SQL a choisi d’effectuer un lien sur la table dont le niveau hiérarchique est le plus bas ; ici, la table des détails des commandes.

Linq.book Page 436 Mercredi, 18. février 2009 7:58 07

436

LINQ to SQL

Partie V

Filtrage et classement Tant que nous en sommes à discuter de la classe DataLoadOptions, nous allons prendre quelques instants pour introduire la méthode AssociateWith, qui permet de filtrer et de classer des objets enfant associés. Dans le Listing 14.8, plusieurs clients sont extraits de la base de données, puis énumérés en affichant les noms des clients et les commandes qu’ils ont passées. Si vous vous reportez aux résultats, vous verrez que les dates des commandes apparaissent dans un ordre chronologique inverse. Le Listing 14.12 montre comment utiliser l’opérateur AssociateWith pour filtrer et classer les classes associées. Listing 14.12 : Utilisation de la classe DataLoadOptions pour filtrer et classer des enregistrements. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); DataLoadOptions dlo = new DataLoadOptions(); dlo.AssociateWith(c => from o in c.Orders where o.OrderID < 10700 orderby o.OrderDate descending select o); db.LoadOptions = dlo; IQueryable custs = from c in db.Customers where c.Country == "UK" && c.City == "London" orderby c.CustomerID select c; foreach (Customer cust in custs) { Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName); foreach (Order order in cust.Orders) { Console.WriteLine(" {0} {1}", order.OrderID, order.OrderDate); } }

L’argument de la méthode AssociateWith utilise une expression lambda pour ne retenir que les enregistrements dont le champ OrderId est inférieur à 10 700 et pour classer les résultats par ordre décroissant sur le champ OrderDate. Voici les résultats : Around the Horn - Thomas Hardy 10558 6/4/1997 12:00:00 AM 10453 2/21/1997 12:00:00 AM 10383 12/16/1996 12:00:00 AM 10355 11/15/1996 12:00:00 AM B’s Beverages - Victoria Ashworth 10599 7/15/1997 12:00:00 AM 10578 6/24/1997 12:00:00 AM 10539 5/16/1997 12:00:00 AM 10538 5/15/1997 12:00:00 AM 10484 3/24/1997 12:00:00 AM 10471 3/11/1997 12:00:00 AM 10289 8/26/1996 12:00:00 AM Consolidated Holdings - Elizabeth Brown 10462 3/3/1997 12:00:00 AM 10435 2/4/1997 12:00:00 AM

Openmirrors.com

Linq.book Page 437 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

437

Eastern Connection - Ann Devon 10532 5/9/1997 12:00:00 AM 10400 1/1/1997 12:00:00 AM 10364 11/26/1996 12:00:00 AM North/South - Simon Crowther 10517 4/24/1997 12:00:00 AM Seven Seas Imports - Hari Kumar 10547 5/23/1997 12:00:00 AM 10523 5/1/1997 12:00:00 AM 10472 3/12/1997 12:00:00 AM 10388 12/19/1996 12:00:00 AM 10377 12/9/1996 12:00:00 AM 10359 11/21/1996 12:00:00 AM

Seuls les enregistrements dont le champ OrderId est inférieur à 10 700 sont retenus. Par ailleurs, les résultats sont classés par ordre décroissant sur le champ OrderDate. Les jointures automatiques sont une coïncidence Les associations ont un avantage : elles effectuent des jointures automatiques. À titre d’exemple, lorsqu’une requête est appliquée sur la table des clients de la base de données Northwind, chaque client possède une collection de commandes accessible via la propriété Orders de l’objet Customer. L’obtention des commandes pour les différents clients est donc automatique. La réciproque est également vraie. Dans la classe Order, chacune des commandes a une propriété Customer qui fait référence au client approprié. Pour accéder aux données de deux tables liées, la technique conventionnelle aurait consisté à créer une jointure. Lorsqu’un objet enfant a une relation avec un objet parent, il est logique de pouvoir accéder au parent via une référence dans l’objet enfant. À titre d’exemple, lorsque l’on travaille avec le langage XML, il paraît logique de pouvoir accéder au nœud parent d’un nœud enfant via une variable membre du nœud enfant. Qui s’attendrait à devoir effectuer une requête sur la structure XML et à fournir le nœud enfant comme argument de la recherche ? De même, il est logique de pouvoir accéder aux enfants d’un nœud en utilisant une référence dans le nœud parent. La jointure automatique est certes pratique. Cependant, son implémentation est dictée par la nature des relations entre les objets et les attentes comportementales du programmeur. De ce point de vue, le côté automatique des jointures n’est donc qu’une pure coïncidence… Jointures Nous venons de voir que bon nombre des relations d’une base de données sont définies en tant qu’associations et que les objets associés sont accessibles via un membre de classe. Sachez cependant que seules les relations qui utilisent des clés étrangères auront ce comportement. Étant donné que toutes les relations n’utilisent pas des clés étrangères, vous aurez parfois besoin d’effectuer des jointures explicites entre deux tables.

Linq.book Page 438 Mercredi, 18. février 2009 7:58 07

438

LINQ to SQL

Partie V

Jointures internes L’opérateur join permet d’effectuer une équijointure interne. Comme il est de coutume dans ce type de jointure, les éventuels enregistrements de la table externe sont omis s’ils ne contiennent pas un enregistrement correspondant dans la table interne (voir Listing 14.13). Listing 14.13 : Jointure interne. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var entities = from s in db.Suppliers join c in db.Customers on s.City equals c.City select new { SupplierName = s.CompanyName, CustomerName = c.CompanyName, City = c.City }; foreach (var e in entities) { Console.WriteLine("{0}: {1} - {2}", e.City, e.SupplierName, e.CustomerName); }

Ce code effectue une jointure interne sur les fournisseurs et les clients. Pour un client donné, si aucun enregistrement fournisseur possédant la même valeur du même champ city n’est trouvé, l’enregistrement fournisseur est omis des résultats. London: Exotic Liquids - Around the Horn London: Exotic Liquids - B’s Beverages London: Exotic Liquids - Consolidated Holdings London: Exotic Liquids - Eastern Connection London: Exotic Liquids - North/South London: Exotic Liquids - Seven Seas Imports Sao Paulo: Refrescos Americanas LTDA - Comércio Mineiro Sao Paulo: Refrescos Americanas LTDA - Familia Arquibaldo Sao Paulo: Refrescos Americanas LTDA - Queen Cozinha Sao Paulo: Refrescos Americanas LTDA - Tradiçao Hipermercados Berlin: Heli Süßwaren GmbH & Co. KG - Alfred Futterkiste Paris: Aux joyeux ecclésiastiques - Paris spécialités Paris: Aux joyeux ecclésiastiques - Spécialités du monde Montréal: Ma Maison - Mère Paillarde

Comme vous pouvez le voir, certains fournisseurs apparaissent à plusieurs reprises, et d’autres n’apparaissent pas du tout. Les fournisseurs omis sont ceux pour lesquels aucun client ne se trouve dans la même ville. Pour afficher tous les fournisseurs, sans tenir compte de l’existence d’un client dans la même ville, nous devons utiliser une jointure externe. Jointures externes Au Chapitre 4, nous avons étudié l’opérateur de requête standard DefaultIfEmpty et indiqué qu’il pouvait être utilisé pour effectuer une jointure externe. Dans le Openmirrors.com

Linq.book Page 439 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

439

Listing 14.14, nous allons utiliser la clause into pour placer les résultats de la jointure dans une séquence temporaire sur laquelle nous appellerons l’opérateur DefaultIfEmpty. De la sorte, si l’enregistrement ne se trouve pas dans les résultats joints, il sera remplacé par une valeur par défaut. Nous allons utiliser la fonctionnalité de log du DataContext afin d’afficher la déclaration SQL correspondante. Listing 14.14 : Une jointure externe. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; var entities = from s in db.Suppliers join c in db.Customers on s.City equals c.City into temp from t in temp.DefaultIfEmpty() select new { SupplierName = s.CompanyName, CustomerName = t.CompanyName, City = s.City }; foreach (var e in entities) { Console.WriteLine("{0}: {1} - {2}", e.City, e.SupplierName, e.CustomerName); }

Les résultats de la clause join sont redirigés temporairement dans la séquence temp (le nom de la séquence peut être tout autre, à condition qu’il n’entre pas en conflit avec un autre nom de variable ou un mot-clé). L’opérateur DefaultIfEmpty est appliqué au résultat de la jointure. Cet opérateur n’a pas encore été étudié. Mais sachez qu’il diffère de l’opérateur de même nom étudié au Chapitre 4 de cet ouvrage. Comme vous le verrez dans quelques lignes, les requêtes LINQ to SQL sont transformées en déclarations SQL, elles-mêmes exécutées dans la base de données. SQLServer ne disposant d’aucun moyen pour appeler l’opérateur de requête standard DefaultIfEmpty, ce dernier est transformé en une déclaration SQL équivalente. C’est pour mettre en évidence cette transformation que nous avons affecté la valeur Console.out à la propriété Log du DataContext. Une nouvelle requête est appliquée au résultat de l’opérateur DefaultIfEmpty. Remarquez que le nom de la ville provient de la table Suppliers, et non de la collection temp. Ceci parce que nous savons qu’il y a toujours un enregistrement correspondant dans la table Suppliers. En revanche, dans le cas d’un fournisseur pour lequel aucun client n’a un champ City identique, le champ City est absent de la collection temp. Cet exemple diffère du précédent, pour lequel le champ City était obtenu à partir de la jointure. Dans cet exemple, la table depuis laquelle le champ City était obtenu importait peu car, dans tous les cas, si aucun client ne correspondait à un fournisseur, il n’aurait pas été inclus dans les résultats, puisque la jointure était interne.

Linq.book Page 440 Mercredi, 18. février 2009 7:58 07

440

LINQ to SQL

Partie V

Voici les résultats du Listing 14.14 : SELECT [t0].[CompanyName], [t1].[CompanyName] AS [value], [t0].[City] FROM [dbo].[Suppliers] AS [t0] LEFT OUTER JOIN [dbo].[Customers] AS [t1] ON [t0].[City] = [t1].[City] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 London: Exotic Liquids - Around the Horn London: Exotic Liquids - B’s Beverages London: Exotic Liquids - Consolidated Holdings London: Exotic Liquids - Eastern Connection London: Exotic Liquids - North/South London: Exotic Liquids - Seven Seas Imports New Orleans: New Orleans Cajun Delights Ann Arbor: Grandma Kelly’s Homestead Tokyo: Tokyo Traders Oviedo: Cooperativa de Quesos ’Las Cabras’ Osaka: Mayumi’s Melbourne: Pavlova, Ltd. Manchester: Specialty Biscuits, Ltd. Göteborg: PB Knäckebröd AB Sao Paulo: Refrescos Americanas LTDA - Comércio Mineiro Sao Paulo: Refrescos Americanas LTDA - Familia Arquibaldo Sao Paulo: Refrescos Americanas LTDA - Queen Cozinha Sao Paulo: Refrescos Americanas LTDA - Tradiçao Hipermercados Berlin: Heli Süßwaren GmbH & Co. KG - Alfreds Futterkiste Frankfurt: Plutzer Lebensmittelgroßmärkte AG Cuxhaven: Nord-Ost-Fisch Handelsgesellschaft mbH Ravenna: Formaggi Fortini s.r.l. Sandvika: Norske Meierier Bend: Bigfoot Breweries Stockholm: Svensk Sjöföda AB Paris: Aux joyeux ecclésiastiques - Paris spécialités Paris: Aux joyeux ecclésiastiques - Spécialités du monde Boston: New England Seafood Cannery Singapore: Leka Trading Lyngby: Lyngbysild Zaandam: Zaanse Snoepfabriek Lappeenranta: Karkki Oy Sydney: G’day, Mate Montréal: Ma Maison - Mère Paillarde Salerno: Pasta Buttini s.r.l. Montceau: Escargots Nouveaux Annecy: Gai pâturage Ste-Hyacinthe: Forêts d’érables -

Comme vous pouvez le voir, chaque fournisseur a au moins un enregistrement, mais certains fournisseurs ne sont associés à aucun client. Ceci prouve que la jointure est externe. Si vous en doutez encore, jetez un œil à la déclaration SQL… Aplatir ou ne pas aplatir ? Dans les Listings 14.13 et 14.14, les résultats des requêtes ont été projetés sur une structure "aplatie". En d’autres termes, nous avons créé un objet en utilisant une classe anonyme dans laquelle chacun des champs demandés est un membre de cette classe anonyme. Une autre approche aurait consisté à créer une classe anonyme composée d’objets Supplier et des objets Customer correspondants. Openmirrors.com

Linq.book Page 441 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

441

Si nous adoptions l’approche "aplatie", comme dans les deux exemples précédents, étant donné que la classe de sortie n’est pas une classe d’entité, il ne serait pas possible de modifier les objets et de demander à l’objet DataContext de sauvegarder ces modifications dans la base de données. L’approche "aplatie" ne convient que si les données ne sont pas modifiées. Parfois, il peut être nécessaire de modifier les objets obtenus en sortie. Dans ce cas, vous devrez adopter une approche "non aplatie". Si vous modifiez les objets obtenus, l’objet DataContext pourra sauvegarder les modifications dans la base de données. Le Listing 14.15 donne un exemple de résultats non aplatis. Pour avoir des informations complémentaires sur les projections non aplaties, consultez le Chapitre 16. Listing 14.15 : Les résultats n’étant pas aplatis, ils peuvent donc être rendus persistants via le DataContext. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var entities = from s in db.Suppliers join c in db.Customers on s.City equals c.City into temp from t in temp.DefaultIfEmpty() select new { s, t }; foreach (var e in entities) { Console.WriteLine("{0}: {1} - {2}", e.s.City, e.s.CompanyName, e.t != null ? e.t.CompanyName : ""); }

Dans cet exemple, les résultats de la requête ne sont pas retournés dans un objet anonyme aplati contenant un membre pour chaque champ : ils sont placés dans un objet anonyme composé des objets d’entité Supplier, et éventuellement Customer. Dans la dernière ligne de l’instruction Console.WriteLine, un test est effectué pour savoir si la valeur temporaire du champ CompanyName est nulle. Dans ce cas, aucun client ne correspond au fournisseur et rien n’est affiché. Voici les résultats : London: Exotic Liquids - Around the Horn London: Exotic Liquids - B’s Beverages London: Exotic Liquids - Consolidated Holdings London: Exotic Liquids - Eastern Connection London: Exotic Liquids - North/South London: Exotic Liquids - Seven Seas Imports New Orleans: New Orleans Cajun Delights Ann Arbor: Grandma Kelly’s Homestead Tokyo: Tokyo Traders Oviedo: Cooperativa de Quesos ’Las Cabras’ Osaka: Mayumi’s Melbourne: Pavlova, Ltd. Manchester: Specialty Biscuits, Ltd. Göteborg: PB Knäckebröd AB Sao Paulo: Refrescos Americanas LTDA - Comércio Mineiro Sao Paulo: Refrescos Americanas LTDA - Familia Arquibaldo Sao Paulo: Refrescos Americanas LTDA - Queen Cozinha Sao Paulo: Refrescos Americanas LTDA - Tradiçao Hipermercados Berlin: Heli Süßwaren GmbH & Co. KG - Alfreds Futterkiste

Linq.book Page 442 Mercredi, 18. février 2009 7:58 07

442

LINQ to SQL

Partie V

Frankfurt: Plutzer Lebensmittelgroßmärkte AG Cuxhaven: Nord-Ost-Fisch Handelsgesellschaft mbH Ravenna: Formaggi Fortini s.r.l. Sandvika: Norske Meierier Bend: Bigfoot Breweries Stockholm: Svensk Sjöföda AB Paris: Aux joyeux ecclésiastiques - Paris spécialités Paris: Aux joyeux ecclésiastiques - Spécialités du monde Boston: New England Seafood Cannery Singapore: Leka Trading Lyngby: Lyngbysild Zaandam: Zaanse Snoepfabriek Lappeenranta: Karkki Oy Sydney: G’day, Mate Montréal: Ma Maison - Mère Paillarde Salerno: Pasta Buttini s.r.l. Montceau: Escargots Nouveaux Annecy: Gai pâturage Ste-Hyacinthe: Forêts d’érables –

En observant ces résultats, vous voyez que certains fournisseurs n’ont aucun client dans leur ville. Contrairement à la séquence d’objets anonymes retournée par la requête du Listing 14.14, les objets anonymes retournés par la requête du Listing 14.15 contiennent des objets entité de type Supplier et Customer. Il est donc possible de tirer parti des services afférents. En particulier, ces objets peuvent être modifiés et les modifications, rendues persistantes via le DataContext. Exécution de requêtes différées Un petit rappel pour ceux de nos lecteurs qui n’auraient pas lu assez attentivement le chapitre dédié aux requêtes différées.

Une requête LINQ to SQL, LINQ to XML ou LINQ to Objects différée ne s’exécute pas au moment où elle est définie. Prenons l’exemple de la requête suivante : IQueryable custs = from c in db.Customers where c.Country == "UK" select c;

Cette requête n’est pas exécutée lorsque la déclaration est exécutée. Elle est juste affectée à la variable custs. La requête sera exécutée à l’énumération de la séquence. Plusieurs conséquences en découlent. Conséquences de l’exécution différée des requêtes Première conséquence. Si une requête différée contient des erreurs, celles-ci seront détectées lors de son exécution (et non de sa définition) et produiront des exceptions. Ceci peut se révéler trompeur, en particulier si le débogueur ne détecte aucune erreur dans la requête et que l’exception se produit bien plus loin dans le code, lors de l’énumération de la séquence ou lorsque vous appliquez un opérateur à la séquence qui provoque son énumération. Openmirrors.com

Linq.book Page 443 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

443

Seconde conséquence. Une requête SQL différée étant exécutée lors de l’énumération de la séquence, plusieurs énumérations provoqueront plusieurs exécutions de la requête. Ceci peut être désastreux en termes de performances. Pour prévenir ce problème, il suffit d’appliquer un opérateur de requête standard de conversion (tel que ToArray, ToList, ToDictionary ou ToLookup) à la séquence. Ces opérateurs convertissent la séquence en une structure du type spécifié et la placent dans une "mémoire tampon" qui pourra être énumérée autant de fois que nécessaire sans que la requête SQL doive être exécutée à plusieurs reprises. Tirer avantage de l’exécution différée des requêtes Premier avantage. Étant donné qu’une requête différée est exécutée à chaque fois que la séquence est énumérée, vous pouvez la définir une fois et l’exécuter autant de fois que nécessaire, lorsque la situation le justifie. Si le code n’examine pas les résultats de la requête, la requête SQL n’est pas exécutée. Les performances sont donc améliorées. Second avantage. Étant donné que la requête n’est pas exécutée lors de sa définition, vous pouvez, si cela se révèle nécessaire, lui ajouter des opérateurs a fortiori. Imaginez une application qui permette d’effectuer des requêtes sur une table Customers. Supposons que l’utilisateur puisse filtrer les clients issus de la requête. Imaginez une interface de filtrage qui inclut une liste déroulante pour chaque colonne de la table Customer. Ainsi, vous pourriez disposer d’une liste déroulante pour la colonne City et d’une autre pour la colonne Country. La première donnerait accès aux villes de tous les clients de la base de données et la seconde, aux pays de tous les clients de la base de données. La première option de chaque liste déroulante aurait pour valeur [TOUS] et serait sélectionnée par défaut. Si l’utilisateur ne change pas les réglages par défaut des listes déroulantes City et Country, aucune clause where n’est ajoutée à la requête. Le Listing 14.16 montre comment fabriquer une requête à partir d’une telle interface. Listing 14.16 : Création d’une requête par programmation. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // Activation du logging db.Log = Console.Out; // Simulation de valeurs sélectionnées dans des listes déroulantes string dropdownListCityValue = "Cowes"; string dropdownListCountryValue = "UK"; IQueryable custs = (from c in db.Customers select c); if (!dropdownListCityValue.Equals("[TOUS]")) { custs = from c in custs where c.City == dropdownListCityValue select c; } if (!dropdownListCountryValue.Equals("[TOUS]")) { custs = from c in custs where c.Country == dropdownListCountryValue

Linq.book Page 444 Mercredi, 18. février 2009 7:58 07

444

LINQ to SQL

Partie V

select c; } foreach (Customer cust in custs) { Console.WriteLine("{0} - {1} - {2}", cust.CompanyName, cust.City, cust.Country); }

Dans ce listing, nous simulons les listes déroulantes City et Country. Si l’une d’entre elles n’a pas la valeur [TOUS], un opérateur where est ajouté à la fin de la requête. Étant donné que la requête n’est pas exécutée jusqu’à l’énumération de la séquence, il est possible de la construire en plusieurs étapes. Voici les résultats de ce code : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1) •@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK] •@p1: Input String (Size = 5; Prec = 0; Scale = 0) [Cowes] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Island Trading - Cowes - UK

Étant donné que la liste déroulante dropdownListCityValue a pour valeur "Cowes" et que la liste déroulante dropdownListCountryValue a pour valeur "UK", nous obtenons les enregistrements des clients qui résident à Cowes, au Royaume-Uni. Remarquez qu’une seule requête SQL est utilisée. Étant donné que l’exécution de la requête est différée jusqu’à l’énumération de la séquence, il est possible de lui ajouter des clauses restrictives ou de tri sans pour autant devoir mettre en place plusieurs requêtes SQL. Vous pouvez voir que les deux critères du filtre (la ville et le pays) apparaissent dans la clause where de la requête SQL exécutée. Nous allons donner un autre exemple. Dans le Listing 14.17, nous allons affecter la valeur "[TOUS]" à la variable dropdownListCityValue, afin de voir l’allure de la déclaration SQL et des résultats qui en découlent. Étant donné que la valeur par défaut "[TOUS]" est spécifiée, aucune restriction ne devrait être opérée sur la ville dans la requête SQL. Listing 14.17 : Construction d’une autre requête par programme. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // Activation du logging db.Log = Console.Out; // Simulation de valeurs sélectionnées dans des listes déroulantes string dropdownListCityValue = "[ALL]"; string dropdownListCountryValue = "UK"; IQueryable custs = (from c in db.Customers select c); if (!dropdownListCityValue.Equals("[ALL]"))

Openmirrors.com

Linq.book Page 445 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

445

{ custs = from c in custs where c.City == dropdownListCityValue select c; } if (!dropdownListCountryValue.Equals("[ALL]")) { custs = from c in custs where c.Country == dropdownListCountryValue select c; } foreach (Customer cust in custs) { Console.WriteLine("{0} - {1} - {2}", cust.CompanyName, cust.City, cust.Country); }

Voici les résultats : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] WHERE [t0].[Country] = @p0 •@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Around the Horn - London - UK B’s Beverages - London - UK Consolidated Holdings - London - UK Eastern Connection - London - UK Island Trading - Cowes - UK North/South - London - UK Seven Seas Imports - London - UK

Comme vous le voyez, la clause where de la déclaration SQL ne spécifie plus la condition restrictive portant sur le champ City. En conséquence, les clients affichés habitent différentes villes britanniques. Bien entendu, il est toujours possible d’ajouter un appel à l’opérateur de requête standard ToArray, ToList, ToDictionary ou ToLookup pour forcer l’exécution immédiate de la requête. J’espère que vous êtes maintenant convaincu que les requêtes différées peuvent être très utiles et que le Log du DataContext est une précieuse source de renseignements. L’opérateur Contains en lieu et place de la déclaration SQL IN Les premières versions de LINQ to SQL n’implémentaient pas la déclaration SQL IN. Voici un exemple d’une telle déclaration : SELECT * FROM Customers WHERE (City IN (’London’, ’Madrid’))

Pour combler ce manque, Microsoft a défini l’opérateur Contains. Cet opérateur est quelque peu différent de la déclaration SQL IN. Par son intermédiaire, il aurait été

Linq.book Page 446 Mercredi, 18. février 2009 7:58 07

446

LINQ to SQL

Partie V

logique de pouvoir énoncer qu’un membre d’une classe d’entité doit se trouver dans un ensemble de valeurs. Cependant, comme vous pourrez le constater dans le Listing 14.18, cet opérateur fonctionne d’une façon diamétralement opposée. Listing 14.18 : L’opérateur Contains. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; string[] cities = { "London", "Madrid" }; IQueryable custs = db.Customers.Where(c => cities.Contains(c.City)); foreach (Customer cust in custs) { Console.WriteLine("{0} - {1}", cust.CustomerID, cust.City); }

Plutôt que d’indiquer que le champ City de la table Customer doit se trouver dans un ensemble de valeurs, l’instruction en gras de cette requête indique qu’un ensemble de valeurs doit contenir le champ City. Dans cet exemple, nous définissons un tableau de villes nommé cities. La requête appelle l’opérateur Contains sur le tableau cities en lui passant le champ City de la table Customer. Si le tableau cities contient la ville du client, la valeur true est retournée à l’opérateur Where, et l’objet Customer est inclus dans la séquence de sortie. Voici les résultats de ce listing : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] WHERE [t0].[City] IN (@p0, @p1) •@p0: Input String (Size = 6; Prec = 0; Scale = 0) [London] •@p1: Input String (Size = 6; Prec = 0; Scale = 0) [Madrid] •Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 AROUT BOLID BSBEV CONSH EASTC FISSA NORTS ROMEY SEVES



London Madrid London London London Madrid London Madrid London

En observant la déclaration SQL, vous voyez que l’opérateur Contains a été transformé en une déclaration SQL IN. Mises à jour Avec LINQ to SQL, la mise à jour d’une base de données revient à modifier les propriétés d’un objet, à appeler la méthode SubmitChanges de l’objet DataContext et à gérer d’éventuels conflits d’accès concurrentiel. Que ce dernier point ne vous intimide pas Openmirrors.com

Linq.book Page 447 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

447

outre mesure : les différentes options qui vous sont offertes sont loin d’être hors de portée. Nous y reviendrons en détail au Chapitre 17. Pour gérer facilement les conflits d’accès concurrentiel, les classes d’entité doivent être convenablement mappées à la base de données et le graphe correspondant doit être cohérent. Pour avoir plus d’informations sur le mappage des classes d’entité à la base de données, consultez la section "Attributs des classes d’entité et propriétés des attributs" du Chapitre 15. Pour avoir des informations complémentaires sur la cohérence du graphe, reportez-vous à la section "Cohérence du graphe" du Chapitre 15. SQLMetal et le Concepteur Objet/Relationnel vous fourniront tout ce dont vous avez besoin pour que ces deux conditions soient respectées : il vous suffit de les laisser créer vos classes d’entité. Mise à jour d’une référence parent d’un enfant À titre d’exemple, le Listing 14.19 vous montre comment modifier l’employé à l’origine d’une commande dans la base de données Northwind. Cet exemple est assez complexe, c’est pourquoi nous donnerons des explications à chaque fois que cela sera nécessaire. Listing 14.19 : Modification d’une relation en affectant un nouveau parent. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Order order = (from o in db.Orders where o.EmployeeID == 5 orderby o.OrderDate descending select o).First(); // Mémorisation de l’employé pour pouvoir le restaurer à la fin du listing Employee origEmployee = order.Employee;

La première ligne obtient le DataContext de la base de données. Le bloc d’instructions suivant obtient la commande la plus récente de l’employé dont le champ EmployeeID vaut 5. Pour ce faire, les commandes sont classées par dates décroissantes et l’opérateur First est appelé. La dernière instruction sauvegarde la référence de l’employé dans la variable origEmployee. Cette référence pourra ainsi être restaurée à la fin du listing. Console.WriteLine("Avant la modification de l’employé"); Console.WriteLine("OrderID = {0} : OrderDate = {1} : EmployeeID = {2}", order.OrderID, order.OrderDate, order.Employee.EmployeeID);

La première ligne affiche un message dans la console indiquant que l’employé n’a pas encore été modifié. Les deux lignes suivantes affichent le numéro de la commande, la date et l’identifiant de l’employé qui en est à l’origine. Employee emp = (from e in db.Employees where e.EmployeeID == 9 select e).Single(); // Affectation d’un nouvel employé à la commande order.Employee = emp; db.SubmitChanges();

Le premier bloc d’instructions extrait l’employé dont le champ EmployeeID vaut 9. Cet employé est alors affecté à la commande obtenue par la première requête et la modification est mémorisée dans la base de données avec la méthode SubmitChanges.

Linq.book Page 448 Mercredi, 18. février 2009 7:58 07

448

LINQ to SQL

Partie V

Pour prouver que la modification a été effectuée des deux côtés de la relation, nous pourrions afficher la valeur de la propriété EmployeeID de l’objet order. Mais cela n’aurait pas beaucoup de sens puisque, deux lignes plus haut, l’objet order a été modifié dans ce sens. Par ailleurs, cela ne prouverait pas que la modification a été répercutée dans la table Employees. Nous allons donc récupérer la commande qui vient d’être modifiée dans la collection de commandes de l’employé en utilisant une nouvelle requête. Order order2 = (from o in emp.Orders where o.OrderID == order.OrderID select o).First();

Cette requête retrouve la commande par son numéro (order.OrderID). Si cette commande existe, cela prouvera que la mise à jour a bien été répercutée sur la table Employees. Console.WriteLine("{0}Après la modification de l’employé", System.Environment.NewLine); Console.WriteLine("OrderID = {0} : OrderDate = {1} : EmployeeID = {2}", order2.OrderID, order2.OrderDate, order2.Employee.EmployeeID);

La première ligne affiche un message indiquant que l’employé a été modifié. Les deux lignes suivantes affichent le numéro de la commande, la date et l’identifiant de l’employé qui en est à l’origine. Si tout a bien fonctionné, l’identifiant de l’employé devrait être égal à 9 (il valait 5 avant la modification). // Annulation de la modification pour permettre l’exécution multiple du programme order.Employee = origEmployee; db.SubmitChanges();

Ces dernières lignes restaurent les données originales dans la base de données. Ainsi, le programme pourra s’exécuter plusieurs fois en donnant toujours les mêmes résultats : Avant la modification de l’employé OrderID = 11043 : OrderDate = 4/22/1998 12:00:00 AM : EmployeeID = 5 Après la modification de l’employé OrderID = 11043 : OrderDate = 4/22/1998 12:00:00 AM : EmployeeID = 9

Tout a fonctionné comme prévu : le champ EmployeeID de la table Orders est passé de 5 à 9. Après la modification, le code ne s’est pas contenté d’afficher les résultats à partir de la variable order : la commande modifiée a été retrouvée dans la base de données, à partir de l’employé dont le champ EmployeeID avait pour valeur 9. Ceci prouve que la modification a bien eu lieu dans la table Employee. Dans cet exemple, nous avons mis à jour la référence parent (employé) d’un objet enfant (commande). Une approche diamétralement opposée aurait permis d’arriver au même résultat : nous aurions pu mettre à jour la référence enfant (commande) d’un objet parent (employé). Mise à jour d’une référence enfant d’un parent Pour changer la relation entre deux objets, une autre approche consiste à enlever l’objet enfant de la collection EntitySet de l’objet parent, puis de l’ajouter dans une Openmirrors.com

Linq.book Page 449 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

449

collection EntitySet différente de l’objet parent. Dans le Listing 14.20, nous supprimons une commande dans la collection de commandes d’un employé. Cet exemple étant semblable à celui du Listing 14.19, nous serons plus avares en explications. Les principales modifications entre les deux listings apparaîtront en gras. Listing 14.20 : Modification d’une relation en supprimant puis en ajoutant un enfant à une collection EntitySet du parent. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Order order = (from o in db.Orders where o.EmployeeID == 5 orderby o.OrderDate descending select o).First(); // Mémorisation de l’employé pour pouvoir le restaurer à la fin du listing Employee origEmployee = order.Employee; Console.WriteLine("Avant la modification de l’employé"); Console.WriteLine("OrderID = {0} : OrderDate = {1} : EmployeeID = {2}", order.OrderID, order.OrderDate, order.Employee.EmployeeID); Employee emp = (from e in db.Employees where e.EmployeeID == 9 select e).Single(); // Suppression de la commande dans les commandes de l’employé original origEmployee.Orders.Remove(order); // Ajout de la commande dans les commandes du nouvel employé emp.Orders.Add(order); db.SubmitChanges(); Console.WriteLine("{0}Après la modification de l’employé", ➥System.Environment.NewLine); Console.WriteLine("OrderID = {0} : OrderDate = {1} : EmployeeID = {2}", order.OrderID, order.OrderDate, order.Employee.EmployeeID); // Annulation de la modification pour permettre l’exécution multiple du programme order.Employee = origEmployee; db.SubmitChanges();

Après avoir retrouvé la commande la plus récente de l’employé dont la propriété EmployeeID vaut 5, nous la sauvegardons dans l’objet origEmployee pour pouvoir la restaurer à la fin du listing. Cette commande est alors affichée, avant la modification de l’employé. L’employé dont la propriété EmployeeID vaut 9 est alors récupéré, puis sa référence est mémorisée dans la variable emp. À ce point précis, le code est identique à celui du Listing 14.19. La commande est supprimée de la collection de commandes de l’employé numéro 5 puis ajoutée à la collection de commandes de l’employé numéro 9. La méthode SubmitChanges est alors appelée pour sauvegarder les modifications dans la base de données. Une fois la modification effectuée, la commande est affichée dans la console, puis l’état original de la base de données est restauré, de telle sorte que le programme puisse être exécuté plusieurs fois.

Linq.book Page 450 Mercredi, 18. février 2009 7:58 07

450

LINQ to SQL

Partie V

Voici les résultats affichés dans la console : Avant la modification de l’employé OrderID = 11043 : OrderDate = 4/22/1998 12:00:00 AM : EmployeeID = 5 Après la modification de l’employé OrderID = 11043 : OrderDate = 4/22/1998 12:00:00 AM : EmployeeID = 9

Suppressions Pour supprimer un enregistrement dans une base de données en utilisant LINQ to SQL, vous devez supprimer l’objet entité du Table dont il est membre en utilisant la méthode DeleteOnSubmit de l’objet Table. Ensuite, vous devez appeler la méthode SubmitChanges (voir Listing 14.21). ATTENTION Contrairement aux deux autres exemples de ce chapitre, l’état initial de la base de données ne sera pas restauré. Ceci parce qu’une des tables utilisées contient une colonne d’identité et que cette colonne ne peut pas être facilement restaurée par programme. Avant d’exécuter cet exemple, assurez-vous que vous avez effectué une sauvegarde de votre base de données. Vous pourrez ainsi facilement la restaurer. Si vous avez téléchargé la version ZIP de la base de données étendue Northwind, vous pouvez aussi utiliser le contenu du fichier compressé pour restaurer la base de données dans son état original. Listing 14.21 : Suppression d’un enregistrement en agissant sur le Table dont il est membre. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // Récupération du client à supprimer Customer customer = (from c in db.Customers where c.CompanyName == "Alfreds Futterkiste" select c).Single(); db.OrderDetails.DeleteAllOnSubmit( customer.Orders.SelectMany(o => o.OrderDetails)); db.Orders.DeleteAllOnSubmit(customer.Orders); db.Customers.DeleteOnSubmit(customer); db.SubmitChanges(); Customer customer2 = (from c in db.Customers where c.CompanyName == "Alfreds Futterkiste" select c).SingleOrDefault(); Console.WriteLine("Le client {0} trouvé.", customer2 != null ? "a été" : "n’a pas été");

Cet exemple est assez simple, mais nous allons nous arrêter sur quelques points de détail intéressants. La table Order contenant une clé étrangère qui la lie à la table Customer, il n’est pas possible de supprimer un client sans avoir supprimé au préalable ses commandes. La table OrderDetails contenant une clé étrangère qui la lie à la table Orders, il n’est pas possible de supprimer une commande sans avoir au préalable supprimé les enregistrements Openmirrors.com

Linq.book Page 451 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

451

correspondants dans la table OrderDetails. Pour pouvoir supprimer un client, il faut donc supprimer les enregistrements de la table OrderDetails puis les enregistrements de la table Orders qui correspondent à toutes ses commandes. Grâce à l’opérateur DeleteAllOnSubmit (qui peut supprimer une séquence de commandes), la suppression des commandes est élémentaire. La suppression des commandes détaillées est un peu plus complexe. Bien entendu, il serait possible d’énumérer les différentes commandes et d’appeler l’opérateur DeleteAllOnSubmit sur les séquences correspondantes dans la table OrderDetail. Mais, ici, nous allons utiliser l’opérateur SelectMany pour obtenir une séquence composée de séquences de commandes détaillées. Cette séquence sera alors passée à l’opérateur DeleteAllOnSubmit. Avouez que LINQ permet bien des prouesses ! Après avoir supprimé les commandes détaillées, les commandes et le client, la méthode SubmitChanges est appelée pour sauvegarder les modifications dans la base de données. Voici le résultat affiché dans la console : Le client n’a pas été trouvé.

Cette sortie écran prouve que le client a bel et bien été supprimé. Bien qu’initialement conçu pour montrer comment supprimer un objet entité, cet exemple a également révélé la puissance de l’opérateur SelectMany. INFO Rappelez-vous que cet exemple ne restaure pas l’état original de la base de données. La restauration devra se faire manuellement.

Suppression d’objets entité attachés Alors que l’insertion d’un objet parent attaché à un objet entité dépendant provoque l’insertion automatique de ce dernier dans la base de données (voir Listing 14.3), cet automatisme n’est plus d’actualité en ce qui concerne la suppression d’un objet parent (ici, le mot "dépendant" se réfère aux objets entité qui contiennent une clé étrangère). Ceci a été illustré dans le Listing 14.21, où les enregistrements de la table Orderdetails devaient être supprimés avant les enregistrements de la table Orders, et les enregistrements de la table Orders avant ceux de la table Customers.

Dans la base de données Northwind, si vous tentez de supprimer une commande, les enregistrements correspondants dans la table OrderDetails ne seront pas automatiquement supprimés. De plus, vous provoquerez une violation de clé étrangère. Veillez donc à supprimer tous les objets entité enfant associés avant de supprimer un objet entité. Les Listings 14.21 et 14.3 illustrent ce principe. Dans chacun d’entre eux, les objets entité attachés ont été préalablement supprimés afin que leurs parents ne puissent l’être.

Linq.book Page 452 Mercredi, 18. février 2009 7:58 07

452

LINQ to SQL

Partie V

Suppression de relations Pour supprimer une relation entre deux objets entité dans LINQ to SQL, il suffit de réaffecter cette relation à un autre objet ou de lui affecter la valeur null. Dans le second cas, la relation entre les deux objets entité est perdue. Cela ne signifie aucunement que l’enregistrement est supprimé. Si c’est ce que vous souhaitez faire, vous devez supprimer les objets entité correspondants dans l’objet Table approprié. Le Listing 14.22 donne un exemple de suppression d’une relation. Listing 14.22 : Suppression d’une relation entre deux objets entité. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // Obtention de la commande pour laquelle la relation doit être supprimée Order order = (from o in db.Orders where o.OrderID == 11043 select o).Single(); // Sauvegarde du client pour pouvoir le restaurer à la fin du listing Customer c = order.Customer; Console.WriteLine("Les commandes avant la suppression de la relation :"); foreach (Order ord in c.Orders) { Console.WriteLine("OrderID = {0}", ord.OrderID); } // Suppression de la relation avec le client order.Customer = null; db.SubmitChanges(); Console.WriteLine("{0}Les commandes après la suppression de la relation :", System.Environment.NewLine); foreach (Order ord in c.Orders) { Console.WriteLine("OrderID = {0}", ord.OrderID); } // Restauration de la base de données à son état original order.Customer = c; db.SubmitChanges();

Après avoir obtenu la commande dont le champ OrderID vaut 11043, le client correspondant à la commande est sauvegardé en vue d’une restauration en fin de listing. Les différentes commandes de ce client sont alors affichées dans la console. La valeur null est alors affectée au client de la commande 11043 et la méthode SubmitChanges est appelée pour mémoriser les modifications dans la base de données. Les commandes du client sont à nouveau affichées. Comme vous pouvez le voir, la commande 11043 ne fait plus partie de la liste. Voici les résultats affichés dans la console. Les commandes avant la suppression de la relation : OrderID = 10738 OrderID = 10907 OrderID = 10964 OrderID = 11043

Openmirrors.com

Linq.book Page 453 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

453

Les commandes après la suppression de la relation : OrderID = 10738 OrderID = 10907 OrderID = 10964

Comme vous pouvez le constater, après avoir supprimé la relation entre la commande 11043 et le client, cette commande ne fait plus partie de la collection de commandes du client.

Surcharger les méthodes de mise à jour des bases de données Si vous pensez ne pas pouvoir utiliser LINQ to SQL dans votre environnement, peutêtre parce que vous devez utiliser des procédures stockées pour toutes vos modifications dans la base de données, vous serez intéressé(e) de savoir que le code appelé pour effectuer des mises à jour peut être surchargé. Pour surcharger le code appelé pour insérer, mettre à jour et supprimer des données, il suffit de définir une méthode partielle nommée avec la signature appropriée. Le DataContext appelle automatiquement les méthodes surchargées à la place des méthodes habituelles. Microsoft propose une autre méthode qui tire parti des méthodes partielles : vous pouvez ajouter des instructions dans le code sans surcharger les méthodes, si vous le souhaitez. Mais faites bien attention, si vous adoptez cette approche, vous devrez mettre en place une détection de conflits d’accès concurrentiel. Prenez le temps de consulter le Chapitre 17 avant de prendre cette décision. Si vous décidez de surcharger les méthodes de mise à jour, c’est le nom de la méthode partielle et le type d’entité de ses paramètres qui entraînent le DataContext à appeler les méthodes surchargées. Jetons un œil aux prototypes à mettre en place pour surcharger les méthodes insert, update et delete. Surcharge de la méthode Insert Vous pouvez surcharger la méthode appelée pour insérer un enregistrement dans la base de données en implémentant une méthode partielle prototypée comme suit : partial void Insert[EntityClassName](T instance)

où [EntityClassName] est le nom de la classe d’entité à partir de laquelle la méthode insert est surchargée et T est le type de la classe d’entité. Par exemple, pour surcharger la méthode insert avec la classe d’entité Shipper, vous utiliserez le code suivant : partial void InsertShipper(Shipper instance)

Linq.book Page 454 Mercredi, 18. février 2009 7:58 07

454

LINQ to SQL

Partie V

Surcharge de la méthode Update Vous pouvez surcharger la méthode appelée pour mettre à jour un enregistrement dans la base de données en implémentant une méthode partielle prototypée comme suit : partial void Update[EntityClassName](T instance)

où [EntityClassName] est le nom de la classe d’entité à partir de laquelle la méthode update est surchargée et T est le type de la classe d’entité. Par exemple, pour surcharger la méthode update avec la classe d’entité Shipper, vous utiliserez le code suivant : partial void UpdateShipper(Shipper instance)

Surcharge de la méthode Delete Vous pouvez surcharger la méthode appelée pour supprimer un enregistrement dans la base de données en implémentant une méthode partielle prototypée comme suit : partial void Delete[EntityClassName](T instance)

où [EntityClassName] est le nom de la classe d’entité à partir de laquelle la méthode delete est surchargée et T est le type de la classe d’entité. Par exemple, pour surcharger la méthode delete avec la classe d’entité Shipper, vous utiliserez le code suivant : partial void DeleteShipper(Shipper instance)

Exemple Pour illustrer la surcharge des méthodes insert, update et delete, nous n’allons pas modifier le fichier de classe d’entité généré. Nous allons plutôt créer un nouveau fichier. Ainsi, s’il est nécessaire de restaurer le fichier de classe d’entité, les méthodes partielles surchargées ne seront pas perdues. Le nouveau fichier a pour nom NorthwindExtended.cs : using System; using System.Data.Linq; namespace nwind { public partial class Northwind : DataContext { partial void InsertShipper(Shipper instance) { Console.WriteLine("La méthode surchargée Insert a été appelée pour l’affréteur {0}.", instance.CompanyName); } partial void UpdateShipper(Shipper instance) { Console.WriteLine("La méthode surchargée Update a été appelée pour l’affréteur {0}.", instance.CompanyName); }

Openmirrors.com

Linq.book Page 455 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

455

partial void DeleteShipper(Shipper instance) { Console.WriteLine("La méthode surchargée Delete a été appelée pour l’affréteur {0}.", instance.CompanyName); } } }

INFO Le fichier NorthwindExtended.cs doit être ajouté au projet Visual Studio.

Les méthodes surchargées sont des méthodes partielles définies au niveau DataContext, et non dans la classe d’entité relative. Comme vous pouvez le voir, ces méthodes se contentent d’afficher un message indiquant qu’elles ont été appelées. Cependant, dans la plupart des situations réelles, la surcharge servira à appeler une procédure stockée… Le Listing 14.23 contient le code qui appelle les méthodes surchargées. Listing 14.23 : Un exemple d’utilisation des méthodes surchargées Update, Insert et Delete. Northwind db = new Northwind(@ "Data Source=.\SQLEXPRESS;Initial ➥Catalog=Northwind "); Shipper ship = (from s in db.Shippers where s.ShipperID == 1 select s).Single(); ship.CompanyName = "Jiffy Shipping"; Shipper newShip = new Shipper { ShipperID = 4, CompanyName = "Vickey Rattz Shipping" , Phone ="(800) SHIP-NOW" }; db.Shippers.InsertOnSubmit(newShip); Shipper deletedShip = (from s in db.Shippers where s.ShipperID == 3 select s).Single(); db.Shippers.DeleteOnSubmit(deletedShip); db.SubmitChanges();

La note n’est plus d’actualité Ce code est composé de trois grandes parties. Dans la première, on accède à l’affréteur dont le champ ShipperID vaut 1 et son champ CompanyName est modifié. Dans la deuxième, un nouvel affréteur (Vickey Rattz Shipping) est défini. Enfin, dans la troisième, l’affréteur dont le champ ShipperID vaut 3 est supprimé. Bien entendu, étant donné que les méthodes surchargées se contentent d’afficher un message dans la

Linq.book Page 456 Mercredi, 18. février 2009 7:58 07

456

LINQ to SQL

Partie V

console, la base de données reste vierge de toute modification. Voici les résultats affichés dans la console : La méthode surchargée Update a été appelée pour l’affréteur Jiffy Shipping. La méthode surchargée Insert a été appelée pour l’affréteur Vickey Rattz Shipping. La méthode surchargée Delete a été appelée pour l’affréteur Federal Shipping.

Les trois méthodes surchargées ont bien été appelées. Supposons maintenant que vous vouliez surcharger les méthodes Update, Insert et Delete tout en maintenant les actions effectuées par défaut par ces méthodes. La technique à utiliser est très simple. Elle consiste à appeler les méthodes DataContext.ExecuteDynamicUpdate, DataContext.ExecuteDynamicInsert et DataContext.ExecuteDynamicDelete pour (respectivement) maintenir le comportement initial des méthodes Update, Insert et Delete. À titre d’exemple, voici la transformation à appliquer dans les méthodes partielles du code précédent pour continuer à afficher les messages dans la console, mais cette foisci en effectuant les modifications dans la base de données : namespace nwind { public partial class Northwind : DataContext { partial void InsertShipper(Shipper instance) { Console.WriteLine("La méthode surchargée Insert a été appelée pour l’affréteur {0}.", instance.CompanyName); this.ExecuteDynamicInsert(instance); } partial void UpdateShipper(Shipper instance) { Console.WriteLine("La méthode surchargée Update a été appelée pour l’affréteur {0}.", instance.CompanyName); this.ExecuteDynamicUpdate(instance); } partial void DeleteShipper(Shipper instance) { Console.WriteLine("La méthode surchargée Delete a été appelée pour l’affréteur {0}.", instance.CompanyName); this.ExecuteDynamicDelete(instance); } } }

Comme vous pouvez le voir, une instruction en gras a été ajoutée dans chacune des méthodes partielles pour appeler la méthode ExecuteDynamic appropriée. Vous voyez donc qu’il est facile d’étendre ou de modifier le comportement d’une classe d’entité. LINQ to SQL est vraiment très flexible ! Openmirrors.com

Linq.book Page 457 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

457

Surcharge dans le Concepteur Objet/Relationnel Comme il a été dit au Chapitre 13, la surcharge des méthodes insert, update et delete peut également être effectuée dans le Concepteur Objet/Relationnel. Considérations Lorsque vous surchargez les méthodes insert, update et delete, vous avez la responsabilité de mettre en place une détection de conflits d’accès concurrentiel. Pour ce faire, vous devez être familier avec le fonctionnement de la détection de conflits utilisée par défaut. Ainsi, par exemple, Microsoft spécifie tous les champs impliqués dans des vérifications de mises à jour dans la clause where de l’instruction update. La logique teste alors le nombre d’enregistrements mis à jour par l’instruction update. Si deux enregistrements ou plus ont été mis à jour, il y a eu un conflit d’accès concurrentiel. Vous devriez suivre une démarche similaire et lever l’exception ChangeConflictException si un conflit d’accès concurrentiel se produit. Si vous décidez de surcharger les méthodes insert, update et/ou delete, n’hésitez pas à vous reporter au Chapitre 17 pour en savoir plus à ce sujet.

Traduction SQL Lorsque vous écrivez des requêtes LINQ to SQL, vous avez sans doute remarqué que les clauses where et les autres expressions du même type sont spécifiées en utilisant les instructions du langage natif (C# dans cet ouvrage), et non en SQL. À titre d’exemple, voici la requête utilisée dans le Listing 14.2 : Un exemple de requête LINQ to SQL Customer cust = (from c in db.Customers where c.CustomerID =="LONEP" select c).Single();

Comme vous pouvez le voir, la requête est exprimée en C#. Si elle était écrite en SQL, elle aurait l’allure suivante : Un exemple invalide de requête LINQ to SQL Customer cust = (from c in db.Customers where c.CustomerID = ’LONEP’ select c).Single();

Ici, ce n’est pas l’opérateur d’égalité C# "==", mais c’est celui d’égalité SQL "="qui est utilisé. Par ailleurs, la chaîne littérale LONEP est entourée non pas de guillemets mais d’apostrophes. LINQ a un gros avantage : il permet au programmeur d’utiliser son langage de programmation pour effectuer des requêtes. Quoi de plus normal, puisque LINQ signifie Language Integrated Query (langage de requête intégré). Cependant, la base de données n’étant pas en mesure d’exécuter des instructions C#, ces dernières doivent être traduites en instructions SQL.

Linq.book Page 458 Mercredi, 18. février 2009 7:58 07

458

LINQ to SQL

Partie V

En général, la traduction est de très bonne qualité. Plutôt que redéfinir une référence comparable à MSDN, en indiquant ce qui peut et ce qui ne peut pas être traduit, je vais vous montrer ce qui va se passer quand une requête LINQ to SQL ne peut pas être traduite. Sachez que le code en question peut passer l’étape de la compilation et que le problème peut n’être mis en évidence qu’à l’exécution. Étant donné que l’exécution des requêtes peut être différée, la ligne de code contenant la requête peut également s’exécuter sans problème. Ce n’est que lors de l’exécution réelle de la requête qu’un message d’erreur du type suivant peut s’afficher. Exception non gérée : System.NotSupportedException : La méthode ’TrimEnd’ n’a pas pu ➥être traduite en SQL. ...

Ce message est très clair. Le Listing 14.24 représente le code qui en est à l’origine. Listing 14.24 : Une requête LINQ to SQL qui ne peut pas être traduite. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable custs = from c in db.Customers where c.CustomerID.TrimEnd(’K’) =="LAZY" select c; foreach (Customer c in custs) { Console.WriteLine("{0}", c.CompanyName); }

La méthode TrimEnd, à l’origine de l’exception, est appelée sur un champ de la base de données, et non sur la chaîne littérale"LAZY". Dans le Listing 14.25, nous allons modifier la position de la méthode TrimEnd. Listing 14.25 : Une requête LINQ to SQL qui peut être traduite. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable custs = from c in db.Customers where c.CustomerID =="LAZY".TrimEnd(’K’) select c; foreach (Customer c in custs) { Console.WriteLine("{0}", c.CompanyName); }

L’exécution de ce code ne produit aucun message d’erreur en rapport avec la traduction SQL. L’appel d’une méthode non supportée sur une colonne d’une base de données produit donc une exception, alors que l’appel de cette même méthode sur un paramètre est supporté. Ceci peut se comprendre : LINQ to SQL peut en effet appeler la méthode TrimEnd sur un paramètre, car cette opération s’effectue sur l’ordinateur qui exécute le programme. Si nous appelons la méthode TrimEnd sur une colonne de la base de données, elle est exécutée sur l’ordinateur qui héberge la base de données. Elle doit Openmirrors.com

Linq.book Page 459 Mercredi, 18. février 2009 7:58 07

Chapitre 14

Opérations standard sur les bases de données

459

donc être traduite en SQL, passée à la base de données et exécutée. La méthode TrimEnd n’étant pas traduisible, une exception est levée. Si vous êtes amené à appeler une méthode non supportée sur une colonne d’une base de données, peut-être pourriez-vous utiliser une autre méthode qui a l’effet contraire sur le paramètre. Supposons par exemple que vous vouliez appeler la méthode ToUpper sur une colonne d’une base de données et que cette méthode ne soit pas supportée. Si vous appelez la méthode ToLower sur le paramètre, elle sera supportée. Vous devez également vous assurer que la méthode appelée a l’effet souhaité. Dans notre exemple, la colonne de la base de données pourrait avoir des casses différentes. L’appel de la méthode ToLower ne produirait donc pas l’effet escompté. Si la colonne de la base de données a pour valeur "Smith" et le paramètre a pour valeur "SMITH", la méthode ToUpper appliquée à la colonne de la base de données donnerait l’effet recherché. Malheureusement, cette méthode n’est pas supportée, et appliquer la méthode opposée, ToLower, au paramètre ne résout en rien le problème. Vous vous demandez certainement comment savoir que la méthode TrimEnd n’est pas traduisible en SQL. Les types et méthodes primitifs supportés étant dynamiques et sujets à des changements, ce livre ne saurait les couvrir d’une façon exhaustive. S’il existe de nombreuses restrictions liées à la traduction, Microsoft fait des efforts continuels pour améliorer les choses. Pour avoir un aperçu des méthodes qui peuvent être traduites en SQL, le mieux est de vous reporter à la documentation MSDN intitulée "NET Framework Function Translation", dans la section "LINQ to SQL". Quoi qu’il en soit, comme vous avez pu le constater au travers des exemples précédents, il reste très simple de déterminer si une méthode est supportée ou non.

Résumé Ce chapitre vous a montré comment utiliser LINQ to SQL pour réaliser des opérations courantes sur les bases de données. Essentiellement, insertion, interrogation, mise à jour et suppression d’enregistrements. Vous avez également vu que les requêtes LINQ to SQL étaient différentes des requêtes LINQ to Object. Ayez bien à l’esprit que, si un code LINQ to SQL modifie le contenu d’une base de données, il doit également se charger de la détection des conflits d’accès concurrentiel. Par souci de clarté, aucun exemple de ce chapitre n’a implémenté le code correspondant. Le cas échéant, reportez-vous au Chapitre 17 pour avoir toutes les informations nécessaires à ce sujet. Il ne suffit pas de savoir comment effectuer des opérations de base sur des objets entité. Vous devez également comprendre comment ces opérations affectent le contenu de la base de données. En particulier, lorsque vous ajoutez un objet dans la base de données, les objets attachés sont automatiquement ajoutés sans qu’aucune action de votre part ne soit nécessaire. Mais, attention, cet automatisme ne s’applique pas aux suppressions :

Linq.book Page 460 Mercredi, 18. février 2009 7:58 07

460

LINQ to SQL

Partie V

pour supprimer un objet entité parent dans une association, vous devez au préalable supprimer les objets entité enfants afin d’éviter qu’une exception ne soit levée ! Dans ce chapitre, vous avez également appris à surcharger les méthodes utilisées par défaut pour modifier les objets entité correspondant aux enregistrements de la base de données. Ceci permet au développeur de contrôler les modifications effectuées dans la base de données et facilite l’utilisation des procédures stockées. Enfin, vous avez appris que les requêtes LINQ to SQL devaient être traduites en déclarations SQL. Il faut toujours garder à l’esprit qu’une telle traduction doit être opérée et que, parfois, certaines requêtes LINQ to SQL ne sont pas traduisibles. Jusqu’ici, les classes d’entité ont souvent été citées, mais jamais étudiées en détail. Le Chapitre 15 vous donnera toutes les informations nécessaires pour les connaître comme le fond de votre poche.

Openmirrors.com

Linq.book Page 461 Mercredi, 18. février 2009 7:58 07

15 Les classes d’entité LINQ to SQL Le chapitre précédent a utilisé à de nombreuses reprises des classes d’entité. Dans ce chapitre, nous allons les décrire en détail. Vous apprendrez ce qu’elles sont et les différentes techniques qui permettent de les créer. Vous apprendrez également vers quelles difficultés vous vous acheminez si vous décidez de créer manuellement vos classes d’entité. Mais, avant d’entrer dans le vif du sujet, voyons quelques prérequis nécessaires à l’exécution des exemples de ce chapitre.

Prérequis pour exécuter les exemples Pour exécuter les exemples de ce chapitre, vous devez être en possession de la version étendue de la base de données Northwind et avoir généré les classes d’entité correspondantes. Si nécessaire, reportez-vous à la section "Prérequis pour exécuter les exemples" du Chapitre 12 pour savoir comment résoudre ces deux points.

Les classes d’entité Les classes mappées à une base de données SQL Server par l’intermédiaire de LINQ to SQL sont appelées "classes d’entité". Un objet instancié à partir d’une classe d’entité est appelé "objet entité". Les classes d’entité sont de traditionnelles classes C# pour lesquelles des attributs LINQ to SQL additionnels sont spécifiés. Elles peuvent également être définies en fournissant un fichier de mappage XML lors de l’instanciation de l’objet DataContext. Les attributs ou le fichier de mappage définissent le mappage entre les classes d’entité et la base de données SQL Server au travers de LINQ to SQL. C’est en utilisant ces classes d’entité que LINQ to SQL va vous permettre de définir des requêtes et de modifier une base de données.

Linq.book Page 462 Mercredi, 18. février 2009 7:58 07

462

LINQ to SQL

Partie V

Création de classes d’entité Les classes d’entité sont l’élément de base permettant de définir des requêtes LINQ to SQL. Il est donc nécessaire de les fabriquer en premier lieu. Deux techniques sont utilisables. Vous pouvez les générer en utilisant les modes opératoires décrits aux Chapitres 12 et 13, mais également les rédiger "à la main" ou, pourquoi pas, utiliser une combinaison de ces deux techniques. Si vous n’avez pas encore de classes métier pour les entités stockées dans la base de données, la génération des classes d’entité est certainement la meilleure approche. Si vous avez déjà un modèle objet, c’est l’écriture manuelle des classes d’entité qui est sans doute la meilleure approche. Si vous commencez un nouveau projet, je vous recommande de définir votre base de données, puis de générer les classes d’entités à partir de la base de données. Cela vous permettra d’avoir une prise sur les classes d’entité générées et vous évitera d’avoir à les écrire, ce qui, comme vous le verrez par la suite, n’est pas toujours chose évidente. Génération de classes d’entité Jusqu’ici, la seule façon envisagée pour créer des classes d’entité consistait à les générer. Au Chapitre 12, vous avez vu comment générer les classes d’entité de la base de données Northwind. Par leur intermédiaire, vous êtes en mesure d’exécuter les exemples des chapitres dédiés à LINQ to SQL. Au Chapitre 13, vous avez appris à utiliser l’outil en ligne de commande SQLMetal ou le Concepteur Objet/Relationnel pour générer des classes d’entité.

SQLMetal est très simple à utiliser, mais il ne permet pas de choisir le nom des classes d’entité générées. Il produit un fichier XML intermédiaire, permet de l’éditer et fabrique les classes d’entité à partir de ce fichier. Les classes d’entité sont définies pour tous les champs de toutes les tables de la base de données, en utilisant la procédure fortement consommatrice en ressources décrite précédemment. Cela ne vous laisse que peu de contrôle sur le nom des classes d’entité et sur leurs propriétés. Le Concepteur Objet/ Relationnel peut prendre plus de temps pour créer un modèle objet complet, mais il vous permet de spécifier précisément pour quelles tables et quels champs vous voulez générer des classes d’entité. Vous pouvez également choisir le nom des classes d’entité et de leurs propriétés. Si nécessaire, reportez-vous au Chapitre 13 pour avoir de plus amples renseignements sur ces deux outils. Rien ne vous oblige à générer les classes d’entité de toutes les tables d’une base de données. Par ailleurs, vous pouvez, si nécessaire, ajouter des fonctionnalités métier aux classes d’entité générées. À titre d’exemple, la classe Customer a été générée avec SQLMetal au Chapitre 12. Il est possible d’ajouter des méthodes métier ou des membres de classes non persistants à cette classe. Si vous le faites, assurez-vous que vous ne modifiez pas le code de la classe d’entité générée. Pour ce faire, la meilleure Openmirrors.com

Linq.book Page 463 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

463

méthode consiste à créer un autre module de classe Customer et à profiter du fait que les classes d’entité sont générées en tant que classes partielles. Cette nouveauté intéressante de C# permet de séparer très simplement des fonctionnalités dans différents modules. Ainsi, si pour une raison quelconque la classe d’entité doit à nouveau être générée, vous ne perdrez pas les méthodes et/ou membres qui y ont été ajoutés. Écriture manuelle des classes d’entité L’écriture manuelle des classes d’entité est l’approche la plus difficile. Elle nécessite une bonne compréhension des attributs LINQ to SQL et du schéma de mappage externe. Notez cependant que l’écriture manuelle de classes d’entité est une bonne façon d’apprendre le langage LINQ to SQL.

L’écriture manuelle de classes peut se révéler intéressante pour des classes déjà existantes. Vous pouvez avoir une application existante avec un modèle objet déjà implémenté. Il ne serait pas très astucieux de générer les classes d’entité à partir de la base de données, puisque vous avez déjà un modèle objet utilisé par l’application. La solution consiste à ajouter les attributs nécessaires au modèle objet existant ou à créer un fichier de mappage. Grâce à la flexibilité de LINQ to SQL, il n’est pas nécessaire que les classes aient le nom de la table correspondante ni que les noms des propriétés de la classe correspondent aux noms des colonnes dans la table. Pour créer des classes d’entité manuellement, vous devez ajouter des attributs dans vos classes, qu’il s’agisse de classes métier existantes ou de nouvelles classes créées spécifiquement en tant que classes entité. Reportez-vous à la section intitulée "Attributs de classes d’entité et propriétés d’attributs" dans ce chapitre pour prendre connaissance des attributs et propriétés disponibles. Pour créer des classes d’entité en utilisant un fichier de mappage externe, vous devez créer un fichier XML qui se conforme au schéma présenté dans la section "Schéma de fichier de mappage externe XML", un peu plus loin dans ce chapitre. Une fois ce fichier créé, vous utiliserez le constructeur approprié lors de l’instanciation du DataContext pour charger le fichier de mappage. Deux constructions vous permettent de spécifier un fichier de mappage externe. Responsabilités annexes des classes d’entité Malheureusement, il ne suffit pas de comprendre comment fonctionnent les attributs et les propriétés des attributs pour être en mesure d’écrire des classes d’entité à la main. Vous devez également avoir des connaissances sur les responsabilités annexes des classes d’entité. Les classes d’entité doivent entre autres être attentives aux notifications de changement et assurer la cohérence entre les classes parents et enfants. Ces responsabilités annexes sont toutes gérées de façon transparente lorsque vous utilisez SQLMetal ou le Concepteur

Linq.book Page 464 Mercredi, 18. février 2009 7:58 07

464

LINQ to SQL

Partie V

Objet/Relationnel. Si vous écrivez les classes d’entité manuellement, vous devrez implémenter le code correspondant par vous-même. Notifications de changement Un peu plus loin, au Chapitre 16, nous nous intéresserons à la détection de changement. Cette dernière n’est pas très élégante ni efficace sans l’assistance des classes d’entité. Si vos classes d’entité ont été générées par SQLMetal ou le Concepteur Objet/Relationnel, détendez-vous : ces deux outils fabriquent automatiquement le code nécessaire lors de la génération des classes d’entité. Si vous écrivez vos classes d’entité manuellement, vous devez bien appréhender les notifications de changement et implémenter du code pour les gérer. Les classes d’entité peuvent participer ou ne pas participer aux notifications de changement. Dans le second cas, le DataContext fournit une trace des changements en conservant deux copies de chaque objet entité : une avec la valeur originale et une avec la valeur actuelle. La première copie est créée à la première lecture d’une entité dans la base de données. C’est alors que le traçage des changements se met en branle. Vous pouvez améliorer l’efficacité du processus en implémentant les interfaces de notifications de changement System.ComponentModel.INotifyPropertyChanging et System.ComponentModel.INotifyPropertyChanged dans les classes d’entité que vous avez écrites manuellement. Comme fréquemment dans les chapitres relatifs à LINQ to SQL, je vais me référer au code généré par SQLMetal pour vous montrer la meilleure façon de gérer certaines situations. Ici, je vais utiliser ce code pour vous montrer comment gérer les notifications de changement. Pour implémenter les interfaces System.ComponentModel.INotifyPropertyChanging et System.ComponentModel.INotifyPropertyChanged, quatre étapes doivent être accomplies. Première étape. Nous devons définir la classe d’entité de telle sorte qu’elle implémente les interfaces System.ComponentModel.INotifyPropertyChanging et System.ComponentModel.INotifyPropertyChanged : Dans la classe d’entité générée Customer [Table(Name="dbo.Customers")] public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged { … }

Comme la classe d’entité implémente ces deux interfaces, le DataContext définit deux gestionnaires d’événements pour ces événements (voir un peu plus loin dans cette section). Comme vous le voyez, le code précédent utilise l’attribut Table. Pour des raisons de contexte, les attributs sont spécifiés dans cette section, sans toutefois les expliquer en détail. Vous en saurez plus à leur sujet un peu plus loin dans ce chapitre. Openmirrors.com

Linq.book Page 465 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

465

Deuxième étape. Nous devons déclarer une classe private static de type PropertyChangingEventsArgs et passer String.Empty à son constructeur. Dans la classe d’entité générée Customer [Table(Name="dbo.Customers")] public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged { private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); ... }

L’objet emptyChangingEventArgs sera passé à un des gestionnaires d’événements mentionnés précédemment lorsqu’un événement approprié sera levé. Troisième étape. Deux membres public event (PropertyChanging, de type System.ComponentModel.PropertyChangingEventHandler et PropertyChanged, de type System.ComponentModel.PropertyChangedEventHandler) doivent être ajoutés. Dans la classe d’entité générée Customer [Table(Name="dbo.Customers")] public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged { private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); ... public event PropertyChangingEventHandler PropertyChanging; public event PropertyChangedEventHandler PropertyChanged; ... }

Lorsque l’objet DataContext initie une détection de changement pour un objet entité, deux cas peuvent se produire. Si la classe d’entité implémente les deux interfaces de notification de changement, les gestionnaires d’événements correspondants sont mis en place. Dans le cas contraire, une copie de l’objet entité est effectuée, comme mentionné précédemment. Quatrième étape. Chaque fois qu’une propriété d’une entité mappée est modifiée, l’événement PropertyChanging doit être levé avant la modification et l’événement PropertyChanged, après la modification. SQLMetal génère les méthodes SendPropertyChanging et SendPropertyChanged à votre place (la gestion des événements ne doit pas forcément être implémentée de cette façon). Dans la classe d’entité générée Customer protected virtual void SendPropertyChanging() { if ((this.PropertyChanging != null)) { this.PropertyChanging(this, emptyChangingEventArgs); } }

Linq.book Page 466 Mercredi, 18. février 2009 7:58 07

466

LINQ to SQL

Partie V

protected virtual void SendPropertyChanged(String propertyName) { if ((this.PropertyChanged != null)) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }

Lorsque l’événement PropertyChanged est levé, un nouvel objet PropertyChangedEventArgs est créé et le nom de la propriété qui a été modifiée lui est passé. De cette façon, l’objet DataContext sait quelle propriété a été modifiée. Lorsque la méthode SendPropertyChanging est appelée, elle lève l’événement PropertyChanging, ce qui provoque l’appel du gestionnaire d’événements enregistré par l’objet DataContext. Le même principe s’applique à la méthode SendPropertyChanged et à l’événement PropertyChanged. Bien entendu, vous pouvez choisir d’inclure manuellement cette logique dans votre code plutôt que créer des méthodes toutes faites mais, croyez-moi, cela est plutôt pénible et le code à maintenir est assez important. Dans la méthode set de chaque propriété, il faut appeler les méthodes SendPropertyChanging et SendPropertyChanged juste avant, puis juste après la modification de la propriété. Dans la classe d’entité générée Customer [Column(Storage="_ContactName", DbType="NVarChar(30)")] public string ContactName { get { return this._ContactName; } set { if ((this._ContactName != value)) { this.OnContactNameChanging(value); this.SendPropertyChanging(); this._ContactName = value; this.SendPropertyChanged("ContactName"); this.OnContactNameChanged(); } } }

Le nom de la propriété (ici ContactName) est passé dans l’appel à la méthode SendPropertyChanged. Lorsque la méthode SendPropertyChanged est appelée, l’objet DataContext sait ainsi que la propriété ContactName de cet objet entité a été modifiée. Les mêmes événements doivent également être levés dans les méthodes set des propriétés qui représentent l’association entre les tables Order et Customer. Du côté "plusieurs" de l’association "un-à-plusieurs", le code en gras doit être ajouté : Dans la classe Order, puisque Customer n’a pas de propriété EntityRef [Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID", IsForeignKey=true)]

Openmirrors.com

Linq.book Page 467 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

467

public Customer Customer { get { return this._Customer.Entity; } set { Customer previousValue = this._Customer.Entity; if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) { this.SendPropertyChanging(); if ((previousValue != null)) { this._Customer.Entity = null; previousValue.Orders.Remove(this); } this._Customer.Entity = value; if ((value != null)) { value.Orders.Add(this); this._CustomerID = value.CustomerID; } else { this._CustomerID = default(string); } this.SendPropertyChanged("Customer"); } } }

Du côté "un" de l’association "un-à-plusieurs", le code en gras doit être ajouté : Dans la classe d’entité générée Customer public Customer() { ... this._Orders = new EntitySet(new Action(this.attach_Orders), new Action(this.detach_Orders)); } ... private void attach_Orders(Order entity) { this.SendPropertyChanging(); entity.Customer = this; this.SendPropertyChanged("Orders"); } private void detach_Orders(Order entity) { this.SendPropertyChanging(); entity.Customer = null; this.SendPropertyChanged("Orders"); }

Si le délégué générique Action utilisé dans le code précédent ne vous est pas familier, sachez qu’il se trouve dans l’espace de noms System et qu’il a été ajouté au framework .NET 3.0. Le code précédent instancie un objet délégué Action pour la classe d’entité Order et le passe en tant que délégué à la méthode attach_Orders. LINQ to SQL utilisera

Linq.book Page 468 Mercredi, 18. février 2009 7:58 07

468

LINQ to SQL

Partie V

ce délégué un peu plus tard pour relier un client et une commande. De la même manière, un autre objet délégué Action est instancié et un délégué vers la méthode detach_Orders lui est passé. LINQ to SQL utilisera ce délégué un peu plus tard pour supprimer le lien entre un client et une commande. En implémentant la notification de changement comme il vient d’être décrit, les traces des modifications sont plus efficaces. De cette façon, l’objet DataContext sait quand et quelle propriété d’une classe d’entité a été modifiée. Lorsque nous appelons la méthode SubmitChanges, l’objet DataContext oublie la valeur originale des propriétés, la valeur actuelle devient la valeur originale et le traçage des modifications est réinitialisé. Pour en savoir plus à ce sujet, reportez-vous au Chapitre 16. Bien entendu, comme il a été dit précédemment, si vous définissez vos classes d’entité via SQLMetal ou le Concepteur Objet/Relationnel, tout le code dont nous venons de parler est généré automatiquement. Vous n’aurez à implémenter la notification de changement que dans le cas où vous décidez d’écrire les classes d’entité à la main. Cohérence du graphe En mathématiques, lorsque des nœuds sont connectés l’un à l’autre, le réseau constitué par les connexions est appelé "graphe". De la même manière, le réseau qui représente les connexions créées par des classes qui référencent d’autres classes est appelé "graphe". Lorsque deux classes d’entité partagent une relation (une association), étant donné que chacune d’entre elles référence l’autre, il existe un graphe pour les représenter. Lorsque vous modifiez une relation entre deux objets entité (un client et une commande, par exemple), la référence de chaque côté de la relation doit être correctement mise à jour, afin que chaque objet entité référence correctement (ou ne référence plus) l’autre. Ceci reste valable que vous définissiez ou supprimiez une relation. Avec LINQ to SQL, le programmeur qui utilise des classes d’entité ne doit modifier qu’un seul côté d’une relation : l’autre côté est modifié de façon transparente, mais pas par LINQ to SQL… C’est la responsabilité de la classe d’entité que mettre à jour l’autre côté de la relation. Si vos classes d’entité ont été générées via SQLMetal ou le Concepteur Objet/Relationnel, cette étape est automatique. Vous n’aurez à implémenter la mise à jour de l’autre côté de la relation que dans le cas où vous décidez d’écrire les classes d’entité à la main. Le graphe est dit "cohérent" si les deux côtés de la relation sont correctement mis à jour. Dans le cas contraire, il est dit "incohérent" et… le chaos n’est pas loin : un client pourrait ainsi être relié à une commande et cette commande, reliée à un autre client ou à aucun client ! Cela rend toute navigation impossible dans la base de données et la situation est inacceptable. Openmirrors.com

Linq.book Page 469 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

469

Heureusement, Microsoft fournit un modèle qui permet de s’assurer que les graphes des classes d’entité sont cohérents. Voyons leur implémentation, générée par SQLMetal pour la base de données Northwind : Dans la classe d’entité générée Customer public Customer() { ... this._Orders = new EntitySet(new Action(this.attach_Orders), new Action(this.detach_Orders)); } ... private void attach_Orders(Order entity) { this.SendPropertyChanging(); entity.Customer = this; this.SendPropertyChanged("Orders"); } private void detach_Orders(Order entity) { this.SendPropertyChanging(); entity.Customer = null; this.SendPropertyChanged("Orders"); }

Dans cet exemple, la classe Customer est la classe parent (le côté "un" de la relation "un-à-plusieurs"). La classe Order est la classe enfant (le côté "plusieurs" de la relation "un-à-plusieurs"). Dans le code précédent, deux objets délégués Action sont passés au constructeur de la classe parent Customer lors de l’initialisation de la collection de classes enfant _Orders. Le délégué passé en argument au premier Action représente la méthode chargée de l’affectation de l’objet Customer courant à l’objet Order. Dans ce délégué, le mot-clé this fait référence au Customer de l’objet Order passé à la méthode attach_Orders. Le délégué passé en argument au deuxième Action représente la méthode chargée de la désaffectation de l’objet Customer courant à l’objet Order. Dans ce délégué, le mot-clé this fait référence au Customer de l’objet Order passé à la méthode detach_Orders. Bien que le code précédent se trouve dans la classe parent Customer, l’affectation de la classe enfant Order au client est gérée par la propriété Customer de l’objet Order. Vous pouvez voir que les méthodes attach_Orders et detach_Orders se contentent de modifier la propriété Customer de l’objet Order : la propriété entity.Customer est initialisée à this (attach_Orders)/à null (detach_Orders) pour (respectivement) attacher/ détacher le Customer à/de l’objet Order. C’est dans les méthodes get et set de la classe enfant Order que se fera tout le travail de fond qui assurera la cohérence du graphe. La classe parent, quant à elle, "se contente" de maintenir la cohérence du graphe.

Linq.book Page 470 Mercredi, 18. février 2009 7:58 07

470

LINQ to SQL

Partie V

Avant de continuer, remarquez que les notifications de changement sont résolues en appelant les méthodes SendPropertyChanging et SetPropertyChanged dans les méthodes attach_Orders et detach_Orders. Voyons maintenant ce qui doit être fait dans la classe enfant d’une relation "parent-versenfant" pour maintenir la cohérence du graphe. Dans la classe d’entité générée Order [Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID", IsForeignKey=true)] public Customer Customer { get { return this._Customer.Entity; } set { Customer previousValue = this._Customer.Entity; if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) { this.SendPropertyChanging(); if ((previousValue != null)) { this._Customer.Entity = null; previousValue.Orders.Remove(this); } this._Customer.Entity = value; if ((value != null)) { value.Orders.Add(this); this._CustomerID = value.CustomerID; } else { this._CustomerID = default(string); } this.SendPropertyChanged("Customer"); } } }

Ce code correspond à la méthode set de la propriété Customer : le côté "parent" de la relation lui a donné la charge de maintenir la cohérence du graphe. Cette méthode étant assez complexe, nous allons décrire son fonctionnement pas à pas. set { Customer previousValue = this._Customer.Entity;

La première ligne de la méthode set copie le client original affecté à la commande dans la variable previousValue. Que la référence this._Customer.Entity ne vous surprenne pas : la variable membre _Customer est de type Entity et non de type Customer. Pour obtenir l’objet Customer, le code doit faire référence à la propriété Entity de l’objet EntityRef. Le type EntityRef étant apparenté à un Customer, Openmirrors.com

Linq.book Page 471 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

471

le type de Entity sera Customer. Aucun casting n’est donc nécessaire. Avouez que les génériques, apparus dans C# 2.0, sont vraiment pratiques. if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) {

Ce bloc de code teste si l’objet Customer actuel associé à l’objet Order via le paramètre value n’est pas le même que celui qui est déjà associé à l’objet Order. Dans ce cas, aucune action n’est nécessaire, sauf si l’objet Customer n’est pas encore initialisé. Étant donné la nature récursive de ce code, cette ligne est très importante, car c’est elle qui va arrêter l’itération. this.SendPropertyChanging();

La méthode SendPropertyChanging est appelée pour lever la notification de l’événement de changement. if ((previousValue != null)) {

Le code détermine alors si un objet Customer (parent) est déjà affecté à l’objet Order (enfant) en testant si l’objet previousValue a pour valeur null. Si un objet Customer est associé à l’objet Order (en d’autres termes si previousValue n’est pas nul), la valeur null doit être affectée à la propriété Entity de l’objet EntityRef Customer de l’objet Order. this._Customer.Entity = null;

La propriété Entity est initialisée à null dans la ligne précédente pour arrêter le parcours récursif qui sera mis en branle sur la ligne suivante. Étant donné que cette propriété est nulle, qu’elle ne représente pas l’objet Customer actuel et que la propriété Orders de l’objet Customer contient toujours cette commande, le graphe est incohérent. Dans la ligne suivante, la méthode Remove est appelée sur la propriété Orders de l’objet Customer et l’objet Order courant est passé en argument pour qu’il soit supprimé. previousValue.Orders.Remove(this); }

L’appel de la méthode Remove provoque l’appel de la méthode detach_Orders de la classe Customer en lui passant l’objet Order à désaffecter. Dans la méthode detach_Orders, la propriété Customer de l’objet Order est initialisée à null. Pour vous rafraîchir la mémoire, voici à quoi ressemble la méthode detach_Orders : La méthode detach_Orders, juste pour vous rafraîchir la mémoire private void detach_Orders(Order entity) { this.SendPropertyChanging(); entity.Customer = null; this.SendPropertyChanged("Orders"); }

Lors de l’appel de la méthode detach_Orders, la propriété Customer de l’objet Order passé en argument est initialisée à null. Ceci provoque l’appel de la méthode set de la

Linq.book Page 472 Mercredi, 18. février 2009 7:58 07

472

LINQ to SQL

Partie V

propriété Customer de l’objet Order, celle-là même qui est à l’origine de l’invocation de la méthode detach_Orders. La méthode qui a lancé le processus de désaffectation de l’objet Order est donc appelée récursivement et la valeur null est passée à la méthode set. L’exécution consiste donc en un appel récursif à la méthode set de l’objet Customer. La méthode detach_Orders provoque l’appel récursif de la méthode set set { Customer previousValue = this._Customer.Entity; if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) {

La quatrième ligne de la méthode set teste l’objet value. S’il est égal à la propriété Entity de la propriété Customer actuellement affectée, l’appel récursif à la méthode set n’effectue aucune action. Dans la ligne de code précédente, le premier appel non récursif de la méthode set qui appelle la propriété Entity de la propriété Customer avait pour valeur null. Étant donné que la valeur null est passée dans l’objet value de la méthode detach_Orders, ces deux valeurs sont égales. L’invocation récursive de la méthode set se termine sans qu’aucune action n’ait été accomplie, et le flux de contrôle retourne à la première invocation de la méthode set. C’est ce que nous voulions dire quelques lignes plus haut lorsque nous indiquions que la propriété Entity était initialisée à la valeur null pour arrêter la boucle récursive. Il est temps de marquer une pause, car le besoin d’aspirine est de plus en plus pressant… Lorsque l’appel récursif à la méthode set prend fin, le flux retourne à la ligne qui suit l’invocation initiale de la méthode set. Par commodité, cette ligne de code peut être réécrite en utilisant un snippet previousValue.Orders.Remove(this); }

Une fois l’exécution de la méthode Orders.Remove terminée, la propriété Orders de l’objet Customer ne contient plus aucune référence à cette commande. Le graphe est donc à nouveau cohérent. Si vous prévoyez d’écrire vos classes d’entité, vous devez vous attendre à passer un peu de temps dans le débogueur sur ce que nous venons de présenter. Placez des points d’arrêt dans les méthodes detach_Orders et set, et observez ce qui se passe. Le nouvel objet Customer, qui était passé à la méthode set dans le paramètre value, est alors stocké dans la propriété Entity de l’objet Customer. this._Customer.Entity = value;

Après tout, il s’agit de la méthode set de la propriété Customer. Nous voulions affecter l’objet Order à un nouvel objet Customer. À nouveau, à ce point du code, l’objet Order a une référence vers l’objet Customer nouvellement assigné, mais ce dernier n’a pas de référence vers l’objet Order. Le graphe n’est donc plus cohérent. Openmirrors.com

Linq.book Page 473 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

473

Le code teste la valeur de l’objet Customer. Si elle est différente de null, l’objet Customer doit être affecté à l’objet Order. if ((value != null)) {

Si l’objet Customer passé dans le paramètre value n’est pas nul, l’objet Order actuel est ajouté à la collection d’objets Order de l’objet Customer. value.Orders.Add(this);

Dans cette ligne, le délégué qui avait été passé à la méthode du constructeur EntitySet de l’objet Customer est appelé. L’affectation provoquera donc l’appel de la méthode attach_Orders de l’objet Customer. Ceci, tour à tour, affectera l’objet Customer courant de l’objet Order à l’objet Customer passé. Ce qui provoquera l’appel de la méthode set de la propriété Customer de l’objet Order. Le code effectuera un appel récursif dans la méthode set, comme il a été indiqué un peu plus tôt. Cependant, deux instructions plus haut, et avant de commencer la récursion, la propriété Entity de la propriété Customer de l’objet Order a été initialisée avec un nouvel objet Customer, celui-là même qui a été passé à la méthode set via la méthode attach_Orders. Ici encore, la méthode set est appelée récursivement, puis la deuxième ligne de code est appelée. Cette ligne de code est issue d’une autre invocation de la méthode set if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false)))

Étant donné que l’objet Customer courant de l’objet Order (actuellement stocké dans l’objet previousValue) et le paramètre value sont identiques, la méthode set ne fait aucune action et la récursivité prend fin. Dans la ligne suivante, le membre CustomerID courant de l’objet Order est initialisé avec le CustomerID du nouvel objet Customer. this._CustomerID = value.CustomerID; }

Si l’objet Customer qui vient d’être assigné est nul, le code se contente d’affecter la valeur par défaut du type du membre (ici, String) au membre CustomerID de l’objet Order. else { this._CustomerID = default(string); }

Si le membre CustomerID avait été de type int, le code aurait réalisé une affectation de type default(int). Dans la dernière ligne de code, le nom de la propriété modifiée est passé à la méthode SendPropertyChanged pour lever une notification de changement. this.SendPropertyChanged("Customer"); }

Linq.book Page 474 Mercredi, 18. février 2009 7:58 07

474

LINQ to SQL

Partie V

Cette technique fonctionne pour les relations "un-à-plusieurs". Dans une relation "un-àun", chaque côté de la relation correspond, à quelques détails près, au côté "enfant" de cet exemple. Dans une relation "un-à-un", la notion de parent et d’enfant n’existe pas. Supposons que la relation entre les clients et les commandes est de type "un-à-un". Si vous écrivez les classes d’entité à la main et que la relation entre les classes Customer et Order soit de type "un-à-un", sachez que chacune de ces classes contient une propriété de type EntityRef, où T est l’autre classe d’entité. Ainsi, la classe Customer contient un EntityRef et la classe Order, un EntityRef. Comme aucune des deux classes ne contient un EntitySet, les méthodes Add et Remove des relations "un-à-plusieurs" ne sont pas appelées. Si nous supposons qu’il existe une relation "un-à-un" entre les commandes et les clients, la méthode set de la propriété Customer de la classe Order aura la même allure qu’auparavant, mis à part que la commande courante ne sera plus affectée au client original. Le client original ne pouvant passer qu’une commande, nous ne supprimerons pas la commande courante d’une collection d’objets Order : il suffira d’affecter la valeur null à la propriété Order de l’objet Customer. La ligne de code suivante : previousValue.Orders.Remove(this);

sera remplacée par : previousValue.Order = null;

Comme vous pouvez le voir, le maintien de la cohérence dans le graphe est loin d’être trivial, et il est facile de s’y perdre. Heureusement, deux outils gèrent cette tâche pour vous : SQLMetal et le Concepteur Objet/Relationnel. Pour assurer la cohérence du graphe et implémenter les notifications de changement, ils n’ont pas leur pareil ! L’outil en ligne de commande aurait pu s’appeler SQLGold tant il est pratique, mais je suppose que la partie "metal" de son nom vient du mot "metalangage"… Appel des méthodes partielles appropriées Lorsque Microsoft a mis au point les méthodes partielles pour permettre l’extension du code généré (des classes d’entité, par exemple), il a encore alourdi votre travail si vous décidez d’implémenter manuellement vos classes d’entité. Ainsi, vous devez déclarer plusieurs méthodes partielles : partial partial partial partial partial

void void void void void

OnLoaded(); OnValidate(ChangeAction action); OnCreated(); On[Property]Changing(int value); On[Property]Changed();

Vous devriez également définir les méthodes On[Property]Changing On[Property]Changed pour chacune des propriétés des classes d’entité. Openmirrors.com

et

Linq.book Page 475 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

475

Les méthodes OnLoaded et OnValidate ne nécessitent aucun appel complémentaire dans la classe d’entité : elles seront automatiquement appelées par l’objet DataContext. En revanche, vous devez ajouter du code pour appeler la méthode OnCreated dans le constructeur de la classe d’entité. Appel de la méthode partielle OnCreated public Customer() { OnCreated(); ... }

Puis, pour chacune des propriétés de classe d’entité mappée, vous devez ajouter un appel aux méthodes On[Property]Changing et On[Property]Changed, juste avant et juste après la modification d’une propriété d’une classe d’entité. Appel des méthodes On[Property]Changing et On[Property]Changed dans la méthode set d’une classe d’entité public string CompanyName { get { return this._CompanyName; } set { if ((this._CompanyName != value)) { this.OnCompanyNameChanging(value); this.SendPropertyChanging(); this._CompanyName = value; this.SendPropertyChanged("CompanyName"); this.OnCompanyNameChanged(); } } }

La méthode On[Property]Changing est appelée avant la méthode SendPropertyChanging et la méthode On[Property]Changed, après la méthode SendPropertyChanged. En déclarant et en appelant ces méthodes partielles, vous donnez aux autres développeurs la capacité d’étendre les possibilités de base, sans pour autant que cela influe sur les performances, et ce qu’ils en tirent parti ou non. C’est là toute la beauté des méthodes partielles… Problèmes relatifs à EntityRef Alors que les membres d’une classe privée associée ont pour type EntityRef, leurs propriétés publiques doivent être du type de la classe d’entité, et non EntityRef. Voyons comment SQLMetal génère une propriété pour un membre privé EntityRef.

Linq.book Page 476 Mercredi, 18. février 2009 7:58 07

476

LINQ to SQL

Partie V

La propriété publique d’un membre de classe retourne le type de la classe et non EntityRef [Table(Name="dbo.Orders")] public partial class Order : INotifyPropertyChanging, INotifyPropertyChanged { ... private EntityRef _Customer; ... [Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID", IsForeignKey=true)] public Customer Customer { get { return this._Customer.Entity; } set { ... } } ... }

Comme vous pouvez le voir, même si le membre de classe privée _Customer est de type EntityRef, la propriété Customer est de type Customer, et non EntityRef. Ceci est important car, dans une requête, une référence à un type EntityRef ne pourra pas être traduite en SQL. Problèmes relatifs à EntitySet Si les propriétés publiques des membres de classes privés de type EntityRef devraient être de type T (et non EntityRef), ceci n’est plus vrai en ce qui concerne les propriétés publiques des membres de classes privés de type EntitySet. Examinons le code généré par SQLMetal pour un membre de classe privé de type EntitySet. Un membre de classe privé EntitySet et ses propriétés [Table(Name="dbo.Customers")] public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged { ... private EntitySet _Orders; ... [Association(Name="FK_Orders_Customers", Storage="_Orders", OtherKey="CustomerID", DeleteRule="NO ACTION")] public EntitySet Orders { get { return this._Orders; } set { this._Orders.Assign(value); } }

Openmirrors.com

Linq.book Page 477 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

477

... }

Comme vous le voyez, le type de la propriété retournée est EntitySet, c’est-àdire le même type que le membre de la classe privée. Comme EntitySet implémente l’interface ICollection, la propriété retournée peut être au format EntitySet si vous voulez cacher les détails de l’implémentation. Si vous écrivez vos propres classes d’entité, vous devez également savoir que, lorsque vous utilisez une instruction d’affectation publique pour une propriété EntitySet, vous devez passer par la méthode Assign, et non affecter la valeur au membre de la classe EntitySet. Cela permet à l’objet entité de continuer à utiliser la collection originale d’objets entité associés, puisque cette dernière est peut-être déjà surveillée par le code de pistage de changements de l’objet DataContext. Observez à nouveau le listing précédent. Vous verrez qu’au lieu d’affecter la variable membre this._Orders à la valeur de la variable value la méthode Assign est utilisée. Attributs des classes d’entité et propriétés des attributs Les classes d’entité sont définies par les attributs et les propriétés des attributs qui mappent la totalité de la classe à une table d’une base de données et les propriétés de la classe d’entité aux colonnes d’une table de la base de données. Les attributs définissent l’existence d’un mappage et les propriétés des attributs définissent comment se mapper. À titre d’exemple, c’est l’attribut Table qui définit qu’une classe est mappée sur une table, mais c’est la propriété Name qui spécifie le nom de la table à laquelle la classe est mappée.

La meilleure façon de comprendre comment fonctionnent les attributs et les propriétés des attributs consiste à examiner les attributs générés par un outil dédié à cette tâche. À titre d’exemple, nous allons examiner l’objet entité Customer généré par SQLMetal. Voici une partie de la classe d’entité Customer : [Table(Name="dbo.Customers")] public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged { ... [Column(Storage="_CustomerID", DbType="NChar(5) NOT NULL", CanBeNull=false, IsPrimaryKey=true)] public string CustomerID { get { return this._CustomerID; } set { if ((this._CustomerID != value)) { this.OnCustomerIDChanging(value); this.SendPropertyChanging(); this._CustomerID = value; this.SendPropertyChanged("CustomerID");

Linq.book Page 478 Mercredi, 18. février 2009 7:58 07

478

LINQ to SQL

Partie V

this.OnCustomerIDChanged(); } } } ... [Association(Name="FK_Orders_Customers", Storage="_Orders", OtherKey="CustomerID", DeleteRule="NO ACTION")] public EntitySet Orders { get { return this._Orders; } set { this._Orders.Assign(value); } } ... } }

Dans un souci de brièveté, nous n’avons conservé que les lignes qui contiennent des attributs LINQ to SQL (les attributs redondants ont également été supprimés). Le bloc de code ci-après correspond à une procédure stockée et à une fonction définie par l’utilisateur. [Function(Name="dbo.Get Customer And Orders")] [ResultType(typeof(GetCustomerAndOrdersResult1))] [ResultType(typeof(GetCustomerAndOrdersResult2))] public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID) { ... } [Function(Name="dbo.MinUnitPriceByCategory", IsComposable=true)] [return: Parameter(DbType="Money")] public System.Nullable MinUnitPriceByCategory( [Parameter(DbType="Int")] System.Nullable categoryID) { ... }

Les attributs apparaissent en gras dans les fragments de code précédents. Ces fragments ont été spécifiquement choisis pour alimenter notre discussion sur les attributs. L’attribut Database Dans une classe dérivée de DataContext, l’attribut Database spécifie le nom par défaut de la base de données mappée, si ce nom n’est pas spécifié dans les informations de connexion lors de l’instanciation du DataContext. Si aucune de ces deux alternatives n’est définie, le nom de la classe dérivée de DataContext est supposé être celui de la base de données. Openmirrors.com

Linq.book Page 479 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

479

Afin de clarifier les choses, voici l’ordre de précédence (du plus grand au plus petit) des différents endroits d’où peut provenir le nom de la base de données : 1. Les informations de connexion fournies lors de l’instanciation de la classe DataContext. 2. Le nom de la base de données spécifié dans l’attribut Database. 3. Le nom de la classe dérivée de DataContext. Voici la portion de code correspondante de la classe Northwind dérivée de DataContext et générée par SQLMetal : public partial class Northwind : System.Data.Linq.DataContext {

Comme vous pouvez le voir, l’attribut Database n’est pas spécifié dans la classe Northwind générée, dérivée de la classe DataContext. Étant donné que cette classe a été générée par un programme Microsoft, nous pouvons supposer que ceci est intentionnel. Pour spécifier implicitement la base de données NorthwindTest via l’attribut Database, vous devriez utiliser le code ci-après : L’attribut Database [Database(Name="NorthwindTest")] public partial class Northwind : System.Data.Linq.DataContext {

Je ne vois aucune raison d’omettre l’attribut Database. Peut-être est-ce parce que, si le nom de la base de données était indiqué dans les informations de connexion, il serait prépondérant par rapport au nom de la classe dérivée de DataContext et à l’attribut Database. Peut-être Microsoft a-t-il pensé que, si le nom de la base de données n’était pas spécifié dans les informations de connexion, ce serait celui de la classe dérivée de DataContext qui serait utilisé à la place. Après avoir réfléchi à tout ceci, je n’aime pas beaucoup l’idée d’une classe générée, dérivée de DataContext, qui se connecterait à une base de données par défaut. La possibilité d’exécuter une application – peut-être non intentionnellement – qui n’a pas encore été configurée et qui accéderait à une base de données par défaut me rend particulièrement mal à l’aise. C’est pourquoi je vous recommande de spécifier un attribut Database avec un nom intentionnellement ridicule dans le but d’éviter toute connexion à une base de données par défaut. Peut-être quelque chose comme ceci : Une classe dérivée de DataContext qui n’a pratiquement aucune chance de se connecter à une base de données par défaut [Database(Name="goopeygobezileywag")] public partial class Northwind : System.Data.Linq.DataContext {

Ce code empêchera la connexion à une base de données par défaut, à moins que son nom n’ait été spécifié dans les informations de connexion lors de l’instanciation du DataContext.

Linq.book Page 480 Mercredi, 18. février 2009 7:58 07

480

LINQ to SQL

Partie V

Name (string) La propriété Name d’un attribut est un objet de type string qui spécifie le nom de la base de données à laquelle se connecter si aucun nom n’a été indiqué dans les informations de connexion lors de l’instanciation de la classe dérivée de DataContext. Si la propriété Name n’est pas spécifiée et que le nom de la base de données n’est pas indiqué dans les informations de connexion, le nom de la classe dérivée de DataContext est supposé être celui de la base de données. Table L’attribut Table indique dans quelle table de la base de données la classe d’entité doit être enregistrée (le nom de la classe d’entité n’est pas forcément le même que celui de la table). Voici la portion de code correspondante dans la classe d’entité :

L’attribut Table [Table(Name="dbo.Customers")] public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged {

L’attribut Table spécifie le nom de la table de la base de données via la propriété Name. Si le nom de la classe d’entité est le même que celui de la table, la propriété Name peut être omise : par défaut, le nom de la classe sera alors celui de la table sur laquelle elle est mappée. Dans cet exemple, l’option "pluriel" ayant été spécifiée à SQLMetal lors de la génération des classes d’entité de la base de données Northwind par défaut, le nom de la classe est la version au singulier (Customer) du nom de la table de la base de données (Customers). Le nom de la classe étant différent du nom de la table, la propriété Name doit être spécifiée. Name (string) La propriété Name d’un attribut est un objet de type string qui spécifie le nom de la table sur laquelle la classe d’entité doit être mappée. Si la propriété Name n’est pas spécifiée, le nom de la classe d’entité sera mappé par défaut à une table de même nom. Column L’attribut Column met en relation une propriété d’une classe d’entité et une colonne dans une table d’une base de données. Voici la portion de code correspondante dans la classe d’entité :

L’attribut Column Private string _CustomerID; ... [Column(Storage="_CustomerID", DbType="NChar(5) NOT NULL", CanBeNull=false, IsPrimaryKey=true)] public string CustomerID {

Openmirrors.com

Linq.book Page 481 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

481

Dans cet exemple, la propriété Storage de l’attribut Column est spécifiée. LINQ to SQL peut donc accéder directement à la variable membre privée _CustomerID, en évitant de passer par l’accesseur de propriété publique CustomerID. Si la propriété Storage de l’attribut n’était pas spécifiée, l’accesseur public serait utilisé. Ce détail peut se révéler utile si vous voulez éviter d’exécuter le code qui se trouve dans les accesseurs de propriétés publiques. Le type de la base de données est spécifié dans l’attribut DbType. Ici, Nchar, de longueur 5 caractères. L’attribut CanBeNull étant initialisé à la valeur false, le champ Customer-ID ne peut avoir la valeur Null dans la base de données. Comme l’attribut IsPrimaryKey est spécifié et a la valeur true, cette colonne est un identifiant. Toutes les propriétés d’une classe d’entité ne sont pas nécessairement mappées à la base de données. Certaines propriétés n’ont une signification qu’à l’exécution et ne doivent pas être sauvegardées dans la base de données. Pour ces propriétés, vous omettrez l’attribut Column. Certaines colonnes sauvegardées dans la base de données peuvent n’être accessibles qu’en lecture seulement. Pour ce faire, il suffit de mapper la colonne, de spécifier la propriété Storage de l’attribut pour référencer la variable membre privée, mais de ne pas implémenter la méthode set de la propriété. Le DataContext peut toujours accéder au membre privé, mais, étant donné que la méthode set n’est pas définie, sa valeur ne peut être modifiée. AutoSync (AutoSync enum) La propriété Autosync d’un attribut est de type Autosync (enum). À l’exécution, elle demande de récupérer la valeur de la colonne mappée après une opération insert ou update dans la base de données. Les valeurs possibles sont Default, Always, Never, OnInsert et OnUpdate. Quelle est d’après vous la valeur utilisée par défaut ? Si l’on se réfère à la documentation Microsoft, il s’agit de la valeur Never.

Cette propriété est surchargée lorsque la propriété IsDbGenerated ou IsVersion est initialisée à true. CanBeNull (bool) La propriété CanBeNull d’un attribut est un booléen qui indique si une valeur de colonne dans la base de données mappée peut être nulle. La valeur par défaut est true. DbType (string) La propriété DbType d’un attribut est de type string. Elle indique le type de la colonne à laquelle cette propriété de classe d’entité est mappée. Si la propriété DbType n’est pas

Linq.book Page 482 Mercredi, 18. février 2009 7:58 07

482

LINQ to SQL

Partie V

spécifiée, le type de la colonne sera inféré à partir du type de donnée de la propriété de la classe d’entité. Cette propriété n’est utilisée que dans l’appel à la méthode CreateDatabase. Expression (string) La propriété Expression d’un attribut est de type string. Elle définit une colonne calculée dans la base de données. Elle n’est utilisée que conjointement à la méthode CreateDatabase. La valeur par défaut est String.Empty. IsDbGenerated (bool) La propriété IsDbGenerated d’un attribut est de type bool. Elle indique si la colonne de la table de la base de données sur laquelle est mappée la propriété de classe est automatiquement générée par la base de données. Si une clé primaire est spécifiée avec une propriété d’attribut IsDbGenerated initialisée à true, la propriété DbType de l’attribut de la propriété de classe doit être initialisée à IDENTITY.

Une propriété de classe dont la propriété IsDbGenerated de l’attribut est initialisée à true est immédiatement synchronisée après qu’un enregistrement eut été inséré dans la base de données, et ce quel que soit la valeur de la propriété de l’attribut AutoSync. Par ailleurs, la valeur synchronisée de la propriété de classe sera visible dans la propriété de classe dès que la méthode SubmitChanges aura été totalement exécutée. La valeur par défaut de cette propriété est false. IsDiscriminator (bool) La propriété IsDiscriminator d’un attribut est de type bool. Elle indique si la propriété de la classe d’entité mappée contient une valeur de discriminant pour une hiérarchie d’héritage. La valeur par défaut de cette propriété est false. Reportez-vous à la section relative à l’attribut InheritanceMapping, un peu plus loin dans ce chapitre, et à la section "Héritage des classes d’entité", au Chapitre 18, pour avoir des renseignements complémentaires. IsPrimaryKey (bool) La propriété IsPrimaryKey d’un attribut est de type bool. Elle indique si la colonne de la table à laquelle est mappée cette propriété de classe d’entité est une clé primaire. Plusieurs propriétés de la classe peuvent être spécifiées comme clés primaires. Dans ce cas, toutes les colonnes de la base de données mappée se comportent comme une clé primaire composite. Pour qu’un objet entité puisse être mis à jour, au moins une des propriétés des classes d’entité doit avoir une propriété d’attribut IsPrimaryKey initialisée à true. Dans le cas contraire, les objets entité mappés à cette table ne seront accessibles qu’en lecture seule. La valeur par défaut de cette propriété est false.

Openmirrors.com

Linq.book Page 483 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

483

IsVersion (bool) La propriété IsVersion d’un attribut est de type bool. Elle indique si la colonne de la base de données mappée est un numéro de version ou un timestamp (compteur numérique servant de référence temporelle) qui représente une information de version pour l’enregistrement. Si la propriété IsVersion est spécifiée et initialisée à true, la colonne de la base de données mappée est incrémentée (s’il s’agit d’un numéro de version) ou mise à jour (s’il s’agit d’un timestamp) chaque fois que l’enregistrement de la table de la base de données est mis à jour.

Une propriété de classe dont la propriété IsVersion d’un attribut vaut true est immédiatement synchronisée après l’insertion ou la mise à jour d’un enregistrement dans la base de données, et ce quelle que soit la valeur de la propriété Autosync. Par ailleurs, la valeur synchronisée de la propriété de la classe sera visible dans la propriété de la classe lorsque la méthode SubmitChanges aura été exécutée. La valeur par défaut de cette propriété est false. Name (string) La propriété d’un attribut Name est de type string. Elle spécifie le nom de la colonne sur laquelle cette propriété de classe est mappée. Si la propriété Name n’est pas spécifiée, la propriété de classe sera mappée par défaut sur une colonne de même nom. Storage (string) La propriété Storage d’un attribut est de type string. Elle indique la variable membre privée dans laquelle est mémorisée la valeur de la propriété de la classe d’entité. Cette propriété permet à LINQ to SQL de ne pas utiliser les accesseurs publics des propriétés et la logique métier qu’ils contiennent et d’accéder directement à la variable membre privée. Si la propriété Storage n’est pas spécifiée, les accesseurs publics de propriétés sont utilisés par défaut. UpdateCheck (UpdateCheck enum) La propriété UpdateCheck est de type UpdateCheck (enum). Elle contrôle le comportement de la détection de conflits concurrentiels, dans le cas où aucune propriété mappée n’a une propriété d’attribut IsVersion initialisée à true. Les trois valeurs possibles sont UpdateCheck.Always, UpdateCheck.WhenChanged et UpdateCheck.Never. Si aucune propriété d’une classe d’entité n’a une propriété IsVersion initialisée à true, la valeur de la propriété d’attribut UpdateCheck aura la valeur par défaut Always. Reportez-vous au Chapitre 17 pour avoir plus d’informations sur cette propriété et sur ses effets. Association L’attribut Association est utilisé pour définir des relations entre deux tables. Par exemple une relation entre une clé primaire et une clé étrangère. Dans l’exemple qui suit, l’entité dont la table mappée contient la clé primaire est appelée "parent" et l’entité dont

Linq.book Page 484 Mercredi, 18. février 2009 7:58 07

484

LINQ to SQL

Partie V

la table mappée contient la clé étrangère est appelée "enfant". Voici les portions de code correspondantes extraites de deux classes d’entité liées par une association : L’association de la classe d’entité parent (Customer) [Association(Name="FK_Orders_Customers", Storage="_Orders", OtherKey="CustomerID", DeleteRule="NO ACTION")] public EntitySet Orders {

L’association de la classe d’entité enfant (Order) [Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID", IsForeignKey=true)] public Customer Customer {

Dans cet exemple, nous utilisons la classe d’entité parent Customer et la classe d’entité enfant Order. Les attributs Association qui existent dans les deux classes d’entité ont été initialisés en conséquence. Certaines propriétés de l’attribut Association se rapportent à la classe dans laquelle l’attribut Association existe. D’autres seront relatives à la classe d’entité associée. Dans ce contexte, la classe dans laquelle l’attribut Association existe sera appelée "classe source", et la classe associée sera appelée "classe cible". Donc, si nous discutons des propriétés de l’attribut Association spécifié dans la classe d’entité Customer, cette dernière sera la classe source, et la classe d’entité Order sera la classe cible. Si nous parlons des propriétés de l’attribut Association spécifié dans la classe d’entité Order, cette dernière sera la classe source, et la classe d’entité Customer sera la classe cible. L’attribut Association indique que la classe d’entité source (Customer) a une relation avec la classe d’entité cible (Order). Dans les exemples précédents, la propriété Name est spécifiée pour donner un nom à la relation. La valeur de cette propriété correspond au nom de la clé étrangère de la base de données. Elle sera utilisée pour créer une restriction de clé étrangère si la méthode CreateDatabase est appelée. La propriété Storage est également spécifiée. Par son intermédiaire, LINQ to SQL peut contourner les accesseurs publics pour accéder directement à la valeur de la propriété de la classe d’entité. Une classe d’entité parent mémorise la référence à la classe ou aux classes d’entité enfant dans une collection EntitySet. En effet, il se peut qu’il y ait plusieurs enfants. En revanche, une classe d’entité enfant mémorise la référence à la classe d’entité parent dans un EntityRef. En effet, il ne peut y avoir qu’un seul parent. Reportez-vous aux sections intitulées "EntitySet" et "EntityRef", au Chapitre 14, pour avoir de plus amples informations sur les associations et leurs caractéristiques. Openmirrors.com

Linq.book Page 485 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

485

DeleteOnNull (bool) La propriété DeleteOnNull d’un attribut est de type bool. Elle indique si un objet entité situé du côté enfant d’une association doit être supprimé lorsque la référence à son parent a pour valeur null.

La valeur de cette propriété est déduite par SQLMetal si une règle Delete "en cascade" est spécifiée pour définir une contrainte sur la clé étrangère et si la colonne de la clé étrangère n’autorise pas la valeur null. DeleteRule (string) La propriété DeleteRule d’un attribut est de type string. Elle ajoute le comportement de suppression à une association. Elle est seulement utilisée par LINQ to SQL lorsqu’une contrainte est créée dans la base de données par la méthode CreateDatabase.

Les valeurs possibles sont "NO ACTION", "CASCADE", "SET NULL" et "SET DEFAULT". Si nécessaire, reportez-vous à votre documentation SQL Server pour prendre connaissance de la signification de ces valeurs. IsForeignKey (bool) La propriété IsForeignKey d’un attribut est de type bool. Si elle est initialisée à la valeur true, elle indique que la classe d’entité source est le côté de la relation qui contient la clé étrangère. C’est donc le côté enfant de la relation. La valeur par défaut de cette propriété est false.

Dans les exemples de l’attribut Association précédents, mettant en jeu les classes d’entité Customer et Order, la propriété IsForeignKey de l’attribut Association est initialisé à true dans la classe d’entité Order. Cette dernière joue donc le rôle de l’enfant dans la relation. IsUnique (bool) La propriété IsUnique d’un attribut est de type bool. Si elle vaut true, elle indique que la clé étrangère est unique. Il s’agit donc d’une relation "un-à-un" entre les deux classes d’entité. La valeur par défaut de cette propriété est false. Name (string) La propriété Name d’un attribut est une chaîne qui spécifie le nom de la contrainte appliquée à la clé étrangère. La contrainte est définie lorsque la méthode CreateDatabase est appelée. Cette propriété permet également de différencier des relations multiples entre deux entités. Dans ce cas, si les côtés parent et enfant de la relation spécifient tous deux un nom, il doit être identique.

S’il n’existe aucune relation multiple entre deux classes d’entité et que la méthode CreateDatabase ne soit pas appelée, cette propriété n’a aucun intérêt. Cette propriété n’a aucune valeur par défaut.

Linq.book Page 486 Mercredi, 18. février 2009 7:58 07

486

LINQ to SQL

Partie V

OtherKey (string) La propriété OtherKey d’un attribut est une chaîne délimitée par des virgules. Elle liste toutes les propriétés de la classe d’entité cible qui constituent la clé (primaire ou étrangère, en fonction du côté de la relation dans lequel se trouve l’entité cible). Si cette propriété n’est pas spécifiée, les membres clé primaire de la classe d’entité cible sont utilisés par défaut.

Il est important de réaliser que l’attribut Association, spécifié de chaque côté de l’association (Customer et Order), indique où se trouvent les clés des deux côtés de la relation. L’attribut Association spécifié dans la classe d’entité Customer spécifie quelle propriété de la classe d’entité Customer contient la clé pour la relation et quelle propriété de la classe d’entité Order contient la clé pour la relation. Les deux côtés d’une relation ne spécifient pas toujours l’emplacement des clés des deux côtés de la relation. Ceci parce que, généralement, du côté parent de la relation la clé primaire de la table est la clé utilisée. La propriété ThisKey n’a pas besoin d’être spécifiée, puisque la clé primaire est la valeur par défaut. Du côté enfant, la propriété OtherKey n’a pas besoin d’être spécifiée, car la clé primaire du parent est la valeur par défaut. Par conséquent, il est courant de voir la propriété OtherKey spécifiée uniquement du côté parent et la propriété ThisKey, uniquement du côté enfant. Grâce aux valeurs par défaut, chaque côté de la relation connaît la clé de l’autre. Storage (string) La propriété Storage d’un attribut est de type string. Elle spécifie la variable membre privée dans laquelle est stockée la valeur provenant de la base de données. Par son intermédiaire, LINQ to SQL peut accéder directement à la variable membre privée, sans passer par les accesseurs publics des propriétés des classes d’entité. Ceci permet de ne pas dépendre d’une éventuelle logique métier qui se trouverait dans les accesseurs. Si la propriété Storage n’est pas spécifiée, les accesseurs publics de la propriété sont utilisés par défaut.

Microsoft recommande que les deux membres d’une association soient des propriétés de classe d’entité, qu’ils stockent les données dans des variables membres de classes d’entité séparées et qu’enfin la propriété Storage soit spécifiée. ThisKey (string) La propriété ThisKey de l’attribut est une chaîne contenant des données délimitées par des virgules. Elle liste toutes les propriétés de la classe d’entité source qui constituent la clé (primaire ou étrangère, en fonction du côté de la relation dans lequel se trouve l’entité cible). Si cette propriété n’est pas spécifiée, les membres clé primaire de la classe d’entité source sont utilisés par défaut.

Quelques pages plus tôt, l’attribut Association pris en exemple pour la classe d’entité Customer ne contenait pas la propriété IsForeignKey. Nous savons donc que la classe Openmirrors.com

Linq.book Page 487 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

487

d’entité Customer est le côté parent de la relation, c’est-à-dire le côté qui contient la clé primaire. Comme l’attribut Association ne spécifiait pas la propriété d’attribut ThisKey, nous savons que la valeur de la clé primaire de la table Customer est la clé étrangère de la table associée, Orders. Comme l’attribut Association de la classe d’entité Order prise en exemple précédemment spécifie l’attribut IsForeignKey et lui affecte la valeur true, nous savons que la table Orders est le côté de l’association qui contient la clé étrangère. Par ailleurs, comme l’attribut Association spécifie la propriété ThisKey et lui affecte la valeur CustomerID, nous savons que la clé étrangère sera stockée dans la colonne CustomerID de la table Orders. Il est important de réaliser que l’attribut Association spécifié de chaque côté de l’association (Customer et Order) indique où les clés sont situées des deux côtés de l’association. L’attribut Association spécifié dans la classe d’entité Customer spécifie quelle propriété de la classe d’entité Customer contient la clé pour la relation et quelle propriété de la classe d’entité Order contient la clé pour la relation. De la même façon, l’attribut Association spécifié dans la classe d’entité Order indique quelle propriété de classe d’entité Order contient la clé pour la relation et quelle propriété de la classe d’entité Customer contient la clé pour la relation. Les deux côtés d’une relation ne spécifient pas toujours l’emplacement des clés des deux côtés de la relation. Ceci parce que, généralement, du côté parent de la relation la clé primaire de la table est la clé utilisée. La propriété ThisKey n’a pas besoin d’être spécifiée, puisque la clé primaire est la valeur par défaut. Du côté enfant, la propriété OtherKey n’a pas besoin d’être spécifiée, car la clé primaire du parent est la valeur par défaut. Par conséquent, il est courant de voir la propriété OtherKey spécifiée uniquement du côté parent et la propriété ThisKey, uniquement du côté enfant. Grâce aux valeurs par défaut, chaque côté de la relation connaît la clé de l’autre. Function L’attribut Function définit la procédure stockée ou la fonction définie par l’utilisateur (valeur scalaire ou table) appelée lors de l’appel d’une méthode de classe. Voici la portion de code dérivée de la classe DataContext pour une procédure stockée.

Une fonction Attribute qui mappe une méthode vers une procédure stockée dans la base de données Northwind [Function(Name="dbo.Get Customer And Orders")] [ResultType(typeof(GetCustomerAndOrdersResult1))] [ResultType(typeof(GetCustomerAndOrdersResult2))] public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID) { ... }

Linq.book Page 488 Mercredi, 18. février 2009 7:58 07

488

LINQ to SQL

Partie V

Dans ce code, la méthode GetCustomerAndOrders appelle la procédure stockée "Get Customers And Orders". Nous savons que la méthode sera mappée à une procédure stockée, et non à une fonction définie par l’utilisateur. En effet, la propriété IsComposable n’étant pas spécifiée, sa valeur par défaut est false et le mappage se fait vers une procédure stockée. Nous pouvons également voir que la fonction retourne plusieurs formes de résultats, car deux attributs ResultType sont spécifiés. L’écriture d’une classe dérivée de DataContext qui est capable d’appeler une procédure stockée n’est pas aussi simple que le mappage d’une classe d’entité à une table. En plus de spécifier les bons attributs, vous devez également appeler la bonne version de la méthode ExecuteMethodCall de la classe DataContext. Vous en apprendrez plus à ce sujet au Chapitre 16. Bien entendu, ceci n’est nécessaire que si vous écrivez votre classe DataContext à la main. Dans le cas contraire, SQLMetal ou le Concepteur Objet/Relationnel feront le travail à votre place. Voici la portion de code dérivée de la classe DataContext pour une fonction définie par l’utilisateur. Une fonction Attribute qui mappe une méthode vers une fonction définie par l’utilisateur dans la base de données Northwind [Function(Name="dbo.MinUnitPriceByCategory", IsComposable=true)] [return: Parameter(DbType="Money")] public System.Nullable MinUnitPriceByCategory( [Parameter(DbType="Int")] System.Nullable categoryID) { ... }

Dans ce code, la méthode MinUnitPriceByCategory appelle la fonction définie par l’utilisateur "MinUnitPriceByCategory". Nous savons que la méthode sera mappée à une fonction définie par l’utilisateur, et non à une procédure stockée. En effet, la propriété IsComposable est initialisée à true. Nous pouvons également voir dans l’attribut return que la fonction définie par l’utilisateur retournera une valeur de type Money. L’écriture d’une classe dérivée de DataContext qui est capable d’appeler une fonction définie par l’utilisateur n’est pas aussi simple que le mappage d’une classe d’entité à une table. En plus de spécifier les bons attributs, vous devez également appeler la bonne version de la méthode ExecuteMethodCall (pour les fonctions qui renvoient une valeur de type scalaire) ou de la méthode CreateMethodCallQuery (pour les fonctions qui renvoient une valeur de type table). Vous en apprendrez plus à ce sujet au Chapitre 16. Bien entendu, ceci n’est nécessaire que si vous écrivez votre classe DataContext à la main. Dans le cas contraire, SQLMetal ou le Concepteur Objet/Relationnel feront le travail à votre place. Openmirrors.com

Linq.book Page 489 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

489

IsComposable (bool) La propriété IsComposable d’un attribut est de type bool. Elle indique si la fonction mappée appelle une procédure stockée (false) ou une fonction définie par l’utilisateur (true). Lorsqu’elle n’est pas spécifiée, la valeur par défaut de cette propriété est false. Une méthode mappée avec l’attribut Function appellera donc une procédure stockée si la propriété d’attribut IsComposable n’est pas spécifiée. Name (string) La propriété Name d’un attribut est un string. Elle indique le nom actuel de la procédure stockée ou de la fonction définie par l’utilisateur dans la base de données. Si la propriété Name n’est pas spécifiée, le nom de la procédure stockée ou de la fonction définie par l’utilisateur est supposé être le même que celui de la méthode. return L’attribut return est utilisé pour spécifier la donnée retournée par une procédure stockée ou une fonction définie par l’utilisateur. Il contient généralement un attribut Parameter.

Un attribut return pour la classe Northwind [Function(Name="dbo.MinUnitPriceByCategory", IsComposable=true)] [return: Parameter(DbType="Money")] public System.Nullable MinUnitPriceByCategory( [Parameter(DbType="Int")] System.Nullable categoryID) { ... }

Dans ce code, la ligne en gras nous montre que la fonction définie par l’utilisateur retournera une valeur de type Money (voir attribut return et propriété dbType de l’attribut Parameter). ResultType L’attribut ResultType indique le ou les types de données qui peuvent être retournés par une procédure stockée. Si une procédure stockée retourne plusieurs formes, plusieurs attributs ResultType doivent être spécifiés, dans l’ordre de leur retour.

Attributs ResultType issus de la classe Northwind [Function(Name="dbo.Get Customer And Orders")] [ResultType(typeof(GetCustomerAndOrdersResult1))] [ResultType(typeof(GetCustomerAndOrdersResult2))] public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID) { ... }

En examinant le code précédent, nous voyons que la procédure stockée à laquelle cette méthode est mappée retournera une forme de type GetCustomerAndOrdersResult1, puis une forme de type GetCustomerAndOrdersResult2. SQLMetal est assez prévenant

Linq.book Page 490 Mercredi, 18. février 2009 7:58 07

490

LINQ to SQL

Partie V

pour générer automatiquement des classes d’entité pour GetCustomerAndOrdersResult1 et GetCustomerAndOrdersResult2. Parameter L’attribut Parameter mappe un paramètre d’une méthode à un paramètre d’une procédure stockée/d’une fonction définie par l’utilisateur. Voici une portion de code correspondante dans la classe dérivée de DataContext pour la base de données Northwind :

Un attribut Parameter issu de la classe Northwind [Function(Name="dbo.Get Customer And Orders")] [ResultType(typeof(GetCustomerAndOrdersResult1))] [ResultType(typeof(GetCustomerAndOrdersResult2))] public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID) { ... }

En examinant ce code, nous voyons que la méthode GetCustomersAndOrders, mappée à la procédure stockée "Get Customers And Orders", lui passe le paramètre CustomerID de type NChar(5). DbType (string) La propriété DbType d’un attribut est au format string. Elle indique le type d’un paramètre de la procédure stockée/de la fonction définie par l’utilisateur. Name (string) La propriété Name d’un attribut est au format string. Elle spécifie le nom actuel du paramètre de la procédure stockée/de la fonction définie par l’utilisateur. Si la propriété Name n’est pas spécifiée, ce nom est supposé être identique à celui du paramètre de la méthode. InheritanceMapping L’attribut InheritanceMapping est utilisé pour mapper un "code discriminant" à une classe de base ou une sous-classe de cette classe de base. Le code discriminant est la valeur de la colonne discriminateur d’une classe d’entité (celle dont la propriété IsDiscriminator de l’attribut vaut true). Examinons l’attribut InheritanceMapping : [InheritanceMapping(Code = "G", Type = typeof(Shape), IsDefault = true)]

L’attribut InheritanceMapping précédent indique que si le code discriminant d’un enregistrement de la base de données vaut "G" (en d’autres termes, si la valeur de sa colonne discriminateur est "G"), cet enregistrement sera instancié en tant qu’un objet Shape en utilisant la classe Shape. La propriété IsDefault de l’attribut étant initialisée à true, si le code discriminant d’un enregistrement ne correspond à aucune des valeurs Code des attributs InheritanceMapping, cet enregistrement sera instancié en tant qu’objet Shape en utilisant la classe Shape. Openmirrors.com

Linq.book Page 491 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

491

Pour utiliser le mappage d’héritage, il suffit d’affecter la valeur true à la propriété qui correspond à la propriété Column de l’attribut IsDiscriminator lors de la déclaration de la classe d’entité de base. La valeur de cette colonne détermine donc, par discrimination, de quelle classe (classe de base ou sous-classe) un enregistrement d’une table d’une base de données est une instance. Un attribut InheritanceMapping est précisé dans la classe de base pour chacune de ses sous-classes et pour la classe de base ellemême. Un et un seul de ces attributs InheritanceMapping doit avoir une propriété IsDefault initialisée à true. Un enregistrement d’une table de la base de données dont la colonne discriminateur ne correspond à aucun des codes discriminants spécifiés dans les attributs InheritanceMapping sera donc quand même instancié dans une classe. Il est probablement plus courant pour l’attribut InheritanceMapping de la classe de base d’être spécifié comme l’attribut InheritanceMapping par défaut. Rappelons à nouveau que les attributs InheritanceMapping sont seulement spécifiés dans la classe de base et qu’ils associent un code discriminant à la classe de base ou à une de ses sous-classes. Aucune table de la base de données Northwind n’étant utilisée de la sorte, nous allons faire appel à trois autres classes. Quelques classes exemples pour illustrer le mappage d’héritage [Table] [InheritanceMapping(Code = "G", Type = typeof(Shape), IsDefault = true)] [InheritanceMapping(Code = "S", Type = typeof(Square))] [InheritanceMapping(Code = "R", Type = typeof(Rectangle))] public class Shape { [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL IDENTITY")] public int Id; [Column(IsDiscriminator = true, DbType = "NVarChar(2)")] public string ShapeCode; [Column(DbType = "Int")] public int StartingX; [Column(DbType = "Int")] public int StartingY; } public class Square : Shape { [Column(DbType = "Int")] public int Width; } public class Rectangle : Square { [Column(DbType = "Int")] public int Length; }

Linq.book Page 492 Mercredi, 18. février 2009 7:58 07

492

LINQ to SQL

Partie V

Dans ce listing, la classe Shape a été mappée à une table. La propriété Name de l’attribut n’ayant pas été spécifiée, la classe Shape sera mappée par défaut à une table nommée Shape. Les premières lignes définissent trois attributs InheritanceMapping. Le premier indique que, si la valeur de la colonne discriminateur d’un enregistrement de la table Shape a pour valeur "G", cet enregistrement doit être instancié en tant qu’objet Shape en utilisant la classe Shape. Ici, la lettre "G" signifie "générique". Il s’agit donc d’une forme non définie générique. Le discriminant étant la propriété ShapeCode de la classe Shape (sa propriété IsDiscriminator est initialisée à true), si un enregistrement a une propriété ShapeCode égale à "G", il sera instancié en un objet Shape. Vous pouvez également remarquer que le premier attribut InheritanceMapping a une propriété IsDefault initialisée à true. Si la colonne ShapeCode d’un enregistrement Shape ne correspond à aucun des codes discriminants spécifiés ("G", "S" et "R"), le mappage par défaut sera utilisé et l’enregistrement sera instancié en tant qu’objet Shape. Le deuxième attribut InheritanceMapping associe le code discriminant "S" à la classe Square. Si un enregistrement dans la table Shape a un ShapeCode égal à "S", il sera instancié en tant qu’objet Square. Le troisième attribut InheritanceMapping associe le code discriminant "R" à la classe Rectangle. Si un enregistrement dans la table Shape a un ShapeCode égal à "R", il sera instancié en tant qu’objet Rectangle. Tout enregistrement dont le ShapeCode diffère des trois valeurs spécifiées sera instancié en un objet Shape, car la classe Shape a été spécifiée par défaut en utilisant la propriété IsDefault. INFO Vous trouverez une présentation plus complète et des exemples du mappage d’héritage au Chapitre 18.

Code (object) La propriété Code d’un attribut spécifie quel est le code discriminant du mappage de la classe spécifiée dans la propriété Type de l’attribut. IsDefault (bool) La propriété IsDefault d’un attribut est de type bool. Elle indique si l’attribut InheritanceMapping doit être utilisé dans le cas où une colonne discriminateur d’un enregistrement d’une table de la base de données ne correspond à aucun des attributs InheritanceMapping spécifiés.

Openmirrors.com

Linq.book Page 493 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

493

Type (Type) La propriété Type d’un attribut spécifie le type de la classe en lequel l’enregistrement sera instancié lorsque la colonne discriminateur correspond au code discriminant mappé.

Compatibilité de type de donnée Certains attributs de classe d’entité possèdent une propriété DbType qui permet de spécifier le type de donnée de la colonne. Cette propriété n’est utilisée qu’à la création de la base de données avec la méthode CreateDatabase. Le mappage entre les types de données .NET et les types de données SQL Server n’étant pas de type "un-à-un", vous devrez spécifier la propriété DbType si vous utilisez la méthode CreateDatabase. Étant donné que les types de données de la CLR (Common Language Runtime) .NET utilisés dans le code LINQ ne sont pas identiques à ceux utilisés dans la base de données, vous devriez consulter la documentation MSDN à la rubrique "SQL-CLR Type Mapping (LINQ to SQL)". Vous y trouverez un tableau qui définit les correspondances entre les types CLR et les types SQL. Si vous avez du mal à trouver ces informations, reportez-vous à la section "Additional LINQ to SQL Resources" sur le site web de l’ouvrage, www.linqdev.com. Vous y trouverez un lien qui mène directement à la bonne page sur MSDN. Faites attention, toutes les conversions ne sont pas possibles, et d’autres peuvent provoquer la perte de données, en fonction du type des données impliquées et du sens de la conversion ! N’ayez crainte, le processus de conversion fonctionnera convenablement dans la plupart des situations. Lors de la mise au point des exemples de ce chapitre, je n’ai rencontré aucun problème lié à la conversion des types de données. Dans tous les cas, utilisez votre bon sens : si vous essayez de convertir des types incompatibles, par exemple un type numérique .NET en un type caractère SQL, vous courez droit vers un problème de conversion… Schéma de fichier de mappage externe XML Comme il a été dit dans la section relative à SQLMetal du Chapitre 13, cet outil peut être utilisé pour mapper des classes à une base de données, mais vous pouvez également passer par un fichier de mappage externe XML. Vous en apprendrez plus à ce sujet lorsque nous parlerons des constructeurs de la classe DataContext, au Chapitre 16. La façon la plus simple d’obtenir un fichier de mappage externe XML (voir Chapitre 13) consiste à utiliser le programme SQLMetal, en spécifiant l’option /map. Si vous avez l’intention de définir le fichier de mappage manuellement, vous devrez connaître le schéma à utiliser.

Linq.book Page 494 Mercredi, 18. février 2009 7:58 07

494

LINQ to SQL

Partie V

Reportez-vous à la documentation MSDN, et en particulier à la page "External Mapping Reference (LINQ to SQL)" pour avoir des renseignements complémentaires à ce sujet. Si vous avez du mal à trouver cette page, reportez-vous à la section "Additional LINQ to SQL Resources" sur le site web de l’ouvrage, www.linqdev.com. Vous y trouverez un lien qui mène directement à la bonne page sur MSDN. Projection dans des classes d’entité/des classes de non-entité Lorsque vous effectuez des requêtes LINQ to SQL, les résultats retournés peuvent être projetés dans une classe d’entité ou dans une classe de non-entité (une classe nommée ou anonyme). Il existe une différence majeure entre ces deux types de projections. Lors d’une projection dans une classe d’entité, cette dernière profite de la recherche d’identité de l’objet DataContext, ainsi que de la recherche de changement et des services associés. Ainsi, vous pouvez effectuer des modifications dans la classe d’entité et les rendre persistantes dans la base de données en utilisant la méthode SubmitChanges. Lors d’une projection dans une classe de non-entité, mis à part un cas bien particulier, vous ne pouvez pas profiter de la recherche d’identité de l’objet DataContext, de la recherche de changement et des services associés. Cela signifie que vous ne pouvez pas modifier la classe de non-entité et voir les modifications reportées dans la base de données en utilisant LINQ to SQL. Ceci est facile à comprendre. Effectivement, la classe n’a pas les attributs ou le fichier de mappage nécessaires pour mapper la classe à la base de données. Si elle les avait, ce serait par définition… une classe d’entité. Voici un exemple de requête qui se projette dans une classe d’entité : La projection dans une classe d’entité donne accès aux services du DataContext IEnumerable custs = from c in db.Customers select c;

Après avoir exécuté cette requête, nous pourrions modifier n’importe quel objet entité Customer en utilisant la séquence custs et rendre des modifications permanentes en appelant la méthode SubmitChanges. Voici un exemple de requête qui se projette dans une classe de non-entité : La projection dans une classe d’entité ne donne pas accès aux services du DataContext var custs = from c in db.Customers select new { Id = c.CustomerID, Name = c.ContactName };

En projetant les résultats dans une classe de non-entité, il n’est pas possible de rendre permanentes les modifications apportées aux objets via la séquence custs en appelant la méthode SubmitChanges. Un peu plus haut, il a été dit qu’un cas particulier permettait à la projection dans une classe de non-entité de profiter des facilités de persistance inhérentes au DataContext. Ce cas se produit lorsque la classe dans laquelle sont projetés les résultats contient des membres qui sont des classes d’entité (voir Listing 15.1). Openmirrors.com

Linq.book Page 495 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

495

Listing 15.1 : Projection dans une classe de non-entité qui contient des classes d’entité. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); var cusorders = from o in db.Orders where o.Customer.CustomerID == "CONSH" orderby o.ShippedDate descending select new { Customer = o.Customer, Order = o }; // Première commande Order firstOrder = cusorders.First().Order; // Sauvegarde du premier champ shipcountry de la commande pour une utilisation future string shipCountry = firstOrder.ShipCountry; Console.WriteLine("Avant modification, pays d’expédition de la commande : {0}", shipCountry); // Modification de la valeur du champ shipcountry en "USA" firstOrder.ShipCountry = "USA"; db.SubmitChanges(); // Interrogation de la base pour voir si le champ ShipCountry a été modifié string country = (from o in db.Orders where o.Customer.CustomerID == "CONSH" orderby o.ShippedDate descending select o.ShipCountry).FirstOrDefault(); Console.WriteLine("Avant modification, pays d’expédition de la commande : {0}", ➥country); // Restauration de la valeur par défaut dans le champ ShipCountry firstOrder.ShipCountry = shipCountry; db.SubmitChanges();

Le Listing 15.1 récupère les commandes passées par le client "CONSH". Les commandes sont retournées dans un type anonyme qui contient un client ( Customer) et une ou plusieurs commandes (Order). La classe anonyme ne bénéficie pas des services du DataContext. Cependant, ses composants Customer et Order en bénéficient, car il s’agit de classes d’entité. Une deuxième requête est appliquée aux résultats de la première pour obtenir la première commande. Le champ ShipCountry est sauvegardé, afin de pouvoir restaurer la valeur originale à la fin de l’exemple, et sa valeur actuelle est affichée dans la console. La valeur du champ ShipCountry est alors modifiée et la modification est enregistrée en appelant la méthode SubmitChanges. La base de données est à nouveau interrogée pour obtenir la valeur du champ ShipCountry. Cette valeur est affichée pour prouver que la modification a été reportée dans la base de données. La méthode SubmitChanges a donc bien fonctionné et les composants des classes d’entité du type anonyme ont bénéficié des services de l’objet DataContext. Pour terminer, le champ ShipCountry est restauré à sa valeur originale et sauvegardé dans la base de données. Ceci afin de pouvoir exécuter plusieurs fois l’exemple et de ne pas affecter les exemples suivants. Voici les résultats du Listing 15.1 : Avant modification, pays d’expédition de la commande : UK Après modification, pays d’expédition de la commande : USA

Linq.book Page 496 Mercredi, 18. février 2009 7:58 07

496

LINQ to SQL

Partie V

Cet exemple montre qu’il est possible de profiter des services de persistance inhérents au DataContext lorsque la projection est effectuée dans une classe de non-entité, à condition qu’une classe d’entité soit incluse dans la classe de non-entité. Dans le code précédent, vous avez peut-être remarqué que la requête qui obtient une référence à la première commande apparaît en gras. C’était dans le but d’attirer votre attention. L’opérateur First a été invoqué avant de sélectionner la portion de la séquence à laquelle le code s’intéresse, à savoir le membre Order. Ceci a été fait pour améliorer les performances. En effet, d’une manière générale, plus vite vous pouvez limiter les résultats, meilleures sont les performances. Dans une projection, préférez l’initialisation d’objet à la construction paramétrée Il est toujours possible d’effectuer une projection dans des classes avant la fin d’une requête pour effectuer d’autres opérations de requêtes. Si vous avez recours à ce procédé, préférez l’initialisation d’objet à la construction paramétrée. Pour bien comprendre pourquoi, reportez-vous au Listing 15.2, qui utilise l’initialisation d’objet dans la projection. Listing 15.2 : Projection en utilisant l’initialisation d’objet. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; var contacts = from c in db.Customers where c.City == "Buenos Aires" select new { Name = c.ContactName, Phone = c.Phone } into co orderby co.Name select co; foreach (var contact in contacts) { Console.WriteLine("{0} - {1}", contact.Name, contact.Phone); }

Dans ce listing, la projection a été effectuée dans une classe anonyme et nous avons utilisé l’initialisation d’objets pour initialiser les objets anonymes créés. Examinons le résultat du Listing 15.2 : SELECT [t0].[ContactName] AS [Name], [t0].[Phone] FROM [dbo].[Customers] AS [t0] WHERE [t0].[City] = @p0 ORDER BY [t0].[ContactName] -- @p0: Input String (Size = 12; Prec = 0; Scale = 0) [Buenos Aires] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Patricio Simpson - (1) 135-5555 Sergio Gutiérrez - (1) 123-5555 Yvonne Moncada - (1) 135-5333

Openmirrors.com

Linq.book Page 497 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

497

Les résultats de la requête importent peu. Ce qui importe ici, c’est la requête SQL générée. Mais alors, pourquoi avoir utilisé une boucle foreach ? Eh bien, tout simplement pour provoquer l’exécution de la requête différée. Les parties intéressantes de la requête LINQ to SQL sont les déclarations select et orderby. Ici, nous demandons à la requête de créer le membre Name dans la classe anonyme et de lui affecter le champ ContactName de la table Customers. Nous demandons alors à la requête de trier les membres Name de l’objet anonyme dans lequel a été effectuée la projection. Toutes ces informations sont passées à l’objet DataContext. En effet, l’initialisation d’objet mappe le champ source ContactName de la classe Customer dans le champ de destination Name de la classe anonyme, et l’objet DataContext est au courant de ce mappage. Fort de cette information, il est en mesure de savoir que les clients sont triés par l’intermédiaire du champ ContactName. Il peut donc générer la requête SQL correspondante. Voyons maintenant ce qui se passe lorsque la projection s’effectue dans une classe nommée en utilisant la construction paramétrée. Pour commencer, nous aurons besoin d’une classe nommée : La classe nommée utilisée dans le Listing 15.3 class CustomerContact { public string Name; public string Phone; public CustomerContact(string name, string phone) { Name = name; Phone = phone; } }

Cette classe compte un seul constructeur qui admet deux paramètres : Name et Phone. Le Listing 15.3 reprend le code du Listing 15.2 mais, ici, la projection s’effectue dans la classe CustomerContact en utilisant la construction paramétrée. Listing 15.3 : Projection en utilisant la construction paramétrée. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; var contacts = from c in db.Customers where c.City == "Buenos Aires" select new CustomerContact(c.ContactName, c.Phone) into co orderby co.Name select co; foreach (var contact in contacts) { Console.WriteLine("{0} - {1}", contact.Name, contact.Phone); }

Linq.book Page 498 Mercredi, 18. février 2009 7:58 07

498

LINQ to SQL

Partie V

Ici encore, nous nous intéresserons aux déclarations select et orderby. Comme vous pouvez le voir, la projection se fait non pas dans une classe anonyme, mais dans la classe CustomerContact. Par ailleurs, plutôt qu’initialiser les objets créés en utilisant l’initialisation d’objets, nous avons recours à un constructeur paramétré. Ce code passe l’étape de la compilation mais, à l’exécution, une exception est levée : Exception non gérée : System.InvalidOperationException: erreur de binding : ➥le membre’LINQChapter15.CustomerContact.Name’ n’est pas mappé à ➥’LINQChapter15.CustomerContact’. …

Que s’est-il passé ? Observez la requête LINQ to SQL précédente et posez-vous la question "comment le DataContext sait-il quel champ de la classe Customer est mappé au membre CustomerContact.name auquel nous tentons d’accéder ?". Dans le Listing 15.2, comme les noms des champs de la classe anonyme lui étaient passés, il savait que le champ source dans la classe Customer était ContactName et que le champ de destination dans la classe anonyme était Name. Dans le Listing 15.3, ce mappage n’est pas effectué dans la requête LINQ to SQL : il se produit dans le constructeur de la classe CustomerContact, et le DataContext n’en est pas informé. Par ailleurs, il ne sait pas sur quel champ de la classe source Customer il doit agir lors de la génération de la déclaration SQL. Voilà d’où vient le problème ! Sachez cependant qu’il est possible d’utiliser la construction paramétrée tant qu’aucune référence aux membres de la classe nommée n’est faite après la projection dans la requête (voir Listing 15.4). Listing 15.4 : Projection en utilisant la construction paramétrée sans référencer des membres. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; var contacts = from c in db.Customers where c.City == "Buenos Aires" select new CustomerContact(c.ContactName, c.Phone); foreach (var contact in contacts) { Console.WriteLine("{0} - {1}", contact.Name, contact.Phone); }

Dans ce listing, étant donné que nous utilisons la syntaxe de l’expression de requête et que cette dernière nécessite que la requête se termine par une déclaration select, nous pouvons utiliser la construction paramétrée dans l’élément select (le dernier élément) de la requête. Ceci est possible car aucune référence aux membres de la classe nommée n’est faite après la déclaration select. Voici les résultats du Listing 15.4 : SELECT [t0].[ContactName], [t0].[Phone] FROM [dbo].[Customers] AS [t0]

Openmirrors.com

Linq.book Page 499 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

499

WHERE [t0].[City] = @p0 -- @p0: Input String (Size = 12; Prec = 0; Scale = 0) [Buenos Aires] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 Patricio Simpson - (1) 135-5555 Yvonne Moncada - (1) 135-5333 Sergio Gutiérrez - (1) 123-5555

La syntaxe traditionnelle "à point" ne nécessite pas que la requête se termine par une déclaration select. Il est donc possible qu’une requête qui utilise la construction paramétrée n’arrive pas à s’exécuter. Dans le Listing 15.5, la requête utilise une syntaxe standard "à point". La dernière projection (Listing 15.3) utilise la construction paramétrée. Cependant, comme la ligne suivante de la requête référence un membre de la classe nommée, l’exécution de la requête produit une exception. Listing 15.5 : Projection en utilisant la construction paramétrée et en référençant un membre. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; var contacts = db.Customers.Where(c => c.City == "Buenos Aires"). Select(c => new CustomerContact(c.ContactName, c.Phone)). OrderBy(c => c.Name); foreach (var contact in contacts) { Console.WriteLine("{0} - {1}", contact.Name, contact.Phone); }

La requête du Listing 15.5 est très semblable à celle du Listing 15.4, si ce n’est qu’elle utilise une syntaxe traditionnelle "à point" et non une syntaxe d’expression de requête et qu’un appel à l’opérateur OrderBy est effectué après la déclaration select. Bien que nous utilisions la construction paramétrée dans la dernière projection, une exception est générée parce que l’opérateur OrderBy fait référence à un membre de la classe nommée. Voici les résultats du Listing 15.5 : Exception non gérée : System.InvalidOperationException: erreur de binding : ➥le membre’LINQChapter15.CustomerContact.Name’ n’est pas mappé à ➥’LINQChapter15.CustomerContact’. …

Pour éviter ce genre de problème, je vous conseille d’utiliser l’initialisation d’objet et non la construction paramétrée à chaque fois que cela est possible.

Extension des classes d’entité avec des méthodes partielles Les nouveaux programmeurs LINQ se sont souvent plaints qu’il n’était pas possible de savoir ce qui se passait à l’intérieur d’une classe d’entité. Pendant la phase d’incubation de LINQ, il n’y avait aucun moyen pour un développeur de savoir quand une propriété

Linq.book Page 500 Mercredi, 18. février 2009 7:58 07

500

LINQ to SQL

Partie V

d’un objet d’une classe d’entité était changée ou quand une classe d’entité était créée, si ce n’est en modifiant le code de classe de l’entité générée. Malheureusement, toute modification dans ce sens est systématiquement perdue lorsque le code nécessite d’être généré une nouvelle fois. Cette technique n’est donc pas pérenne. Heureusement, les ingénieurs de Microsoft étaient à l’écoute… Au Chapitre 2, nous avons introduit les méthodes partielles. C’est treize chapitres plus loin qu’elles vont se révéler très utiles. Microsoft a déterminé l’endroit précis dans la vie d’une classe d’entité où les développeurs ont le plus de chance de vouloir apporter des modifications et ajouter des appels à des méthodes partielles. Voici la liste des méthodes partielles utilisables dans une classe d’entité : Les méthodes partielles utilisables dans une classe d’entité partial partial partial partial partial

void void void void void

OnLoaded(); OnValidate(ChangeAction action); OnCreated(); On[Property]Changing([Type] value); On[Property]Changed();

Dans les deux dernières méthodes, [Property] doit être remplacé par un nom de propriété et [Type], par un type de propriété. Pour illustrer quelques-unes des méthodes partielles supportées par les classes d’entité, nous allons ajouter la classe suivante dans la classe d’entité Contact : Une autre déclaration pour la classe Contact permettant d’implémenter des méthodes partielles namespace nwind { public partial class Contact { partial void OnLoaded() { Console.WriteLine("OnLoaded() appelée."); } partial void OnCreated() { Console.WriteLine("OnCreated() appelée."); } partial void OnCompanyNameChanging(string value) { Console.WriteLine("OnCompanyNameChanging()appelée."); } partial void OnCompanyNameChanged() { Console.WriteLine("OnCompanyNameChanged()appelée."); } } }

L’espace de noms est nwind. Ce nom a été intentionnellement choisi : il doit correspondre à l’espace de noms de la classe étendue. Comme le nom nwind a été spécifié lorsque les classes d’entité ont été générées avec SQLMetal, la classe partielle Contact doit Openmirrors.com

Linq.book Page 501 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

501

également se trouver dans l’espace de noms nwind. Dans un code de production, vous préférerez certainement loger cette classe partielle dans un module séparé. Comme vous pouvez le voir, les méthodes OnLoaded, OnCreated, OnCompanyNameChanging et OnCompanyNameChanged ont été implémentées. Ces méthodes se contentent d’afficher un message dans la console. Bien entendu, vous pouvez utiliser un tout autre code dans vos implémentations. Nous allons maintenant illustrer l’utilisation des méthodes partielles. Le Listing 15.6 accède à un enregistrement Contact dans la base de données et modifie sa propriété CompanyName. Listing 15.6 : Interrogation d’une classe avec des méthodes partielles. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Contact contact = db.Contacts.Where(c => c.ContactID == 11).SingleOrDefault(); Console.WriteLine("CompanyName = {0}", contact.CompanyName); contact.CompanyName = "Joe’s House of Booze"; Console.WriteLine("CompanyName = {0}", contact.CompanyName);

Ce code n’a rien de très original, si ce n’est qu’il implémente des méthodes partielles utilisables dans les classes d’entité. Dans un premier temps, nous effectuons une requête sur un contact et affichons le nom de sa société dans la console. Dans un second temps, le nom du contact est modifié et affiché dans la console. Voici les résultats affichés suite à l’appui sur Ctrl+F5 : OnCreated() appelée. OnLoaded() appelée. CompanyName = B’s Beverages OnCompanyNameChanging() appelée. OnCreated() appelée. OnCompanyNameChanged() appelée. CompanyName = Joe’s House of Booze

Après l’appel des méthodes OnCreated puis OnLoaded, l’enregistrement est extrait de la base de données et chargé dans l’objet entité Contact. La troisième ligne indique le nom du contact. La méthode OnCompanyNameChanging est alors appelée, suivie par la méthode OnCreated. Apparemment, le DataContext crée un autre objet entité Contact lors de la procédure de pistage des modifications. La méthode OnCompanyNameChanged est enfin appelée, suivie par l’affichage du nouveau nom de la société. Vous savez maintenant comment étendre des classes d’entité en utilisant des méthodes partielles, et ce sans modifier le code généré.

Les classes API importantes de System.Data.Linq Il existe un certain nombre de classes de l’espace de noms System.Data.Linq que vous utiliserez régulièrement lors de votre pratique de LINQ to SQL. Cette section donne

Linq.book Page 502 Mercredi, 18. février 2009 7:58 07

502

LINQ to SQL

Partie V

une vue d’ensemble de ces classes, indique leur utilité et leur utilisation dans LINQ to SQL.

EntitySet Une classe d’entité située du côté "un" d’une relation "un-à-plusieurs" stocke les classes d’entité "plusieurs" dans un membre de classe du type EntitySet, où T est le type de la classe d’entité associée. Dans la base de données Northwind, la relation entre les tables Customers et Orders est de type "un-à-plusieurs". Dans la classe Customer, les Orders sont stockés dans un EntitySet. private EntitySet _Orders;

La classe EntitySet est une collection particulière utilisée par LINQ to SQL. Elle implémente l’interface IEnumerable et peut donc être interrogée avec des requêtes LINQ. Elle implémente également l’interface ICollection.

EntityRef Une classe d’entité située du côté "plusieurs" d’une relation "un-à-plusieurs" stocke les classes d’entité "un" dans un membre de classe du type EntityRef, où T est le type de la classe d’entité associée. Dans la base de données Northwind, la relation entre les tables Customers et Orders est de type "un-à-plusieurs". Dans la classe Order, le Customer est stocké dans un EntityRef. private EntityRef _Customer;

Entity Lorsque l’on référence une classe d’entité associée qui se trouve sur le côté "un" d’une relation "un-à-plusieurs" ou "un-à-un", il est facile de penser que la variable membre est du même type que la classe d’entité. Par exemple, lorsque l’on référence le Customer d’un objet Order, il est facile de penser que l’objet Customer est stocké dans un membre Customer de la classe Order. En réalité, le Customer est stocké dans un EntityRef. Si vous devez faire référence à l’objet Customer référencé par le membre EntityRef, vous utiliserez la propriété Entity de l’objet EntityRef. Dans certains cas, il est important d’être au courant de ces subtilités. En particulier si vous décidez d’écrire vos classes d’entité à la main. Si vous observez la classe d’entité Order générée par SQLMetal, vous verrez que les méthodes get et set de la propriété Customer utilisent la propriété Entity de l’objet EntityRef pour référencer un Customer. Openmirrors.com

Linq.book Page 503 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

503

Une propriété publique qui utilise la propriété EntityRef.Entity pour accéder à l’objet entité private EntityRef _Customer; ... public Customer Customer { get { return this._Customer.Entity; } set { Customer previousValue = this._Customer.Entity; ... } }

HasLoadedOrAssignedValue Cette propriété est de type bool. Elle vous permet de savoir si une propriété d’une classe d’entité stockée dans un EntityRef a été initialisée, c’est-à-dire si une valeur lui a été affectée ou si un chargement a été effectué. Elle est typiquement utilisée dans les méthodes set du côté "un" d’une association "unà-plusieurs" pour empêcher la propriété de la classe d’entité contenant l’identifiant d’être différente de l’EntityRef qui contient la référence au côté "un". À titre d’exemple, voici la méthode set des propriétés CustomerID et Customer de la classe d’entité Order : La méthode set de la propriété CustomerID public string CustomerID { get { return this._CustomerID; } set { if ((this._CustomerID != value)) { if (this._Customer.HasLoadedOrAssignedValue) { throw new System.Data.Linq.ForeignKeyReferenceAlreadyHasValueException(); } this.OnCustomerIDChanging(value); this.SendPropertyChanging(); this._CustomerID = value; this.SendPropertyChanged("CustomerID"); this.OnCustomerIDChanged(); } } }

Dans la méthode set de la propriété CustomerID, si l’EntityRef utilisé pour stocker un Customer a une propriété HasLoadedOrAssignedValue initialisée à true, une exception est levée. Ceci empêche le développeur de modifier le CustomerID d’un objet entité

Linq.book Page 504 Mercredi, 18. février 2009 7:58 07

504

LINQ to SQL

Partie V

Order si une entité Customer lui est déjà affectée. Grâce à cet artifice, les propriétés CustomerID et Customer de l’entité objet Order ne peuvent pas être incohérentes.

D’une manière similaire, dans la méthode set de la propriété Customer, la référence Customer peut être affectée si la propriété HasLoadedOrAssignedValue a pour valeur false. La méthode set de la propriété Customer public Customer Customer { get { return this._Customer.Entity; } set { Customer previousValue = this._Customer.Entity; if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) { this.SendPropertyChanging(); if ((previousValue != null)) { this._Customer.Entity = null; previousValue.Orders.Remove(this); } this._Customer.Entity = value; if ((value != null)) { value.Orders.Add(this); this._CustomerID = value.CustomerID; } else { this._CustomerID = default(string); } this.SendPropertyChanged("Customer"); } } }

En vérifiant la propriété HasLoaderOrAssigned dans chaque méthode set, le développeur ne peut pas provoquer une incohérence entre les références CustomerID et Customer.

Table Ce type est utilisé par LINQ to SQL pour interfacer une table ou une base de données SQL Server. Généralement, la classe dérivée de DataContext, souvent référencée sous la forme [Your]DataContext dans les chapitres relatifs à LINQ to SQL, a une propriété publique de type Table (où T est une classe d’entité) pour chaque table de la base de données mappée à la classe dérivée de DataContext. Openmirrors.com

Linq.book Page 505 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

505

Pour faire référence à la table Customers de la base de données Northwind, vous utiliserez généralement la propriété publique Customers de type Table dans la classe dérivée de DataContext : Une propriété Table pour la table Customers de la base de données public System.Data.Linq.Table Customers { get { return this.GetTable(); } }

Table implémente l’interface IQueryable, qui elle-même étend IEnumerable. Vous pouvez donc lui appliquer des requêtes LINQ to SQL. C’est le type de données utilisé par la plupart des requêtes LINQ to SQL.

IExecuteResult Lorsqu’une procédure stockée ou une fonction définie par l’utilisateur est appelée par l’intermédiaire de la méthode IExecuteMethodCall, les résultats sont retournés dans un objet qui implémente l’interface IExecuteResult. La méthode IExecuteMethodCall retourne un IExecuteResult IExecuteResult result = this.ExecuteMethodCall(...);

L’interface IExecuteResult fournit la propriété ReturnValue (valeur retournée) et la méthode GetParameterValue (paramètres de sortie).

ReturnValue En dehors des paramètres de sortie, les résultats d’une procédure stockée/d’une fonction définie par l’utilisateur de type scalaire sont retournés dans la variable IExecuteResult.ReturnValue. Pour accéder à la valeur retournée par une procédure stockée/une fonction définie par l’utilisateur de type scalaire, il suffit de référencer le membre ReturnValue de l’objet retourné. Voici le code à utiliser : Accès à la valeur Integer retournée par une procédure stockée IExecuteResult result = this.ExecuteMethodCall(...); int returnCode = (int)(result.ReturnValue);

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montrerons comment obtenir un entier retourné par une procédure stockée. Si une procédure stockée retourne d’autres données que sa valeur de retour, la variable ReturnValue implémente l’interface ISingleResult ou IMultipleResults, en fonction du nombre de données retournées.

Linq.book Page 506 Mercredi, 18. février 2009 7:58 07

506

LINQ to SQL

Partie V

GetParameterValue Pour accéder aux paramètres de sortie d’une procédure stockée, vous appellerez la méthode GetParameterValue sur l’objet retourné, en lui passant l’index du paramètre à accéder (l’index du premier paramètre est 0). Si, par exemple, le troisième paramètre retourné par la procédure stockée est CompanyName, vous utiliserez le code suivant : Accès aux paramètres retournés par une procédure stockée IExecuteResult result = this.ExecuteMethodCall(..., param1, param2, companyName); string CompanyName = (string)(result.GetParameterValue(2));

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montrerons comment obtenir un entier retourné par une procédure stockée.

ISingleResult Lorsqu’une procédure stockée retourne un résultat unique, il est placé dans l’objet IExecuteResult.ReturnValue, qui implémente l’interface ISingleResult (où T est une classe d’entité). Le code à utiliser est le suivant : Accès à un résultat unique IExecuteResult result = this.ExecuteMethodCall(...); ISingleResult results = (ISingleResult)(result.ReturnValue);

Pour obtenir le résultat, nous avons effectué un casting de type ISingleResult sur le membre ReturnValue de l’objet IExecuteResult. ISingleResult héritant de IEnumerable, on accède aux résultats retournés aussi simplement qu’à une séquence LINQ d’un autre type.

Accès aux résultats d’un ISingleResult foreach (CustomersByCityResult cust in results) { ... }

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montrerons comment obtenir la valeur unique retournée par une procédure stockée.

ReturnValue L’interface ISingleResult a une propriété ReturnValue qui se comporte comme la propriété de même nom dans l’interface IExecuteResult. Reportez-vous à la section précédente pour savoir comment accéder à cette propriété.

IMultipleResults Lorsqu’une procédure stockée retourne plusieurs résultats, ils sont placés dans l’objet IExecuteResult.ReturnValue, qui implémente l’interface IMultipleResults. Le code à utiliser est le suivant : Openmirrors.com

Linq.book Page 507 Mercredi, 18. février 2009 7:58 07

Chapitre 15

Les classes d’entité LINQ to SQL

507

Accès à des résultats multiples IExecuteResult result = this.ExecuteMethodCall(...); IMultipleResults results = (IMultipleResults)(result.ReturnValue);

Pour accéder aux différents résultats retournés, il suffit d’appeler la méthode IMultipleResults.GetResult. Vous en saurez plus sur cette méthode à la fin de cette section. Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montrerons comment obtenir les valeurs retournées par une procédure stockée. L’interface IMultipleResults a une propriété ReturnValue qui donne accès à la valeur retournée par la procédure stockée et une méthode GetResult qui permet d’obtenir un IEnumerable (où T est une classe d’entité qui correspond à la valeur) pour chaque valeur retournée.

ReturnValue L’interface IMultipleResult a une propriété ReturnValue qui se comporte comme la propriété de même nom dans l’interface IExecuteResults. Reportez-vous à la section précédente pour savoir comment accéder à cette propriété.

GetResult L’interface IMultipleResult dispose d’une méthode GetResult (où T représente le type de donnée de la forme retournée). Cette méthode permet d’obtenir les divers enregistrements de la forme retournée. Ces enregistrements sont retournés dans un IEnumerable (où T est la classe d’entité de la forme). Le code à utiliser est le suivant : Accès à plusieurs formes retournées par une procédure stockée [StoredProcedure(Name="A Stored Procedure")] [ResultType(typeof(Shape1))] [ResultType(typeof(Shape2))] ... IExecuteResult result = this.ExecuteMethodCall (...); IMultipleResults results = (IMultipleResults)(result.ReturnValue); foreach(Shape1 x in results.GetResult()) {…} foreach(Shape2 y in results.GetResult()) {…}

Les premières lignes du code correspondent aux attributs ResultType. Vous avez ainsi une idée de leur contexte et des formes retournées par la procédure stockée. Dans cet exemple, nous savons que les enregistrements mappés au type Shape1 seront retournés par la procédure stockée, suivis des enregistrements mappés au type Shape2. Les deux dernières instructions énumèrent les séquences IEnumerable et IEnumerable, retournées respectivement par le premier et le deuxième appel à la méthode GetResult. Il est important de connaître la chronologie des formes et d’utiliser la méthode GetResult pour les retrouver dans le même ordre.

Linq.book Page 508 Mercredi, 18. février 2009 7:58 07

508

LINQ to SQL

Partie V

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montrerons comment obtenir les formes multiples retournées par une procédure stockée.

Résumé Ce chapitre a analysé en détail les classes d’entité LINQ to SQL, les difficultés inhérentes à leur écriture manuelle, leurs attributs et les propriétés de leurs attributs. Il est important de bien avoir en mémoire que, si vous écrivez vos propres classes d’entité, vous devrez implémenter les notifications de changement et assurer la cohérence du graphe. Il ne s’agit pas là de détails triviaux et leur implémentation est loin d’être un jeu d’enfant. Heureusement, vous pouvez utiliser SQLMetal et le Concepteur Objet/Relationnel pour reléguer ces complications aux oubliettes. Par ailleurs, pour écrire vos propres classes d’entité, vous devez avoir une connaissance exhaustive des attributs des classes d’entité et de leurs propriétés. Ces éléments ont été analysés en détail dans ce chapitre et vous avez découvert leur implémentation à travers l’analyse des classes d’entité générées par SQLMetal pour la base de données Northwind. Vous avez également vu qu’il était préférable de projeter les résultats de vos requêtes dans des classes d’entité plutôt que dans des classes de non-entité. Si vous n’avez pas besoin de modifier les données et de les enregistrer dans la base de données, les classes de non-entité peuvent cependant être utilisées. Dans le cas contraire, projetez vos données dans des classes d’entité. La fin du chapitre a passé en revue quelques-unes des classes fréquemment utilisées dans l’espace de noms System.Data.Linq et vous a montré comment les utiliser en LINQ to SQL. Arrivé à ce point dans la lecture du livre, vous devriez être un expert en anatomie des classes d’entité. Elles ont été étudiées en détail et le code généré a été analysé. Ces classes d’entité sont généralement référencées par une classe dérivée de la classe DataContext. Vous saurez tout sur cette classe en parcourant le chapitre suivant.

Openmirrors.com

Linq.book Page 509 Mercredi, 18. février 2009 7:58 07

16 La classe DataContext Les chapitres précédents relatifs à LINQ to SQL ont utilisé la classe DataContext, sans pour autant expliquer ses tenants et ses aboutissants. Cette lacune va maintenant être réparée. Ce chapitre va vous montrer ce que la classe DataContext peut faire pour vous et comment en tirer le maximum. Nous passerons en revue ses méthodes principales et fournirons un exemple pour chacune d’entre elles. Il est nécessaire de bien comprendre le fonctionnement de la classe DataContext pour bien utiliser LINQ to SQL. Poursuivez votre lecture et la classe DataContext n’aura plus aucun secret pour vous.

Prérequis pour exécuter les exemples Pour exécuter les exemples de ce chapitre, vous devez avoir téléchargé la version étendue de la base de données Northwind et généré ses classes d’entité. Si nécessaire, reportez-vous à la section intitulée "Prérequis pour exécuter les exemples" du Chapitre 12 pour savoir comment procéder. Méthodes communes Pour exécuter les exemples de ce chapitre, vous aurez également besoin de plusieurs méthodes communes. Reportez-vous à la section intitulée "Méthodes communes" au Chapitre 12 pour avoir toutes les informations nécessaires à ce sujet. Utilisation de l’API LINQ to SQL Pour exécuter les exemples de ce chapitre, vous devez également ajouter les références et directives using appropriées à votre projet. Encore une fois, reportez-vous à la

Linq.book Page 510 Mercredi, 18. février 2009 7:58 07

510

LINQ to SQL

Partie V

section "Utilisation de l’API LINQ to SQL" du Chapitre 12 pour avoir plus de renseignements à ce sujet. Pour quelques exemples de ce chapitre, vous devrez également ajouter une directive using pour l’espace de noms System.Data.Linq.Mapping : using System.Data.Linq.Mapping;

La classe [Your]DataContext Bien qu’elle n’ait pas encore été étudiée, la classe System.Data.Linq.DataContext sera fréquemment utilisée dans LINQ to SQL. Elle permet en effet d’établir la connexion avec la base de données utilisée. Lorsque vous définissez ou générez des classes d’entité, elles dérivent généralement de la classe DataContext. La classe dérivée a le plus souvent le même nom que la base de données à laquelle elle se réfère. Les exemples de ce chapitre utilisent la base de données Northwind. La classe dérivée aura donc pour nom Northwind. Cependant, étant donné que le nom de la classe dérivée est fonction de la base de données, le nom de la classe sera également amené à changer, de code en code. Par commodité d’écriture, la classe dérivée sera souvent référencée sous le nom [Your]DataContext.

La classe DataContext C’est la classe DataContext qui gère la connexion avec la base de données. Elle gère également les requêtes, les mises à jour, les insertions, la recherche d’identité, la recherche de changements, le processus de changement, l’intégrité transactionnelle et même la création de la base de données. La classe DataContext traduit les requêtes de classes d’entité en déclarations SQL qui sont exécutées dans la base de données. La classe [Your]DataContext étant dérivée de la classe DataContext, elle a accès à des méthodes communes, telles que ExecuteQuery, ExecuteCommand et SubmitChanges. Outre ces méthodes héritées, la classe [Your]DataContext contient également des propriétés de type System.Data.Linq.Table (où chaque T représente une classe d’entité mappée à une table ou une vue spécifique) pour chaque table et vue de la base de données pour lesquelles vous voulez utiliser LINQ to SQL. À titre d’exemple, examinons la classe Northwind, générée par l’outil SQLMetal. Il s’agit de la classe [Your]DataContext de la base de données Northwind. Les passages en gras sont les plus intéressants. Un extrait de la classe générée Northwind public partial class Northwind : System.Data.Linq.DataContext { ... static Northwind()

Openmirrors.com

Linq.book Page 511 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

511

{ } public Northwind(string connection) : base(connection, mappingSource) { OnCreated(); } public Northwind(System.Data.IDbConnection connection) : base(connection, mappingSource) { OnCreated(); } public Northwind(string connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { OnCreated(); } public Northwind(System.Data.IDbConnection connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { OnCreated(); } ... public System.Data.Linq.Table Customers { get { return this.GetTable(); } } ... }

Comme vous le laisse voir la première ligne, cette classe hérite de la classe DataContext. Elle contient cinq constructeurs. Le constructeur par défaut est privé. En effet, l’indicateur de visibilité n’étant pas spécifié, il sera impossible d’instancier [Your]DataContext sans paramètre. Les autres constructeurs sont tous publics. Ils sont corrélés aux constructeurs de la classe DataContext. Chaque constructeur [Your]DataContext appelle : m

dans son initialiseur, le constructeur de base équivalent de la classe DataContext ;

m

dans le corps du constructeur, la méthode partielle OnCreated.

Linq.book Page 512 Mercredi, 18. février 2009 7:58 07

512

LINQ to SQL

Partie V

Le programmeur peut donc implémenter une méthode partielle OnCreated qui lui est propre. Cette méthode sera appelée à chaque instanciation d’un objet [Your]DataContext. Dans la classe Northwind, remarquez la propriété Customers de type Table, où Customer est une classe d’entité. C’est la classe d’entité Customer qui est mappée à la table Customers de la base de données Northwind. Il n’est pas nécessaire d’écrire du code qui utilise la classe [Your]DataContext : la classe DataContext standard peut très bien convenir. Cependant, l’écriture de code est plus pratique si vous utilisez la classe [Your]DataContext. Si vous le faites, chaque table est une propriété à laquelle on peut accéder directement à partir de l’objet [Your]DataContext (voir Listing 16.1). Listing 16.1 : Accès à une table à l’aide d’une propriété. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable query = from cust in db.Customers where cust.Country == "USA" select cust; foreach(Customer c in query) { Console.WriteLine("{0}", c.CompanyName); }

INFO Dans les exemples, vous devrez peut-être modifier les chaînes de connexion pour qu’elles s’adaptent à votre configuration.

Dans le code précédent, la connexion s’établit en utilisant la classe [Your]DataContext Northwind. Les clients (Table) sont donc accessibles comme de simples propriétés (Customers) de la classe [Your]DataContext. Voici les résultats du Listing 16.1 : Great Lakes Food Market Hungry Coyote Import Store Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets

Openmirrors.com

Linq.book Page 513 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

513

Si nous établissons la connexion en utilisant la classe DataContext, nous devons utiliser la méthode GetTable de l’objet DataContext, comme illustré dans le Listing 16.2. Listing 16.2 : Accès à une table à l’aide de la méthode GetTable. DataContext dc = new DataContext(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable query = from cust in dc.GetTable() where cust.Country == "USA" select cust; foreach(Customer c in query) { Console.WriteLine("{0}", c.CompanyName); }

Ce code donne les mêmes résultats que le précédent : Great Lakes Food Market Hungry Coyote Import Store Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets

L’utilisation de la classe [Your]DataContext facilite donc l’écriture. Utilisez-la aussi souvent que possible. Principaux objectifs Outre les méthodes décrites dans ce chapitre, la classe DataContext fournit trois services principaux : la recherche d’identité, le traçage des modifications et l’exécution des modifications. Recherche d’identité Un des problèmes résolus par LINQ to SQL est connu sous le nom object-relational impedance mismatch, que l’on pourrait traduire par "désaccord d’impédance objet/relationnel". Ce terme se réfère aux difficultés inhérentes au fait que la plupart des bases de données sont relationnelles et que la plupart des langages de programmation sont orientés objets. Cette différence est à l’origine du problème.

Linq.book Page 514 Mercredi, 18. février 2009 7:58 07

514

LINQ to SQL

Partie V

Une manifestation du désaccord d’impédance objet/relationnel concerne le comportement de l’identité. Si une requête est effectuée sur le même enregistrement à plusieurs emplacements dans le code, il semble logique de s’attendre à ce que les données retournées soient stockées dans différents emplacements en mémoire. Nous nous attendons également à ce que la modification des champs dans une partie du code n’affecte pas les mêmes champs obtenus dans une autre partie du code. Ceci parce que les données sont stockées dans différents emplacements en mémoire. Comparez ce qui vient d’être dit avec le comportement des objets. Lorsqu’un objet est en mémoire – un objet Customer, par exemple –, nous nous attendons à ce que toutes les portions du code qui le référencent pointent vers une même adresse en mémoire. Si la propriété name de cet objet est modifiée à un emplacement du programme, nous nous attendons à ce que le nouveau nom soit également modifié dans la totalité du programme. Le service de recherche d’identité de la classe DataContext induit ce comportement : lorsqu’une requête est effectuée sur un enregistrement pour la première fois depuis l’instanciation de l’objet DataContext, l’enregistrement résultant est mémorisé dans une table d’identités en utilisant sa clé primaire, et un objet entité est créé et placé dans une mémoire cache. Les requêtes suivantes qui produisent un même résultat parcourent la table d’identités. Si l’enregistrement en question existe, l’objet entité correspondant est retourné à partir du cache. Ce concept est fondamental. Nous allons le formuler d’une autre manière. Lorsqu’une requête est exécutée, si un enregistrement de la base de données correspond aux critères de sélection, et si l’objet entité correspondant se trouve déjà dans le cache, il est retourné depuis le cache. Cela signifie que la donnée retournée par la requête peut être différente de l’enregistrement de la base de données. La requête détermine quelles entités doivent être retournées en se basant sur les données contenues dans la base de données. Mais le service de recherche d’identité de l’objet DataContext détermine quelle donnée doit être retournée. Ceci peut conduire au problème de "discordance des résultats dans le cache". Discordance des résultats dans le cache La discordance des résultats dans le cache se produit lorsqu’un enregistrement de la base de données diffère du même enregistrement dans le cache de l’objet DataContext. Lorsqu’une requête est exécutée, des enregistrements satisfaisant les critères sont recherchés dans la base de données. Si un enregistrement correspond au critère, l’objet entité de cet enregistrement est placé dans les résultats. Cependant, si un enregistrement correspondant au critère se trouve dans le cache de l’objet DataContext, il est placé dans les résultats. Si un objet entité se trouve dans le cache du DataContext et que l’enregistrement correspondant est mis à jour dans la base de données, il y aura discordance entre les Openmirrors.com

Linq.book Page 515 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

515

deux représentations de l’enregistrement. Si vous lancez une requête dont le résultat est précisément cet enregistrement, c’est la valeur du cache qui sera retournée… Nous allons raisonner sur un exemple. Dans un premier temps, nous allons lancer une requête sur le client LONEP et afficher sa région : OR. Dans un deuxième temps, nous allons rechercher puis afficher les clients dont la région est WA. Dans un troisième temps, nous allons utiliser ADO.NET pour affecter la région WA au client LONEP, comme si un autre contexte, extérieur au process, avait effectué cette modification. À ce point précis, la région du client LONEP est OR dans l’objet entité et WA dans la base de données. Dans un quatrième temps, nous allons lancer la même requête que dans le deuxième temps, afin d’afficher les clients dont la région est WA. Examinez le code : aucune requête n’est exécutée dans ce sens. Juste une énumération de la séquence custs. Cela n’a rien de surprenant : étant donné la nature différée de la requête, il suffit d’énumérer la séquence de sortie pour obtenir les résultats. Le champ région du client LONEP ayant pour valeur WA dans la base de données, il sera inclus dans les résultats. Mais, comme cet enregistrement se trouve déjà dans le cache, c’est l’enregistrement du cache qui sera placé dans les résultats. Pas de chance : le champ région de cet enregistrement a pour valeur OR ! Enfin, pour terminer, nous allons afficher la région des objets entité retournés. Lors de l’affichage du client LONEP, c’est la région OR qui sera affichée, et ce bien que la requête ait demandé la liste des enregistrements dont la région avait pour valeur WA. Le Listing 16.3 illustre cette discordance. Listing 16.3 : Illustration de la discordance des résultats dans le cache. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // Sélection d’un client dont le champ region vaut "WA" Customer cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single(); Console.WriteLine("Client {0}, région = {1}.{2}", cust.CustomerID, cust.Region, System.Environment.NewLine); // La région de LONEP est OR // Sélection d’une séquence de clients dont le champ région vaut "WA" // LONEP n’en fait pas partie puisque sa région est OR IEnumerable custs = (from c in db.Customers where c.Region == "WA" select c); Console.WriteLine("Clients dont le champ région vaut WA, avant la modification ➥ADO.NET ..."); foreach(Customer c in custs) { // Affichage de la région de chaque objet entité Console.WriteLine("Le client {0}a pour région {1}.", c.CustomerID, c.Region); }

Linq.book Page 516 Mercredi, 18. février 2009 7:58 07

516

LINQ to SQL

Partie V

Console.WriteLine("Clients dont le champ région vaut WA avant la modification ➥ADO.NET - début{0}", System.Environment.NewLine); // Affectation de la région WA au client LONEP // La modification se fait avec ADO.NET Console.WriteLine("Modification de la région de LONEP en WA avec ADO.NET..."); ExecuteStatementInDb( "update Customers set Region = ’WA’ where CustomerID = ’LONEP’"); Console.WriteLine("La région de LONEP a été mise à jour.{0}", System.Environment.NewLine); Console.WriteLine("La région de LONEP est WA dans la base de données, mais ..."); Console.WriteLine("Le client {0} a pour région = {1} dans l’objet entité{2}", cust.CustomerID, cust.Region, System.Environment.NewLine); // La région de LONEP est WA dans la base de données, mais toujours OR dans l’objet

➥entité // Effectuons une autre requête // Affichage des régions de l’objet entité Console.WriteLine("Requête des objets entité après la modification ADO.NET – début ➥..."); foreach(Customer c in custs) { // Affichage de la région de chaque entité objet Console.WriteLine("Le client {0}a pour région {1}.", c.CustomerID, c.Region); } Console.WriteLine("Requête des objets entité après la modification ADO.NET ➥fin{0}", System.Environment.NewLine); // Les valeurs modifiées doivent être rétablies pour que le code // puisse être exécuté plusieurs fois. Console.WriteLine("{0}Restauration des valeurs originales.", System.Environment.NewLine); ExecuteStatementInDb( "update Customers set Region = ’OR’ where CustomerID = ’LONEP’");

Voici les résultats : Le client LONEP a pour région OR Clients dont le champ région vaut WA avant la modification ADO.NET – début ... Le client LAZYK a pour région WA. Le client TRAIH a pour région WA. Le client WHITC a pour région WA. Clients dont le champ région vaut WA avant la modification ADO.NET – fin Modification de la région de LONEP en WA avec ADO.NET... La région de LONEP a été mise à jour. La région de LONEP est WA dans la base de données, mais... Le client LONEP a pour région OR dans l’objet entité. Requête des objets entité après la modification ADO.NET – début ... Le client LAZYK a pour région WA. Le client LONEP a pour région OR. Le client TRAIH a pour région WA. Le client WHITC a pour région WA. Requête des objets entité après la modification ADO.NET – fin Restauration des valeurs originales

Openmirrors.com

Linq.book Page 517 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

517

Comme vous le voyez, bien que la requête porte sur les clients dont la région est WA, LONEP fait partie des résultats alors que sa région est OR. Bien sûr, la région de LONEP est bien WA dans la base de données, mais la région est différente dans l’objet référencé par le code. Ce comportement erratique est également mis en évidence si vous tentez d’interroger la base de données sur un enregistrement qui vient d’être ajouté ou supprimé. Après l’ajout d’un enregistrement, le résultat de la requête est déterminé à partir du contenu de la base de données, et non par le cache de l’objet DataContext. Si l’ajout de l’enregistrement n’a pas été entériné par la méthode SubmitChanges, l’entité insérée ne se trouve pas encore dans la base de données. Une situation comparable concerne les entités supprimées (voir Listing 16.4). Listing 16.4 : Une autre illustration de la discordance des résultats dans le cache. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Console.WriteLine("Ajout du client LAWN"); db.Customers.InsertOnSubmit( new Customer { CustomerID = "LAWN", CompanyName = "Lawn Wranglers", ContactName = "Mr. Abe Henry", ContactTitle = "Owner", Address = "1017 Maple Leaf Way", City = "Ft. Worth", Region = "TX", PostalCode = "76104", Country = "USA", Phone = "(800) MOW-LAWN", Fax = "(800) MOW-LAWO" }); Console.WriteLine("Requête sur le client LAWN"); Customer cust = (from c in db.Customers where c.CustomerID == "LAWN" select c).SingleOrDefault(); Console.WriteLine("Le client LAWN {0}.{1}", cust == null ? "n’existe pas" : "existe", System.Environment.NewLine); Console.WriteLine("Suppression du client LONEP"); cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).SingleOrDefault(); db.Customers.DeleteOnSubmit(cust); Console.WriteLine("Requête sur le client LONEP"); cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).SingleOrDefault(); Console.WriteLine("Le client LONEP {0}.{1}", cust == null ? "n’existe pas" : "existe", System.Environment.NewLine); // Inutile de réinitialiser la base de données, puisque la méthode // SubmitChanges() n’a pas été appelée

Linq.book Page 518 Mercredi, 18. février 2009 7:58 07

518

LINQ to SQL

Partie V

Dans ce code, nous avons inséré le client LAWN et lancé une requête pour voir s’il existe. Nous avons ensuite supprimé le client LONEP et lancé une requête pour voir s’il existe. Ces différentes actions ont été effectuées sans appeler la méthode SubmitChanges. Les objets entité mis en cache n’ont donc pas été sauvegardés dans la base de données. Voici les résultats de ce code : Ajout du client LAWN Requête sur le client LAWN Le client LAWN n’existe pas Suppression du client LONEP Requête sur le client LONEP Le client LONEP existe

Pour éviter ce genre de situation, toutes ces actions devraient être incluses dans une transaction. Reportez-vous à la section intitulée "Concurrence pessimiste" au Chapitre 17 pour avoir un exemple concret. Traçage des modifications Lorsque le service de recherche d’identité crée un objet entité dans le cache, le traçage des modifications se met en branle pour cet objet. Ce processus consiste à stocker la valeur originale d’un objet entité. Il se poursuit jusqu’à ce que la méthode SubmitChanges soit exécutée. Cette dernière sauvegarde les modifications apportées aux objets entité dans la base de données. Les valeurs originales sont donc perdues et remplacées par les valeurs actuelles, et le traçage des modifications peut reprendre.

Ceci fonctionne tant que les objets entité sont récupérés dans la base de données. Cependant, l’instanciation d’un nouvel objet entité ne lui associe aucune identité ni aucun traçage des modifications tant que le DataContext n’est pas au courant de son existence. Pour lui faire connaître l’existence de ce nouvel objet, il suffit d’insérer l’objet entité dans une propriété Table. À titre d’exemple, la classe Northwind possède la propriété Table Customers. Si nous appelons la méthode InsertOnSubmit sur la propriété Customers pour insérer un objet entité Customer dans Table, le service d’identité et le traçage des modifications commenceront sur cet objet entité. Voici le code permettant d’insérer un client : db.Customers.InsertOnSubmit( new Customer { CustomerID = "LAWN", CompanyName = "Lawn Wranglers", ContactName = "Mr. Abe Henry", ContactTitle = "Owner", Address = "1017 Maple Leaf Way", City = "Ft. Worth", Region = "TX", PostalCode = "76104", Country = "USA", Phone = "(800) MOW-LAWN", Fax = "(800) MOW-LAWO"});

Openmirrors.com

Linq.book Page 519 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

519

L’appel de la méthode InsertOnSubmit déclenche le démarrage du service d’identité et du traçage des modifications sur le client LAWN. Lorsque j’ai découvert le traçage des modifications, ce concept me paraissait quelque peu obscur. L’appréhension du concept de base est chose assez simple, mais la compréhension de son fonctionnement n’est pas aussi immédiate. Si vous prévoyez d’écrire vos classes d’entité à la main, vous devez comprendre le fonctionnement du traçage des modifications. Reportez-vous à la section intitulée "Notifications de changement" au Chapitre 15 pour compléter votre apprentissage. Exécution des modifications Un des services les plus importants fournis par le DataContext est le traçage des modifications pour les objets entité : lorsque vous insérez, modifiez ou supprimez un objet entité, le DataContext monitore tout changement. Cependant, aucun changement n’est sauvegardé dans la base de données : chacun d’entre eux est mis en cache par le DataContext jusqu’à ce que la méthode SubmitChanges soit exécutée.

Lorsque vous appelez la méthode SubmitChanges, le processeur de changement de l’objet DataContext effectue la mise à jour de la base de données. Dans un premier temps, il insère les nouveaux objets entité dans sa liste d’objets entité tracés. Ensuite, il ordonne les objets entité modifiés en utilisant leurs dépendances (clé étrangère et contrainte d’unicité). Si aucune transaction n’est définie, il en crée une afin d’assurer l’intégrité transactionnelle de toutes les commandes SQL exécutées pendant l’invocation de la méthode SubmitChanges. Pour ce faire, il utilise le niveau d’isolation par défaut de SQL Server, ReadCommitted. Les données lues ne seront pas physiquement corrompues, et seules les données validées seront lues. Cependant, étant donné que le verrou du mode ReadCommitted est partagé, rien n’empêche de modifier les données avant la fin de la transaction. Enfin, il énumère la liste ordonnée d’objets entité modifiés, crée les déclarations SQL nécessaires et les exécute. Si une erreur se produit pendant l’énumération des objets entité modifiés : m

Si la méthode SubmitChanges utilise un ConflictMode de type FailOnFirstConflict, l’énumération est avortée, la transaction défait toutes les modifications effectuées dans la base de données et une exception est levée.

m

Si la méthode SubmitChanges utilise un ConflictMode de type ContinueOnConflict, tous les objets entité modifiés sont énumérés et traités malgré les erreurs, et le DataContext dresse la liste des conflits. La transaction défait toutes les modifications effectuées dans la base de données et une exception est levée.

Tant que les modifications n’ont pas été sauvegardées dans la base de données, les objets entité modifiés restent en l’état. Cela donne au développeur l’opportunité d’essayer de résoudre le problème, puis d’appeler à nouveau la méthode SubmitChanges.

Linq.book Page 520 Mercredi, 18. février 2009 7:58 07

520

LINQ to SQL

Partie V

Si toutes les modifications ont pu être sauvegardées dans la base de données, la transaction est validée et le pistage des transactions pour les objets entité modifiés est supprimé. Un nouveau pistage peut donc être initié.

Datacontext() et [Your]DataContext() La classe DataContext est généralement dérivée pour créer une classe [Your]DataContext. Elle est utilisée pour établir la connexion et gérer les interactions avec la base de données. Vous utiliserez un des constructeurs suivants pour instancier un objet Datacontext()/[Your]DataContext(). Prototypes Quatre prototypes du constructeur DataContext seront étudiés dans cet ouvrage.

Premier prototype DataContext(string fileOrServerOrConnection);

Ce prototype utilise une chaîne de connexion ADO.NET. C’est certainement celui que vous utiliserez le plus fréquemment. Il est utilisé par la plupart des exemples LINQ to SQL de cet ouvrage. Deuxième prototype DataContext (System.Data.IDbConnection connection);

Comme System.Data.SqlClient.SqlConnection hérite de System.Data.Common.DbConnection, qui lui-même implémente System.Data.IDbConnection, vous pouvez instancier un DataContext ou un [Your]DataContext avec un SqlConnection déjà créé. Ce prototype est utile si vous souhaitez mélanger du code LINQ to SQL avec du code ADO.NET existant. Troisième prototype DataContext(string fileOrServerOrConnection, System.Data.Linq.MappingSource mapping);

Ce prototype est utile si vous disposez non pas d’une classe [Your]DataContext, mais d’un fichier de mappage XML. Dans certaines situations, vous pouvez disposer d’une classe métier existante dans laquelle vous ne pouvez pas ajouter les attributs LINQ to SQL appropriés. Peut-être n’avez-vous même pas accès au code source. Dans ce cas, vous pouvez générer un fichier de mappage avec SQLMetal ou l’écrire à la main pour travailler avec une classe métier existante, ou une autre classe quelconque. Dans le premier argument de ce prototype, vous devrez fournir une chaîne de connexion traditionnelle ADO.NET. Quatrième prototype DataContext (System.Data.IDbConnection connection, System.Data.Linq.MappingSource mapping)

Openmirrors.com

Linq.book Page 521 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

521

Ce prototype vous permet de créer une connexion LINQ to SQL à partir d’une connexion ADO.NET existante et de fournir un fichier de mappage XML. Ce prototype est utile si vous devez combiner du code LINQ to SQL avec du code ADO.NET existant et que vous ne disposiez pas des classes d’entité agrémentées d’attributs. Exemples Pour illustrer le premier prototype du constructeur DataContext, nous allons nous connecter à un fichier physique .mdf en utilisant une chaîne de connexion ADO.NET (voir Listing 16.5). Listing 16.5 : Utilisation du premier prototype du constructeur DataContext pour se connecter à un fichier de base de données. DataContext dc = new DataContext(@"C:\Northwind.mdf"); IQueryable query = from cust in dc.GetTable() where cust.Country == "USA" select cust; foreach (Customer c in query) { Console.WriteLine("{0}", c.CompanyName); }

INFO Vous serez certainement amené à changer le chemin passé au constructeur du DataContext pour référencer votre fichier .mdf.

Dans ce listing, nous avons seulement fourni le chemin du fichier .mdf pour instancier l’objet DataContext. Étant donné qu’un objet DataContext (et non un objet [Your]DataContext) est créé, la méthode GetTable doit être appelée pour accéder aux clients. Voici les résultats : Great Lakes Food Market Hungry Coyote Import Store Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets

Linq.book Page 522 Mercredi, 18. février 2009 7:58 07

522

LINQ to SQL

Partie V

Nous allons maintenant utiliser le même code basique, mais cette fois-ci en utilisant la classe [Your]DataContext. Ici, la classe Northwind (voir Listing 16.6). Listing 16.6 : Utilisation du premier prototype du constructeur [Your]DataContext pour se connecter à un fichier de base de données. Northwind db = new Northwind(@"C:\Northwind.mdf"); IQueryable query = from cust in db.Customers where cust.Country == "USA" select cust; foreach(Customer c in query) { Console.WriteLine("{0}", c.CompanyName); }

Dans ce listing, au lieu d’appeler la méthode GetTable, nous nous sommes contentés de faire référence à la propriété Customers pour accéder aux clients. Ce code donne les mêmes résultats que le précédent : Great Lakes Food Market Hungry Coyote Import Store Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets

Pour être complet, nous allons donner un dernier exemple du premier prototype. Cette fois-ci, nous utiliserons une chaîne de connexion pour nous connecter à la base de données Northwind, hébergée sur un serveur de bases de données SQL Express. Dans le Listing 16.7, nous utiliserons la classe [Your]DataContext. Listing 16.7 : Utilisation du premier prototype du constructeur [Your]DataContext pour se connecter à une base de données. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable query = from cust in db.Customers where cust.Country == "USA" select cust; foreach(Customer c in query) { Console.WriteLine("{0}", c.CompanyName); }

Openmirrors.com

Linq.book Page 523 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

523

Les résultats sont toujours les mêmes : Great Lakes Food Market Hungry Coyote Import Store Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets

Le deuxième prototype de la classe DataContext est utile lorsque du code LINQ to SQL doit être combiné avec du code ADO.NET (voir Listing 16.8). Dans un premier temps, nous créons un objet SqlConnection et insérons un enregistrement dans la table Customers par son intermédiaire. Dans un deuxième temps, nous utilisons l’objet SqlConnection pour instancier une classe [Your]DataContext. Dans un troisième temps, une requête LINQ to SQL est appliquée à la table Customers et les résultats sont affichés. Enfin, dans un quatrième temps, nous utilisons ADO.NET pour supprimer l’enregistrement inséré dans la table Customers, lançons une requête LINQ to SQL sur la table Customers et affichons les résultats. Listing 16.8 : Utilisation du deuxième prototype du constructeur [Your]DataContext pour effectuer une connexion ADO.NET partagée. System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;"); string cmd = @"insert into Customers values (’LAWN’, ’Lawn Wranglers’, ’Mr. Abe Henry’, ’Owner’, ’1017 Maple Leaf Way’, ’Ft. Worth’, ’TX’, ’76104’, ’USA’, ’(800) MOW-LAWN’, ’(800) MOW-LAWO’)"; System.Data.SqlClient.SqlCommand sqlComm = new System.Data.SqlClient.SqlCommand(cmd); sqlComm.Connection = sqlConn; try { sqlConn.Open(); // Insertion de l’enregistrement sqlComm.ExecuteNonQuery(); Northwind db = new Northwind(sqlConn); IQueryable query = from cust in db.Customers where cust.Country == "USA" select cust; Console.WriteLine("Les clients après l’insertion et avant la suppression"); foreach (Customer c in query) {

Linq.book Page 524 Mercredi, 18. février 2009 7:58 07

524

LINQ to SQL

Partie V

Console.WriteLine("{0}", c.CompanyName); } sqlComm.CommandText = "delete from Customers where CustomerID = ’LAWN’"; // Delete the record. sqlComm.ExecuteNonQuery(); Console.WriteLine("{0}{0}Les clients après la suppression", System.Environment.NewLine); foreach (Customer c in query) { Console.WriteLine("{0}", c.CompanyName); } } finally { // Fermeture de la connexion sqlComm.Connection.Close(); }

Comme vous pouvez le voir, la requête LINQ a été définie une seule fois. En revanche, elle a été énumérée à deux reprises. Étant donné que la requête est différée, sa définition ne provoque pas son exécution immédiate : ce n’est qu’au moment de son énumération qu’elle est réellement exécutée. Ceci est mis en évidence par le fait que les résultats diffèrent dans les deux énumérations. Le Listing 16.8 montre également qu’ADO.NET et LINQ to SQL peuvent cohabiter harmonieusement. Voici les résultats : Les clients après l’insertion et avant la suppression Great Lakes Food Market Hungry Coyote Import Store Lawn Wranglers Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets Les clients après la suppression Great Lakes Food Market Hungry Coyote Import Store Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets

Openmirrors.com

Linq.book Page 525 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

525

Pour illustrer le troisième prototype, nous n’allons pas utiliser les classes d’entité de la base de données Northwind. Nous utiliserons une classe Customer qui a été écrite manuellement et un fichier de mappage abrégé. Pour dire la vérité, la classe Customer est issue de SQLMetal, privée des attributs LINQ to SQL. La classe "écrite à la main" namespace Linqdev { public partial class Customer { private string _CustomerID; private string _CompanyName; private string _ContactName; private string _ContactTitle; private string _Address; private string _City; private string _Region; private string _PostalCode; private string _Country; private string _Phone; private string _Fax; public Customer() { } public string CustomerID { get { return this._CustomerID; } set { if ((this._CustomerID != value)) { this._CustomerID = value; } } } public string CompanyName { get { return this._CompanyName; } set { if ((this._CompanyName != value)) { this._CompanyName = value; } } } public string ContactName { get

Linq.book Page 526 Mercredi, 18. février 2009 7:58 07

526

LINQ to SQL

{ return this._ContactName; } set { if ((this._ContactName != value)) { this._ContactName = value; } } } public string ContactTitle { get { return this._ContactTitle; } set { if ((this._ContactTitle != value)) { this._ContactTitle = value; } } } public string Address { get { return this._Address; } set { if ((this._Address != value)) { this._Address = value; } } } public string City { get { return this._City; } set { if ((this._City != value)) { this._City = value; } } } public string Region { get { return this._Region;

Openmirrors.com

Partie V

Linq.book Page 527 Mercredi, 18. février 2009 7:58 07

Chapitre 16

} set { if ((this._Region != value)) { this._Region = value; } } } public string PostalCode { get { return this._PostalCode; } set { if ((this._PostalCode != value)) { this._PostalCode = value; } } } public string Country { get { return this._Country; } set { if ((this._Country != value)) { this._Country = value; } } } public string Phone { get { return this._Phone; } set { if ((this._Phone != value)) { this._Phone = value; } } } public string Fax { get { return this._Fax; } set

La classe DataContext

527

Linq.book Page 528 Mercredi, 18. février 2009 7:58 07

528

LINQ to SQL

Partie V

{ if ((this._Fax != value)) { this._Fax = value; } } } } }

Cette classe d’entité est certainement la pire qui ait jamais été écrite : elle ne gère pas les notifications de changement, et ses attributs LINQ to SQL ont été supprimés. Je vous invite à consulter le Chapitre 15 pour apprendre à écrire des classes d’entité de bonne facture. Cette classe se trouve dans l’espace de noms Linqdev. Cette information est importante car elle doit être spécifiée dans le code de l’exemple (pour la différencier de celle de même nom qui se trouve dans l’espace de noms nwind), mais aussi dans le fichier de mappage externe. Une chose est importante dans cet exemple : une propriété a été définie pour chaque champ de la base de données mappé au fichier externe. Voyons maintenant le fichier de mappage externe utilisé dans cet exemple. Un fichier de mappage externe XML abrégé













Openmirrors.com

Linq.book Page 529 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

529

Comme vous pouvez le voir dans les premières lignes, il est précisé que ce fichier de mappage s’applique à la classe d’entité Customer située dans l’espace de noms Linqdev. Ce fichier XML a pour nom abbreviatednorthwindmap.xml. Il a été sauvegardé dans le dossier bin\Debug. Dans le Listing 16.9, nous allons utiliser la classe d’entité écrite à la main Customer et le fichier de mappage abbreviatednorthwindmap.xml pour effectuer une requête LINQ to SQL sans utiliser un seul attribut. Listing 16.9 : Utilisation du troisième prototype du constructeur pour se connecter à une base de données en utilisant un fichier de mappage. string mapPath = "abbreviatednorthwindmap.xml"; XmlMappingSource nwindMap = XmlMappingSource.FromXml(System.IO.File.ReadAllText(mapPath)); DataContext db = new DataContext( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;", nwindMap); IQueryable query = from cust in db.GetTable() where cust.Country == "USA" select cust; foreach (Linqdev.Customer c in query) { Console.WriteLine("{0}", c.CompanyName); }

INFO Le fichier de mappage abbreviatednorthwindmap.xml a été placé dans le dossier bin\Debug du projet Visual Studio, car nous allons compiler et exécuter le projet en mode de débogage.

Comme vous pouvez le voir, le premier bloc instancie l’objet XmlMappingSource à partir du fichier de mappage et le deuxième passe cet objet au constructeur du DataContext. Dans le troisième bloc, notez qu’il n’est pas possible d’utiliser la propriété Customers Table dans l’objet DataContext de la requête LINQ to SQL. Ceci est dû au fait que nous utilisons la classe de base DataContext et non une classe [Your]DataContext. Notez également que chaque référence à la classe Customer spécifie explicitement l’espace de noms Linqdev. Ceci afin de s’assurer que la classe Customer générée par SQLMetal n’est pas utilisée.

Linq.book Page 530 Mercredi, 18. février 2009 7:58 07

530

LINQ to SQL

Partie V

Voici les résultats du Listing 16.9 : Great Lakes Food Market Hungry Coyote Import Store Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets

Cet exemple utilise une classe Customer dans laquelle la plupart du code qui assure le bon fonctionnement d’une classe d’entité est absent. Cependant, il vous a montré qu’il était possible d’effectuer des requêtes en utilisant un fichier de mappage et une classe dénuée d’attributs LINQ to SQL. Le quatrième prototype est une combinaison des deuxième et troisième prototypes. Nous allons l’illustrer dans le Listing 16.10. Listing 16.10 : Utilisation du quatrième prototype du constructeur DataContext pour se connecter à une base de données avec une connexion ADO.NET partagée et un fichier de mappage. System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;"); string cmd = @"insert into Customers values (’LAWN’, ’Lawn Wranglers’, ’Mr. Abe Henry’, ’Owner’, ’1017 Maple Leaf Way’, ’Ft. Worth’, ’TX’, ’76104’, ’USA’, ’(800) MOW-LAWN’, ’(800) MOW-LAWO’)"; System.Data.SqlClient.SqlCommand sqlComm = new System.Data.SqlClient.SqlCommand(cmd); sqlComm.Connection = sqlConn; try { sqlConn.Open(); // Insertion de l’enregistrement sqlComm.ExecuteNonQuery(); string mapPath = "abbreviatednorthwindmap.xml"; XmlMappingSource nwindMap = XmlMappingSource.FromXml(System.IO.File.ReadAllText(mapPath)); DataContext db = new DataContext(sqlConn, nwindMap); IQueryable query = from cust in db.GetTable() where cust.Country == "USA" select cust; Console.WriteLine("Clients après l’insertion et avant la suppression"); foreach (Linqdev.Customer c in query)

Openmirrors.com

Linq.book Page 531 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

531

{ Console.WriteLine("{0}", c.CompanyName); } sqlComm.CommandText = "delete from Customers where CustomerID = ’LAWN’"; // Suppression de l’enregistrement sqlComm.ExecuteNonQuery(); Console.WriteLine("{0}{0}Customers after deletion.", System.Environment.NewLine); foreach (Linqdev.Customer c in query) { Console.WriteLine("{0}", c.CompanyName); } } finally { // Fermeture de la connexion sqlComm.Connection.Close(); }

Tout comme le listing précédent, celui-ci s’appuie sur la classe Linqdev.Customer et le fichier de mappage externe abbreviatednorthwindmap.xml. Cet exemple montre comment interroger une base de données avec LINQ to SQL en utilisant une connexion ADO.NET et une classe d’entité privée d’attributs. Les résultats sont conformes aux attentes : Clients après l’insertion et avant la suppression Great Lakes Food Market Hungry Coyote Import Store Lawn Wranglers Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets Clients après la suppression. Great Lakes Food Market Hungry Coyote Import Store Lazy K Kountry Store Let’s Stop N Shop Lonesome Pine Restaurant Old World Delicatessen Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail’s Head Gourmet Provisioners White Clover Markets

Les exemples précédents vous ont montré que la connexion via un DataContext ou un [Your]DataContext est un vrai jeu d’enfant.

Linq.book Page 532 Mercredi, 18. février 2009 7:58 07

532

LINQ to SQL

Partie V

SubmitChanges() Le DataContext place dans un cache les modifications effectuées dans les objets entité, jusqu’à ce que la méthode SubmitChanges soit appelée. Cette méthode lance le processeur de changement et les objets entité modifiés sont sauvegardés dans la base de données. Si une transaction n’est pas disponible pour le DataContext lorsque la méthode SubmitChanges est appelée, elle est créée et les modifications s’effectuent par son intermédiaire. Ainsi, si une transaction échoue, toutes les modifications déjà effectuées dans la base de données peuvent être défaites. Si un conflit d’accès concurrentiel se produit, une exception ChangeConflictException est levée afin que vous puissiez essayer de le résoudre et tenter une nouvelle sauvegarde. Une chose appréciable : dans le DataContext, la méthode ResolveAll de la collection ChangeConflicts peut être utilisée pour résoudre automatiquement tous les conflits ! Si nécessaire, reportez-vous au Chapitre 17 pour avoir de plus amples détails sur les conflits d’accès concurrentiel. Prototypes Deux prototypes de la méthode SubmitChanges seront étudiés dans cet ouvrage.

Premier prototype void SubmitChanges()

Ce prototype ne demande aucun argument. Il se comporte comme le second prototype, lorsque l’argument ConflictMode a pour valeur ConflictMode.FailOnFirstConflict. Second prototype void SubmitChanges(ConflictMode failureMode)

Ce prototype permet de préciser le mode de gestion des conflits. Les valeurs possibles sont ConflictMode.FailOnFirstConflict et ConflictMode.ContinueOnConflict. Si vous choisissez la première valeur, une exception ChangeConflictException est levée dès qu’un conflit est détecté. Si vous choisissez la seconde, SubmitChanges tente d’effectuer toutes les modifications dans la base de données, de telle sorte qu’elles puissent être prises en compte et résolues en une seule étape lorsque l’exception ChangeConflictException est levée. La comptabilisation des conflits s’effectue au niveau du nombre d’enregistrements et non du nombre de champs. Si, par exemple, deux champs d’un même enregistrement produisent un conflit, un seul conflit sera généré. Openmirrors.com

Linq.book Page 533 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

533

Exemples La plupart des exemples du Chapitre 14 font appel à la méthode SubmitChanges. Il est donc un peu tard pour présenter un exemple d’utilisation basique de cette méthode. Plutôt que vous montrer pour la énième fois comment enregistrer les entités modifiées dans une base de données en utilisant la méthode SubmitChanges, nous allons donc choisir quelque chose d’un peu différent.

Pour illustrer le premier prototype, nous allons montrer que les modifications ne sont pas reportées dans la base de données tant que la méthode SubmitChanges n’est pas appelée. Cet exemple étant plus complexe que les précédents, nous donnerons des explications à chaque fois que cela sera nécessaire (voir Listing 16.11). Listing 16.11 : Un exemple d’utilisation du premier prototype de SubmitChanges. System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;"); try { sqlConn.Open(); string sqlQuery = "select ContactTitle from Customers where CustomerID = ’LAZYK’"; string originalTitle = GetStringFromDb(sqlConn, sqlQuery); string title = originalTitle; Console.WriteLine("ContactTitle lu dans la base de données : {0}", title); Northwind db = new Northwind(sqlConn); Customer c = (from cust in db.Customers where cust.CustomerID == "LAZYK" select cust). Single(); Console.WriteLine("ContactTitle lu dans l’entité objet : {0}", c.ContactTitle);

Dans les premières lignes, une connexion à une base de données ADO.NET est créée et ouverte. Le client dont le champ ContactTitle a pour valeur LAZYK est alors récupéré en utilisant la méthode GetStringFromDb, puis affiché. Un objet Northwind est alors créé en utilisant la connexion ADO.NET puis interrogé en utilisant LINQ to SQL, et son champ ContactTitle est affiché. À ce point, les deux ContactTitle devraient être identiques. Console.WriteLine(String.Format( "{0}Affectation de la valeur ’Director of Marketing’ à l’entité objet ContactTitle", System.Environment.NewLine)); c.ContactTitle = "Director of Marketing"; title = GetStringFromDb(sqlConn, sqlQuery); Console.WriteLine("ContactTitle lu dans la base de données : {0}", title); Customer c2 = (from cust in db.Customers where cust.CustomerID == "LAZYK" select cust). Single(); Console.WriteLine("Title from entity object : {0}", c2.ContactTitle);

Linq.book Page 534 Mercredi, 18. février 2009 7:58 07

534

LINQ to SQL

Partie V

Dans les premières lignes, l’objet entité LINQ to SQL ContactTitle du client est modifié. Une requête sur le champ ContactTitle de la base de données et sur l’entité objet correspondante est à nouveau effectuée et ces deux informations sont affichées. Cette fois-ci, les deux valeurs ne devraient pas être identiques car la modification n’a pas encore été sauvegardée dans la base de données. db.SubmitChanges(); Console.WriteLine(String.Format( "{0}La méthode SubmitChanges a été appelée", System.Environment.NewLine)); title = GetStringFromDb(sqlConn, sqlQuery); Console.WriteLine("ContactTitle lu dans la base de données : {0}", title); Console.WriteLine("Restauration de la valeur originale de ContactTitle "); c.ContactTitle = "Marketing Manager"; db.SubmitChanges(); Console.WriteLine("ContactTitle restauré."); } finally { sqlConn.Close(); }

La méthode SubmitChanges est appelée, puis le champ ContactTitle est récupéré dans la base de données. Cette fois-ci, la valeur stockée dans la base de données a dû être mise à jour, puisque la méthode SubmitChanges a été exécutée. Les dernières lignes redonnent la valeur originale à l’entité objet ContactTitle et l’enregistrent dans la base de données en utilisant la méthode SubmitChanges. Ainsi, cet exemple pourra être exécuté plusieurs fois et les exemples suivants ne seront pas affectés. Ce code montre que les modifications effectuées dans les objets entité ne sont pas reportées dans la base de données tant que la méthode SubmitChanges n’est pas appelée. Il montre également qu’il suffit d’appeler la méthode GetStringFromDb pour lire le champ ContactTitle dans la base de données en utilisant ADO.NET. Voici les résultats : ContactTitle lu dans la base de données : Marketing Manager ContactTitle lu dans l’entité objet : Marketing Manager Affectation de la valeur ’Director of Marketing’ à l’entité objet ContactTitle ContactTitle lu dans la base de données : Marketing Manager ContactTitle lu dans l’entité objet : Director of Marketing La méthode SubmitChanges() a été appelée ContactTitle lu dans la base de données : Director of Marketing Restauration de la valeur originale de ContactTitle ContactTitle restauré

Comme le montrent les résultats, la valeur du champ ContactTitle n’est pas modifiée tant que la méthode SubmitChanges n’est pas appelée. Pour illustrer le second prototype de la méthode SubmitChanges, nous allons intentionnellement induire deux erreurs d’accès concurrentiel sur deux enregistrements. Pour ce Openmirrors.com

Linq.book Page 535 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

535

faire, nous les mettrons à jour avec ADO.NET entre le moment où ils sont obtenus (avec LINQ to SQL) et le moment où on tente de les mettre à jour (avec LINQ to SQL). Nous pourrons ainsi montrer la différence entre les modes ConflictMode.FailOnFirstConflict et ConflictMode.ContinueOnConflict. Vers la fin du code, nous restaurerons les valeurs originales dans la base de données afin que l’exemple puisse être exécuté plusieurs fois. Si vous stoppez le code avant qu’il ne soit entièrement exécuté, vous devrez peut-être réinitialiser manuellement ces valeurs. Dans le premier exemple du second prototype (voir Listing 16.12), nous initialiserons le paramètre ConflictMode à la valeur ContinueOnConflict afin de montrer que SubmitChanges est en mesure de gérer plusieurs conflits. Cet exemple étant assez complexe, nous donnerons des explications chaque fois que cela sera nécessaire. Listing 16.12 : Démonstration du mode ContinueOnConflict via le second prototype de la méthode SubmitChanges. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Console.WriteLine("Requête LINQ sur le client LAZYK"); Customer cust1 = (from c in db.Customers where c.CustomerID == "LAZYK" select c).Single(); Console.WriteLine("Requête LINQ sur le client LONEP"); Customer cust2 = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single();

Ces quelques lignes créent un DataContext Northwind et effectuent une requête sur les clients LAZYK et LONEP. string cmd = @"update Customers set ContactTitle = ’Director of Marketing’ where CustomerID = ’LAZYK’; update Customers set ContactTitle = ’Director of Sales’ where CustomerID = ’LONEP’"; ExecuteStatementInDb(cmd);

La valeur du champ ContactTitle des deux enregistrements est ensuite modifiée dans la base de données en utilisant la méthode commune ExecuteStatementInDb (les modifications sont faites via ADO.NET). Arrivés à ce point, nous sommes potentiellement prêts pour déclencher les conflits d’accès concurrentiel pour ces deux enregistrements. Console.WriteLine("Modification de la colonne ContactTitle dans les objets entité pour LAZYK et LONEP"); cust1.ContactTitle = "Vice President of Marketing"; cust2.ContactTitle = "Vice President of Sales";

Ce bloc de code modifie le champ ContactTitle des deux clients. Ainsi, lorsque nous appellerons la méthode SubmitChanges dans le prochain bloc de code, le processeur de changement de l’objet DataContext tentera d’enregistrer les modifications dans la base de données et de détecter les conflits d’accès concurrentiel. try {

Linq.book Page 536 Mercredi, 18. février 2009 7:58 07

536

LINQ to SQL

Partie V

Console.WriteLine("Appel de SubmitChanges() ..."); db.SubmitChanges(ConflictMode.ContinueOnConflict); Console.WriteLine("L’appel à SubmitChanges() a réussi"); }

Ce bloc de code appelle la méthode SubmitChanges. Les modifications effectuées dans les objets entité vont tenter d’être sauvegardées dans la base de données par le processeur de changement. Comme les valeurs des champs ContactTitle ont été modifiées depuis leur première lecture dans la base de données, un conflit d’accès concurrentiel va être détecté. catch (ChangeConflictException ex) { Console.WriteLine("Un ou plusieurs conflits se sont produits en appelant ➥SubmitChanges() : {0}.", ex.Message); foreach (ObjectChangeConflict objectConflict in db.ChangeConflicts) { Console.WriteLine("Un conflit pour {0} a été détecté", ((Customer)objectConflict.Object).CustomerID); foreach (MemberChangeConflict memberConflict in objectConflict.MemberConflicts) { Console.WriteLine(" Valeur LINQ : {0}{1} Valeur dans la base de données : ➥{2}", memberConflict.CurrentValue, System.Environment.NewLine, memberConflict.DatabaseValue); } } }

Ce bloc de code est là pour gérer l’exception ChangeConflictException. C’est là que les choses deviennent intéressantes. Dans un premier temps, nous énumérons la collection ChangeConflicts, composée d’objets DataContext db. Cette collection mémorisera des objets ObjectChangeConflict. Ces objets possèdent une propriété nommée Object qui fait référence à l’objet entité qui est à l’origine du conflit d’accès concurrentiel. Afin d’accéder aux valeurs des propriétés de l’objet entité Object, un casting lui est appliqué en utilisant le type de la classe d’entité. Nous pouvons ainsi accéder à la propriété CustomerID. Pour chacun des objets ObjectChangeConflict, nous énumérons sa collection d’objets MemberChangeConflict et affichons les informations qui nous intéressent. Ici la valeur issue de LINQ et la valeur issue de la base de données. Console.WriteLine("{0}Réinitialisation des données à leurs valeurs initiales", System.Environment.NewLine); cmd = @"update Customers set where CustomerID = update Customers set where CustomerID = ExecuteStatementInDb(cmd);

ContactTitle = ’Marketing Manager’ ’LAZYK’; ContactTitle = ’Sales Manager’ ’LONEP’";

Ce bloc de code restaure les enregistrements initiaux dans la base de données. L’exemple pourra donc être exécuté autant de fois que vous le souhaitez. Openmirrors.com

Linq.book Page 537 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

537

Les énumérations des collections relatives aux conflits ne sont pas une obligation. Cet exemple vous a montré comment extraire quelques-unes des informations correspondant aux conflits, pour le cas où vous en auriez besoin. Comme vous pouvez le voir, ce listing ne fait rien pour résoudre les conflits : il se contente d’afficher les informations correspondantes. Voici les résultats du code : Requête LINQ sur le client LAZYK Requête LINQ sur le client LONEP Exécution d’une déclaration SQL sur la base de données avec ADO.NET Base de données mise à jour Modification de la colonne ContactTitle dans les objets entité pour LAZYK et LONEP Appel de SubmitChanges() Un ou plusieurs conflits se sont produits en appelant SubmitChanges(): 2 sur 2 mises à jour ont échoué Un conflit pour LAZYK a été détecté Valeur LINQ : Vice President of Marketing Valeur dans la base de données : Director of Marketing Un conflit pour LONEP a été détecté Valeur LINQ : Vice President of Sales Valeur dans la base de données : Director of Sales Réinitialisation des données à leurs valeurs initiales Exécution d’une déclaration SQL sur la base de données avec ADO.NET Base de données mise à jour

Deux conflits ont été détectés : un pour chacun des enregistrements modifiés par ADO.NET. Cet exemple montre que le processeur de changement tente toujours de sauvegarder les données après que le premier conflit eut été détecté. Ceci est dû au fait que le mode ContinueOnFirstConflict a été passé en argument lors de l’appel de la méthode SubmitChanges. Le Listing 16.13 est identique au précédent mais, ici, le paramètre ConflictMode a pour valeur FailOnFirstConflict lors de l’appel de la méthode SubmitChanges. Listing 16.13 : Démonstration du mode FailOnFirstConflict via le second prototype de la méthode SubmitChanges. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Console.WriteLine("Requête LINQ sur le client LAZYK"); Customer cust1 = (from c in db.Customers where c.CustomerID == "LAZYK" select c).Single(); Console.WriteLine("Requête LINQ sur le client LONEP"); Customer cust2 = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single(); string cmd = @"update Customers set ContactTitle = ’Director of Marketing’ where CustomerID = ’LAZYK’; update Customers set ContactTitle = ’Director of Sales’ where CustomerID = ’LONEP’";

Linq.book Page 538 Mercredi, 18. février 2009 7:58 07

538

LINQ to SQL

Partie V

ExecuteStatementInDb(cmd); Console.WriteLine("Modification de la colonne ContactTitle dans les objets entité pour LAZYK et LONEP"); cust1.ContactTitle = "Vice President of Marketing"; cust2.ContactTitle = "Vice President of Sales"; try { Console.WriteLine("Appel de SubmitChanges() ..."); db.SubmitChanges(ConflictMode.FailOnFirstConflict); Console.WriteLine("L’appel à SubmitChanges() a réussi"); } catch (ChangeConflictException ex) { Console.WriteLine("Un ou plusieurs conflits se sont produits en appelant ➥SubmitChanges() : {0}.", ex.Message); foreach (ObjectChangeConflict objectConflict in db.ChangeConflicts) { Console.WriteLine("Un conflit pour {0} a été détecté", ((Customer)objectConflict.Object).CustomerID); foreach (MemberChangeConflict memberConflict in objectConflict.MemberConflicts) { Console.WriteLine(" Valeur LINQ : {0}{1} Valeur dans la base de données : ➥{2}", memberConflict.CurrentValue, System.Environment.NewLine, memberConflict.DatabaseValue); } } } Console.WriteLine("{0}Réinitialisation des données à leurs valeurs initiales", System.Environment.NewLine); cmd = @"update Customers set where CustomerID = update Customers set where CustomerID = ExecuteStatementInDb(cmd);

ContactTitle = ’Marketing Manager’ ’LAZYK’; ContactTitle = ’Sales Manager’ ’LONEP’";

Cette fois-ci, les résultats devraient mettre en évidence que le processeur de changement arrête les mises à jour après le premier conflit de concurrence : Requête LINQ sur le client LAZYK Requête LINQ sur le client LONEP Exécution d’une déclaration SQL sur la base de données avec ADO.NET Base de données mise à jour Modification de la colonne ContactTitle dans les objets entité pour LAZYK et LONEP Appel de SubmitChanges() Un ou plusieurs conflits se sont produits en appelant SubmitChanges(): Ligne non ➥trouvée ou modifiée. Un conflit pour LAZYK a été détecté Valeur LINQ : Vice President of Marketing Valeur dans la base de données : Director of Marketing Réinitialisation des données à leurs valeurs initiales Exécution d’une déclaration SQL sur la base de données avec ADO.NET Base de données mise à jour

Openmirrors.com

Linq.book Page 539 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

539

Comme vous pouvez le voir, même si deux conflits ont été provoqués, le processeur de changement a arrêté la mise à jour de la base de données après la détection du premier. Par voie de conséquence, un seul conflit est signalé dans la console.

DatabaseExists() La méthode DatabaseExists peut être utilisée pour déterminer l’existence d’une base de données. Cette détermination se base sur la chaîne de connexion spécifiée lors de l’instanciation du DataContext. Si vous spécifiez un fichier .mdf, il sera recherché dans le chemin indiqué. Si vous spécifiez un serveur, il est recherché. La méthode DatabaseExists est souvent utilisée conjointement aux méthodes DeleteDatabase et CreateDatabase. Prototype Un seul prototype de cette méthode sera étudié dans cet ouvrage : bool DatabaseExists()

Cette méthode retourne la valeur true si la base de données spécifiée dans la chaîne de connexion lors de l’instanciation du DataContext existe. Dans le cas contraire, elle retourne la valeur false. Exemple Pour une fois, la méthode à illustrer est très simple ! Le Listing 16.14 instancie un DataContext et appelle la méthode DatabaseExists pour voir si la base de données Northwind existe. Listing 16.14 : Un exemple d’utilisation de la méthode DatabaseExists. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Console.WriteLine("The Northwind database {0}.", db.DatabaseExists() ? "existe" : "n’existe pas");

Voici les résultats : La base de données Northwind existe.

Juste pour le fun, détachez la base de données Northwind et exécutez à nouveau le programme. Vous obtiendrez le message suivant dans la console : La base de données Northwind n’existe pas.

Si vous avez détaché la base de données Northwind, n’oubliez pas de l’attacher à nouveau pour que les autres exemples du livre puissent être exécutés.

Linq.book Page 540 Mercredi, 18. février 2009 7:58 07

540

LINQ to SQL

Partie V

CreateDatabase() Une classe d’entité possède une telle quantité d’informations sur la structure de la base de données à laquelle elle est mappée que Microsoft a cru bon de définir la méthode CreateDatabase pour… créer la base de données à partir de sa classe d’entité. Soyez bien conscient que la méthode CreateDatabase ne peut créer que les portions de la base de données qui correspondent aux attributs de la classe d’entité ou au fichier de mappage. Les contenus tels que procédures stockées, triggers, fonctions définies par l’utilisateur et limitations de vérification ne seront pas inclus dans une base de données définie par cette méthode, puisque aucun attribut ne spécifie les informations correspondantes. Pour des applications de petite envergure, ceci peut néanmoins suffire… ATTENTION Contrairement à la plupart des modifications effectuées dans une base de données via le DataContext, la méthode CreateDatabase s’exécute immédiatement. Il n’est pas nécessaire d’appeler la méthode SubmitChanges, et l’exécution n’est pas différée. Cette instruction vous permet donc de créer une base de données et d’y insérer immédiatement des données.

Prototype Nous étudierons un seul prototype de la méthode CreateDatabase : void CreateDatabase()

Cette méthode n’admet aucun argument et ne retourne aucune valeur. Exemple Le Listing 16.15 donne un exemple élémentaire de la méthode CreateDatabase. Listing 16.15 : Un exemple de la méthode CreateDatabase. Northwind db = new Northwind(@"C:\Northwnd.mdf"); db.CreateDatabase();

INFO Le nom Northwnd.mdf a été choisi intentionnellement, afin de ne pas détruire la base de données Northwind.

Ce code ne provoque aucune sortie dans la console. Cependant, si vous visualisez le contenu de la racine du disque C:, vous verrez les fichiers Northwnd.mdf et Northwnd.ldf. Par ailleurs, si vous ouvrez l’environnement intégré SQL Server Management Studio, vous verrez que le fichier Northwnd.mdf est attaché. Cette méthode est souvent utilisée conjointement à la méthode DatabaseExists. Si vous essayez d’appeler la méthode CreateDatabase sur une base de données déjà existante, une exception Openmirrors.com

Linq.book Page 541 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

541

est levée. Pour illustrer cette dernière phrase, il vous suffit d’exécuter le code du Listing 16.15 une deuxième fois, sans supprimer ni détacher la base de données de SQL Server Management Studio ou Entreprise Manager. Le message suivant sera alors affiché dans la console : Exception non gérée : System.Data.SqlClient.SqlException : La base de données ➥’C:\Northwnd.mdf’ existe. Choisissez un autre nom de base de données. ...

Ne pensez pas qu’il suffise de supprimer les deux fichiers relatifs à la base de données pour que vous puissiez exécuter à nouveau le code du Listing 16.15 : SQL Server en garde en effet une trace. Pour pouvoir réexécuter ce code, vous devez supprimer ou détacher la base de données en utilisant la technique exposée dans la section suivante.

DeleteDatabase() La méthode DeleteDatabase de l’objet DataContext permet de supprimer "proprement" une base de données. Si vous essayez de supprimer une base de données inexistante, une exception est levée. Il est donc prudent de n’utiliser DeleteDatabase qu’après avoir testé l’existence de la base de données avec la méthode DatabaseExists. ATTENTION Contrairement à la plupart des modifications effectuées dans une base de données via le DataContext, la méthode DeleteDatabase s’exécute immédiatement. Il n’est pas nécessaire d’appeler la méthode SubmitChanges, et l’exécution n’est pas différée.

Prototype Nous étudierons un seul prototype de la méthode CreateDatabase : void DeleteDatabase()

Cette méthode n’admet aucun argument et ne retourne aucune valeur. Exemple Le Listing 16.16 supprime la base de données créée dans le Listing 16.15. Listing 16.16 : Un exemple de la méthode DeleteDatabase. Northwind db = new Northwind(@"C:\Northwnd.mdf"); db.DeleteDatabase();

Tant que la base de données spécifiée existe, ce code ne provoque aucune sortie dans la console. Cependant, si vous visualisez le contenu de la racine du disque C:, vous verrez que les fichiers Northwnd.mdf et Northwnd.ldf ont disparu.

Linq.book Page 542 Mercredi, 18. février 2009 7:58 07

542

LINQ to SQL

Partie V

Si vous exécutez l’instruction DeleteDatabase sur une base de données inexistante, l’exception suivante est levée : Échec d’une tentative d’attachement d’une base de données nommée automatiquement pour le fichier Dataforum.mdf. Il existe une base de données du même nom ou le fichier spécifié ne peut être ouvert ou il se trouve sur un partage UNC. ...

CreateMethodCallQuery() Avant de décrire cette méthode, sachez qu’il s’agit d’une méthode protégée. Cela signifie qu’elle ne peut pas être appelée depuis le code d’une application. Pour pouvoir l’appeler, vous devez dériver une classe de la classe DataContext. La méthode CreateMethodCallQuery permet d’appeler des fonctions table définies par l’utilisateur. La méthode ExecuteMethodCall est utilisée pour appeler des fonctions scalaires définies par l’utilisateur. Vous en saurez plus à son sujet un peu plus loin dans ce chapitre. Prototype Nous étudierons un seul prototype de cette méthode dans cet ouvrage : protected internal IQueryable CreateMethodCallQuery( object instance, System.Reflection.MethodInfo methodInfo, params object[] parameters)

Trois arguments sont passés à la méthode CreateMethodCallQuery : m

une référence au DataContext ou au [Your]DataContext de la méthode appelante ;

m

l’objet MethodInfo de la méthode appelante ;

m

un tableau params contenant les paramètres de la fonction table définie par l’utilisateur.

Exemple Comme il a été dit précédemment, la méthode CreateMethodCallQuery est protégée et ne peut être appelée qu’à partir de la classe DataContext ou d’une classe qui en est dérivée. Plutôt que choisir un exemple qui appelle la méthode CreateMethodCallQuery, nous allons analyser la méthode générée par SQLMetal pour la fonction définie par l’utilisateur ProductsUnderThisUnitPrice de la base de données Northwind : [Function(Name="dbo.ProductsUnderThisUnitPrice", IsComposable=true)] public IQueryable ProductsUnderThisUnitPrice( [Parameter(DbType="Money")] System.Nullable price) { return this.CreateMethodCallQuery( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), price); }

Openmirrors.com

Linq.book Page 543 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

543

Dans ce code, vous pouvez voir que la méthode ProductsUnderThisUnitPrice est passée en argument de l’attribut Function. Cette méthode appellera donc la procédure stockée ou la fonction définie par l’utilisateur ProductsUnderThisUnitPrice. La propriété IsComposable de l’attribut Function étant initialisée à true, il s’agit donc d’une fonction définie par l’utilisateur, et non d’une procédure stockée. Le code généré appelant la méthode CreateMethodCallQuery, nous savons que la fonction table définie par l’utilisateur est ProductsUnderThisUnitPrice. Examinons les paramètres passés à la méthode CreateMethodCallQuery. Le premier argument est une référence à la classe dérivée de DataContext générée par SQLMetal. Le deuxième argument est l’objet MethodInfo de la méthode actuelle. Cela permettra à la méthode CreateMethodCallQuery d’accéder aux attributs : elle aura connaissance des informations nécessaires pour appeler la fonction définie par l’utilisateur. Le troisième argument est le seul paramètre accepté par la fonction définie par l’utilisateur. La valeur retournée par la méthode CreateMethodCallQuery provient de la méthode ProductsUnderThisUnitPrice. Ici, il s’agit d’une séquence d’objets ProductsUnderThisUnitPrice (la classe ProductsUnderThisUnitPrice a été automatiquement générée par SQLMetal). Ce code vous a montré comment appeler la méthode CreateMethodCallQuery. Voyons maintenant un exemple d’appel de la méthode générée ProductsUnderThisUnitPrice (voir Listing 16.17). Listing 16.17 : Un exemple d’appel de la méthode ProductsUnderThisPrice. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable results = db.ProductsUnderThisUnitPrice(new Decimal(5.50)); foreach(ProductsUnderThisUnitPriceResult prod in results) { Console.WriteLine("{0} - {1:C}", prod.ProductName, prod.UnitPrice); }

Voici les résultats : Guaraná Fantástica - $4.50 Geitost - $2.50

ExecuteQuery() À un moment ou à un autre, vous avez certainement éprouvé le besoin de lancer une requête SQL. Eh bien, sachez que LINQ to SQL autorise cette digression et que les valeurs retournées sont… des objets entité ! Il vous suffit pour cela de faire appel à la méthode ExecuteQuery. Spécifiez la requête SQL dans une chaîne et, si nécessaire, ajoutez les paramètres à substituer dans la

Linq.book Page 544 Mercredi, 18. février 2009 7:58 07

544

LINQ to SQL

Partie V

chaîne, tout comme vous le feriez lors de l’appel de la méthode String.Format. Les résultats seront alors automatiquement convertis en une séquence d’objets entité. C’est aussi simple que cela ! Mais qu’en est-il des erreurs d’injection SQL ? Pour les gérer, ne suffit-il pas d’utiliser des paramètres ? La méthode ExecuteQuery gère tout cela pour vous ! Prototype Nous étudierons un seul prototype de cette méthode dans cet ouvrage : IEnumerable ExecuteQuery(string query, params object[] parameters)

Le premier argument de cette méthode est obligatoire. Il représente la requête SQL à exécuter. Un ou plusieurs arguments peuvent alors être passés à la méthode via le deuxième argument. La chaîne de la requête SQL et les paramètres optionnels se comportent comme la méthode String.Format. La méthode retourne une séquence de type T, où T est une classe d’entité. Attention, si vous indiquez une valeur de colonne dans la clause where de la requête SQL, elle doit être encadrée d’apostrophes (tout comme dans une requête SQL traditionnelle) et non de guillemets ! En revanche, si vous passez une valeur de colonne dans un paramètre ({0}, par exemple), il n’est pas nécessaire de le délimiter en utilisant des apostrophes. Pour qu’une colonne dans la requête puisse être transformée en objet entité, son nom doit être identique à celui du champ mappé correspondant de l’objet entité. Bien entendu, ceci peut être réalisé en ajoutant "as " au nom de la colonne (où est une colonne mappée dans l’objet entité). Si tous les champs mappés ne sont pas nécessairement retournés par la requête, les clés primaires le sont forcément. Par ailleurs, vous pouvez obtenir des champs de la requête qui ne sont mappés à aucun champ de l’objet entité, mais ils ne seront pas propagés dans l’objet entité. Exemples Pour illustrer la méthode ExecuteQuery, nous allons effectuer une requête sur la table Customer (voir Listing 16.18). Listing 16.18 : Un exemple d’appel de la méthode ExecuteQuery. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable custs = db.ExecuteQuery( @"select CustomerID, CompanyName, ContactName, ContactTitle from Customers where Region = {0}", "WA"); foreach (Customer c in custs) { Console.WriteLine("ID = {0} : Nom = {1} : Contact = {2}",

Openmirrors.com

Linq.book Page 545 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

545

c.CustomerID, c.CompanyName, c.ContactName); }

Cet exemple est très simple. La valeur "WA" étant spécifiée dans un paramètre (et non incluse dans la requête), la fonctionnalité de substitution de paramètre sera utilisée. Cette valeur doit donc être entourée non pas d’apostrophes mais de guillemets. Voici les résultats : ID = LAZYK : Nom = Lazy K Kountry Store : Contact = John Steel ID = TRAIH : Nom = Trail’s Head Gourmet Provisioners : Contact = Helvetius Nagy ID = WHITC : Nom = White Clover Markets : Contact = Karl Jablonski

Pour exécuter la même requête sans faire appel à la substitution de paramètre, il suffit d’inclure la valeur WA dans la requête en la délimitant d’apostrophes (voir Listing 16.19). Listing 16.19 : Un autre exemple de la méthode ExecuteQuery. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable custs = db.ExecuteQuery( @"select CustomerID, CompanyName, ContactName, ContactTitle from Customers where Region = ’WA’"); foreach (Customer c in custs) { Console.WriteLine("ID = {0} : Name = {1} : Contact = {2}", c.CustomerID, c.CompanyName, c.ContactName); }

La valeur affectée au champ Region apparaît en gras dans le listing. Les résultats sont identiques à ceux du listing précédent : ID = LAZYK : Nom = Lazy K Kountry Store : Contact = John Steel ID = TRAIH : Nom = Trail’s Head Gourmet Provisioners : Contact = Helvetius Nagy ID = WHITC : Nom = White Clover Markets : Contact = Karl Jablonski

Pour en terminer avec cette méthode, nous allons vous montrer comment ajouter un nom d’une colonne, si le nom spécifié n’est pas trouvé dans la base de données. Étant donné qu’il est possible d’effectuer des jointures dans la chaîne de la requête, vous pourriez lancer une requête sur des colonnes en utilisant un nom différent issu d’une autre table, tout en les reliant à des champs mappés dans la classe d’entité (voir Listing 16.20). Listing 16.20 : Appel de la méthode ExecuteQuery en spécifiant un nom de champ mappé. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable custs = db.ExecuteQuery( @"select CustomerID, Address + ’, ’ + City + ’, ’ + Region as Address from Customers where Region = ’WA’"); foreach (Customer c in custs)

Linq.book Page 546 Mercredi, 18. février 2009 7:58 07

546

LINQ to SQL

Partie V

{ Console.WriteLine("Id = {0} : Addresse = {1}", c.CustomerID, c.Address); }

Dans cette requête, nous concaténons plusieurs colonnes de la base de données Northwind avec des chaînes littérales et spécifions un nom de champ mappé. Sont ainsi obtenues l’adresse, la ville et la région (dans le membre Address de l’objet entité). Dans cet exemple, tous les champs viennent de la même table. Mais ils auraient tout aussi bien pu provenir d’une jointure ou d’une autre table. Voici les résultats : Id = LAZYK : Addresse = 12 Orchestra Terrace, Walla Walla, WA Id = TRAIH : Addresse = 722 DaVinci Blvd., Kirkland, WA Id = WHITC : Addresse = 305 - 14th Ave. S. Suite 3B, Seattle, WA

Si vous utilisez cette technique, ayez bien en tête que, si un objet entité est modifié et que la méthode SubmitChanges est appelée, il se peut que vous obteniez des données quelque peu fantaisistes. Cependant, utilisée correctement, cette technique se révèle très pratique.

Translate() La méthode Translate est semblable à la méthode ExecuteQuery, car elle traduit le résultat d’une requête SQL en une séquence d’objets entité. En revanche, la requête SQL lui est passée non pas sous la forme d’une chaîne, mais sous la forme d’un objet System.Data.Common.DbDataReader (un SqlDataReader, par exemple). Cette méthode est très utile quand il s’agit d’intégrer du code LINQ to SQL dans du code ADO.NET existant. Prototype Nous étudierons un seul prototype de cette méthode dans cet ouvrage : IEnumerable Translate(System.Data.Common.DbDataReader reader)

Un objet de type System.Data.Common.DbDataReader est passé à la méthode et la séquence d’objets entité spécifiée est retournée. Exemples Dans le Listing 16.21, nous allons créer et exécuter une requête en utilisant ADO.NET. Cette étape effectuée, nous utiliserons la méthode Translate pour transformer les résultats de la requête en une séquence d’objets entité Customer. Le Listing 16.21 étant assez complexe, nous donnerons des informations chaque fois que cela sera nécessaire. Listing 16.21 : Un exemple de la méthode Translate. System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;");

Openmirrors.com

Linq.book Page 547 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

547

string cmd = @"select CustomerID, CompanyName, ContactName, ContactTitle from Customers where Region = ’WA’"; System.Data.SqlClient.SqlCommand sqlComm = new System.Data.SqlClient.SqlCommand(cmd); sqlComm.Connection = sqlConn; try { sqlConn.Open(); System.Data.SqlClient.SqlDataReader reader = sqlComm.ExecuteReader();

Nous allons supposer que tout le code listé jusqu’ici existe et qu’il s’agit d’un code hérité qui doit être mis à jour en utilisant LINQ. Comme vous pouvez le voir, aucune référence à LINQ n’est faite dans ce code : une connexion SqlConnection est établie, une requête est définie, une commande SqlCommand est créée, la connexion est ouverte et la requête, exécutée. Nous allons maintenant ajouter du code LINQ : Northwind db = new Northwind(sqlConn); IEnumerable custs = db.Translate(reader); foreach (Customer c in custs) { Console.WriteLine("ID = {0} : Nom = {1} : Contact = {2}", c.CustomerID, c.CompanyName, c.ContactName); }

Dans ce bloc de code, le DataContext Northwind est instancié en utilisant la connexion ADO.NET. La méthode Translate est alors appelée en lui passant le reader existant. Les résultats de la requête peuvent donc être convertis en une séquence d’objets entité qui peuvent être énumérés et dont les résultats peuvent être affichés. Étant donné qu’il s’agit d’un code hérité, il devrait y avoir quelques instructions additionnelles qui exploitent les résultats. Mais, pour illustrer la méthode Translate, ce code additionnel n’a aucun intérêt. Le listing se termine par la fermeture de la connexion : } finally { sqlComm.Connection.Close(); }

Ce listing vous a montré à quel point il est simple pour LINQ to SQL de dialoguer avec ADO.NET. Voici les résultats : ID = LAZYK : Nom = Lazy K Kountry Store : Contact = John Steel ID = TRAIH : Nom = Trail’s Head Gourmet Provisioners : Contact = Helvetius Nagy ID = WHITC : Nom = White Clover Markets : Contact = Karl Jablonski

ExecuteCommand() Tout comme la méthode ExecuteQuery, ExecuteCommand vous permet de spécifier la déclaration SQL à exécuter. Vous pouvez utiliser cette méthode pour exécuter une déclaration insert, update ou delete, ou encore une procédure stockée. Une autre analogie

Linq.book Page 548 Mercredi, 18. février 2009 7:58 07

548

LINQ to SQL

Partie V

avec la méthode ExecuteQuery : vous pouvez passer un ou plusieurs paramètres à la méthode. La méthode ExecuteCommand s’exécute immédiatement : aucun appel à la méthode SubmitChanges n’est donc nécessaire. Prototype Nous étudierons un seul prototype de cette méthode dans cet ouvrage : int ExecuteCommand(string command, params object[] parameters)

Cette méthode admet une chaîne de commande et zéro, un ou plusieurs paramètres optionnels. Elle retourne un entier qui indique le nombre de lignes affectées par la requête. Attention, si vous indiquez une valeur de colonne dans la clause where de la requête SQL, elle doit être encadrée d’apostrophes (tout comme dans une requête SQL traditionnelle) et non de guillemets ! En revanche, si vous passez une valeur de colonne dans un paramètre ({0}, par exemple), il n’est pas nécessaire de le délimiter en utilisant des apostrophes. Exemples Dans le Listing 16.22, nous allons insérer un enregistrement en utilisant la méthode ExecuteCommand. Nous utiliserons cette même méthode pour supprimer les modifications effectuées dans la base de données, afin que l’exemple puisse être exécuté à plusieurs reprises. Listing 16.22 : Utilisation de la méthode ExecuteCommand pour insérer et supprimer un enregistrement. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Console.WriteLine("Insertion d’un client ..."); int rowsAffected = db.ExecuteCommand( @"insert into Customers values ({0}, ’Lawn Wranglers’, ’Mr. Abe Henry’, ’Owner’, ’1017 Maple Leaf Way’, ’Ft. Worth’, ’TX’, ’76104’, ’USA’, ’(800) MOW-LAWN’, ’(800) MOW-LAWO’)", "LAWN"); Console.WriteLine("Fin de l’insertion.{0}", System.Environment.NewLine); Console.WriteLine("{0} ligne(s) a(ont) été affectée(s). Le client ajouté est-il dans ➥la base de données ?", rowsAffected); Customer cust = (from c in db.Customers where c.CustomerID == "LAWN" select c).DefaultIfEmpty().Single(); Console.WriteLine("{0}{1}", cust != null ? "Oui, le client a été trouvé dans la base de données." : "Non, le client n’a pas ➥été trouvé dans la base de données.", System.Environment.NewLine);

Openmirrors.com

Linq.book Page 549 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

549

Console.WriteLine("Suppression du client ajouté ..."); rowsAffected = db.ExecuteCommand(@"delete from Customers where CustomerID = {0}", "LAWN"); Console.WriteLine("Fin de la suppression.{0}", System.Environment.NewLine);

Cet exemple est assez simple. Une chaîne SQL et plusieurs paramètres sont fournis à la méthode ExecuteCommand pour définir un nouvel enregistrement dans la table Customers. Une requête LINQ to SQL s’assure que l’enregistrement a bien été inséré dans la table Customers et les résultats sont affichés dans la console. L’enregistrement ajouté dans la table Customers est enfin supprimé en invoquant une nouvelle fois la méthode ExecuteCommand. Voici les résultats affichés dans la console : Insertion d’un client … Fin de l’insertion. 1 ligne(s) a(ont) été affectée(s). Le client ajouté est-il dans la base de données ? Oui, le client a été trouvé dans la base de données. Suppression du client ajouté … Fin de la suppression.

ExecuteMethodCall() Avant de décrire cette méthode, sachez qu’il s’agit d’une méthode protégée. Cela signifie qu’elle ne peut pas être appelée depuis le code d’une application. Pour pouvoir l’appeler, vous devez dériver une classe de la classe DataContext. La méthode ExecuteMethodCall permet d’appeler des procédures stockées et des fonctions scalaires définies par l’utilisateur. Si vous souhaitez appeler des fonctions définies par l’utilisateur de type table, vous devez utiliser la méthode CreateMethodCallQuery. Le cas échéant, reportez-vous à la section correspondante, quelques pages plus tôt dans ce chapitre. Prototype Nous étudierons un seul prototype de cette méthode dans cet ouvrage : protected internal IExecuteResult ExecuteMethodCall( object instance, System.Reflection.MethodInfo methodInfo, params object[] parameters)

Plusieurs paramètres sont passés à la méthode ExecuteMethodCall : m

une référence à l’objet DataContext ou [Your]DataContext dont la méthode appelante est membre ;

m

l’objet MethodInfo de la méthode appelante ;

m

un tableau params contenant les paramètres de la procédure stockée ou de la fonction scalaire définie par l’utilisateur.

Linq.book Page 550 Mercredi, 18. février 2009 7:58 07

550

LINQ to SQL

Partie V

Étant donné qu’un objet MethodInfo est passé à la méthode, cette dernière doit être dotée de l’attribut procédure stockée/fonction définie par l’utilisateur correspondant ainsi que des propriétés d’attribut correspondantes. LINQ to SQL utilise l’objet MethodInfo pour accéder à l’attribut Function de la méthode et ainsi obtenir le nom de la procédure stockée/de la fonction scalaire définie par l’utilisateur. Il utilise également l’objet MethodInfo pour obtenir les noms et types des paramètres. La méthode ExecuteMethodCall retourne un objet qui implémente l’interface IExecuteResult. Si nécessaire, reportez-vous au Chapitre 15 pour en savoir plus sur cette interface. Si vous utilisez SQLMetal pour générer vos classes d’entité, spécifiez l’option : m

/sprocs pour générer les méthodes de classe qui appellent la méthode ExecuteMethodCall pour les procédures stockées de la base de données ;

m

/function pour générer les méthodes de classe qui appellent la méthode ExecuteMethodCall pour les fonctions définies par l’utilisateur de la base de données.

Exemples Avant de nous intéresser au code du premier exemple, nous allons donner quelques explications sur la méthode nommée CustomersCountByRegion, générée par SQLMetal pour appeler la procédure stockée Customers Count By Region de la base de données. Voici le code de la méthode ExecuteMethodCall générée par SQLMetal :

Utilisation de la méthode ExecuteMethodCall pour appeler une procédure stockée [Function(Name="dbo.Customers Count By Region")] [return: Parameter(DbType="Int")] public int CustomersCountByRegion([Parameter(DbType="NVarChar(15)")] string param1) { IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), param1); return ((int)(result.ReturnValue)); }

Comme vous pouvez le voir, la méthode CustomersCountByRegion se voit passer un paramètre chaîne. Ce paramètre est lui-même passé comme paramètre de la méthode ExecuteMethodCall, qui est passé comme paramètre de la procédure stockée Customers Count By Region. La méthode ExecuteMethodCall retourne une variable qui implémente IExecuteResult. Pour obtenir la valeur entière retournée, la méthode ExecuteMethodCall référence l’objet retourné ReturnValue et lui applique un casting de type int. Maintenant, examinons le Listing 16.23, qui appelle la méthode générée CustomersCountByRegion. Openmirrors.com

Linq.book Page 551 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

551

Listing 16.23 : Un exemple d’appel de la méthode générée CustomersCountByRegion. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); int rc = db.CustomersCountByRegion("WA"); Console.WriteLine("Il y a {0} clients dans l’état WA.", rc);

Cet exemple est très simple. Voici les résultats : Il y a 3 clients dans l’état WA.

Nous allons maintenant nous intéresser à l’appel d’une procédure stockée qui retourne un paramètre en sortie. Ici encore, nous allons observer la classe d’entité générée par SQLMetal pour la base de données Northwind, et en particulier la méthode CustOrderTotal, également générée par SQLMetal, qui se charge d’appeler la procédure stockée CustOrderTotal. Un exemple qui utilise la méthode ExecuteMethodCall pour appeler une procédure stockée qui retourne un paramètre en sortie [Function(Name="dbo.CustOrderTotal")] [return: Parameter(DbType="Int")] public int CustOrderTotal( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID, [Parameter(Name="TotalSales", DbType="Money")] ref System.Nullable totalSales) { IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID, totalSales); totalSales = ((System.Nullable)(result.GetParameterValue(1))); return ((int)(result.ReturnValue)); }

Le deuxième paramètre de la méthode CustOrderTotal (TotalSales) utilise le mot-clé ref. Cet indice nous laisse penser que cette valeur va être retournée par la procédure stockée. Après l’appel à la méthode ExecuteMethodCall, pour obtenir cette valeur nous appliquons la méthode GetParameterValue sur l’objet retourné, qui implémente IExecuteResult, et lui appliquons la valeur 1 pour accéder au deuxième paramètre. Le Listing 16.24 appelle la méthode CustOrderTotal. Listing 16.24 : Un exemple d’appel de la méthode générée CustOrderTotal. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); decimal? totalSales = 0; int rc = db.CustOrderTotal("LAZYK", ref totalSales); Console.WriteLine("Ventes totales du client LAZYK : {0:C}", totalSales);

Linq.book Page 552 Mercredi, 18. février 2009 7:58 07

552

LINQ to SQL

Partie V

Comme vous pouvez le voir, le mot-clé ref a été spécifié pour le deuxième paramètre, totalSales. Voici le résultat : Ventes totales du client LAZYK : $357.00

Le prochain exemple appelle une procédure stockée qui retourne ses résultats dans une "forme" unique. Nous allons raisonner sur la procédure stockée Customers By City de la base de données Northwind. Examinons la méthode générée par SQLMetal qui appelle cette procédure stockée via la méthode ExecuteMethodCall. Cet exemple utilise la méthode ExecuteMethodCall pour appeler une procédure stockée qui retourne une simple forme [Function(Name="dbo.Customers By City")] public ISingleResult CustomersByCity([Parameter(DbType="NVarChar(20)")] string param1) { IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), param1); return ((ISingleResult)(result.ReturnValue)); }

La méthode générée retourne un objet de type ISingleResult. La méthode générée récupère cet objet en effectuant un casting de ce type sur la propriété ReturnValue de l’objet retourné. La classe CustomersByCityResult a également été générée par SQLMetal. Nous n’y reviendrons pas. Le Listing 16.25 représente le code qui appelle la méthode CustomersByCity. Listing 16.25 : Un exemple d’appel de la méthode générée CustomersByCity. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); ISingleResult results = db.CustomersByCity("London"); foreach (CustomersByCityResult cust in results) { Console.WriteLine("{0} - {1} - {2} - {3}", cust.CustomerID, cust.CompanyName, cust.ContactName, cust.City); }

Comme vous pouvez le voir, ce code effectue une énumération de l’objet retourné, de type ISingleResult, tout comme s’il s’agissait d’une Openmirrors.com

Linq.book Page 553 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

553

séquence LINQ. Cette énumération est possible parce que ce type dérive de IEnumerable (voir Chapitre 15). Les résultats sont affichés dans la console : AROUT BSBEV CONSH EASTC NORTS SEVES

-

Around the Horn - Thomas Hardy - London B’s Beverages - Victoria Ashworth - London Consolidated Holdings - Elizabeth Brown - London Eastern Connection - Ann Devon - London North/South - Simon Crowther - London Seven Seas Imports - Hari Kumar – London

Examinons maintenant quelques exemples qui retournent plusieurs formes de résultat. Si le mot "forme" ne vous est pas familier, sachez qu’il caractérise les types de données retournés. Lorsqu’une requête retourne un numéro d’une commande et le nom du client, ces deux informations constituent une forme. Lorsqu’une requête retourne un numéro de commande, une date de commande et un code de pays, ces trois informations constituent une autre forme. Si une requête retourne plusieurs ensembles d’informations (comme les deux précédents), on dit qu’elle retourne plusieurs formes de résultats. Les procédures stockées étant en mesure de retourner plusieurs formes de résultats, LINQ to SQL doit être en mesure de gérer ce type d’informations. Dans notre premier exemple de formes multiples, nous allons supposer que la forme du résultat est conditionnelle. La méthode Northwind étendue possède une procédure stockée de ce type nommée Whole Or Partial Customers Set. SQLMetal a généré la méthode WholeOrPartialCustomerSet pour appeler cette procédure stockée. Voici son code : Un exemple qui exécute la méthode ExecuteMethodCall pour appeler une procédure stockée qui retourne conditionnellement plusieurs formes [Function(Name="dbo.Whole Or Partial Customers Set")] [ResultType(typeof(WholeOrPartialCustomersSetResult1))] [ResultType(typeof(WholeOrPartialCustomersSetResult2))] public IMultipleResults WholeOrPartialCustomersSet( [Parameter(DbType="Int")] System.Nullable param1) { IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), param1); return ((IMultipleResults)(result.ReturnValue)); }

Comme vous pouvez le voir, ce code comprend deux attributs ResultType qui correspondent aux deux formes possibles du résultat. Les deux classes correspondantes ont été générées automatiquement par SQLMetal. Le développeur qui appelle la méthode WholeOrPartialCustomersSet doit savoir que la procédure stockée retourne un résultat dont la forme dépend de la valeur de param1. Après avoir examiné la procédure stockée, j’ai pu en déduire que, lorsque param1 vaut 1, la procédure stockée retourne tous les

Linq.book Page 554 Mercredi, 18. février 2009 7:58 07

554

LINQ to SQL

Partie V

champs de la table Customers sous la forme d’une séquence d’objets de type WholeOrPartialCustomersSetResult1. Si param1 vaut 2, une version réduite des champs sera retournée sous la forme d’une séquence d’objets de type WholeOrPartialCustomersSetResult2. Dans ce code, remarquez également que le type de retour de la méthode WholeOrPartialCustomersSet est IMultipleResults. La méthode obtient ce type en effectuant un casting en IMultipleResults de la propriété ReturnValue de l’objet retourné par la méthode ExecuteMethodCall. Cette interface a été étudiée en détail au Chapitre 15. Le Listing 16.26 donne un exemple d’appel de la méthode WholeOrPartialCustomersSet. Listing 16.26 : Un exemple d’appel de la méthode générée WholeOrPartialCustomersSet. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IMultipleResults results = db.WholeOrPartialCustomersSet(1); foreach (WholeOrPartialCustomersSetResult1 cust in results.GetResult()) { Console.WriteLine("{0} - {1} - {2} - {3}", cust.CustomerID, cust.CompanyName, cust.ContactName, cust.City); }

Ce code montre clairement que les résultats sont de type IMultipleResults. La valeur passée étant 1, le résultat est donc de type WholeOrPartialCustomersSetResult1. Remarquez également que, pour obtenir les résultats, la méthode GetResult est appelée sur la variable IMultipleResults (où T est le type de la donnée retournée). Voici les résultats : LAZYK - Lazy K Kountry Store - John Steel - Walla Walla TRAIH - Trail’s Head Gourmet Provisioners - Helvetius Nagy - Kirkland WHITC - White Clover Markets - Karl Jablonski – Seattle

Cette procédure stockée obtient les clients dont le champ région a pour valeur "WA". Si nous avions passé la valeur 2 à la méthode WholeOrPartialCustomersSet, la séquence obtenue aurait été de type WholeOrPartialCustomersSetResult2. Dans le code précédent, chaque occurrence du type WholeOrPartialCustomersSetResult1 aurait dû être remplacée par un type WholeOrPartialCustomersSetResult2. Nous allons maintenant nous intéresser à une procédure qui retourne simultanément plusieurs formes dans un seul appel. Ici encore, nous allons nous servir d’une procédure stockée de la base de données étendue Northwind. Cette procédure a pour nom Get Customer And Orders. Dans un premier temps, nous allons nous intéresser à la méthode générée par SQLMetal pour appeler cette procédure stockée. Openmirrors.com

Linq.book Page 555 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

555

Un exemple qui exécute la méthode ExecuteMethodCall pour appeler une procédure stockée qui retourne plusieurs formes [Function(Name="dbo.Get Customer And Orders")] [ResultType(typeof(GetCustomerAndOrdersResult1))] [ResultType(typeof(GetCustomerAndOrdersResult2))] public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID) { IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID); return ((IMultipleResults)(result.ReturnValue)); }

Comme vous pouvez le voir, la méthode retourne un objet de type IMultipleResults. Étant donné que la procédure renvoie simultanément plusieurs formes, nous devons connaître l’ordre de retour des différentes formes. Après avoir examiné la procédure Get Customer And Orders, j’ai pu en déduire qu’elle retournait l’enregistrement issu de la table Customers en premier, puis les enregistrements correspondants de la table Orders. Le Listing 16.27 appelle la méthode générée à partir du code précédent. Listing 16.27 : Un exemple d’appel de la méthode générée GetCustomerAndOrders. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IMultipleResults results = db.GetCustomerAndOrders("LAZYK"); GetCustomerAndOrdersResult1 cust = results.GetResult().Single(); Console.WriteLine("Commandes de {0} :", cust.CompanyName); foreach (GetCustomerAndOrdersResult2 order in results.GetResult()) { Console.WriteLine("{0} - {1}", order.OrderID, order.OrderDate); }

Sachant que la procédure stockée ne retournera qu’un seul enregistrement de type GetCustomerAndOrdersResult1, l’opérateur Single est appelé sur la séquence de ce type, à condition que le client correspondant au CustomerID spécifié existe. Si cette dernière condition n’était pas toujours vérifiée, nous aurions pu appeler l’opérateur SingleOrDefault à la place de l’opérateur Single. Nous savons également que, après l’objet GetCustomerAndOrdersResult1, zéro, un ou plusieurs objets

Linq.book Page 556 Mercredi, 18. février 2009 7:58 07

556

LINQ to SQL

Partie V

GetCustomerAndOrdersResult2 seront retournés. Il a donc suffi d’énumérer ces objets et d’afficher les données souhaitées. Voici les résultats : Commandes de Lazy K Kountry Store : 10482 - 3/21/1997 12:00:00 AM 10545 - 5/22/1997 12:00:00 AM

Nous en avons fini avec les exemples concernant la méthode ExecuteMethodCall appliquée aux procédures stockées. Au début de cette section, nous avons indiqué que cette méthode pouvait également être utilisée pour appeler des méthodes scalaires définies par l’utilisateur. Nous allons donner un exemple d’un tel appel. Mais, avant tout, commençons par donner le code de la méthode ExecuteMethodCall généré par SQLMetal pour appeler une fonction scalaire définie par l’utilisateur : Un exemple qui exécute la méthode ExecuteMethodCall pour appeler une fonction scalaire définie par l’utilisateur [Function(Name="dbo.MinUnitPriceByCategory", IsComposable=true)] [return: Parameter(DbType="Money")] public System.Nullable MinUnitPriceByCategory( [Parameter(DbType="Int")] System.Nullable categoryID) { return ((System.Nullable)(this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), categoryID).ReturnValue)); }

La valeur scalaire retournée par la fonction définie par l’utilisateur est obtenue en référençant la propriété ReturnValue de l’objet retourné par la méthode ExecuteMethodCall. Nous pourrions nous contenter de créer un exemple qui appelle la méthode générée MinUnitPriceByCategory, mais nous allons aller un peu plus loin. En effet, les fonctions définies par l’utilisateur peuvent être utilisées comme s’il s’agissait de fonctions SQL. Le Listing 16.28 insère la méthode MinUnitPriceByCategory dans une requête pour identifier les produits les moins chers de leur catégorie. Listing 16.28 : Un exemple d’insertion d’une fonction définie par l’utilisateur dans une requête. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable products = from p in db.Products where p.UnitPrice == db.MinUnitPriceByCategory(p.CategoryID) select p; foreach (Product p in products) { Console.WriteLine("{0} - {1:C}", p.ProductName, p.UnitPrice); }

Openmirrors.com

Linq.book Page 557 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

557

Dans cet exemple, l’appel à la méthode MinUnitPriceByCategory (qui provoque l’appel à la fonction scalaire définie par l’utilisateur de même nom) est inséré dans la clause where. Voici les résultats : Aniseed Syrup - $10.00 Konbu - $6.00 Teatime Chocolate Biscuits - $9.20 Guaraná Fantástica - $4.50 Geitost - $2.50 Filo Mix - $7.00 Tourtière - $7.45 Longlife Tofu - $10.00

GetCommand() La méthode GetCommand est potentiellement très utile. Lorsqu’elle est appelée sur l’objet DataContext et qu’un objet LINQ to SQL IQueryable lui est passé, un objet de type System.Data.Common.DbCommand est retourné. Ce dernier donne accès à plusieurs composants clés qui peuvent être utilisés sur la requête passée. Par l’intermédiaire d’un objet DbCommand instancié avec la méthode GetCommand, vous obtenez une référence sur les objets CommandText, CommandTimeout, Connection, Parameters et Transaction, ainsi que d’autres objets relatifs à la requête passée. Par leur intermédiaire, vous pouvez non seulement examiner ces objets, mais aussi modifier leurs valeurs par défaut sans modifier les mêmes valeurs dans toutes les requêtes qui seront exécutées avec l’instance courante du DataContext. À titre d’exemple, pour une requête spécifique, vous pourriez vouloir incrémenter la valeur CommandTimeout sans que les autres requêtes exécutées avec le même objet DataContext n’utilisent la valeur CommandTimeout modifiée. Prototype Un seul prototype de cette méthode sera étudié dans cet ouvrage : System.Data.Common.DbCommand GetCommand(IQueryable query)

Une requête LINQ to SQL est passée à cette méthode, sous la forme d’un IQueryable. L’objet retourné est un System.Data.Common.DbCommand pour la requête LINQ passée en argument. Exemples Dans le Listing 16.29, un objet DbCommand est défini pour modifier le champ CommandTimeout d’une requête et pour afficher le champ CommandText, c’est-à-dire la requête SQL elle-même.

Linq.book Page 558 Mercredi, 18. février 2009 7:58 07

558

LINQ to SQL

Partie V

Listing 16.29 : Un exemple de la méthode GetCommand. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable custs = from c in db.Customers where c.Region == "WA" select c; System.Data.Common.DbCommand dbc = db.GetCommand(custs); Console.WriteLine("Timeout de la requête : {0}{1}", dbc.CommandTimeout, System.Environment.NewLine); dbc.CommandTimeout = 1; Console.WriteLine("Requête SQL : {0}{1}", dbc.CommandText, System.Environment.NewLine); Console.WriteLine("Timeout de la requête : {0}{1}", dbc.CommandTimeout, System.Environment.NewLine); foreach (Customer c in custs) { Console.WriteLine("{0}", c.CompanyName); }

Cet exemple est assez simple à comprendre. Après avoir défini une requête, elle est passée à la méthode GetCommand. La valeur CommandTimeout de l’objet DbCommand retourné est alors affichée. Cette valeur est initialisée à 1, puis la requête SQL et la nouvelle valeur CommandTimeout sont affichées. Enfin, les résultats renvoyés par la requête sont énumérés et affichés. Voici les résultats du code sur ma machine : Timeout de la requête : 30 Requête SQL : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] WHERE [t0].[Region] = @p0 Timeout de la requête : 1 Lazy K Kountry Store Trail’s Head Gourmet Provisioners White Clover Markets

Si l’exécution de cette requête est trop longue sur votre machine, il y aura timeout et les résultats seront différents.

GetChangeSet() Il peut parfois être utile d’obtenir la liste des objets entité qui seront insérés, modifiés ou supprimés par la méthode SubmitChanges. Vous utiliserez pour cela la méthode GetChangeSet. Openmirrors.com

Linq.book Page 559 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

559

Prototype Un seul prototype de cette méthode sera étudié dans cet ouvrage : ChangeSet GetChangeSet()

Cette méthode n’admet aucun argument. Elle retourne un objet ChangeSet qui contient des collections de type IList (où T est une classe d’entité). Les propriétés de collection Inserts, Updates et Deletes représentent respectivement les objets entité insérés, modifiés et supprimés. Il suffit d’énumérer ces collections pour examiner les objets entité correspondants. Exemples Dans le Listing 16.30, nous allons modifier, insérer et supprimer des objets entité. L’objet ChangeSet sera alors récupéré avec la méthode GetChangeSet et ses diverses collections énumérées. Listing 16.30 : Un exemple d’utilisation de la méthode GetChangeSet. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = (from c in db.Customers where c.CustomerID == "LAZYK" select c).Single(); cust.Region = "Washington"; db.Customers.InsertOnSubmit( new Customer { CustomerID = "LAWN", CompanyName = "Lawn Wranglers", ContactName = "Mr. Abe Henry", ContactTitle = "Owner", Address = "1017 Maple Leaf Way", City = "Ft. Worth", Region = "TX", PostalCode = "76104", Country = "USA", Phone = "(800) MOW-LAWN", Fax = "(800) MOW-LAWO" }); Customer cust2 = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single(); db.Customers.DeleteOnSubmit(cust2); cust2 = null; ChangeSet changeSet = db.GetChangeSet(); Console.WriteLine("{0} Entités ajoutées :", System.Environment.NewLine); foreach (Customer c in changeSet.Inserts) { Console.WriteLine("Le client {0} sera ajouté.", c.CompanyName); }

Linq.book Page 560 Mercredi, 18. février 2009 7:58 07

560

LINQ to SQL

Partie V

Console.WriteLine("{0} Entités modifiées :", System.Environment.NewLine); foreach (Customer c in changeSet.Updates) { Console.WriteLine("Le client {0} sera modifié.", c.CompanyName); } Console.WriteLine("{0} Entités supprimées :", System.Environment.NewLine); foreach (Customer c in changeSet.Deletes) { Console.WriteLine("Le client {0} sera supprimé.", c.CompanyName); }

Le premier bloc d’instructions modifie le champ Region du client LAZYK, le deuxième insère le client LAWN et le troisième supprime le client LONEP. L’objet ChangeSet changeset est alors obtenu en appelant la méthode GetChangeSet. Les trois derniers blocs d’instructions énumèrent les collections Inserts, Updates et Deletes et affichent leur contenu. Voici les résultats : Entités ajoutées : Le client Lawn Wranglers sera ajouté. Entités modifiées : Le clientLazy K Kountry Store sera modifié. Entités supprimées : Le client Lonesome Pine Restaurant sera supprimé.

Dans cet exemple, les énumérations des collections s’appuient sur le fait que chaque élément est un objet Customer. Dans de nombreux cas, les objets placés dans les collections peuvent être de plusieurs types et il n’est pas possible de faire des suppositions a priori sur leur type. Le cas échéant, vous devrez écrire le code d’énumération afin de tenir compte des différents types possibles. Dans cette tâche, l’opérateur OfType vous sera d’un grand intérêt.

GetTable() Vous utiliserez la méthode GetTable pour obtenir la référence d’une séquence Table d’un DataContext correspondant à une table mappée. Cette méthode est généralement utilisée lorsque le code s’appuie sur la classe DataContext (et non [Your]DataContext). L’utilisation de la classe [Your]DataContext est bien plus aisée, puisqu’elle dispose de la propriété Table qui fait référence à chacune des tables mappées. Prototypes Nous étudierons deux prototypes de la méthode GetTable dans cet ouvrage.

Premier prototype Table GetTable()

Ce prototype se voit passer une entité mappée de type T. Il retourne une séquence Table de type T. Openmirrors.com

Linq.book Page 561 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

561

Second prototype ITable GetTable(Type type)

Cette méthode se voit passer un objet entité Type. Elle retourne l’interface de la table. Cette interface peut alors être utilisée selon vos besoins. Si vous l’utilisez en tant que table, n’oubliez pas de lui appliquer l’opérateur de casting IQueryable. Exemples Le Listing 16.31 illustre le premier prototype. Dans ce listing, la classe DataContext (et non [Your]DataContext) est utilisée pour récupérer un certain client dans la table Customer de la base de données Northwind. Listing 16.31 : Un exemple du premier prototype de la méthode GetTable. DataContext db = new DataContext(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = (from c in db.GetTable() where c.CustomerID == "LAZYK" select c).Single(); Console.WriteLine("Le client {0} a été récupéré.", cust.CompanyName);

Dans le deuxième bloc d’instructions, la méthode GetTable est appelée pour obtenir une référence à la table Customer et pour extraire le client dont le champ CustomerID vaut "LAZYK". La dernière ligne affiche le nom de la société de ce client. Voici le résultat : Le client Lazy K Kountry Store a été récupéré.

Le Listing 16.32 illustre le second prototype. Tout comme dans le premier listing, la classe DataContext (et non [Your]DataContext) est utilisée pour récupérer un certain client dans la table Customer de la base de données Northwind. Listing 16.32 : Un exemple du second prototype de la méthode GetTable. DataContext db = new DataContext(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = (from c in ((IQueryable)db.GetTable(typeof(Customer))) where c.CustomerID == "LAZYK" select c).Single(); Console.WriteLine("Le client {0} a été récupéré.", cust.CompanyName);

Le résultat de ce listing est le même que celui du listing précédent : Le client Lazy K Kountry Store a été récupéré.

Linq.book Page 562 Mercredi, 18. février 2009 7:58 07

562

LINQ to SQL

Partie V

Refresh() La méthode Refresh vous permet de rafraîchir manuellement les objets entité de la base de données. Dans certaines situations, cette action est effectuée lorsque la méthode ResolveAll de la collection ChangeConflicts de l’objet DataContext est appelée pour résoudre un conflit d’accès concurrentiel, pendant l’appel à la méthode SubmitChanges. Dans d’autres cas, la méthode SubmitChanges n’est jamais appelée, mais il est cependant nécessaire de mettre à jour la base de données. Ce cas peut se produire lorsqu’une application affiche des données à lecture seule provenant d’une entité, du système ou d’un processus : la méthode Refresh permet d’afficher régulièrement les données rafraîchies lues dans la base de données. La méthode Refresh permet de rafraîchir un objet entité unique ou une séquence d’objets entité provenant des résultats d’une requête LINQ to SQL. Prototypes Trois prototypes de la méthode Refresh seront étudiés dans cet ouvrage.

Premier prototype void Refresh(RefreshMode mode, object entity)

Ce prototype admet deux arguments (le mode de rafraîchissement et un objet entité unique) et ne retourne aucune valeur. Deuxième prototype void Refresh(RefreshMode mode, params object[] entities)

Ce prototype admet deux arguments (le mode de rafraîchissement et le tableau params, qui contient plusieurs objets entité) et ne retourne aucune valeur. Troisième prototype void Refresh(RefreshMode mode, System.Collections.IEnumerable entities)

Ce prototype admet deux arguments (le mode de rafraîchissement et une séquence d’objets entité) et ne retourne aucune valeur. Le paramètre RefreshMode peut prendre pour valeur KeepChanges, KeepCurrentValues ou OverwriteCurrentValues. Le Tableau 16.1 donne les définitions de ces trois valeurs par Visual Studio. Tableau 16.1 : L’énumération RefreshMode

Nom du membre

Description

KeepChanges

Force la méthode Refresh à conserver la valeur actuelle qui a été modifiée, mais met à jour les autres valeurs avec les valeurs de base de données.

Openmirrors.com

Linq.book Page 563 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

563

Tableau 16.1 : L’énumération RefreshMode (suite)

Nom du membre

Description

KeepCurrentValues

Force la méthode Refresh à permuter les valeurs d’origine avec les valeurs récupérées de la base de données. Aucune valeur actuelle n’est modifiée.

OverwriteCurrentValues

Force la méthode Refresh à substituer toutes les valeurs actuelles par les valeurs de la base de données.

Le comportement de ces trois valeurs est étudié en détail au Chapitre 17. Exemples Dans le Listing 16.33, nous allons appliquer une requête à un client en utilisant LINQ to SQL, puis afficher ses entités ContactTitle et ContactName. Le nom du contact de ce client sera alors modifié dans la base de données en utilisant ADO.NET, puis l’entité ContactTitle sera modifiée. Pour s’assurer que l’objet entité ContactName n’a pas été modifié et ne reflète pas le contenu de la base de données, nous afficherons à nouveau les entités ContactTitle et ContactName.

La méthode Refresh sera alors appelée avec un paramètre RefreshMode initialisé à KeepChanges, puis les entités ContactTitle et ContactName seront à nouveau affichées. Vous verrez ainsi que le champ ContactName de la base de données a été reporté dans l’entité correspondante et que l’entité ContactTitle reste inchangée. Le champ ContactName sera alors restauré à sa valeur initiale dans la base de données afin que l’exemple puisse être exécuté plusieurs fois. Listing 16.33 : Un exemple du premier prototype de la méthode Refresh. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = (from c in db.Customers where c.CustomerID == "GREAL" select c).Single(); Console.WriteLine("Le nom original du client est {0}, ContactTitle a pour valeur ➥{1}.{2}", cust.ContactName, cust.ContactTitle, System.Environment.NewLine); ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Brad Radaker’ where CustomerID = ’GREAL’")); cust.ContactTitle = "Chief Technology Officer"; Console.WriteLine("Le nom du client avant le rafraîchissement est {0}, ContactTitle

➥a pour valeur {1}.{2}",

cust.ContactName, cust.ContactTitle, System.Environment.NewLine); db.Refresh(RefreshMode.KeepChanges, cust);

Linq.book Page 564 Mercredi, 18. février 2009 7:58 07

564

LINQ to SQL

Partie V

Console.WriteLine("Le nom du client après le rafraîchissement est {0}, ContactTitle ➥a pour valeur {1}.{2}", cust.ContactName, cust.ContactTitle, System.Environment.NewLine); // Les valeurs modifiées doivent être restaurées de telle sorte que le code // puisse être exécuté plusieurs fois Console.WriteLine("{0}Réinitialisation des valeurs originales", System.Environment.NewLine); ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’John Steel’ where CustomerID = ’GREAL’"));

Une requête LINQ to SQL est exécutée pour récupérer la référence à l’objet client GREAL. Les entités ContactName et ContactTitle de cet objet sont alors affichées. Le champ ContactName de ce client est alors modifié dans la base de données en utilisant ADO.NET, et l’entité ContactTitle de ce client est modifiée. Arrivé à ce point dans le code, l’objet entité Customer ne sait pas que le champ ContactName a été modifié dans la base de données. Ceci est mis en évidence par l’affichage des entités objet ContactName et ContactTitle. La méthode Refresh est alors appelée en lui passant un paramètre RefreshMode initialisé à KeepChanges. Cette méthode devrait provoquer la mise à jour des entités objets qui n’ont pas été modifiées par les champs modifiés dans la base de données. Ici, le champ ContactName ayant été modifié dans la base de données, il devrait mettre à jour l’entité correspondante. Les entités ContactName et ContactTitle sont alors affichées. Elles devraient refléter la valeur du champ ContactName modifié dans la base de données et la valeur du champ ContactTitle modifié dans l’entité. Les dernières instructions restaurent les valeurs originales dans la base de données. Ainsi, cet exemple pourra être exécuté plusieurs fois et les suivants ne seront pas affectés. Voici les résultats : Le nom original du client est John Steel, ContactTitle a pour valeur Marketing ➥Manager. Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour. Le nom du client avant le rafraîchissement est John Steel, ContactTitle a pour valeur ➥Chief Technology Officer. Le nom du client avant le rafraîchissement est Brad Radaker, ContactTitle a pour ➥valeur Chief Technology Officer. Réinitialisation des valeurs originales. Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour.

Openmirrors.com

Linq.book Page 565 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

565

Comme vous pouvez le voir, l’objet entité n’est pas au courant de la modification effectuée dans la base de données avant que la méthode Refresh ne soit appelée. Nous allons maintenant illustrer le deuxième prototype de la méthode Refresh. Dans le Listing 16.34, nous allons utiliser LINQ to SQL pour récupérer le client dont la région est "WA". La séquence d’objets Customer retournée sera énumérée et les entités CustomerId, Region et Country seront affichées. Ensuite, en utilisant ADO.NET, nous mettrons à jour le champ Country de chacun des clients dont la région est WA. Lorsque nous sommes arrivés à ce point dans l’exécution du code, le champ Country de ces clients a une valeur différente dans la base de données et dans les objets entité récupérés. Pour mettre en évidence cette différence, les objets entité seront énumérés. L’opérateur ToArray sera alors invoqué sur la séquence d’objets Customer afin d’obtenir un tableau composé d’objets Customer, puis la méthode Refresh sera appelée en initialisant à KeepChanges le paramètre RefreshMode et en passant les premier, deuxième et troisième éléments du tableau d’objets Customer. La séquence d’objets entité Customer sera alors énumérée une dernière fois. Ceci nous permettra de mettre en évidence que les entités CustomerID, Region et Country ont été rafraîchies à partir des informations stockées dans la base de données. Bien entendu, nous restaurerons les données originales dans la base de données pour que l’exemple puisse être exécuté plusieurs fois. Listing 16.34 : Un exemple du deuxième prototype de la méthode Refresh. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable custs = (from c in db.Customers where c.Region == "WA" select c); Console.WriteLine("Objets entité avant la modification ADO.NET et l’appel de ➥Refresh() :"); foreach (Customer c in custs) { Console.WriteLine("La région du client {0} est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country); } Console.WriteLine("{0} Affectation de la valeur United States au pays de ces clients

➥avec ADO.NET ...",

System.Environment.NewLine); ExecuteStatementInDb(String.Format( @"update Customers set Country = ’United States’ where Region = ’WA’")); Console.WriteLine("Pays des clients mis à jour{0}", System.Environment.NewLine); Console.WriteLine("Objets entité après la modification ADO.NET et avant l’appel de

➥Refresh() :");

foreach (Customer c in custs) { Console.WriteLine("La région du client {0}’s est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country); }

Linq.book Page 566 Mercredi, 18. février 2009 7:58 07

566

LINQ to SQL

Partie V

Customer[] custArray = custs.ToArray(); Console.WriteLine("{0} Rafraîchissement du tableau params d’objets entité Customer ➥...", System.Environment.NewLine); db.Refresh(RefreshMode.KeepChanges, custArray[0], custArray[1], custArray[2]); Console.WriteLine("Le tableau a été rafraîchi.{0}", System.Environment.NewLine); Console.WriteLine("Objets entité après la modification ADO.NET et l’appel de

➥Refresh() :"); foreach (Customer c in custs) { Console.WriteLine("La région du client {0}’s est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country); } // Les valeurs modifiées doivent être restaurées de telle sorte que le code // puisse être exécuté plusieurs fois Console.WriteLine("{0}Resetting data to original values.", System.Environment.NewLine); ExecuteStatementInDb(String.Format( @"update Customers set Country = ’USA’ where Region = ’WA’"));

Le code précédent devient intéressant à partir de l’appel à l’opérateur ToArray. Après avoir récupéré le tableau d’objets Customer, la méthode Refresh est appelée, en lui passant les valeurs custArray[0], custArray[1] et custArray[2]. Voici les résultats : Objets entité avant La région du client La région du client La région du client

la modification ADO.NET et l’appel de Refresh() : LAZYK est WA, le pays est USA. TRAIH est WA, le pays est USA. WHITC est WA, le pays est USA.

Affectation de la valeur United States au pays de ces clients avec ADO.NET... Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour. Pays des clients mis à jour. Objets entité après La région du client La région du client La région du client

la modification ADO.NET et avant l’appel de Refresh() : LAZYK est WA, le pays est USA. TRAIH est WA, le pays est USA. WHITC est WA, le pays est USA.

Rafraîchissement du tableau params d’objets entité Customer ... Le tableau a été rafraîchi. Objets entité après La région du client La région du client La région du client

la modification ADO.NET et l’appel de Refresh() : LAZYK est WA, le pays est United States. TRAIH est WA, le pays est United States. WHITC est WA, le pays est United States.

Restauration des données originales. Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour.

Openmirrors.com

Linq.book Page 567 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

567

Comme vous pouvez le voir dans les résultats, les modifications apportées au champ Country de la base de données ne sont pas reportées dans les objets entité Customer jusqu’à ce que la méthode Refresh soit appelée. Dans le Listing 16.34, chaque entité objet rafraîchie était du même type : Customer. Le deuxième prototype de la méthode Refresh s’accommode fort bien de types d’entités objet différents. Dans le cas du Listing 16.34, il aurait été pratique de passer une séquence d’objet entité à la méthode Refresh. Heureusement, le troisième prototype de la méthode va nous permettre de passer un tel objet. Le Listing 16.35 illustre le troisième prototype de la méthode Refresh. Il utilise le même code que le listing précédent, mais, au lieu de définir un tableau et de passer ses éléments à la méthode Refresh, c’est la séquence d’objets Customer récupérée par la requête qui va être passée. Listing 16.35 : Un exemple d’utilisation du troisième prototype de la méthode Refresh. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IEnumerable custs = (from c in db.Customers where c.Region == "WA" select c); Console.WriteLine("Objets entité avant la modification ADO.NET et l’appel de ➥Refresh() :"); foreach (Customer c in custs) { Console.WriteLine("La région du client {0} est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country); } Console.WriteLine("{0} Affectation de la valeur United States au pays de ces clients

➥avec ADO.NET ...", System.Environment.NewLine); ExecuteStatementInDb(String.Format( @"update Customers set Country = ’United States’ where Region = ’WA’")); Console.WriteLine("Pays des clients mis à jour{0}", System.Environment.NewLine); Console.WriteLine("Objets entité après la modification ADO.NET et avant l’appel de

➥Refresh() :"); foreach (Customer c in custs) { Console.WriteLine("La région du client {0}’s est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country); } Console.WriteLine("{0}Rafraîchissement de la séquence d’objets entité Customer ...", System.Environment.NewLine); db.Refresh(RefreshMode.KeepChanges, custs); Console.WriteLine("La séquence d’objets entité Customer a été rafraîchie.{0}", System.Environment.NewLine); Console.WriteLine("Objets entité après la modification ADO.NET et l’appel de

➥Refresh() :");

Linq.book Page 568 Mercredi, 18. février 2009 7:58 07

568

LINQ to SQL

Partie V

foreach (Customer c in custs) { Console.WriteLine("La région du client {0}’s est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country); } // Les valeurs modifiées doivent être restaurées de telle sorte que le code // puisse être exécuté plusieurs fois Console.WriteLine("{0}Resetting data to original values.", System.Environment.NewLine); ExecuteStatementInDb(String.Format( @"update Customers set Country = ’USA’ where Region = ’WA’"));

Le code du Listing 16.35 est le même que celui du Listing 16.34 mais, lors de l’appel de la méthode Refresh, la séquence custs est passée en argument. Voici les résultats : Objets entité avant La région du client La région du client La région du client

la modification ADO.NET et l’appel de Refresh() : LAZYK est WA, le pays est USA. TRAIH est WA, le pays est USA. WHITC est WA, le pays est USA.

Affectation de la valeur United States au pays de ces clients avec ADO.NET... Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour. Pays des clients mis à jour. Objets entité après La région du client La région du client La région du client

la modification ADO.NET et avant l’appel de Refresh() : LAZYK est WA, le pays est USA. TRAIH est WA, le pays est USA. WHITC est WA, le pays est USA.

Rafraîchissement de la séquence d’objets entité Customer ... La séquence d’objets entité Customer a été rafraîchie. Objets entité après La région du client La région du client La région du client

la modification ADO.NET et l’appel de Refresh() : LAZYK est WA, le pays est United States. TRAIH est WA, le pays est United States. WHITC est WA, le pays est United States.

Restauration des données originales. Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour.

Comme vous pouvez le voir dans les résultats, les modifications apportées au champ Country de la base de données ne sont pas reportées dans les objets entité Customer jusqu’à ce que la méthode Refresh soit appelée.

Résumé Il a fallu attendre jusqu’à ce chapitre pour savoir exactement ce que la classe DataContext pouvait faire pour vous. La compréhension de LINQ to SQL n’est pas immédiate, car elle nécessite l’entendement de LINQ, des requêtes de bases de données et de SQL. C’est pour cela que LINQ to SQL est un sujet prolixe et que la plupart des concepts inhérents à la classe DataContext vont de pair avec les classes d’entité. Il a bien fallu qu’un de ces sujets soit traité en premier… Openmirrors.com

Linq.book Page 569 Mercredi, 18. février 2009 7:58 07

Chapitre 16

La classe DataContext

569

Ce chapitre regroupe un grand nombre d’informations, mais les sujets les plus importants sont certainement liés aux services du DataContext : la recherche d’identité, la recherche de changements et le processus de changement. Bien entendu, ces services n’ont aucun intérêt si vous ne savez pas instancier un objet DataContext ou [Your]DataContext. Les constructeurs des classes DataContext et [Your]Datacontext sont donc également très importants. Outre ces constructeurs, vous utiliserez très fréquemment la méthode SubmitChanges du DataContext pour sauvegarder les modifications des classes d’entité dans la base de données. Enfin, il est important d’avoir à l’esprit que, lorsque vous utilisez la méthode SubmitChanges, un conflit d’accès concurrentiel peut se produire et provoquer une exception. Ces conflits ont été mentionnés à plusieurs reprises dans les chapitres relatifs à LINQ to SQL, mais ils n’ont jamais été étudiés en détail. Pour en savoir plus à leur sujet, consultez le chapitre suivant, qui leur est dédié.

Linq.book Page 570 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 571 Mercredi, 18. février 2009 7:58 07

17 Les conflits d’accès concurrentiels Combien de fois avez-vous entendu parler des conflits d’accès concurrentiels et de leur résolution dans cet ouvrage ? Dans la plupart des chapitres précédents relatifs à LINQ to SQL, nous avons parlé de ces types de conflits, sans toutefois leur accorder toute l’attention qu’ils méritent. Ce chapitre va combler cette lacune.

Prérequis pour exécuter les exemples Pour exécuter les exemples de ce chapitre, vous devez être en possession de la version étendue de la base de données Northwind et des classes d’entité générées correspondantes. Si nécessaire, reportez-vous à la section intitulée "Prérequis pour exécuter les exemples" du Chapitre 12 pour savoir comment procéder. Méthodes communes Plusieurs méthodes communes sont également nécessaires à la bonne exécution des exemples. Reportez-vous à la section intitulée "Méthodes communes" au Chapitre 12 pour en savoir plus à ce sujet. Utilisation de l’API LINQ to SQL Pour exécuter les exemples de ce chapitre, vous pouvez être amené à ajouter des références et des directives using à vos projets. Reportez-vous à la section intitulée "Utilisation de l’API LINQ to SQL" au Chapitre 12 pour en savoir plus à ce sujet.

Conflits d’accès concurrentiels Lorsqu’une connexion à une base de données tente de mettre à jour des données qui ont été modifiées par une autre connexion de base de données depuis leur lecture dans la première connexion, un conflit d’accès concurrentiel se produit. Imaginez un processus

Linq.book Page 572 Mercredi, 18. février 2009 7:58 07

572

LINQ to SQL

Partie V

P1 qui lit des données, puis un processus P2 qui lit les mêmes données. Si le processus P2 met à jour les données avant le processus P1, un conflit d’accès concurrentiel se produit lorsque le processus P1 tente de mettre à jour les données. De la même manière, si le processus P1 met à jour les données avant le processus P2, un conflit d’accès concurrentiel se produit lorsque le processus P2 tente de mettre à jour les données. Si plusieurs connexions peuvent accéder à une même base de données, tôt ou tard, un conflit d’accès concurrentiel se produira. Lorsqu’un conflit se produit, l’application doit effectuer plusieurs actions pour le résoudre. À titre d’exemple, un administrateur de site web peut se trouver sur une page qui affiche et permet de mettre à jour les données relatives à un utilisateur. Si, après l’affichage des données par l’administrateur, l’utilisateur va sur une page qui lui permet de mettre à jour ses données et effectue quelques modifications, un conflit se produira lorsque l’administrateur sauvegardera ses modifications dans la base de données. Si aucun conflit ne se produit, les modifications effectuées par l’utilisateur seront écrasées par celles de l’administrateur. Une autre alternative consisterait à sauvegarder les modifications de l’utilisateur et à ignorer celles de l’administrateur. Choisir le bon comportement est un problème assez complexe mais, dans tous les cas, la première étape consiste à détecter le conflit et la seconde, à le régler. Deux approches sont possibles : l’optimiste et la pessimiste. Nous allons les étudier en détail dans les sections suivantes. Contrôle d’accès concurrentiel optimiste Comme son nom l’indique, le contrôle d’accès concurrentiel optimiste suppose que, dans la plupart des cas, aucun conflit concurrentiel ne se produira. Par conséquent, aucun verrou n’est placé sur les données pendant la lecture dans la base de données. Si un conflit se produit lors de la mise à jour de la base de données, il est examiné à ce moment-là. La gestion optimiste est plus complexe que la gestion pessimiste, mais elle fonctionne mieux dans les applications modernes, auxquelles un grand nombre d’utilisateurs accèdent souvent simultanément. Imaginez votre frustration si, lorsque vous voulez visualiser la page d’un objet sur un site de ventes aux enchères, l’accès vous est refusé parce qu’un autre internaute est déjà en train de la visualiser ! Ou encore si vous ne pouvez pas enchérir sur un objet parce qu’un autre utilisateur est en train de le faire ! LINQ to SQL utilise l’approche optimiste du contrôle d’accès concurrentiel. Comme vous allez le voir, la détection et la résolution de conflits d’accès concurrentiel sont grandement simplifiées par LINQ to SQL. Si vous le souhaitez, vous pouvez même utiliser une méthode de résolution automatique… Détection de conflit Comme il a été dit précédemment, la première étape va consister à détecter un conflit. LINQ to SQL propose deux approches. Si la propriété IsVersion est spécifiée dans une

Openmirrors.com

Linq.book Page 573 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

573

propriété de classe d’entité et a pour valeur true, la valeur de cette propriété (et seulement de cette propriété) sera utilisée pour déterminer si un conflit concurrentiel s’est produit. Si aucune propriété de classe d’entité n’a une propriété IsVersion initialisée à true, LINQ to SQL vous permet de choisir les propriétés de classe d’entité qui seront prises en compte pour détecter un conflit. Ceci par l’intermédiaire de la propriété UpdateCheck de l’attribut Column, spécifiée sur une propriété mappée d’une classe d’entité. UpdateCheck peut prendre l’une des trois valeurs suivantes : Never, Always ou WhenChanged. UpdateCheck Si la propriété UpdateCheck d’une propriété de classe d’entité mappée est initialisée à UpdateCheck.Never, elle ne prendra pas part à la détection de conflit concurrentiel. Si la propriété UpdateCheck est initialisée à UpdateCheck.Always, elle prendra toujours part à la détection de conflit, indépendamment du fait que la valeur a été/n’a pas été modifiée depuis sa mise en cache par le DataContext. Si la propriété UpdateCheck est initialisée à UpdateCheck.WhenChanged, elle prendra part à la détection de conflit si la valeur a changé depuis sa mise en cache par le DataContext. Si l’attribut UpdateCheck n’est pas spécifié, sa valeur par défaut est UpdateCheck.Always.

En ayant une idée de l’implémentation de la détection de conflit concurrentiel, vous comprendrez mieux comment elle fonctionne. Lorsque vous appelez la méthode SubmitChanges, le processeur de changement génère les déclarations SQL nécessaires pour sauvegarder les modifications apportées aux objets entité dans la base de données. Lorsqu’un enregistrement doit être mis à jour, plutôt que se contenter de spécifier la clé primaire de l’enregistrement dans la clause where afin de le récupérer, il y ajoute les colonnes qui peuvent être la source du conflit. Si la propriété UpdateCheck d’une propriété d’une classe d’entité est initialisée à UpdateCheck.Always, la colonne mappée de cette propriété et sa valeur originale seront toujours spécifiées dans la clause where. Si la propriété UpdateCheck d’une propriété d’une classe d’entité est initialisée à UpdateCheck.WhenChanged, la colonne mappée de cette propriété et sa valeur originale seront spécifiées dans la clause where si la valeur actuelle de la propriété de l’objet entité est différente de sa valeur originale. Enfin, si la propriété UpdateCheck d’une propriété d’une classe d’entité est initialisée à UpdateCheck.Never, la colonne mappée de cette propriété ne sera pas incluse dans la clause where. À titre d’exemple, concernant l’objet entité Customer, supposons que la propriété UpdateCheck du champ : m

CompanyName ait pour valeur UpdateCheck.Always ;

m

ContactName ait pour valeur UpdateCheck.WhenChanged ;

m

ContactTitle ait pour valeur UpdateCheck.Never.

Linq.book Page 574 Mercredi, 18. février 2009 7:58 07

574

LINQ to SQL

Partie V

Si chacune de ces propriétés a été modifiée dans l’objet entité d’un client, la déclaration SQL aura l’allure suivante : Update Customers Set CompanyName = ’Art Sanders Park’, ContactName = ’Samuel Arthur Sanders’, ContactTitle = ’President’ Where CompanyName = ’Lonesome Pine Restaurant’ AND ContactName = ’Fran Wilson’ AND CustomerID = ’LONEP’

Dans cet exemple, les valeurs de colonnes incluses dans la clause where sont celles qui ont été lues dans la base de données lorsque l’objet entité a été récupéré, lorsque la méthode SubmitChanges a été entièrement exécutée ou lorsque la méthode Refresh a été appelée. La propriété UpdateCheck de la propriété CompanyName ayant pour valeur UpdateCheck.Always, CompanyName est incluse dans la clause where, que cette entité objet ait changé ou non. Comme la propriété UpdateCheck de la propriété ContactName a pour valeur UpdateCheck.WhenChanged et que la valeur de cette entité a changé, ContactName est incluse dans la clause where. Enfin, comme la propriété UpdateCheck de la propriété ContactTitle a pour valeur UpdateCheck.Never, ContactTitle n’est pas incluse dans la clause where, que la valeur de cette propriété ait changé ou non. Lorsque la déclaration SQL est exécutée, si une ou plusieurs des valeurs des propriétés de la classe d’entité spécifiées dans la clause where ne correspondent pas aux valeurs stockées dans la base de données, l’enregistrement n’est pas trouvé. Il n’est donc pas mis à jour. C’est ainsi qu’un conflit concurrentiel est détecté et qu’une exception ChangeConflictException est levée. Ce que nous avons dit à propos de la détection des conflits est un peu vague, mais son implémentation n’est pas spécifiée par Microsoft et le code n’est pas aussi accessible qu’il l’était dans les préversions de LINQ. Dans la version finale, après l’exécution d’une déclaration update, une déclaration select générée contenant une comparaison du @@ROWCOUNT retourné par la déclaration update serait exécutée, permettant ainsi au processeur de changement de savoir si aucun enregistrement n’a été mis à jour et, donc…, qu’un conflit a eu lieu. Dans tous les cas, ne prenez pas ce raisonnement pour argent comptant, puisque l’implémentation du contrôle d’accès concurrentiel n’est pas spécifiée par Microsoft. Quoi qu’il en soit, vous aurez au moins pris connaissance d’une technique d’implémentation du contrôle d’accès concurrentiel. On ne sait jamais, vous pourriez un jour être amené à implémenter quelque chose de similaire dans le cadre d’un projet. Pour avoir une idée précise de la déclaration update générée, examinez le Listing 17.1. Openmirrors.com

Linq.book Page 575 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

575

Listing 17.1 : Mise à jour de la base de données pour voir comment les conflits d’accès concurrentiels sont détectés. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.Log = Console.Out; Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP").SingleOrDefault(); string name = cust.ContactName; // A restaurer par la suite cust.ContactName = "Neo Anderson"; db.SubmitChanges(); // Restauration de la base de données cust.ContactName = name; db.SubmitChanges();

Il n’y a pas grand-chose à dire sur cette requête. La seule chose qui mérite votre attention est l’appel à l’opérateur SingleOrDefault en lieu et place du traditionnel opérateur Single. Ceci afin de ne pas provoquer de problème si un enregistrement n’est pas trouvé. Dans ce cas précis, nous savons que l’enregistrement recherché ne sera pas trouvé. À vous de vous assurer que le code gère en toute sécurité ce genre de situation… Ce qui est vraiment intéressant, c’est la déclaration update générée : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] WHERE [t0].[CustomerID] = @p0 -- @p0: Input String (Size = 5; Prec = 0; Scale = 0) [LONEP] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1 UPDATE [dbo].[Customers] SET [ContactName] = @p11 WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) AND ([ContactTitle] = @p3) AND ([Address] = @p4) AND ([City] = @p5) AND ([Region] = @p6) AND ([PostalCode] = @p7) AND ([Country] = @p8) AND ([Phone] = @p9) AND ([Fax] = @p10) -- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [LONEP] -- @p1: Input String (Size = 24; Prec = 0; Scale = 0) [Lonesome Pine Restaurant] -- @p2: Input String (Size = 11; Prec = 0; Scale = 0) [Fran Wilson] -- @p3: Input String (Size = 13; Prec = 0; Scale = 0) [Sales Manager] -- @p4: Input String (Size = 18; Prec = 0; Scale = 0) [89 Chiaroscuro Rd.] -- @p5: Input String (Size = 8; Prec = 0; Scale = 0) [Portland] -- @p6: Input String (Size = 2; Prec = 0; Scale = 0) [OR] -- @p7: Input String (Size = 5; Prec = 0; Scale = 0) [97219] -- @p8: Input String (Size = 3; Prec = 0; Scale = 0) [USA] -- @p9: Input String (Size = 14; Prec = 0; Scale = 0) [(503) 555-9573] -- @p10: Input String (Size = 14; Prec = 0; Scale = 0) [(503) 555-9646] -- @p11: Input String (Size = 12; Prec = 0; Scale = 0) [Neo Anderson] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Linq.book Page 576 Mercredi, 18. février 2009 7:58 07

576

LINQ to SQL

Partie V

UPDATE [dbo].[Customers] SET [ContactName] = @p11 WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) AND ([ContactTitle] = @p3) AND ([Address] = @p4) AND ([City] = @p5) AND ([Region] = @p6) AND ([PostalCode] = @p7) AND ([Country] = @p8) AND ([Phone] = @p9) AND ([Fax] = @p10) -- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [LONEP] -- @p1: Input String (Size = 24; Prec = 0; Scale = 0) [Lonesome Pine Restaurant] -- @p2: Input String (Size = 12; Prec = 0; Scale = 0) [Neo Anderson] -- @p3: Input String (Size = 13; Prec = 0; Scale = 0) [Sales Manager] -- @p4: Input String (Size = 18; Prec = 0; Scale = 0) [89 Chiaroscuro Rd.] -- @p5: Input String (Size = 8; Prec = 0; Scale = 0) [Portland] -- @p6: Input String (Size = 2; Prec = 0; Scale = 0) [OR] -- @p7: Input String (Size = 5; Prec = 0; Scale = 0) [97219] -- @p8: Input String (Size = 3; Prec = 0; Scale = 0) [USA] -- @p9: Input String (Size = 14; Prec = 0; Scale = 0) [(503) 555-9573] -- @p10: Input String (Size = 14; Prec = 0; Scale = 0) [(503) 555-9646] -- @p11: Input String (Size = 11; Prec = 0; Scale = 0) [Fran Wilson] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Dans la première déclaration update, la clause where a spécifié que le champ ContactName devait être égal à "Fran Wilson", c’est-à-dire la valeur originale du champ. Si un processus avait modifié la valeur du champ depuis sa première lecture, aucun enregistrement n’aurait été sélectionné par la clause where, et aucun enregistrement n’aurait donc été mis à jour. Étant donné qu’aucune des propriétés de la classe d’entité de Customer ne spécifie la propriété UpdateCheck, tous les champs ont la valeur par défaut UpdateCheck.Always, et toutes les propriétés de la classe d’entité mappées sont donc incluses dans la clause where de la déclaration update. SubmitChanges() La détection des conflits d’accès concurrentiels se produit lorsque la méthode SubmitChanges est appelée. Lors de son appel, vous pouvez spécifier si le processus de sauvegarde des modifications dans la base de données doit s’arrêter au premier conflit ou s’il doit se poursuivre jusqu’à la dernière donnée, en collectant les divers conflits. Ce comportement est contrôlé avec l’argument ConflictMode, passé à la méthode SubmitChanges. Cet argument peut prendre les valeurs ConflictMode.FailOnFirstConflict (fin du processus de sauvegarde au premier conflit) ou ConflictMode.ContinueOnConflict (tentative de sauvegarde de toutes les modifications, même si un conflit est détecté). Si l’argument ConflictMode n’est pas spécifié, il prendra la valeur par défaut ConflictMode.FailOnFirstConflict.

Sans tenir compte de la valeur affectée à l’argument ConflictMode, si l’appel à la méthode SubmitChanges ne se trouve pas dans la portée d’une transaction, une transaction est créée pour toutes les tentatives de modifications faites pendant l’invocation de la méthode SubmitChanges. Si SubmitChanges se trouve dans la portée d’une transaction, le DataContext l’utilise. Si une exception est levée pendant l’appel à la méthode SubmitChanges, la transaction est annulée et les modifications effectuées dans la base de données sont restaurées à leur état initial. Openmirrors.com

Linq.book Page 577 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

577

ChangeConflictException Lorsqu’un conflit d’accès concurrentiel se produit, quelle que soit la valeur de l’attribut ConflictMode, une exception ChangeConflictException est générée.

C’est en capturant cet exception que vous détectez les conflits d’accès concurrentiels. Résolution des conflits Après avoir détecté un conflit en capturant l’événement ChangeConflictException, la prochaine étape consiste à le résoudre. Vous pouvez choisir d’effectuer une autre action, mais il y a de grandes chances pour que la résolution du conflit soit votre préoccupation majeure. N’ayez crainte, le code à mettre en place n’est pas aussi complexe que vous pourriez le penser : grâce à la méthode ResolveAll et à deux méthodes Resolve, LINQ to SQL facilite vraiment cette tâche. RefreshMode Si vous utilisez la méthode ResolveAll ou une des deux méthodes Resolve de LINQ to SQL pour résoudre un conflit, vous devez indiquer la façon dont le conflit sera résolu en définissant un RefreshMode. Les trois valeurs possibles sont KeepChanges, KeepCurrentValues et OverwriteCurrentValues. Ces valeurs indiquent quelles données sont conservées dans les valeurs courantes des propriétés des objets d’entité lorsque le DataContext effectue la résolution.

Si RefreshMode est initialisé à RefreshMode.KeepChanges, la méthode ResolveAll/ Resolve affecte les valeurs contenues dans la base de données dans les valeurs courantes des propriétés de la classe d’entité pour toute colonne modifiée depuis sa première lecture dans la base de données. Si l’utilisateur a modifié une propriété, cette dernière est conservée. Les données sont conservées selon l’ordre de priorité suivant (du plus bas au plus haut) : valeurs initiales des propriétés de la classe d’entité, valeurs des colonnes changées dans la base de données et valeurs des propriétés de la classe d’entité modifiées par l’utilisateur. Si RefreshMode est initialisé à RefreshMode.KeepCurrentValues, la méthode ResolveAll/Resolve conserve les valeurs des propriétés de la classe d’entité de l’utilisateur courant et ne tient pas compte des modifications effectuées dans la base de données depuis la première lecture. Les données sont conservées selon l’ordre de priorité suivant (du plus bas au plus haut) : valeurs initiales des propriétés de la classe d’entité et valeurs des propriétés de la classe d’entité modifiées par l’utilisateur. Si RefreshMode est initialisé à RefreshMode.OverwriteCurrentValues, la méthode ResolveAll/Resolve affecte les valeurs contenues dans la base de données dans les valeurs courantes des propriétés de la classe d’entité pour toute colonne modifiée depuis sa première lecture dans la base de données et ne tient pas compte des valeurs modifiées par l’utilisateur dans la classe d’entité. Les données sont conservées selon

Linq.book Page 578 Mercredi, 18. février 2009 7:58 07

578

LINQ to SQL

Partie V

l’ordre de priorité suivant (du plus bas au plus haut) : valeurs initiales des propriétés de la classe d’entité et valeurs des colonnes changées dans la base de données. Résolution des conflits Trois approches permettent de résoudre les conflits : la plus simple, une facile et une autre manuelle. La méthode la plus simple consiste à appliquer la méthode ResolveAll sur la collection DataContext.ChangeConflicts, en lui passant un paramètre RefreshMode et un paramètre bool optionnel qui indique si la résolution doit porter sur les enregistrements supprimés. La résolution automatique des enregistrements supprimés consiste à marquer les objets entité supprimés comme étant effectivement supprimés, même si cela n’est pas le cas à cause d’un conflit d’accès concurrentiel. Ainsi, au prochain appel de la méthode SubmitChanges, le DataContext n’essayera pas de supprimer une nouvelle fois l’enregistrement de la base de données qui correspond à l’objet entité. Par essence, nous demandons à LINQ to SQL de faire semblant d’avoir supprimé l’enregistrement, même si, en fait, quelqu’un d’autre l’a supprimé à sa place. L’approche facile consiste à énumérer chaque ObjectChangeConflict de la collection DataContext.ChangeConflicts et d’appeler la méthode Resolve pour chaque ObjectChangeConflict. Si vous devez effectuer un traitement spécifique, vous pouvez toujours gérer la résolution des conflits manuellement, en énumérant la collection ChangeConflicts du DataContext, puis en énumérant la collection MemberConflicts de chaque ObjectChangeConflict, et en appelant la méthode Resolve sur chaque objet MemberChangeConflict de cette collection. Cette approche n’est pas très compliquée car des méthodes sont là pour vous épauler. DataContext.ChangeConflicts.ResolveAll() La façon la plus simple de résoudre les conflits consiste à capturer l’exception ChangeConflictException et à appeler la méthode ResolveAll sur la collection DataContext.ChangeConflicts. Tout ce que vous avez à faire consiste à choisir quel RefreshMode utiliser et à indiquer si vous voulez résoudre automatiquement les enregistrements supprimés.

Si vous choisissez cette approche, tous les conflits seront résolus d’une manière identique, en fonction du paramètre RefreshMode passé. Si vous avez besoin d’une résolution plus fine, utilisez une des deux autres approches, étudiées un peu plus loin dans cette section. Le Listing 17.2 montre comment résoudre les conflits en utilisant la méthode ResolveAll. Cet exemple étant assez complexe, nous donnerons des explications chaque fois que cela sera nécessaire. Openmirrors.com

Linq.book Page 579 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

579

Listing 17.2 : Résolution des conflits avec la méthode DataContext.ChangeConflicts.ResolveAll(). Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Samuel Arthur Sanders’ where CustomerID = ’LAZYK’"));

Ces instructions créent le DataContext Northwind, lancent une requête LINQ to SQL sur la table Customer et modifient sur la colonne ContactName du client récupéré dans la base de données en utilisant ADO.NET. Cette dernière action met en place un potentiel conflit d’accès concurrentiel. Nous allons maintenant modifier l’objet entité et essayer d’enregistrer la modification dans la base de données. cust.ContactTitle = "President"; try { db.SubmitChanges(ConflictMode.ContinueOnConflict); } catch (ChangeConflictException) {

La méthode SubmitChanges a été insérée dans un bloc try/catch. Pour gérer convenablement les conflits, l’exception ChangeConflictException est donc capturée. Il suffit maintenant d’appeler la méthode ResolveAll pour essayer de sauvegarder les modifications dans la base de données. db.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges); try { db.SubmitChanges(ConflictMode.ContinueOnConflict); cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("ContactName = {0} : ContactTitle = {1}", cust.ContactName, cust.ContactTitle); } catch (ChangeConflictException) { Console.WriteLine("Conflict again, aborting."); } }

Ce code appelle la méthode ResolveAll et lui passe un attribut RefreshMode.KeepChanges. La méthode SubmitChanges est à nouveau appelée à l’intérieur d’un nouveau bloc try/catch. Le client est à nouveau recherché dans la base de données et ses champs ContactName et ContactTitle sont affichés pour prouver que ni les modifications effectuées par ADO.NET ni celles effectuées dans LINQ to SQL n’ont été perdues. Si l’appel à la méthode SubmitChanges lève une exception, cette dernière est signalée et la mise à jour est annulée.

Linq.book Page 580 Mercredi, 18. février 2009 7:58 07

580

LINQ to SQL

Partie V

Il ne reste plus qu’à restaurer la base de données dans son état original pour que l’exemple puisse être exécuté à plusieurs reprises. // Restauration de la base de données ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’John Steel’, ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’"));

Si vous ne tenez pas compte du code qui a mis en place le conflit (dans une situation réelle, ce code ne sera bien sûr pas écrit) ni du code qui restaure la base de données à son état original (de même, ce code ne sera pas écrit dans une situation réelle), cette approche facilite grandement la résolution de conflits : il suffit d’insérer la méthode SubmitChanges dans un bloc try/catch, de capturer l’exception ChangeConflictException, d’appeler la méthode ResolveAll et de répéter l’appel à la méthode SubmitChange. Voici les résultats du Listing 17.2 : Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour. ContactName = Samuel Arthur Sanders : ContactTitle = President Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour.

Comme vous pouvez le voir dans les résultats, la modification effectuée par ADO.NET et celle effectuée par LINQ to SQL ont toutes deux été sauvegardées dans la base de données. ObjectChangeConflict.Resolve() Si la résolution de conflits avec un même RefreshMode et/ou une action autoResolveDeletes ne vous convient pas, vous pouvez utiliser une autre approche consistant à énumérer les conflits dans la collection DataContext.ChangeConflicts et à les gérer individuellement. Vous pouvez par exemple gérer chacun d’entre eux en faisant appel à la méthode Resolve. Cela vous permet de choisir un RefreshMode et/ou un autoResolveDeletes adapté à chaque conflit.

Cette approche revient à résoudre les conflits au niveau des objets entité. L’argument RefreshMode passé à la méthode Resolve s’applique à toutes les propriétés de la classe d’entité de l’objet qui est à l’origine du conflit. Si cette approche vous semble insuffisante, vous utiliserez l’approche manuelle, examinée un peu plus loin dans cette section. Le Listing 17.3 donne un exemple de cette deuxième approche. Le code est identique à celui du Listing 17.2 mais, ici, l’appel à la méthode DataContext.ChangeConflicts.ResolveAll est remplacé par une énumération de la collection ChangeConflicts.

Openmirrors.com

Linq.book Page 581 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

581

Listing 17.3 : Résolution des conflits avec la méthode ObjectChangeConflict.Resolve(). Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Samuel Arthur Sanders’ where CustomerID = ’LAZYK’")); cust.ContactTitle = "President"; try { db.SubmitChanges(ConflictMode.ContinueOnConflict); } catch (ChangeConflictException) { foreach (ObjectChangeConflict conflict in db.ChangeConflicts) { Console.WriteLine("Un conflit s’est produit sur le client {0}.", ((Customer)conflict.Object).CustomerID); Console.WriteLine("Appel de la méthode Resolve ..."); conflict.Resolve(RefreshMode.KeepChanges); Console.WriteLine("Conflit résolu.{0}", System.Environment.NewLine); } try { db.SubmitChanges(ConflictMode.ContinueOnConflict); cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("ContactName = {0} : ContactTitle = {1}", cust.ContactName, cust.ContactTitle); } catch (ChangeConflictException) { Console.WriteLine("Nouveau conflit, annulation des modifications."); } } // Restauration de la base de données. ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’John Steel’, ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’"));

Comme vous pouvez le voir, au lieu d’appeler la méthode DataContext.ChangeConflicts.ResolveAll, ce code énumère la collection ChangeConflicts et appelle la méthode Resolve sur chacun des objets ObjectChangeConflict de la collection. Tout comme dans le listing précédent, la méthode SubmitChanges est à nouveau invoquée, le client est à nouveau récupéré dans la base de données et les propriétés ContactName et ContactTitle sont à nouveau affichées. Bien entendu, le code se termine par la restauration de l’état initial de la base de données.

Linq.book Page 582 Mercredi, 18. février 2009 7:58 07

582

LINQ to SQL

Partie V

Voici les résultats du Listing 17.3 : Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour. Un conflict s’est produit sur le client LAZYK. Appel de la méthode Resolve ... Conflit résolu. ContactName = Samuel Arthur Sanders : ContactTitle = President Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour.

Tout s’est déroulé comme souhaité. Dans un code de production, vous pourriez néanmoins effectuer une boucle sur l’appel de la méthode SubmitChanges et la résolution du conflit, pour le cas où de nouveaux conflits se produiraient. Si vous choisissez cette approche, assurez-vous que la boucle ne s’exécute pas indéfiniment… MemberChangeConflict.Resolve() Dans la première approche, nous avons appelé une méthode pour résoudre tous les conflits d’une seule et unique façon. Dans la deuxième approche, nous avons appelé une méthode spécifique pour chacun des conflits détectés. Ceci permet d’appliquer un traitement particulier à chaque objet entité. Nous allons maintenant nous intéresser à une approche manuelle.

Que le mot "manuel" ne vous impressionne pas trop : la détection manuelle de conflits est certainement bien plus simple que ce que vous imaginez. En choisissant cette approche, vous allez pouvoir appliquer plusieurs valeurs RefreshModes à chacune des propriétés d’objet entité. Tout comme dans la deuxième approche, nous allons énumérer les objets ObjectChangeConflict de la collection DataContext.ChangeConflicts. Mais, ici, au lieu d’appeler la méthode Resolve sur chaque objet ObjectChangeConflict, nous allons énumérer leur collection MemberConflicts et appliquer la méthode Resolve sur chaque membre de cette collection. À ce niveau, un objet MemberChangeConflict est relatif à une propriété spécifique d’une classe d’entité de l’objet ayant provoqué le conflit. Cela vous permet de choisir un RefreshMode différent pour chaque propriété de la classe d’entité qui le nécessite. Le paramètre de la méthode Resolve peut être un Refreshmode ou la valeur courante à affecter à la propriété. Ceci assure une grande flexibilité à cette troisième approche. Le Listing 17.4 donne un exemple de résolution manuelle de conflit d’accès concurrentiel. Dans cet exemple, nous allons décider que, si un conflit se produit sur la colonne ContactName, la base de données doit rester inchangée. En revanche, si un conflit se produit sur une autre colonne, la base de données doit être mise à jour. Openmirrors.com

Linq.book Page 583 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

583

Pour implémenter ce comportement, nous utiliserons le même code que dans le Listing 17.3 mais, au lieu d’appeler la méthode Resolve sur les objets ObjectChangeConflict, nous allons énumérer les objets de la collection MemberConflicts. Pour chacun d’entre eux, si la propriété de l’objet entité qui entre en conflit est ContactName, la valeur contenue dans la base de données sera conservée. Pour ce faire, nous affecterons la valeur RefreshMode.OverwriteCurrentValues au paramètre RefreshMode de la méthode Resolve. Si la propriété de l’objet entité qui entre en conflit n’est pas ContactName, la valeur de l’entité sera sauvegardée dans la base de données. Pour ce faire, nous affecterons la valeur RefreshMode.KeepChanges au paramètre RefreshMode de la méthode Resolve. Pour que l’exemple soit plus intéressant, lors de la mise en place du conflit en modifiant la base de données via ADO.NET, le champ ContactTitle sera également modifié. Cela provoquera deux conflits sur des propriétés d’objet entité. Le conflit sur la propriété ContactName devrait laisser inchangée la valeur stockée dans la base de données, et celui sur la propriété ContactTitle devrait sauvegarder la valeur LINQ to SQL dans la base de données. Listing 17.4 : Un exemple de résolution manuelle de conflits. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Samuel Arthur Sanders’, ContactTitle = ’CEO’ where CustomerID = ’LAZYK’")); cust.ContactName = "Viola Sanders"; cust.ContactTitle = "President"; try { db.SubmitChanges(ConflictMode.ContinueOnConflict); } catch (ChangeConflictException) { foreach (ObjectChangeConflict conflict in db.ChangeConflicts) { Console.WriteLine("Un conflit s’est produit pour le client {0}.", ((Customer)conflict.Object).CustomerID); foreach (MemberChangeConflict memberConflict in conflict.MemberConflicts) { Console.WriteLine("Appel de la méthode Resolve pour {0} ...", memberConflict.Member.Name); if (memberConflict.Member.Name.Equals("ContactName")) { memberConflict.Resolve(RefreshMode.OverwriteCurrentValues); } else { memberConflict.Resolve(RefreshMode.KeepChanges); }

Linq.book Page 584 Mercredi, 18. février 2009 7:58 07

584

LINQ to SQL

Partie V

Console.WriteLine("Conflit résolu.{0}", System.Environment.NewLine); } } try { db.SubmitChanges(ConflictMode.ContinueOnConflict); cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("ContactName = {0} : ContactTitle = {1}", cust.ContactName, cust.ContactTitle); } catch (ChangeConflictException) { Console.WriteLine("Nouveau conflit, abandon de la mise à jour."); } } // Restauration de la base de données ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’John Steel’, ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’"));

Un des changements significatifs dans ce code concerne la modification du champ ContactTitle avec ADO.NET. Ceci provoque un conflit entre la propriété de l’objet entité et la base de données lors de l’appel de la méthode SubmitChanges. Ensuite, plutôt qu’appeler la méthode Resolve sur l’objet ObjectChangeConflict, sa collection MemberConflicts est énumérée pour examiner chaque propriété des objets entité. S’il s’agit de la propriété ContactName, la méthode Resolve est appelée avec un paramètre RefreshMode égal à RefreshMode.OverwriteCurrentValues afin de conserver la valeur stockée dans la base de données. S’il s’agit d’une autre propriété, la méthode Resolve est appelée avec un paramètre RefreshMode égal à RefreshMode.KeepChanges pour sauvegarder la valeur stockée dans le code LINQ to SQL. Voici les résultats du Listing 17.4 : Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour. Un conflict s’est produit sur le client LAZYK. Appel de la méthode Resolve pour ContactName... Conflit résolu. Appel de la méthode Resolve pour ContactTitle... Conflit résolu. ContactName = Samuel Arthur Sanders : ContactTitle = President Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour.

Comme le montrent les résultats, les conflits relatifs aux propriétés ContactName et ContactTitle de l’objet entité ont tous deux été résolus. En examinant la valeur des propriétés ContactName et ContactTitle, vous voyez que la valeur de ContactName Openmirrors.com

Linq.book Page 585 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

585

contenue dans la base de données a été conservée et que la valeur de ContactTitle stockée dans le code LINQ to SQL a été sauvegardée dans la base de données. Avouez que ce code n’est pas très compliqué. Cependant, vous n’adopterez cette approche que dans les cas qui le nécessitent, et vous utiliserez l’une des deux approches précédentes dans tous les autres cas. Contrôle d’accès concurrentiel pessimiste Comme son nom l’indique, le contrôle d’accès concurrentiel pessimiste suppose la pire situation, à savoir que, dans la plupart des cas, un conflit d’accès concurrentiel se produira lors de la sauvegarde dans la base de données. Cette situation n’est pas aussi désespérée qu’il y paraît : il suffit d’inclure la lecture et la mise à jour de la base de données dans une transaction. En adoptant une approche pessimiste, il n’y a aucun conflit à résoudre, puisque la base de données bloque la transaction. Personne ne peut donc modifier son contenu dans votre dos. Pour tester ce mode de fonctionnement (voir Listing 17.5), nous allons créer un objet TransactionScope et récupérer un objet entité pour le client LAZYK. Un autre objet TransactionScope sera alors créé avec l’option TransactionScopeOption initialisée à RequiresNew. Ceci afin que le code ADO.NET ne participe pas à la première transaction. Par la suite, nous essayerons de mettre à jour ce même enregistrement dans la base de données en utilisant ADO.NET. Étant donné qu’une transaction bloque l’accès à la base de données, la mise à jour ADO.NET sera bloquée et finalement annulée par timeout. Par la suite, nous modifierons l’objet entité ContactName, appellerons la méthode SubmitChanges et effectuerons une nouvelle requête sur le client pour afficher son champ ContactName et prouver qu’il a été mis à jour par LINQ to SQL, puis nous fermerons la transaction. INFO Vous devez ajouter une référence à l’assembly System.Transactions.dll dans votre projet pour que cet exemple passe l’étape de la compilation. Listing 17.5 : Un exemple d’accès concurrentiel pessimiste. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); using (System.Transactions.TransactionScope transaction = new System.Transactions.TransactionScope()) { Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); try

Linq.book Page 586 Mercredi, 18. février 2009 7:58 07

586

LINQ to SQL

Partie V

{ Console.WriteLine("Tentative de mise à jour du champ ContactName du client LAZYK ➥avec ADO.NET."); Console.WriteLine("Patience, nous devons attendre le timeout..."); using (System.Transactions.TransactionScope t2 = new System.Transactions.TransactionScope( System.Transactions.TransactionScopeOption.RequiresNew)) { ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Samuel Arthur Sanders’ where CustomerID = ’LAZYK’")); t2.Complete(); } Console.WriteLine("Le champ ContactName de LAZYK a été mis à jour.{0}", System.Environment.NewLine); } catch (Exception ex) { Console.WriteLine( "Une exception s’est produite pendant la mise à jour du client LAZYK avec ➥ADO.NET :{0} {1}{0}", System.Environment.NewLine, ex.Message); } cust.ContactName = "Viola Sanders"; db.SubmitChanges(); cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("Champ ContactName du client : {0}", cust.ContactName); transaction.Complete(); } // Restauration de la base de données ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’John Steel’, ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’"));

ASTUCE Si vous obtenez une exception du type "MSDTC on server ’[server]\SQLEXPRESS’ is unavailable" lorsque vous travaillez avec un exemple qui utilise l’objet TransactionScope, assurezvous que le service Distributed Transaction Coordinator est lancé.

Le code n’est pas aussi complexe qu’il peut le sembler de prime abord. Après avoir créé un objet TransactionScope, nous adoptons de facto une approche pessimiste et toute modification extérieure des données est impossible. Le client LAZYK est récupéré en utilisant une requête LINQ to SQL. Nous créons alors un autre objet TransactionScope pour empêcher le code ADO.NET suivant de participer à la première Openmirrors.com

Linq.book Page 587 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

587

transaction. Le deuxième objet TransactionScope créé, nous tentons de mettre à jour le client dans la base de données en utilisant ADO.NET. Le code de mise à jour n’est pas en mesure d’effectuer son travail, car la première transaction l’en empêchera, et une exception de type timeout sera générée. La propriété ContactName du client est alors modifiée puis enregistrée dans la base de données à l’aide de la méthode SubmitChanges. Le client est à nouveau récupéré à l’aide d’une requête LINQ to SQL et son champ ContactName est affiché pour prouver que la modification a bien été effectuée. La première transaction est enfin terminée en lui appliquant la méthode Complete. Bien entendu, comme toujours, la base de données est restaurée à la fin du code. Voici les résultats : Tentative de mise à jour du champ ContactName du client LAZYK avec ADO.NET. Patience, nous devons attendre le timeout... Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Une exception s’est produite en essayant de mettre à jour le champ LAZYK avec ADO.NET Le timeout a expiré. La période du timeout s’est écoulée avant la fin de l’opération ➥ou le serveur ne répond pas. La déclaration a été arrêtée. Champ ContactName du client: Viola Sanders Exécution de la déclaration SQL sur la base de données avec ADO.NET ... Base de données mise à jour.

Comme vous le voyez, lorsque nous essayons de modifier la base de données avec ADO.NET, une exception timeout se produit. Dans cet exemple, la requête LINQ to SQL appelle l’opérateur SingleOrDefault, qui n’est pas un opérateur différé. La requête n’est donc pas différée, et elle doit être déclarée dans la portée d’un objet TransactionScope. Si l’opérateur SingleOrDefault n’avait pas été appelé, la requête aurait pu être déclarée avant la création de l’objet TransactionScope, à condition qu’elle soit exécutée dans la portée de l’objet TransactionScope. Par conséquent, nous aurions très bien pu obtenir le retour de la requête LINQ (une séquence IEnumerable) avant la création de l’objet TransactionScope, puis lui appliquer l’opérateur SingleOrDefault dans la portée de l’objet TransactionScope, afin d’obtenir le client unique résultant de la requête. Lorsque vous adoptez cette approche, soyez toujours bien conscient des opérations effectuées dans la portée de l’objet TransactionScope, car pendant ces opérations la base de données sera verrouillée. INFO Si vous exécutez cet exemple en mode débogage, il se peut qu’un timeout apparaisse pendant la transaction de l’objet TransactionScope.

Linq.book Page 588 Mercredi, 18. février 2009 7:58 07

588

LINQ to SQL

Partie V

Une approche alternative pour les middle-tier et les serveurs Il existe une autre approche pour gérer les conflits d’accès concurrentiels lorsqu’ils se produisent dans un middle-tier (serveur de couche intermédiaire) et les serveurs. Parfois, lorsqu’un conflit d’accès concurrentiel se produit, il peut être plus simple de créer un nouveau DataContext, d’appliquer les modifications et d’appeler à nouveau la méthode SubmitChanges. Considérons une application web ASP.NET. Étant donné que le navigateur web n’a pas forcément besoin qu’une connexion Internet soit établie avant son utilisation, un nouveau DataContext doit être créé à chaque fois qu’un post HTTP est fait sur le serveur web, et une requête LINQ to SQL doit être exécutée. Étant donné que les données issues d’une base de données sont obsolètes quasiment dès leur lecture, il n’est pas raisonnable de garder un DataContext ouvert pendant une longue période en ayant l’intention de lui appliquer des modifications. Lorsqu’un utilisateur se rend sur une page web et que des données sont récupérées depuis une base de données, il ne paraît pas raisonnable d’utiliser l’objet DataContext pour retourner les données de la page au serveur (postback) afin de mettre à jour la base de données. Le DataContext ne survivra pas à l’attente du postback, à moins qu’il soit mémorisé entre deux connexions, par exemple dans des variables de session. Cependant, même s’il survit, le délai entre deux connexions peut être très long, et une nouvelle connexion peut même ne jamais avoir lieu. Plus le délai entre la première lecture dans la base de données et le postback est élevé, plus les données ont des chances d’être obsolètes. Dans ce type de scénario, plutôt que stocker les données dans le DataContext, il peut être plus intelligent de créer un DataContext à chaque postback, lorsque les données doivent être sauvegardées. Dans ce cas, si un conflit d’accès concurrentiel se produit, un moindre mal consisterait à créer un autre DataContext, à modifier les données et à appeler à nouveau la méthode SubmitChanges pour les enregistrer dans la base de données. Et, comme le délai serait bref entre la lecture des données, leur modification et l’appel de la procédure SubmitChanges, il n’y a que peu de chances qu’un conflit se produise à la première tentative, et encore moins à la seconde. Si vous décidez d’adopter cette approche sur le postback après la construction de l’objet DataContext, vous pourriez récupérer l’objet entité souhaité, comme il vient d’être indiqué, ou utiliser une autre méthode. Plutôt que récupérer l’objet entité, vous pourriez en créer un nouveau, définir ses propriétés et l’attacher à une table en utilisant la méthode Attach de l’objet Table. À ce point, c’est comme si l’objet entité avait été récupéré depuis la base de données, à ceci près que ses champs ne contiennent aucune donnée. Avant d’attacher un objet entité à un Table, vous devez définir les propriétés de la classe d’entité avec les valeurs appropriées. Cela ne signifie nullement que vous deviez Openmirrors.com

Linq.book Page 589 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

589

interroger la base de données pour obtenir ces valeurs : elles pourraient venir d’un endroit quelconque, par exemple d’un autre tier. Les propriétés nécessaires de la classe d’entité correspondent à toutes les propriétés de la classe d’entité qui constituent la clé primaire (ou qui établissent une identité), aux propriétés qui vont être modifiées et à toutes celles qui participent à la vérification de la mise à jour. Vous devez inclure les propriétés de la classe d’entité qui définissent l’identité, afin que le DataContext puisse se repérer dans la classe de l’entité. Vous devez inclure les propriétés de la classe d’entité que vous allez modifier, afin qu’elles puissent être mises à jour et que le processus de détection de conflit concurrentiel puisse être mis en place. Enfin, vous devez inclure toutes les propriétés des classes d’entité qui participent à la vérification de la mise à jour dans la détection des conflits d’accès concurrentiels. Si la classe d’entité a une propriété IsVersion initialisée à true pour l’attribut Column, cette propriété doit être définie avant d’appeler la méthode Attach. Examinons le Listing 17.6. Listing 17.6 : Utilisation de la méthode Attach() pour attacher un objet entité fraîchement construit. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); // Création de l’objet entité Console.WriteLine("Construction d’un objet Customer vide."); Customer cust = new Customer(); // Tous les champs qui définissent l’identité doivent être définis Console.WriteLine("Définition de la clé primaire."); cust.CustomerID = "LAZYK"; // Tous les champs qui vont être modifiés doivent être définis Console.WriteLine("Définition des champs qui vont être modifiés."); cust.ContactName = "John Steel"; // Tous les champs qui participent à la vérification de la mise à jour //doivent être définis. En ce qui concerne la classe Customer, // tous les champs doivent être inclus Console.WriteLine("Définition des champs qui participent à la vérification de la ➥mise à jour."); cust.CompanyName = "Lazy K Kountry Store"; cust.ContactTitle = "Marketing Manager"; cust.Address = "12 Orchestra Terrace"; cust.City = "Walla Walla"; cust.Region = "WA"; cust.PostalCode = "99362"; cust.Country = "USA"; cust.Phone = "(509) 555-7969"; cust.Fax = "(509) 555-6221"; // Utilisation de la méthode Attach() sur la table Customers Table Console.WriteLine("Appel de la méthode Attach() sur le Customers Table."); db.Customers.Attach(cust);

Linq.book Page 590 Mercredi, 18. février 2009 7:58 07

590

LINQ to SQL

Partie V

// À ce point, les modifications peuvent être effectuées // et la méthode SubmitChanges() appelée Console.WriteLine("Modification des données et appel de la méthode ➥SubmitChanges()."); cust.ContactName = "Vickey Rattz"; db.SubmitChanges(); cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("Valeur du champ ContactName dans la base de données : {0}", cust.ContactName); Console.WriteLine("Restauration de l’état original de la base de données."); cust.ContactName = "John Steel"; db.SubmitChanges();

Comme vous pouvez le voir, la clé primaire est définie en premier, suivie des propriétés qui vont être modifiées, puis des propriétés qui participent à la vérification de la mise à jour. Comme il a été mentionné précédemment, les valeurs appropriées doivent être affectées à ces propriétés. Cela ne signifie nullement que vous deviez interroger la base de données pour les obtenir : elles pourraient se trouver dans des variables cachées ou être passées par un autre tier. La méthode Attach est alors appelée sur l’objet Customers Table. Les modifications sont ensuite effectuées, puis la méthode SubmitChanges est appelée. Une requête est alors effectuée dans la base de données, et le champ ContactName du client est affiché, pour prouver qu’il a bien été modifié dans la base de données. Et, pour finir, l’état original de la base de données est restauré. Voici les résultats du Listing 17.6 : Construction d’un objet Customer vide. Définition de la clé primaire. Définition des champs qui vont être modifiés. Définition des champs qui participent à la vérification de la mise à jour. Appel de la méthode Attach() sur le Customers Table. Modification des données et appel de la méthode SubmitChanges(). Valeur du champ ContactName dans la base de données : Vickey Rattz Restauration de l’état original de la base de données.

L’insertion ou la suppression d’objets provenant d’une classe d’entité ne nécessitent pas cette approche : il vous suffit d’insérer ou de supprimer un objet de la classe d’entité avant d’appeler la méthode SubmitChanges. Reportez-vous aux sections intitulées "Insertions" et "Suppressions" du Chapitre 14 pour en savoir plus à ce sujet.

Openmirrors.com

Linq.book Page 591 Mercredi, 18. février 2009 7:58 07

Chapitre 17

Les conflits d’accès concurrentiels

591

Résumé La détection et la résolution de conflits d’accès concurrentiels avaient été utilisées à de nombreuses reprises dans les chapitres relatifs à LINQ to SQL sans être étudiées en détail. Ce chapitre a comblé ce vide. J’espère que vous avez été aussi impressionné que moi par la facilité avec laquelle LINQ to SQL permet de détecter et de résoudre les conflits d’accès concurrentiels. J’espère également que ce chapitre, au demeurant plutôt intimidant, vous a aidé à retrouver une paix intérieure. Notre voyage dans l’univers LINQ to SQL est sur le point de s’achever. Pour conclure cet ouvrage, le chapitre suivant va vous donner des informations complémentaires sur LINQ to SQL.

Linq.book Page 592 Mercredi, 18. février 2009 7:58 07

Openmirrors.com

Linq.book Page 593 Mercredi, 18. février 2009 7:58 07

18 Informations complémentaires sur SQL Le dernier chapitre de cet ouvrage donne des informations diverses et variées sur LINQ to SQL. Nous parlerons entre autres des vues d’une base de données, de l’héritage des classes d’entité et des transactions.

Prérequis pour exécuter les exemples Pour pouvoir exécuter les exemples de ce chapitre, vous devez être en possession de la base de données étendue Northwind et avoir généré les classes d’entité correspondantes. Si nécessaire, reportez-vous à la section intitulée "Prérequis pour exécuter les exemples" du Chapitre 12 pour avoir des informations complémentaires à ce sujet. Utilisation de l’API LINQ to SQL Pour exécuter les exemples de ce chapitre, vous pouvez être amené à ajouter des références et des directives using à vos projets. Reportez-vous à la section intitulée "Utilisation de l’API LINQ to SQL" au Chapitre 12 pour en savoir plus à ce sujet. Utilisation de l’API LINQ to XML Plusieurs exemples de ce chapitre ont besoin d’une directive using pour référencer l’espace de noms System.Xml.Linq.

Les vues d’une base de données Lorsque les classes d’entité de la base de données Northwind ont été générées au Chapitre 12, nous avons utilisé l’option /views pour créer des mappages pour les différentes vues de la base de données. Nous allons maintenant vous montrer comment les

Linq.book Page 594 Mercredi, 18. février 2009 7:58 07

594

LINQ to SQL

Partie V

interroger. Les outils de génération de classes d’entité (SQLMetal et le Concepteur Objet/Relationnel) déclarent une propriété Table dans la classe [Your]DataContext pour chaque vue de la base de données et créent la classe d’entité T correspondante. Les vues peuvent être interrogées comme des tables. Elles se comportent d’une façon similaire, à ceci près qu’elles ne sont accessibles qu’en lecture seule. Étant donné que les classes d’entité générées pour des vues ne contiennent pas de propriétés mappées en tant que clés primaires, elles ne sont accessibles qu’en lecture seule. Et, sans clé primaire, le DataContext n’est pas en mesure d’effectuer une recherche d’identité. À titre d’exemple, la base de données Northwind a une vue nommée "Category Sales for 1997". SQLMetal a donc généré une propriété publique nommée CategorySalesFor1997s : Une propriété publique pour une vue d’une base de données public System.Data.Linq.Table CategorySalesFor1997s { get { return this.GetTable(); } }

SQLMetal a également généré une classe d’entité CategorySalesFor1997s (voir Listing 18.1). Listing 18.1 : Interrogation d’une vue d’une base de données. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); IQueryable seq = from c in db.CategorySalesFor1997s where c.CategorySales > (decimal)100000.00 orderby c.CategorySales descending select c; foreach (CategorySalesFor1997 c in seq) { Console.WriteLine("{0} : {1:C}", c.CategoryName, c.CategorySales); }

Comme vous pouvez le voir, la vue a été interrogée comme s’il s’agissait d’une table. Voici les résultats : Dairy Products : $114,749.78 Beverages : $102,074.31

Comme indiqué précédemment, les vues sont accessibles en lecture seule. Le Listing 18.2 va tenter d’insérer un enregistrement dans une vue. Openmirrors.com

Linq.book Page 595 Mercredi, 18. février 2009 7:58 07

Chapitre 18

Informations complémentaires sur SQL

595

Listing 18.2 : Tentative ratée d’insertion d’un enregistrement dans une vue. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); db.CategorySalesFor1997s.InsertOnSubmit( new CategorySalesFor1997 { CategoryName = "Legumes", CategorySales = (decimal) 79043.92 });

Dans ce listing, nous n’avons pas pris la peine d’appeler la méthode SubmitChanges. Ceci parce que nous savons pertinemment qu’une exception va être levée. Voici les résultats : Exception non gérée : System.InvalidOperationException: Impossible d’effectuer une opération Create, Update ou Delete sur la table ’(CategorySalesFor1997)’, car elle est accessible en lecture seule. ...

Alors que les méthodes InsertOnSubmit et DeleteOnSubmit génèrent des exceptions lorsqu’elles sont appelées sur un Table mappé à une vue d’une base de données, rien ne vous empêche de faire des modifications sur une propriété d’un objet entité d’une vue. Vous pouvez modifier la valeur d’une propriété et même appeler la méthode SubmitChanges sans qu’aucune exception ne soit levée, mais les modifications effectuées dans la propriété de l’objet entité de la vue ne seront pas répercutées dans la base de données.

Héritage des classes d’entité Jusqu’ici, dans tous les chapitres relatifs à LINQ to SQL, chaque classe d’entité était mappée à une et une seule table. Le mappage entre les classes d’entité et les tables était donc de type "un-à-un". ATTENTION L’exemple utilisé dans cette section crée un modèle de données qui contient les classes Square et Rectangle. D’un point de vue géométrique, un carré est un rectangle, mais un rectangle n’est pas nécessairement un carré. Cependant, dans le modèle de données de cet exemple, la relation inverse est également vérifiée. Le modèle de classe indique qu’un rectangle est dérivé d’un carré. Par conséquent, un rectangle est un carré, mais un carré n’est pas nécessairement un rectangle. Ce raisonnement sera expliqué un peu plus bas.

LINQ to SQL offre à ce raisonnement une alternative connue sous le nom "héritage de classe d’entité". Par son intermédiaire, une hiérarchie de classe peut être mappée à une table unique d’une base de données. Pour cette table, il doit y avoir une classe d’entité de base et les mappages appropriés des attributs de la classe d’entité. Cette classe de base contiendra toutes les propriétés communes aux classes de la hiérarchie qui en dérivent, alors que les classes dérivées ne contiendront que les propriétés qui leur sont

Linq.book Page 596 Mercredi, 18. février 2009 7:58 07

596

LINQ to SQL

Partie V

spécifiques. Voici un exemple de classe d’entité de base sans aucune classe dérivée mappée. La classe d’entité de base, sans classes dérivées mappées [Table] public class Shape { [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL IDENTITY")] public int Id; [Column(IsDiscriminator = true, DbType = "NVarChar(2)")] public string ShapeCode; [Column(DbType = "Int")] public int StartingX; [Column(DbType = "Int")] public int StartingY; }

Comme vous pouvez le voir, l’attribut Table a été spécifié. Étant donné qu’aucune propriété de l’attribut Name n’a été spécifiée, la classe d’entité de base est mappée à une table dont le nom est identique à celui de la classe. Elle est donc mappée à la table Shape. La table Shape n’a pas encore été définie. Un peu plus loin dans cette section, nous utiliserons la méthode CreateDatabase de l’objet DataContext pour créer la base de données. Pour l’instant, aucune classe dérivée n’a encore été mappée. Dans quelques pages, nous reviendrons à la classe d’entité de base pour lui mapper quelques classes dérivées. Une idée se cache derrière l’héritage des classes d’entité : la table Shape a une colonne dont la valeur indique dans quelle classe d’entité l’enregistrement devrait être construit lorsqu’il est récupéré par LINQ to SQL. Cette colonne est appelée "discriminant". Elle est spécifiée en utilisant la propriété IsDiscriminator de l’attribut Column. Une valeur affectée à un discriminant est appelée "valeur discriminante" ou "code discriminant". Lorsque vous mappez une classe d’entité de base à une table d’une base de données, outre les attributs Table, vous pouvez spécifier les attributs InheritanceMapping pour mapper les codes discriminants aux classes dérivées de la classe d’entité de base. Dans la classe Shape précédente, aucun héritage n’a été mappé. Cette classe de base contient plusieurs membres publics. Chacun d’entre eux est mappé à une colonne de la base de données, et le type de la colonne est spécifié en argument. Dans notre cas, la spécification des types des colonnes de la base de données est nécessaire à la méthode CreateDatabase, appelée un peu plus loin dans le code. La propriété IsDiscriminator du membre ShapeCode a été initialisée à true. Cette colonne est donc le discriminant. Elle dictera le type de la classe d’entité utilisé pour construire chacun des enregistrements d’un objet de classe d’entité. Openmirrors.com

Linq.book Page 597 Mercredi, 18. février 2009 7:58 07

Chapitre 18

Informations complémentaires sur SQL

597

Les membres de cette classe sont Id, ShapeCode, StartingX et StartingY (les coordonnées X et Y de la forme sur l’écran). À ce point, ce sont les seuls membres dont nous pouvons prévoir qu’ils seront communs aux différentes formes. Pour créer une hiérarchie de classe, il suffit de dériver des classes de la classe de base. Les classes dérivées doivent hériter de la classe de base. Elles ne spécifieront pas l’attribut Table, mais devront indiquer les attributs Column pour chaque membre public mappé à la base de données. Voici les classes d’entité dérivées : Les classes d’entité dérivées public class Square : Shape { [Column(DBType = "Int")] public int Width; } public class Rectangle : Square { [Column(DBType = "Int")] public int Length; }

D’un point de vue géométrique, un carré est un rectangle, mais un rectangle n’est pas forcément un carré. Dans cet exemple, comme les côtés du carré doivent être égaux, une seule mesure est nécessaire : Width. Un rectangle nécessite deux mesures (une largeur et une longueur). Il hérite donc du carré et ajoute un membre pour définir la longueur : Length. D’un point de vue héritage de classe, un rectangle est un carré, mais un carré n’est pas un rectangle. Pendant les quelques pages relatives à cet exemple, vous devez donc oublier tout ce que vous avez appris en classe… Nous avons maintenant nos classes dérivées. Il ne nous manque plus que le mappage entre les valeurs discriminantes, la classe d’entité de base et les classes d’entité dérivées. Après avoir ajouté les attributs InheritanceMapping nécessaires, la classe de base a l’allure suivante : La classe d’entité de base avec les mappages des classes dérivées [Table] [InheritanceMapping(Code = "G", Type = typeof(Shape), IsDefault = true)] [InheritanceMapping(Code = "S", Type = typeof(Square))] [InheritanceMapping(Code = "R", Type = typeof(Rectangle))] public class Shape { [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL IDENTITY")] public int Id; [Column(IsDiscriminator = true, DbType = "NVarChar(2)")] public string ShapeCode; [Column(DbType = "Int")]

Linq.book Page 598 Mercredi, 18. février 2009 7:58 07

598

LINQ to SQL

Partie V

public int StartingX; [Column(DbType = "Int")] public int StartingY; }

Les mappages relient les différentes valeurs discriminantes aux classes d’entité. La colonne ShapeCode est le discriminant. Si un enregistrement a une valeur "G" dans cette colonne, il sera construit dans la classe Shape. S’il a une valeur "S", il sera construit dans la classe Square. S’il a une valeur "R", il sera construit dans la classe Rectangle. Lorsque le discriminateur d’un enregistrement ne correspond à aucune des valeurs mappées aux classes d’entité, une classe est utilisée par défaut. Le mappage utilisé par défaut est défini avec la propriété IsDefault (ici, Shape). Ainsi, par exemple, si un enregistrement a une valeur "Q" dans la colonne ShapeCode, il sera construit dans la classe Shape, puisque cette valeur ne correspond à aucune des valeurs mappées. Voyons maintenant le code complet de la classe DataContext. La classe DataContext public partial class TestDB : DataContext { public Table Shapes; public TestDB(string connection) : base(connection) { } public TestDB(System.Data.IDbConnection connection) : base(connection) { } public TestDB(string connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { } public TestDB(System.Data.IDbConnection connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { } } [Table] [InheritanceMapping(Code = "G", Type = typeof(Shape), IsDefault = true)] [InheritanceMapping(Code = "S", Type = typeof(Square))] [InheritanceMapping(Code = "R", Type = typeof(Rectangle))] public class Shape { [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL IDENTITY")] public int Id;

Openmirrors.com

Linq.book Page 599 Mercredi, 18. février 2009 7:58 07

Chapitre 18

Informations complémentaires sur SQL

599

[Column(IsDiscriminator = true, DbType = "NVarChar(2)")] public string ShapeCode; [Column(DbType = "Int")] public int StartingX; [Column(DbType = "Int")] public int StartingY; } public class Square : Shape { [Column(DbType = "Int")] public int Width; } public class Rectangle : Square { [Column(DbType = "Int")] public int Length; }

Rien de nouveau dans cette classe : nous avons simplement inclus les classes introduites dans les pages précédentes dans le [Your]DataContext TestDB et ajouté quelques constructeurs. Dans le Listing 18.3, nous allons mettre en place le code qui définira la base de données. Listing 18.3 : Ce code définit la base de données exemple héritée de la classe d’entité. TestDB db = new TestDB(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestDB"); db.CreateDatabase();

Ce code ne produit aucune sortie dans la console mais, si vous consultez le serveur de base de données, vous verrez qu’une base de données TestDB comprenant une table Shape a été créée. Si vous ouvrez la table Shape, vous verrez qu’elle ne contient aucune donnée. Nous allons maintenant remplir cette table en utilisant quelques instructions LINQ to SQL (voir Listing 18.4). Listing 18.4 : Définition de données dans la table Shape. TestDB db = new TestDB(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestDB"); db.Shapes.InsertOnSubmit(new db.Shapes.InsertOnSubmit(new db.Shapes.InsertOnSubmit(new db.Shapes.InsertOnSubmit(new db.Shapes.InsertOnSubmit(new db.Shapes.InsertOnSubmit(new

Square { Width = 4 }); Rectangle { Width = 3, Length = 6 }); Rectangle { Width = 11, Length = 5 }); Square { Width = 6 }); Rectangle { Width = 4, Length = 7 }); Square { Width = 9 });

db.SubmitChanges();

Ce code définit le DataContext et les objets de la classe d’entité, puis il insère ces objets dans la table Shapes. Les données sont sauvegardées dans la base de données en

Linq.book Page 600 Mercredi, 18. février 2009 7:58 07

600

LINQ to SQL

Partie V

appelant la méthode SubmitChanges. Après avoir exécuté ce code, la table Shape contient les données représentées dans le Tableau 18.1. Tableau 18.1 : Le résultat du code précédent.

Id

ShapeCode

StartingX

StartingY

Length

Width

1

S

0

0

NULL

4

2

R

0

0

6

3

3

R

0

0

5

11

4

S

0

0

NULL

6

5

R

0

0

7

4

6

S

0

0

NULL

9

La colonne Id étant utilisée pour identifier les enregistrements, les valeurs changeront si vous utilisez le code à plusieurs reprises. Nous allons maintenant appliquer plusieurs requêtes à la table. Dans le Listing 18.5, nous allons faire une requête sur les carrés. Cette requête inclura les rectangles, puisque les rectangles héritent des carrés. Enfin, nous effectuerons une autre requête qui ne portera que sur les rectangles. Listing 18.5 : Interrogation de la base de données. TestDB db = new TestDB(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestDB"); // Récupération des carrés (rectangles inclus) IQueryable squares = from s in db.Shapes where s is Square select s; Console.WriteLine("Carrés :"); foreach (Shape s in squares) { Console.WriteLine("{0} : {1}", s.Id, s.ToString()); } // Récupération des rectangles (carrés exclus) IQueryable rectangles = from r in db.Shapes where r is Rectangle select r; Console.WriteLine("{0}Rectangles :", System.Environment.NewLine); foreach (Shape r in rectangles) { Console.WriteLine("{0} : {1}", r.Id, r.ToString()); }

Ce code effectue deux requêtes identiques, à un petit détail près. Dans la première, les enregistrements instanciés dans la classe Square (pour cause d’héritage, les enregistrements instanciés dans la classe Rectangle font également partie de la requête) sont Openmirrors.com

Linq.book Page 601 Mercredi, 18. février 2009 7:58 07

Chapitre 18

Informations complémentaires sur SQL

601

interrogés. Dans la seconde requête, seuls les enregistrements instanciés dans la classe Rectangle sont interrogés (ceux instanciés dans la classe Square sont exclus). Voici les résultats : Carrés : 1 : LINQChapter18.Square 2 : LINQChapter18.Rectangle 3 : LINQChapter18.Rectangle 4 : LINQChapter18.Square 5 : LINQChapter18.Rectangle 6 : LINQChapter18.Square Rectangles : 2 : LINQChapter18.Rectangle 3 : LINQChapter18.Rectangle 5 : LINQChapter18.Rectangle

L’héritage des classes d’entité est un concept qui peut se révéler utile pour construire une hiérarchie d’entité associée à une base de données.

Transactions Dans le chapitre précédent, vous avez appris que la méthode SubmitChanges s’exécute au sein d’une transaction. Si une transaction n’est pas déjà ouverte, elle est créée de façon à englober toutes les tentatives de modifications faites pendant l’invocation de la méthode SubmitChanges. Ceci est très pratique, mais que faire si vous avez besoin d’une transaction qui doit s’étendre au-delà de la portée de la méthode SubmitChanges ? Nous allons donner un exemple pour montrer comment effectuer des mises à jour en utilisant plusieurs méthodes SubmitChanges à l’intérieur d’une même transaction. Encore plus fort : les appels aux méthodes SubmitChanges vont mettre à jour des bases de données différentes. Dans le Listing 18.6, nous effectuerons des modifications dans les bases de données Northwind et TestDB (créée dans les pages précédentes). Normalement, chaque appel à la méthode SubmitChanges devrait comprendre sa propre transaction. Mais, dans cet exemple, vous allez voir que les deux SubmitChanges font partie de la même transaction. Le Listing 18.6 étant plus complexe que les précédents, nous donnerons des explications à chaque fois que cela sera nécessaire. INFO Cet exemple nécessite une référence à l’assembly System.Transactions.dll.

Linq.book Page 602 Mercredi, 18. février 2009 7:58 07

602

LINQ to SQL

Partie V

Listing 18.6 : Rejoindre une transaction à portée. Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind"); TestDB testDb = new TestDB(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestDB"); Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP").SingleOrDefault(); cust.ContactName = "Barbara Penczek"; Rectangle rect = (Rectangle)testDb.Shapes.Where(s => s.Id == 3).SingleOrDefault(); rect.Width = 15;

Ce code définit le DataContext des deux bases de données, il effectue une requête sur chacune d’entre elles, puis il modifie les objets entité ainsi obtenus. try { using (System.Transactions.TransactionScope scope = new System.Transactions.TransactionScope()) { db.SubmitChanges(); testDb.SubmitChanges(); throw (new Exception("Annulation de la transaction.")); // Un avertissement sera émis car la ligne suivante ne peut pas être exécutée scope.Complete(); } } catch (Exception ex) { Console.WriteLine(ex.Message); }

INFO Du code ayant été défini après la levée de l’exception, le compilateur indiquera que la méthode scope.Complete n’est jamais exécutée.

Ce code instancie un objet TransactionCode de façon à mettre en place une transaction pour "envelopper" les deux méthodes SubmitChanges. Après le second appel à la méthode SubmitChanges, une exception est intentionnellement déclenchée afin que la méthode scope.Complete ne soit pas appelée et que la transaction ne soit pas effectuée. Si nous n’avions pas enveloppé les deux appels à la méthode SubmitChanges dans un objet TransactionScope, chaque méthode SubmitChanges aurait créé sa propre transaction et les modifications auraient été reportées dans la base de données après chaque SubmitChanges. Lorsque l’exception est levée, les méthodes SubmitChanges ne sont plus dans la portée de la transaction. Étant donné que la méthode Complete n’a pas été appelée, les actions effectuées dans la transaction sont annulées. db.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, cust); Console.WriteLine("Nom du contact = {0}", cust.ContactName);

Openmirrors.com

Linq.book Page 603 Mercredi, 18. février 2009 7:58 07

Chapitre 18

Informations complémentaires sur SQL

603

testDb.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, rect); Console.WriteLine("Largeur du rectangle = {0}", rect.Width);

Les modifications n’ont pas été sauvegardées dans la base de données, mais les objets entité contiennent toujours les valeurs modifiées. Vous vous rappelez certainement que, lorsqu’une méthode SubmitChanges ne s’exécute pas jusqu’au bout, les données modifiées sont conservées dans les objets entité afin que les conflits d’accès concurrentiels puissent être résolus et que la méthode SubmitChanges puisse être appelée une nouvelle fois (voir Chapitre 17). De même, vous vous rappelez certainement qu’une nouvelle interrogation de la base de données ne lit pas les valeurs stockées dans la base de données (voir la section "Discordance des résultats dans le cache" du Chapitre 16). La requête se contente de déterminer quelles entités devraient être incluses dans les résultats. Si ces entités font partie du cache du DataContext, elles seront retournées. Pour savoir quelles valeurs étaient stockées dans la base de données, les objets entité doivent être rafraîchis à l’aide de la méthode Refresh. Ainsi donc, les deux objets entité récupérés de la base de données sont rafraîchis puis affichés dans la console afin de prouver que la mise à jour n’a pas eu lieu dans la base de données. Voici les résultats : Annulation de la transaction. Nom du contact = Fran Wilson Largeur du rectangle = 11

ASTUCE Si une exception du type "MSDTC on server ’[server]\SQLEXPRESS’ is unavailable" est levée lorsque vous travaillez avec un exemple qui utilise l’objet TransactionScope, assurez-vous que le service Distributed Transaction Coordinator est démarré.

Résumé Dans ce chapitre, vous avez appris à appliquer des requêtes sur les vues d’une base de données. Rappelez-vous qu’une vue n’est accessible qu’en lecture seule. Nous avons ensuite parlé de l’héritage dans les classes d’entité. Cette technique permet à des enregistrements d’une table unique d’être instanciés dans des objets de classes différentes mais liés par héritage. Enfin, nous nous sommes intéressés d’un peu plus près aux transactions et avons montré comment "envelopper" plusieurs mises à jour LINQ to SQL dans une même transaction. Vous avez pu en juger dans les derniers chapitres de cet ouvrage : LINQ to SQL, c’est de la dynamite ! Mais ne pensez pas que ce soit la seule partie de LINQ. Si vous avez sauté les premiers chapitres pour vous plonger directement dans LINQ to SQL, je vous

Linq.book Page 604 Mercredi, 18. février 2009 7:58 07

604

LINQ to SQL

Partie V

conseille de vous y reporter maintenant. Vous verrez : d’autres API de LINQ sont vraiment formidables ! Vous apprendrez entre autres à interroger des collections en mémoire et à transformer des données issues d’une collection d’un type donné en un autre type. Si vous écrivez du code XML, vous serez certainement émerveillé par les possibilités de LINQ to XML. Ce chapitre étant le dernier dédié à LINQ to SQL, mais également le dernier de cet ouvrage, nous allons nous quitter avec un dernier exemple. J’ai souvent entendu la critique suivante à propos de LINQ, exprimée sous la forme d’une question : "Que peut faire LINQ pour moi que je ne puisse déjà faire ?" Il est vrai qu’il existe de nombreuses techniques qui permettent de faire la plupart des tâches rendues simples par LINQ, mais n’oubliez pas que LINQ a apporté une réelle abstraction en matière d’interrogation de données et qu’il a rassemblé les techniques d’interrogation utilisées dans des domaines très divers. Notre dernier exemple abondera dans ce cens : nous allons mixer des données issues d’une base de données et des données XML… juste pour montrer que cet éclectisme est possible. Dans le Listing 18.7, nous définissons un objet XElement en parsant une chaîne. Les données XML sont utilisées pour mapper les abréviations des différents États des ÉtatsUnis à leur nom in extenso. Nous utilisons alors LINQ to SQL pour interroger les clients qui résident aux États-Unis. La méthode AsEnumerable est appelée sur la séquence retournée, afin de pouvoir exécuter le reste de la requête localement, et non dans la base de données. Ceci est nécessaire, car le reste de la requête ne peut pas être traduit en SQL. Les résultats de la requête sont alors joints aux données XML créées en comparant le champ Region du client à l’attribut ID des éléments XML. En mixant ces deux sources de données, nous pouvons obtenir le nom complet de l’État spécifié dans le champ Region. Les résultats de la jointure sont alors projetés dans un objet de type anonyme qui contient l’objet entité Customer et la description de l’État issue des données XML. Ensuite, nous énumérons les résultats et affichons le nom de la société (CompanyName), le nom de l’État (Region) et l’intitulé de l’État issu des données XML. Nous venons de réaliser une jointure entre des données provenant d’une base de données et d’autres données au format XML. N’est-ce pas remarquable ? Faisiezvous partie des personnes qui déclaraient que LINQ ne pouvait rien apporter de nouveau ? Voici les résultats retournés par la jointure : Client Client Client Client Client Client

Openmirrors.com

= = = = = =

Great Lakes Food Market : OR : Oregon Hungry Coyote Import Store : OR : Oregon Lazy K Kountry Store : WA : Washington Let’s Stop N Shop : CA : California Lonesome Pine Restaurant : OR : Oregon Old World Delicatessen : AK : Alaska

Linq.book Page 605 Mercredi, 18. février 2009 7:58 07

Chapitre 18

Client Client Client Client Client Client

= = = = = =

Informations complémentaires sur SQL

605

Rattlesnake Canyon Grocery : NM : New Mexico Save-a-lot Markets : ID : Idaho The Big Cheese : OR : Oregon The Cracker Box : MT : Montana Trail’s Head Gourmet Provisioners : WA : Washington White Clover Markets : WA : Washington

Comme vous pouvez le voir, nous avons effectivement pu rapprocher les enregistrements sélectionnés par la requête LINQ to SQL des données XML correspondantes pour obtenir l’intitulé de chaque État. Comment auriez-vous fait si vous n’aviez eu qu’ADO.NET et le W3C XML DOM à votre disposition ? Pour terminer en beauté, voici le code responsable de cette jointure. Listing 18.7 : Une requête qui réalise une jointure entre une base de données et des données XML. string statesXml = @"