POURQUOI CE BLOG, POUR QUI ?


POURQUOI CE BLOG, POUR QUI ?
Ce Blog s'adresse à tous ceux qui sont passionnés par les sciences informatiques , Professionnels,Etudiants,Amateurs ...
Les sujets exposés dans la suite se rapporteront essentiellement sur l'analyse informatique,la programmation,le développement ainsi que à l'architecture IT.
QUI SUIS JE ?
Je suis Kangulungu Lubilanji, Consultant-Freelance sur les technologies .NET,C#,ASP.NET ... Contactez moi pour plus d'informations.

La Programmation OO: Chapitre 19. Persistance d'objets

Chapitre 19. Persistance d'objets
Dans ce chapitre, nous nous intéressons aux différentes manières de sauvegarde, durant l'exécution du programme, les objets sur le disque dur.

Le disque dur, tout fichier est stocké physiquement comme un ensemble d'octets enchaînés par une liste liée. Ces octets sont codés magnétiquement sur le disque. Ce dernier est doté d'une immense capacité, mais d'un accès très lent par rapport à la mémoire RAM. A chaque accès, il faut d'abord retrouver l'emplacement du bloc, le cylindre, le secteur, etc., puis commencer à lire bit par bit et, quand il s'agit de l'entièreté du fichier, bloc par bloc. C'est un processus long et pénible, présentant le désavantage de ralentir les applications qui, au cour de leur déroulement, doivent extraire de l'information ou en déposer sur le disque dur.

Quatre manières d'assurer la persistance des objets, il y a, aujourd'hui quatre manières d'assurer la persistance des objets, qui par sophistication croissante, sont la simple écriture et lecture des valeurs d'attributs à sauvegarder sur un fichier, la sérialisation des objets, l'interaction avec une base de données relationnelle, et l'utilisation de bases de données orienté objets. Nous pourrions également distinguer en fonction de leur respect de la nature « objet » des données à sauvegarder, leur facilité d'emploi, leur diffusion.  

Simple sauvegarde sur un fichier
Utilisation des « streams », C# permet l'utilisation d'un « stream » (traduit littérale : « flux »), et de manière assez semblable, d'écrire et de lire des octets sur un fichier. Un « stream » est un tube à octet, qui connecte le programme en train de s'exécuter et tout type de périphérique, disque et autres (réseau, imprimantes...). Cette abstraction permet de traiter tous les périphériques : comme un seul, en phase avec la manière dont le processeur se connecte aux différents périphériques : un bus unique, que la présence de « pilotes » permet ensuite de ramifier et de spécialiser en fonction du périphérique.

Comme à l'état primitif, le « stream » ne fait circuler que des octets, cette classe est souvent sujette à des spécifications par le jeu de l'héritage, ou à des emboîtements par lesquels on spécialise, par un second « stream », connecté à même le premier, un mode d'écriture ou de lecture sur le périphérique. On parle alors de « flux filtré ». Par exemple, les données à lire peuvent être des nombres, des caractères, des images, du son. Selon le cas, la manière d'associer les octets et de les transmettre à l'application est différente.

FileInputStream fin = new FileInputStream("fichier.dat");
DataInputStream din = new DataInputStream(fin);
double s = din.readDouble;

La méthode readDouble n'existe que dans la classe DataInputStream. Un autre type de filtrage très fréquent consiste à adjoindre, soit à la lecture, soit à l'écriture, un mécanisme de temporisation (en anglais « buffering »), qui permet, par exemple, d'écrire les octets dans une zone RAM locale, avant de transférer d'un seul coup tous ces octets sur le disque dur (pour minimiser le nombre d'accès disques, très lents).

L'héritage entre les « streams » est une caractériqtique commune aux langages avec, comme toujours, au grand dam des programmeurs, quelques nuances de syntaxe ou de pratique, ici et là, les différenciant. La spécialisation de la lecture ou de l'écriture se fait souvent en fonction du type de périphérique avec lequel on communique, ou en fonction du type très particulier d'information à échanger. On conçoit aisément la raison d'être de l'héritage. Dans tous les cas, on lit ou on écrit, mais il est nécessaire de redéfinir le mode de lecture ou le mode d'écriture en fonction du support ou des données à transmettre.

static void Main(string[] args)
        {
            Predateur[] lesLions = new Predateur[3];
            lesLions[0] = new Predateur();
            lesLions[1] = new Predateur();
            lesLions[2] = new Predateur();
/* obtient la connexion au fichier en mode lecture */ 
            FileStream fso = new FileStream("leFichierPredateur.txt", FileMode.Open);
/* flux filtré pour utiliser "ReadLine()" */ 
            StreamReader monEntree = new StreamReader(fso);
            for (int i = 0; i < lesLions.Length; i++)
            {
                lesLions[i].litLesDonnees(monEntree);
            }
            monEntree.Close(); /* fermeture indispensable  du tube */ 
/* obtient la connexion au fichier en mode écriture */
            FileStream fsr = new FileStream("leFichierPredateur.txt", FileMode.Create);
/* flux filtré pour utiliser "WriteLine()" */ 
            StreamWriter maSortie = new StreamWriter(fsr);
            for (int i = 0; i < lesLions.Length; i++)
            {
                lesLions[i].sauveDonnees(maSortie);
            }
            maSortie.Close(); /* fermeture indispensable  du tube */ 
            Console.ReadLine();
        }
La différence entre la lecture et l'écriture se se fait dans les arguments du constructeur du FileStream, et ne requiert pas de classes séparées. Ces arguments suffisent à spécifier le mode d'accès ou l'emploi d'un mécanisme de temporisation. Par exemple, le FileMode peut être OpenOrCreate, indiquant que le fichier doit être s'il existe, ou créé s'il n'existe pas. D'autres informations peuvent être spécifiées à la construction, comme FileAccess.Read quand seule la lecture est autorisée, ou FileAccess.ReadWrite, qui est l'option par défaut.

Sauvegarder les objets sans les dénaturer : la sérialisation, c'est un mode de sauvegarde qui permet de reproduire sur le disque dur une copie des objets et de leur structure d'interconnexion, c'est à dire de faire, à un instant donné, une copie miroir du tissu relationnel des objets qui se trouve  dans la RAM. La lecture d'un des objets du réseau suffirait, pareillement, à la reproduction dans la RAM du réseau dans son entièreté. Ce mécanisme en C# porte le nom de « sérialisation »

[Serializable]
    class Predateur {....}

La présence de [Serializable] devant la classe indique que la classe peut l'être.

 public void sauveDonnees(BinaryFormatter maSortie, Stream sw)
        {
            maSortie.Serialize(sw, this);
        }
        public void litLesDonnees(BinaryFormatter monEntree , Stream sr)
        {
            Predateur unPredateur;
            unPredateur = (Predateur)monEntree.Deserialize(sr);
        }
On retrouve un stream pour la lecture comme pour l'écriture, mais on recourt à l'utilisation de la classe BinaryFormatter pour le formatage des objets sauvés ou lus. La lecture et l'écriture se font au moyen des méthodes Serialize et Deserialize et, il est nécessaire de « caster » l'objet lu dans la classe d'accueil car la méthode Deserialize()  retourne n'importe quel type d'objet .
En C# et .Net, il  est également possible de sérialiser très facilement des objets dans le format XML, soit en substituant simplement le BinaryFormatter par un SoapFormatter, soit par l'utilisation de la classe XMLSerialize et des méthodes Serialize et Deserialize recevant des « stream » en argument.

Les bases de données relationnelles, les bases de données relationnelles sont, aujourd'hui, le moyen le plus répandu pour stocker l'information de manière permanente. Les raisons du quasi-monopole de ce mode de stockage sont nombreuses. Une première en est qu'à l'instar de l'approche OO, les données sont concentrées dans des structures de type « objet ». Les informations à stocker le sont en tant que valeurs d'attribut, regroupées dans un structure appelée  « table », très voisine de nos classes. Chaque instance de cette table, obtenue en fixant les valeurs d'attribut pour un cas donné, et qui donnerait naissance à un objet dans le cas de la classe, donne ici naissance à un enregistrement.
Une deuxième raison est l'existence d'un mode d'organisation de ces tables, mode dit relationnel, et dont la qualité première est d'éviter d'avoir à reproduire plusieurs fois la même information. On imagine très aisément tous les problèmes liés à la duplication des informations : mises à jour plus pénibles et erreurs d'encodage plus probables. Chaque table se doit de coder un concept donné, et il est nécessaire de séparer l'encodage des informations dans des tables distinctes, mais conservant entre elles des relations qui permettent, à partir de l'une d'entre elles, de retrouver les informations de l'autre.
Une troisième raison est la qualité importante de solutions techniques disponibles aujourd'hui, pour gérer de façon automatique des problèmes aussi critiques que les  « backups automatisés », les accès concurrentiels, les accès sécurisés et fiabilisés, les accès de type transactionnel (quand une étape défaillante dans une succession d'opérations rend caduques toutes les opérations effectuées jusque-là), le stockage réparti sur plusieurs ordinateur, etc.

SQL, une dernière raison est l'existence d'un langage d'interrogation de ces bases de données, langage standardisé nommé SQL(Structured Query Language). Toute application informatique qui doit s'interfacer avec une base de données relationnelle le fera en  « parlant » ce langage. C'est ce standard, additionné à toutes les solutions techniques proposées par des environnements logiciels, bien installés aujourd'hui, comme Oracle, MS SQL SERVER, Access, additionné, enfin à toutes les couches logicielles de visualisation et d'analyse de données s'exécutant à partir de ces bases, qui rendent les bases de données relationnelles incontournables lors de l'étude des solutions de persistance des objets.
Sql est un  langage d'interaction avec les bases de données qui sont utilisables, généralement, dans des programmes informatiques qui interagissent avec ces bases. Ces instructions permettent la création des tables  et des relations, la consultation, l'insertion, et la modification des enregistrements, la définition des permission au niveau des utilisateurs.

Select pour lire les enregistrements : 
SELECT [ALL] | [DISTINCT] 
<liste des noms de colonnes> | *
FROM <liste des tables>
[WHERE <conditions logique>]

Insert pour insérer des nouveaux enregistrements :  
INSERT INTO Nom_de_la_table
(colonne1,colonne2,colonne3,...)
VALUES (valeur1,valeur2,valeur3,...)

Update pour mettre à jour ces enregistrements : 
UPDATE Nom_de_la_table
SET Colonne = Valeur
[WHERE qualification]

Delete pour effacer ces enregistrements : 
DELETE FROM Nom_de_la_table
WHERE qualification

Clé primaire, la clé primaire d'une table est l'attribut qui permet d'accéder, de manière unique, et sans aucune équivoque à un des enregistrement de la table. La valeur de cet attribut doit être distincte pour chaque enregistrement, une valeur entière étant ce qu'il y a de plus courant.

Référent versus clè, les bases de données relationnelles fonctionnent et s'utilisent indépendamment de tout mode de stockage, là où les objets ne s'utilisent et ne fonctionnent que sur un mode stocké. Un enregistrement est indicé par un nombre unique plutôt qu'un emplacement unique. C'est la valeur d'une variable fondamentale (la clè) et non pas un système d'adressage qui permet l'accès à l'objet et les connexions entre les objets. C'est la toute la différence.

Relations entre tables et association entre classes, les tables peuvent être tout autan en relation que les classes le sont, le passage de l'OO vers le relationnel se trouve facilité par cette ressemblance structurelle. Quelques requêtes suffisent à mener à bien le va-et-viens des objets entre le programme et la base de données, quelles que soient les liaisons qui existes entre objets.

Relation 1-n, supposons une nouvelle classe, Tanière, habitat de notre prédateur et admettons, de surcroît, que chacun des prédateurs peut se loger dans plusieurs de ses tanières.
Dans l'approche OO, le prédateur interagit avec ses tanières en possédant un vecteur de référents vers celles-ci, la valeur de chaque référent étant l'adresse d'un objet tanière, avec lequel le prédateur peut interagir. Chacun des prédateurs a donc une connaissance directe de ses tanières. Il possède l'adresse de chacune d'entre elles. Dans l'approche base de données relationnelle, il y a lieu, tout comme en OO, de tenir séparée ces deux tables, puisqu'elles concernent, en effet deux réalités très distinctes.

Cependant, il n'est plus ici question d'un adressage physique explicite, mais simplement d'une manière, pour chaque enregistrement de la première table, de découvrir avec quels enregistrements de la seconde table, il entre en relation. Cela se fait, tout simplement, en rajoutant à la seconde table un clé dite « étrangère » dont les valeurs doivent être partagées avec celles prises par la clé primaire de la première table.
La relation s'établit alors comme indiqué dans le schéma ci-dessus, entre la clé primaire de la première table et la clé étrangère de la seconde table. La tanière saura de quel prédateur elle est la tanière, par l'entremise de cette clé étrangère, dont la valeur correspondra à une des valeurs de la clé primaire des prédateurs. Il existe une façon de forcer le partage des valeurs entre les deux clés, en imposant l'intégrité référentielle dans la relation entre ces deux tables. Les tanières ne peuvent être alors les tanières que des seuls prédateurs existants.

Pour ce qui est des classes, nous nous trouvons en pleine logique de programmation : l'ordinateur exécute des instructions, et deux classes doivent se parler, dès lors que la première déclenche sur l'autre une séquence d'instruction. Pour ce faire, elle n'a d'autre moyen, plus direct, que de connaître l'adresse des données que ces instructions doivent manipuler.

Pour ce qui est des tables, nous sommes en pleine logique de stockage de données, l'important étant la facilité d'utilisation, d'écriture et de lecture de ces données. Les relations sont parfaitement justifiées lors de l'écriture ou de la mise à jour des données, car elles permettent d'éviter des redondances malvenues, souvent sources d'erreur lors de l'écriture ou de cette mise à jour. Ces relations existent, alors, sans aucun besoin d'emplacement physique. Elle existent à titre « conceptuel »,peu importe le lieu de stockage des tables.

En conséquence de quoi, on se rend compte que, dès qu'un diagramme de classe intègre une relation de type 1-n entre deux classes, il est indispensable de penser une traduction dans le mode relationnel. Cette traduction, idéalement, passe par l'addition, en plus des attributs issus des deux classes, d'une clé primaire dans une des tables et d'une clé étrangère dans l'autre, ainsi que par une assignation de valeur pour clés, qui traduisent les relations existant entre les objets.

Les instructions SQL peuvent se complexifier quelque peu, car elles devrons prendre en considération l'existence des relations. Par exemple, si l'on désire accéder à la dimension de la seconde tanière du premier prédateur, la requête SQL le permettant sera la suivante (INNER JOIN indiquant la jointure entre les deux tables) : 
SELECT Taniere.dimension FROM Predateur INNER JOIN Taniere ON
Predateur.idPredateur = Taniere.IdPredateur WHERE Predateur.idPredateur = 1 and idTaniere = 2µ

Alors que son équivalent OO se limiterait à quelque chose comme : Predateur[0].getMesTanieres()[1].getDiemension(). 

Relation n-n
Dans le diagramme de classe, l'association qui existe entre le prédateur et, disons, les points d'eau est de type n-n. En effet, plusieurs prédateurs peuvent s'abreuver à un même point d'eau et un prédateur quelconque peut s'abreuver à plusieurs point d'eau.
Il n'est pas possible de reproduire cela, de la manière la plus simple, par une relation équivalente, n-n, entre les deux tables.C'est parfaitement prohibé étant donné le mécanisme de jointure qui sous-tend les relation 1-n, et de cela contribuerait à dégrader considérablement les relations. La seule table du côté du n comprend une clé à associer à la clé primaire de la table du côté du 1. Une seule clé étrangère est nécessaire, car chaque enregistrement de cette table n'est associé qu'à un seul enregistrement de l'autre table, quitte à ce que cela soit plusieurs fois le même (pour que l'on ait bien  « 1 » d'un coté et « n » de l'autre.

Or si nous voulions une relations de type n-n, il faudrait permettre plusieurs clés étrangères dans la table du côté du n, pour que chaque « eau » puisse référer plusieurs prédateurs. C'est parfaitement impensable, car nous ne saurions combien en mettre et comment se ferait la jointure entre la clé primaire et ces multiples clés étrangères.

En conclusion, les relations n-n ne sont pas admises dans les bases de données relationnelles, alors qu'il peut en exister, sans le moindre problème, en OO.On conçoit bien qu'un premier objet puisse pointer vers de multiples objets, et que ces derniers puissent faire de même. Cette relation n-n nous permet de comprendre davantage encore les deux mécanismes très différents mis en oeuvre pour relier les tables et les classes entre elles : lien conceptuel d'un coté, lien physique de l'autre. Le seul moyen de s'en sortir dans l'univers relationnel est de procéder, comme montré dans la figure ci-après, à l'ajout d'une table intermédiaire, une table qui associe les points d'eau aux prédateurs.

Cette table additionnelle, sans équivalent dans la version OO, complexifie encore la traduction du monde objet vers le relationnel, inévitable pour pouvoir sauvegarder les objets et leur relations dans une base de données. Il faudra via les requêtes SQL, créer, accéder et modifier cette table d'association, alors qu'elle ne paraît être la contrepartie sémantique d'aucune des classes en présence.

Linq, La bibliothèque Linq (Langage-INtegrated Query), sous les feux de la rampe informatique à l'heure actuelle, est sans conteste l'innovation majeure de la nouvelle version de la plate-forme de développement de microsoft .Net.3. On peut aller jusqu'à penser que l'essentiel des autres innovations présentes dans cette troisième version, telles les expressions lambda, les  « méthodes d'extension », les variables anonymes typées implicitement  « var » et la manipulation des  « query », sont autant d'étapes majeures pour enfin aboutir à la réalisation de Linq. Si la bibliothèque Linq est à ce point discutée dans le monde informatique, c'est parce qu'elle est présentée par Microsoft comme la solution à cet épineux problème encore irrésolu de la mise en correspondance entre le monde du stockage des données (tel le relationnel) et de l'orienté objet, auquel vient s'ajouter depuis quelques années,avec l'invasion irrépressible des application web, la nécessaire prise en charge de l'information stockée et représentée sous forme XML. Linq veut aboutir une fois pour toute à la réalisation et la concrétisation parfaite de l'équation DATA=OBJECT, c'est-à-dire présenter une syntaxe unifiée faisant abstraction de la manière définitive dont sont stockées les données en mémoire.

Une même information, et trois manières de la présenter : l'objet, le relationnel et XML. Il est en effet grand temps de parvenir à homogénéiser un mode d'accès et de traitement de cette informations qui soit indépendant de la manière de la représenter. Linq se présente comme un candidat possible à cette homogénéisation : une même requête, dans un langage voisin du nôtre, agissant dans ces trois univers d'information.

De surcroît, Linq s'intègre parfaitement aux langages de programmation .Net tels C#, il en est une évolution. Toute requête Linq se trouve compilée au même titre que n'importe quelle autre instruction. Fini donc les requêtes SQL qui ne posent problème qu'à l'exécution, c'est à dire quand, en effet, elles interagissent directement avec la base de données relationnelle. Linq permet de prévenir par la compilation des erreurs d'interrogation de bases de données (par exemple des attributs mal orthographiés) qui, auparavant, ne se seraient produites que pendant la phase d'exécution. De plus, l'homogénéisation permettra à une même  expression d'interroger indifféremment des système d'information objet, XML et relationnels et d'en mélanger les résultats. Le programmeur est enfin libre de ses mouvements OO. Il n'a plus besoin de maîtriser autant de langages qu'il y a de manière de stocker l'information : SQL, XML... 

Exemple de Linq agissant sur une collection d'objets


static void Main(string[] args)
        {
            string[] names = {"Burke","Tom","Frank","Pierre","Kat","Bob","Karl","Bob" };
            var test = from s in names
                       where s.Length == 5
                       orderby s
                       select s.ToUpper ();

            foreach (var item in test) { Console.WriteLine(item); }
            Console.ReadLine();
        }
Cette requête select n'ira pas sans vous rappeler sa version SQL, mais dans une version qui serait inversée par rapport à l'original. L'inversion de la requête est justifiée par l'utilisation des aides à la programmation. C'est parce qu'il découvre que « s » est de type string, que l'environnement de développement pourra vous faciliter la programmation des opérateurs qui suivent. Le typage anonyme y est réalisé par la présence du mot-clé var. Cette inférence sur le typage permet au compilateur de déduire le type de variables sans que le développeur ait besoin de le lui signaler explicitement. Comme cette étape se passe à la compilation, les risques d'erreur restent limités tout en autorisant une syntaxe plus libre. La clause Where agit comme une condition de filtrage du résultat. Ces clauses (par exemple plusieurs Where à la suite l'un de l'autre) peuvent se concaténer, car Linq permet en effet d'enchaîner l'opérateur comme : 

"List.Where(clause where).Select(clause select).OrderBy(critère de tri)".

L'opérateur Select effectue une projection de la liste de départ sur une nouvelle liste obtenue à l'issue de son action. C'est à lui que l'on doit la création d'un nouveau typage en ligne comme résultat de cette projection.
Les opérateurs de tri classique que son OrderBy et ThenBy permettent de trier les éléments d'un résultat pour peu que ceux-ci implémente l'interface IComparable<T>, comme dans le code qui suit : 

class Program
    {
        static void Main(string[] args)
        {
            List <Etudiant> lesEtudiants = new List<Etudiant>();
            lesEtudiants.Add(new Etudiant("Kangulungu","Pite", 33));
            lesEtudiants.Add(new Etudiant("Hubert","Karl", 21));
            lesEtudiants.Add(new Etudiant("Kurt","Nora", 20));
            lesEtudiants.Add(new Etudiant("Marcca","Jane", 43));
            lesEtudiants.Add(new Etudiant("Dupond", "Dupond", 76));

            var test = from s in lesEtudiants
                      where s.Age > 25
                      orderby s.Nom
                      select new { s.Nom, s.Prenom };
            foreach (var item in test) Console.WriteLine(item.Prenom);
            Console.ReadLine();
        }
    }
    class Etudiant {

        public Etudiant(string Nom, string Prenom, int Age)
        {
            this.Nom = Nom;
            this.Prenom = Prenom;
            this.Age = Age;
        }
        public int Age
        {
            get;
            set;
        }
        public string Nom
        {
            get;
            set;
        }
        public string Prenom
        {
            get;
            set;
        }
    }
Deuxième exemple de Linq agissant sur une base de données relationnelle

 [Table]
    public class Predateur
    {
        [Column(IsPrimaryKey = true)] public int idPredateur;
        [Column] public int vitx;
        [Column] public int vity;
        [Column] public int energie;
    }
    class SomeDataContext : DataContext
    {
        public SomeDataContext(IDbConnection connection) : base(connection) { }
    }
    class Program
    {
        static void Main(string[] args)
        {

            DbProviderFactory dbpf = DbProviderFactories.GetFactory("System.Data.SqlClient");
            DbConnection connexion = dbpf.CreateConnection();
            connexion.ConnectionString = "Data Source=(local);";
            connexion.ConnectionString += "Initial Catalog=ecosysteme;";
            connexion.ConnectionString += "Integrated Security=true;";

            SomeDataContext dataContext = new SomeDataContext(connexion);
            Table<Predateur> lesPredateur = dataContext.GetTable<Predateur>();

            IQueryable<int> query = from p in lesPredateur
                                    where p.energie > 10
                                    orderby p.idPredateur descending
                                    select p.idPredateur;

            foreach (int id in query) Console.WriteLine(id);

            Console.ReadLine();
        }
    }
On découvre qu'un même opérateur de projection select, toujours très inspiré de SQL mais inversé, composé d'un même filtre de type where et/ou d'un mécanisme de tri orderby, peut s'appliquer indifféremment sur un tableau d'objets entreposés dans la mémoire RAM et sur une base de données relationnelle. Une troisième illustration aurait pu être possible, en appliquant, une fois encore, ce même type de requête à même l'information stockée en XML, comme dans le schéma ci-dessous
Si , par mégarde, il y a erreur dans la requête select - attribut mal orthographié par exemple -, le code produira une erreur à la compilation. Cela est évidemment possible grâce à la déclaration de la table|classe Predateur et des attributs|Column, qui font le lien avec la base de données relationnelle. La traduction en une requête SQL se fait à la volée, après que le compilateur ait vérifié l'exactitude de la syntaxe. Elle s'écrirait en requêtes XQuery en présence d'un fichier XML. Un objet de classe DataContext représente le lien entre la base de données et les classes entités qui lui sont assignées. Il maintient la correspondance entre les objets du code et les enregistrements de la base de données. Il est responsable de la génération du code SQL qui sera soumis à la base de données.
L'addition ou le retrait d'enregistrements dans la base de données peut s'effectuer à partir du DataContext de la manière suivante :
dataContext.Predateur.InsertOnSubmit(p) ; // p serait un nouvel objet prédateur
dataContext.SubmitChanges() ; // exécution de l'insertion


dataContext.Predateur.DeleteOnSubmit(p) ; // p serait l'objet à supprimer
dataContext.SubmitChanges() ; // exécution de la suppression






Aucun commentaire: