IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Introduction par l'exemple à Entity Framework 5 Code First


précédentsommairesuivant

III. Étude de cas avec SQL Server Express 2012

III-A. Introduction

Les exemples trouvés sur le net pour Entity Framework sont dans leur majorité des exemples avec SQL Server. C'est assez normal. Il est probable que c'est le SGBD le plus répandu dans le monde .NET des entreprises. Nous allons suivre cette tendance. Les exemples seront ensuite étendus à toutes les bases de données citées au paragraphe , page .

III-B. Installation des outils

Nous ne décrirons pas l'installation des outils. En effet, cela nécessiterait énormément de copies d'écran qui deviennent assez vite obsolètes. C'est une tâche (pas toujours facile, il est vrai) que nous laissons au lecteur.

Il nous faut installer les outils suivants :

Image non disponible

Une fois le SGBD installé, nous le lançons :

Image non disponible
Image non disponible
  • [1] : dans le Menu Démarrer, lancer le " Gestionnaire de configuration SQL Server " ;
  • [2] : dans ce gestionnaire, lancer le serveur ;
  • [3] : il est lancé.

Nous lançons maintenant l'outil d'administration de SQL Server :

Image non disponible
  • [1] : dans le Menu Démarrer, lancer " SQL Server Management Studio " ;
  • [2] : l'outil d'administration.

Nous allons nous connecter au serveur :

Image non disponible
  • en [1], on connecte l'explorateur d'objets ;
  • en [2], on donne les paramètres de la connexion :
  • [3] : le serveur (local) (attention aux parenthèses nécessaires) désigne le serveur installé sur la machine,
  • [4] : on choisit l'authentification Windows. Il faut être administrateur de son poste pour que cette connexion réussisse,
  • [5] : on se connecte ;
Image non disponible
  • [6] : on est connecté ;
  • [7] : on veut modifier certaines propriétés du serveur ;
Image non disponible
  • [8] : on demande à ce qu'il y ait deux modes d'authentification :
  • authentification Windows comme il vient d'être utilisé. Un utilisateur windows avec les bons droits peut alors se connecter,
  • authentification SQL Server. L'utilisateur doit faire partie des utilisateurs enregistrés dans le SGBD ;

Ceci fait, on peut valider les propriétés du serveur ;

  • [9] : on édite les propriétés de l'utilisateur sa (system administrator) ;
Image non disponible
  • en [10], on lui fixe un mot de passe. Dans la suite du document, celui-ci est sqlserver2012 ;
Image non disponible
  • en [10], on lui donne l'autorisation de se connecter ;
  • en [11], la connexion est activée. Ceci fait l'assistant peut être validé ;
  • en [12], on se déconnecte du serveur.

Maintenant, nous nous reconnectons avec le login sa/sqlserver2012 :

Image non disponible
  • en [1], on se reconnecte ;
  • en [2], en authentification SQL Server ;
  • en [3], l'utilisateur est sa ;
  • en [4], son mot de passe est sqlserver2012 ;
  • en [5], on se connecte ;
Image non disponible
  • en [6], on est connecté.

Nous allons maintenant créer une base de démonstration :

  • en [1], on crée une nouvelle BD ;
  • en [2], elle s'appellera demo ;
  • en [3], on valide ;
Image non disponible
  • en [4], la base créée ;
  • en [5], on crée une nouvelle table à la base demo ;
Image non disponible
Image non disponible
Image non disponible
Image non disponible
  • en [6], on définit une table à deux colonnes ID et NOM ;
  • en [7], on fait de la colonne [ID] la clé primaire ;
  • en [8], la clé primaire est symbolisée par une clé ;
  • en [9], on sauvegarde la table ;
  • en [10], on lui donne un nom ;
  • en [11], pour que la table apparaisse dans la base [demo], il faut actualiser la base ;
  • en [12], la table [PERSONNES] a bien été créée.

Nous en savons assez pour l'instant sur l'utilisation de l'outil d'administration de SQL Server.

III-C. Le serveur embarqué (localdb)\v11.0

VS Express 2012 vient avec un serveur SQL embarqué. On suppose ici que VS Express 2012 a été installé [http://www.microsoft.com/visualstudio/fra/downloads]. On lance VS 2012 [1] :

Image non disponible

On lance l'outil d'administration de SQL Server 2012 [2] et on se connecte [3].

Image non disponible
  • en [4], on se connecte au serveur (localdb)\v11.0 ;
  • en [5], avec une authentification Windows ;
  • en [6], la connexion réussie affiche les bases de données du serveur. On pourrait comme précédemment créer une nouvelle base.

Nous n'utiliserons pas ce serveur embarqué dans VS 2012.

III-D. Création de la base à partir des entités

Entity Framework 5 Code First permet de créer une base de données à partir des entités. C'est ce que nous voyons maintenant. Avec VS Express 2012, nous créons un premier projet console en C# :

Image non disponible
Image non disponible
  • en [1], la définition du projet ;
  • en [2], le projet créé.

Tous nos projets vont avoir besoin de la DLL d'Entity Framework 5. Nous l'ajoutons :

Image non disponible
  • en [1], l'outil NuGet permet de télécharger des dépendances ;
Image non disponible
  • en [2], on télécharge la dépendance Entity Framework ;
  • en [3], la référence a été ajoutée au projet.

On peut en savoir plus en regardant les propriétés de la référence ajoutée :

Image non disponible
  • en [1], la version de la DLL. Il faut la version 5 ;
  • en [2], sa place dans le système de fichiers : <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll où <solution> est le dossier de la solution VS. Tous les packages ajoutés par NuGet iront dans le dossier <solution>/packages ;
  • en [3], un fichier [packages.config] a été créé. Son contenu est le suivant :
packages.config
Sélectionnez
1.
2.
3.
4.
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>

Il liste les paquetages importés par NuGet.

Revenons au projet VS et créons un dossier [Models] dans le projet :

Image non disponible
  • en [1], ajout d'un dossier au projet ;
  • en [2], il s'appellera [Models].

Nous garderons cette habitude par la suite de mettre la définition de nos entités dans le dossier [Models].

Pour construire nos entités, nous allons nous aider de la définition de la base de données MySQL 5 utilisée dans le projet NHibernate. Rappelons le rôle des entités EF :

Les entités doivent refléter les tables de la base de données. La couche d'accès aux données utilise ces entités au lieu de travailler directement avec les tables. Commençons par la table [MEDECINS] :

III-D-1. L'entité [Medecin]

Elle contient des informations sur les médecins gérés par l'application [RdvMedecins].

Image non disponible
  • ID : n° identifiant le médecin - clé primaire de la table
  • VERSION : n° identifiant la version de la ligne dans la table. Ce nombre est incrémenté de 1 à chaque fois qu'une modification est apportée à la ligne.
  • NOM : le nom du médecin
  • PRENOM : son prénom
  • TITRE : son titre (Mlle, Mme, Mr)

Nous pourrions partir de la classe [Medecin] suivante :

[Medecin]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
using System;

[Table("MEDECINS", Schema = "dbo")]
  namespace RdvMedecins.Entites
{
  public class Medecin
  {
    // data
    public int Id { get; set; }
    public string Titre { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
}
  • ligne 3 : la classe [Medecin] est associée à la table [MEDECINS] de la base de données. Celle-ci se trouvera dans un schéma nommé " dbo ".

Nous mettons cette classe dans un fichier [Entites.cs] [1]. C'est là que nous placerons toutes nos entités.

Image non disponible

Toujours dans le dossier [Models], nous créons le fichier [Context.cs] suivant :

[Context.cs]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
using System.Data.Entity;
using RdvMedecins.Entites;

namespace RdvMedecins.Models
{

  // le contexte
  public class RdvMedecinsContext : DbContext
  {
    // les médecins
    public DbSet<Medecin> Medecins { get; set; }
  }

  // initialisation de la base
  public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
  {
  }
}
  • ligne 8 : la classe [RdvMedecinsContext] va représenter le contexte de persistance, c.-à-d. l'ensemble des entités gérées par l'ORM. Elle doit dériver de la classe [System.Data.Entity.DbContext] ;
  • ligne 11 : le champ [Medecins] va représenter les entités de type [Medecin] du contexte de persistance. Il est de type DbSet<Medecin>. On trouvera généralement autant de [DbSet] que de tables dans la base, un par table ;
  • ligne 15 : on définit une classe [RdvMedecinsInitializer] pour initialiser la base créée. Ici elle dérive de la classe [DropCreateDataBaseAlways] qui comme son nom l'indique supprime la base si elle existe déjà puis la recrée. C'est pratique en phase de développement de la BD. Le paramètre de la classe [DropCreateDataBaseAlways] est le type de contexte de persistance associé à la base. On peut utiliser d'autres classes mères que [DropCreateDataBaseAlways] pour la classe d'initialisation :
  • [DropCreateDatabaseIfModelChanges] : recrée la base si les entités ont changé,
  • [CreateDatabaseIfNotExists] : crée la base si elle n'existe pas ;

Il nous reste à créer un programme principal. Ce sera le suivant [CreateDB_01.cs] :

[CreateDB_01.cs]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
using System;
using System.Data.Entity;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class CreateDB_01
  {
    static void Main(string[] args)
    {
      // on crée la base
      Database.SetInitializer(new RdvMedecinsInitializer());
      using (var context = new RdvMedecinsContext())
      {
        context.Database.Initialize(false);
      }
    }
  }
}
  • ligne 12 : [System.Data.Entity.DataBase] est une classe offrant des méthodes statiques pour gérer la base associée à un contexte de persistance. La méthode statique [SetInitializer] permet de préciser la classe d'initialisation de la base. Cela ne lance pas l'initialisation ;
  • ligne 13 : pour travailler avec un contexte de persistance, il faut instancier celui-ci. C'est ce qui est fait ici. On utilise une clause using afin que le contexte soit automatiquement fermé à la sortie de la clause. Donc ligne 17, le contexte est fermé ;
  • ligne 15 : on lance explicitement la génération de la base associée au contexte de persistance [RdvMedecinsContext]. Le paramètre false indique que cette opération ne doit pas être faite si elle a déjà été faite pour ce contexte. Ici, on aurait tout aussi bien pu mettre true.

Lorsqu'on travaille avec une base de données, les paramètres de connexion sont généralement inscrits dans le fichier [App.config]. Constatons que pour l'instant, ils n'y sont pas :

[App.config]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>
</configuration>

Les éléments ci-dessus ont été inscrits dans [App.config] lorsqu'on a ajouté la dépendance Entity Framework aux références du projet.

Exécutons le projet (Ctrl-F5) après avoir lancé SQL Server Express (c'est important) :

Image non disponible

L'exécution doit se terminer sans erreur. Ouvrons maintenant l'outil d'administration de SQL Server et rafraîchissons l'affichage :

Image non disponible

On constate qu'une base portant le nom complet de la classe [RdvMedecinsContext] a été créée et qu'elle contient une table [dbo.MEDECINS] (c'est le nom qu'on lui avait donné) avec des colonnes qui reprennent les noms des champs de l'entité [Medecin]. Si le code s'est bien exécuté et que la base ci-dessus n'apparaît pas, il faut regarder le serveur embarqué (localdb)\v11.0 (cf. page ). Avec VS 2012 pro, ce serveur est utilisé si SQL Server n'est pas actif au moment de l'exécution du code. Avec VS 2012 Express, non.

Examinons la structure de la table [MEDECINS] :

  • elle reprend les noms des champs de l'entité [Medecin] ;
  • la colonne [Id] est clé primaire. C'est une convention d'EF : si l'entité E a un champ Id ou Eid (MedecinId), alors cette colonne est clé primaire dans la table associée ;
  • les types des colonnes de la table sont ceux des champs de l'entité ;
  • pour les colonnes Titre, Nom, Prenom, un type [nvarchar(max)] a été utilisé. On pourrait être plus précis, 5 caractères pour le titre, 30 pour les nom et prénom ;
  • les colonnes Titre, Nom, Prenom peuvent avoir la valeur NULL. Nous allons changer cela.

Regardons les propriétés de la clé primaire [Id] :

Image non disponible

En [1], on voit que la clé primaire est de type [Identité], ce qui signifie que sa valeur est automatiquement générée par SQL Server. Nous adopterons cette stratégie avec tous les SGBD.

Nous allons laisser moins de part aux conventions d'EF en utilisant des annotations. Le code de l'entité dans [Entites.cs] devient le suivant :

[Entites.cs]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RdvMedecins.Entites
{
  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin
  {
    // data
    [Key]
    [Column("ID")]
    public virtual int Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public virtual string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public virtual string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public virtual string Prenom { get; set; }
    [Required]
    [Column("VERSION")]
    public virtual int Version { get; set; }
  }
}
  • lignes 2 et 3 : les annotations sont trouvées dans les espaces de noms [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength] et [System.ComponentModel.DataAnnotations.Schema] (Column). On trouvera d'autres annotations à l'URL [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
  • ligne 11 : [Key] désigne la clé primaire ;
  • ligne 12 : [Column] fixe le nom de la colonne correspondant au champ ;
  • ligne 14 : [Required] indique que le champ est obligatoire (SQL NOT NULL) ;
  • ligne 15 : [MaxLength] fixe la taille maxi de la chaîne de caractères, [MinLength] sa taille mini ;

Exécutons le projet avec cette nouvelle définition de l'entité [Medecin]. La base créée est alors la suivante :

Image non disponible
  • les colonnes ont le nom qu'on leur a fixé ;
  • l'annotation [Required] a été traduite par un SQL NOT NULL ;
  • l'annotation [MaxLength(N)] a été traduite par un type SQL nvarchar(N).

Dans l'application NHibernate, la colonne [VERSION] était là pour prévenir les accès concurrents à une même ligne d'une table. Le principe est le suivant :

  • un processus P1 lit une ligne L de la table [MEDECINS] au temps T1. La ligne a la version V1 ;
  • un processus P2 lit la même ligne L de la table [MEDECINS] au temps T2. La ligne a la version V1 parce que le processus P1 n'a pas encore validé sa modification ;
  • le processus P1 valide sa modification de la ligne L. La version de la ligne L passe alors à V2=V1+1 ;
  • le processus P2 valide sa modification de la ligne L. L'ORM lance alors une exception car le processus P2 a une version V1 de la ligne L différente de la version V2 trouvée en base.

On appelle cela la gestion optimiste des accès concurrents. Avec EF 5, un champ jouant ce rôle doit avoir l'un des deux attributs [Timestamp] ou [ConcurrencyCheck]. SQL Server a un type [timestamp]. Une colonne ayant ce type voit sa valeur automatiquement générée par SQL Server à toute insertion / modification d'une ligne. Une telle colonne peut alors servir à gérer la concurrence d'accès. Pour reprendre l'exemple précédent, le processus P2 trouvera un timestamp différent de celui qu'il a lu, car entre-temps la modification faite par le processus P1 l'aura modifié.

Notre entité [Medecin] évolue comme suit :

[Medecin]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
  {
    // data
    [Key]
    [Column("ID")]
    public int Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
  }
}
  • lignes 26-28 : la nouvelle colonne avec l'attribut [Timestamp] de la ligne 27. Le type du champ doit être byte[] (ligne 28). Le nom du champ peut être quelconque. On ne lui met pas l'attribut [Required] car ce n'est pas l'application qui fournira cette valeur mais le SGBD lui-même.

Si on exécute le projet avec cette nouvelle entité, la base évolue comme suit :

Image non disponible

Il nous reste à régler un dernier point. Le contexte de persistance " sait " qu'une entité doit faire l'objet d'une insertion en base parce qu'alors elle a sa clé primaire égale à null. C'est l'insertion en base qui va donner une valeur à la clé primaire. Ici, le type int donné à la clé primaire [Id] ne convient pas parce que ce type n'accepte pas la valeur null. On lui donne alors le type int? qui accepte les valeurs int plus le pointeur null. L'entité [Medecin] utilisée sera donc la suivante :

[Medecin]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
public class Medecin
  {
    // data
    [Key]
    [Column("ID")]
    public virtual int? Id { get; set; }
    ...

Il nous reste à voir comment représenter dans une entité le concept de clé étrangère entre tables.

III-D-2. L'entité [Creneau]

La table [CRENEAUX] liste les créneaux horaires où les RV sont possibles :

Image non disponible
Image non disponible
  • ID : n° identifiant le créneau horaire - clé primaire de la table
  • VERSION : n° identifiant la version de la ligne dans la table. Ce nombre est incrémenté de 1 à chaque fois qu'une modification est apportée à la ligne.
  • ID_MEDECIN : n° identifiant le médecin auquel appartient ce créneau - clé étrangère sur la colonne MEDECINS(ID).
  • HDEBUT : heure début créneau
  • MDEBUT : minutes début créneau
  • HFIN : heure fin créneau
  • MFIN : minutes fin créneau

La seconde ligne de la table [CRENEAUX] (cf [1] ci-dessus) indique, par exemple, que le créneau n° 2 commence à 8 h 20 et se termine à 8 h 40 et appartient au médecin n° 1 (Mme Marie PELISSIER).

Avec ce que nous savons, nous pouvons définir l'entité [Creneau] comme suit dans [Entites.cs] :

[Creneau]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
[Table("CRENEAUX", Schema = "dbo")]
  public class Creneau
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [Column("HDEBUT")]
    public int Hdebut { get; set; }
    [Required]
    [Column("MDEBUT")]
    public int Mdebut { get; set; }
    [Required]
    [Column("HFIN")]
    public int Hfin { get; set; }
    [Required]
    [Column("MFIN")]
    public int Mfin { get; set; }
    [Required]
    public virtual Medecin Medecin { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

La seule nouveauté réside aux lignes 20-21. Le fait que la table [CRENEAUX] ait une clé étrangère sur la table [MEDECINS] est reflété dans l'entité [Creneau] par la présence d'une référence sur l'entité [Medecin], ligne 21. Le nom du champ importe peu, seul le type est important. La propriété doit être déclarée virtuelle avec le mot clé virtual . En effet, EF est amené à redéfinir toutes les propriétés dites navigationnelles , c'-a-d celles qui correspondent à une clé étrangère et qui permettent de passer d'une table à l'autre.

Pour tester la nouvelle entité, il nous faut faire quelques modifications dans [Context.cs] :

[Context.cs]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
using System.Data.Entity;
using RdvMedecins.Entites;

namespace RdvMedecins.Models
{

  // le contexte
  public class RdvMedecinsContext : DbContext
  {
    // les entités
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
  }

  // initialisation de la base
  public class RdvMedecinsInitializer :  DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
  }
}

La ligne 12 reflète le fait que le contexte a une entité de plus à gérer. Lorsque nous exécutons le projet, nous obtenons la nouvelle base suivante :

Image non disponible

La table [CRENEAUX] a bien été créée et la nouveauté est la présence d'une clé étrangère [1] et [2]. Son nom a été généré à partir du nom du champ correspondant dans l'entité (Medecin) suffixé par " _Id ". Pour connaître les propriétés de cette clé étrangère, on essaie de la modifier [3].

Image non disponible

La copie d'écran ci-dessus montre que [Medecin_Id] est clé étrangère de la table [CRENEAUX] et qu'elle référence la clé primaire [ID] de la table [MEDECINS].

Si on crée les entités pour une base existante, la colonne clé étrangère ne s'appellera pas forcément [Medecin_Id]. Pour les autres colonnes, on avait vu que l'annotation [Column] réglait ce problème. Bizarrement c'est plus compliqué pour une clé étrangère. Il faut procéder de la façon suivante :

Creneau
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
public class Creneau
  {
    // data
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
}
  • lignes 5-7 : on crée un champ du type de la clé étrangère (int). A l'aide de l'attribut [Column], on précise le nom de la colonne qui sera clé étrangère dans la table associée à l'entité ;
  • ligne 9 : on ajoute l'annotation [ ForeignKey ] au champ de type [Medecin]. L'argument de cette annotation est le nom du champ (pas de la colonne) qui est associé à la colonne clé étrangère de la table.

L'exécution du projet crée cette fois-ci la table suivante :

Image non disponible

Ci-dessus, la colonne clé étrangère porte bien le nom qu'on lui a donné. Il faut noter que les champs :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }

n'ont donné naissance qu'à une seule colonne, la colonne [MEDECIN_ID]. Néanmoins, la présence du champ [MedecinId] est importante. Lors de la lecture d'une ligne de la table [CRENEAUX], elle recevra la valeur de la colonne [MEDECIN_ID], c'-à-d. la valeur de la clé étrangère sur la table [MEDECINS]. Cela est souvent utile.

Le champ [Medecin] ci-dessus reflète la relation plusieurs à un qui lie l'entité [Creneau] à l'entité [Medecin]. Plusieurs objets [Creneau] sont reliés à un même [Medecin]. La relation inverse, un objet [Medecin] est associé à plusieurs objets [Creneau] peut être modélisée par un champ supplémentaire dans l'entité [Medecin] :

[Medecin]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
public class Medecin
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    ...
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

Ligne 8, on a rajouté le champ [Creneaux] qui est une collection d'objets [Creneau]. Ce champ nous donnera accès à tous les créneaux horaires du médecin.

Lorsqu'on exécute de nouveau le projet, on constate que la table [MEDECINS] n'a pas bougé :

Image non disponible

Aucune colonne n'a été rajoutée. La relation de clé étrangère qui existe entre la table [CRENEAUX] et la table [MEDECINS] est suffisante pour que EF sache générer les champs liés à celle-ci :

[Medecin]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
public class Medecin
  {
    ...
    public ICollection<Creneau> Creneaux { get; set; }
    ...
  }

  public class Creneau
  {
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
  }

Nous savons l'essentiel. Nous pouvons terminer avec la création des deux autres entités.

III-D-3. Les entités [Client] et [Rv]

Avec ce que nous avons appris, nous pouvons écrire les entités [Client] et [Rv]. L'entité [Client] contient des informations sur les clients gérés par l'application [RdvMedecins].

Image non disponible
  • ID : n° identifiant le client - clé primaire de la table
  • VERSION : n° identifiant la version de la ligne dans la table. Ce nombre est incrémenté de 1 à chaque fois qu'une modification est apportée à la ligne.
  • NOM : le nom du client
  • PRENOM : son prénom
  • TITRE : son titre (Mlle, Mme, Mr)

L'entité [Client] pourrait être la suivante :

[Client]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
  [Table("CLIENTS", Schema = "dbo")]
  public class Client
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // les Rvs du client
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

La classe [Client] est quasi identique à la classe [Medecin]. On pourrait les faire dériver d'une même classe parent. La nouveauté est ligne 21. Elle reflète le fait qu'un client peut avoir plusieurs rendez-vous et dérive de la présence d'une clé étrangère de la table [RVS] vers la table [CLIENTS].

L'entité [Rv] représente un rendez-vous :

Image non disponible
  • ID : n° identifiant le RV de façon unique - clé primaire
  • JOUR : jour du RV
  • ID_CRENEAU : créneau horaire du RV - clé étrangère sur la colonne [ID] de la table [CRENEAUX] - fixe à la fois le créneau horaire et le médecin concerné.
  • ID_CLIENT : n° du client pour qui est faite la réservation - clé étrangère sur la colonne [ID] de la table [CLIENTS]

L'entité [Rv] pourrait être la suivante :

[Rv]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
[Table("MEDECINS", Schema = "dbo")]
  public class Rv
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [Column("JOUR")]
    public DateTime Jour { get; set; }
    [Column("CLIENT_ID")]
    public int ClientId { get; set; }
    [ForeignKey("ClientId")]
    [Required]
    public virtual Client Client { get; set; }
    [Column("CRENEAU_ID")]
    public int CreneauId { get; set; }
    [ForeignKey("CreneauId")]
    [Required]
    public virtual Creneau Creneau { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}
  • lignes 5-7 : clé primaire ;
  • lignes 8-10 : date du rendez-vous ;
  • lignes 11-12 : la clé étrangère de la table [RVS] vers la table [CLIENTS] ;
  • lignes 13-15 : le client qui a rendez-vous ;
  • lignes 16-17 : la clé étrangère de la table [RVS] vers la table [CRENEAUX] ;
  • lignes 18-20 : le créneau horaire du rendez-vous ;
  • lignes 21-23 : le champ de gestion des accès concurrents.

Ligne 17, on voit une relation plusieurs à un : à un créneau horaire peuvent correspondre plusieurs rendez-vous (pas le même jour). La relation inverse peut être reflétée dans l'entité [Creneau] :

  • public class Creneau
  • {
  • // les Rvs du créneau
  • public ICollection < Rv > Rvs { get ; set ; }
  • }

Ligne 4, la collection des rendez-vous pris sur ce créneau horaire.

Lorsqu'on exécute le projet, la base générée est la suivante :

Image non disponible

Les tables [MEDECINS] et [CRENEAUX] n'ont pas changé. Les tables [CLIENTS] et [RVS] sont les suivantes :

Image non disponible

C'est ce qui était attendu. Il nous reste quelques détails à régler :

  • gérer le nom de la base. Ici il a été généré par EF ;
  • remplir la base avec des données.

III-D-4. Fixer le nom de la base

Pour fixer le nom de la base générée par EF, nous utiliserons une chaîne de connexion définie dans [App.config]. Ce fichier de configuration évolue comme suit :

[App.config]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>

  <!-- chaîne de connexion sur la base -->
  <connectionStrings>
    <add name="RdvMedecinsContext"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- le factory provider -->
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider"
       invariant="System.Data.SqlClient"
       description=".Net Framework Data Provider for SqlServer"
       type="System.Data.SqlClient.SqlClientFactory, System.Data,
     Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    />
    </DbProviderFactories>
  </system.data>

</configuration>
  • lignes 15-19 : la chaîne de connexion à la base ;
  • ligne 16 : l'attribut [name] reprend le nom de la classe [RdvMedecinsContext] utilisé pour le contexte de persistance. Il est important de s'en souvenir. Cette contrainte peut être contournée dans le constructeur du contexte :
 
Sélectionnez
1.
2.
3.
4.
5.
// constructeur
    public RdvMedecinsContext()
      : base("monContexte")
    {
    }

Dans ce cas, on pourra avoir name= "monContexte " . C'est ce que nous aurons dans la suite du document.

  • ligne 17 : la chaîne de connexion. [Data Source] : le nom du serveur sur lequel se trouve le SGBD, [Initial Catalog] : le nom de la base de données, donc ici [rdvmedecins-ef], [User Id] : le propriétaire de la connexion, [Password] : son mot de passe. Le lecteur adaptera cette chaîne à son environnement ;
  • lignes 21-29 : définissent un [DbProviderFactory]. Je ne sais pas ce que c'est. Si j'en crois le nom, ce pourrait être une classe permettant de générer la couche [ADO.NET] qui sépare EF du SGBD :
Image non disponible

En fait, ces lignes sont inutiles pour SQL Server mais j'ai du les ajouter pour les autres SGBD. Aussi c'est pour mémoire que je les mets là. Elles ne gênent pas. Le seul point important est la version de la ligne 27. C'est celle de la DLL [System.Data] présente dans les références du projet :

Image non disponible

Voilà. Nous sommes prêts. Nous exécutons le projet et nous obtenons la base [rdvmedecins-ef] suivante :

Image non disponible

Ce sera notre base définitive. Il nous reste à mettre des données dedans.

III-D-5. Remplissage de la base

La classe d'initialisation de la base peut être utilisée pour y insérer des données :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
    // initialisation de la base
    public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
    {
      protected override void Seed(RdvMedecinsContext context)
      {
        base.Seed(context);
        // on initialise la base
        // les clients
        Client[] clients ={
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Mlle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
        foreach (Client client in clients)
        {
          context.Clients.Add(client);
        }
        // les médecins
        Medecin[] medecins ={
        new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
        new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
        new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
        new Medecin { Titre = "Mlle", Nom = "Jacquemot", Prenom = "Justine" }
     };
        foreach (Medecin medecin in medecins)
        {
          context.Medecins.Add(medecin);
        }
        // les créneaux horaires
        Creneau[] creneaux ={
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
      };
        foreach (Creneau creneau in creneaux)
        {
          context.Creneaux.Add(creneau);
        }
        // les Rdv
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
      }

    }
  }
  • ligne 6 : l'initialisation se passe dans la méthode [Seed]. Celle-ci existe dans la classe mère. Elle est ici redéfinie. L'argument est le contexte de persistance [RdvMedecinsContext] de l'application ;
  • ligne 8 : l'argument est passé à la classe mère ; il est probable que celle-ci ouvre le contexte de persistance qu'on lui a passé car cette ouverture n'est plus nécessaire par la suite ;
  • lignes 11-16 : création de 4 clients ;
  • lignes 17-20 : ceux-ci sont ajoutés au contexte de persistance, plus exactement aux médecins de celui-ci. On notera la méthode [Add] qui permet cela. Il faut se rappeler ici la définition du contexte :
[RdvMedecinsContext]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
public class RdvMedecinsContext : DbContext
  {
    // les entités
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
    public DbSet<Client> Clients { get; set; }
public DbSet<Rv> Rvs { get; set; }
...

On dit aussi que les clients ont été attachés au contexte, c'-à-d. qu'ils sont désormais gérés par EF. Avant ils en étaient détachés . Ils existaient en tant qu'objets mais n'étaient pas gérés par EF ;

  • lignes 21-27 : création de 4 médecins ;
  • lignes 28-31 : on les met dans le contexte de persistance ;
  • lignes 33-70 : création de créneaux horaires. Lignes 34-57, pour le médecin medecins[0] , lignes 58-69, pour le médecin medecins[1] . Les autres médecins sont sans créneaux horaires ;
  • lignes 71-74 : on met ces créneaux dans le contexte de persistance ;
  • ligne 76 : création d'un rendez-vous pour le 1 er client avec le 1 er créneau horaire et sa mise dans le contexte de persistance.

Lorsqu'on exécute le projet, on obtient la base suivante :

Image non disponible

Ci-dessus, on voit la table [CLIENTS] remplie.

III-D-6. Modification des entités

Actuellement, les classes [Medecin] et [Client] sont quasi identiques. En fait si on enlève les champs ajoutés pour la gestion de la persistance avec EF 5, elles sont identiques. Nous allons les faire dériver d'une classe [Personne]. Ces deux entités deviennent alors les suivantes :

[Personne]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
// une personne
  public abstract class Personne
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // signature
    public override string ToString()
    {
      return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // signature courte
    public string ShortIdentity()
    {
      ...
    }

    // utilitaire
    private string dump(byte[] timestamp)
    {
      ...
    }

  }

  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin : Personne
  {
    // les créneaux horaires du médecin
    public ICollection<Creneau> Creneaux { get; set; }
    // signature
    public override string ToString()
    {
      return String.Format("Medecin {0}", base.ToString());
    }
  }

[Table("CLIENTS", Schema = "dbo")]
    public class Client : Personne
  {
    // les Rvs du client
    public ICollection<Rv> Rvs { get; set; }
    // signature
    public override string ToString()
    {
      return String.Format("Client {0}", base.ToString());
    }
  }

Lorsqu'on exécute le projet, on obtient la même base. EF 5 a mappé les classes les plus basses de l'héritage chacune dans une table. En fait, EF 5 a différentes stratégies de génération de tables pour représenter l'héritage d'entités. Nous ne les présenterons pas ici. On pourra lire par exemple " Entity Framework Code First Inheritance : Table Per Hierarchy and Table Per Type ", à l'URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].

Nous utiliserons désormais cette version des entités.

III-D-7. Ajouter des contraintes à la base

Il nous reste un détail à régler. La table [RVS] des rendez-vous est la suivante :

Image non disponible

Cette table doit avoir une contrainte d'unicité : pour un jour donné, un créneau horaire d'un médecin ne peut être réservé qu'une fois pour un rendez-vous. En termes de table, cela signifie que le couple (JOUR,CRENEAU_ID) doit être unique. Je ne sais pas si cette contrainte peut être exprimée directement dans le code, soit sur les entités soit sur le contexte. C'est probable mais je n'ai pas vérifié. Nous allons prendre une autre démarche. Nous allons utiliser un client d'administration de SQL Server pour ajouter cette contrainte.

Avec " SQL Server Management Studio ", je n'ai pas trouvé de méthode simple pour ajouter cette contrainte hormis exécuter l'ordre SQL qui la crée :

Image non disponible
  • en [1] on crée une requête SQL pour la base [rdvmedecins-ef] ;
  • en [2], la requête SQL qui crée la contrainte d'unicité ;
  • en [3], l'exécution de cette requête a créé un nouvel index dans la table [RVS].

Il existe d'autres outils d'administration de SQL Server. Nous allons utiliser ici l'outil EMS SQL Manager for SQL Server Freeware [http://www.sqlmanager.net/fr/products/mssql/manager/download]. Une fois installé, nous le lançons :

Image non disponible
  • en [1], on enregistre une base de données ;
  • en [2], on se connecte au serveur (local)  ;
  • en [3], avec une authentification SQL Server ;
  • en [4], sous l'identité sa  ;
  • en [5], et le mot de passe sqlserver2012  ;
  • en [6], on passe à l'étape suivante ;
Image non disponible
  • en [7], on choisit la base [rdvmedecins-ef] ;
  • en [8], on termine l'assistant ;
  • en [9], la base apparaît dans l'arborescence des bases. On s'y connecte [10] ;
  • en [11], on est connecté.

" SQL Manager Lite for SQL Server " permet de créer la contrainte d'unicité sur la table [RVS].

Image non disponible
  • en [1], on voit la contrainte d'unicité que nous avons créée précédemment ;
  • en [2], on la supprime ;
  • en [3], l'indice correspondant à cette contrainte d'unicité a disparu.

On recrée la contrainte supprimée :

Image non disponible
  • en [1], on crée un nouvel index pour la table [RVS] ;
  • en [2], on lui donne un nom ;
  • en [3], c'est une contrainte d'unicité ;
  • en [4], sur les colonnes JOUR et CRENEAU_ID ;

L'onglet DDL nous donne le code SQL qui va être exécuté :

Image non disponible
  • en [6], on compile l'ordre SQL ;
Image non disponible
  • en [7], on confirme ;
  • en [8], le nouvel indice est apparu.

L'interface offerte par " SQL Manager Lite for SQL Server " est analogue à celle offerte par " SQL Server Management Studio ". On peut trouver des interfaces analogues pour les SGBD Oracle, PostgreSQL, Firebird, MySQL. Aussi continuerons-nous désormais avec cette famille d'outils d'administration de SGBD.

Pour avoir accès aux informations d'une table, il suffit de double-cliquer dessus :

Image non disponible

Les informations sur la table sélectionnée sont disponibles dans des onglets. Ci-dessus, on voit l'onglet [Fields] de la table [CLIENTS]. L'onglet [Data] affiche le contenu de la table :

Image non disponible

III-D-8. La base définitive

Nous avons notre base définitive. Nous exportons son script SQL afin de pouvoir la régénérer si besoin est.

Image non disponible
  • en [1], début de l'assistant ;
  • en [2], le serveur ;
  • en [3], la base de données qui va être exportée ;
Image non disponible
  • en [4], précisez le nom du fichier où sera enregistré le script SQL ;
  • en [5], précisez son encodage ;
  • en [6], précisez ce que vous voulez extraire (tables, contraintes, données) ;
Image non disponible
  • en [7], vous pouvez affiner le script qui va être généré ;
  • en [8], terminez l'assistant.

Le script a été généré et chargé dans l'éditeur de script. Vous pouvez consulter le code SQL généré. Nous allons reconstruire la base de données à partir de ce script.

Image non disponible
  • en [1], on supprime la base ;
  • en [2] et [3], on la recrée ;
Image non disponible
  • en [4], on s'authentifie ;
  • en [5], on exécute le script SQL de création de la base ;
Image non disponible
  • en [6], on l'enregistre dans " SQL Manager " ;
  • en [7], on se connecte à la base qui vient d'être créée ;
Image non disponible
  • en [8], la base n'a pour l'instant pas de tables ;
  • en [9a], on ouvre un éditeur de script SQL ;
Image non disponible
  • en [9b], on ouvre le script SQL créé précédemment ;
  • en [10], on l'exécute ;
Image non disponible
  • en [11], les tables ont été créées ;
  • en [12], elles sont remplies ;
Image non disponible
  • en [14], nous retrouvons la contrainte d'unicité que nous avions créée pour la table [RVS].

Nous allons désormais travailler avec cette base existante. Si elle est détruite ou détériorée, nous savons la régénérer.

III-E. Exploitation de la base avec Entity Framework

Nous allons :

  • ajouter, supprimer, modifier des éléments de la base ;
  • requêter la base avec LINQ to Entities ;
  • gérer les accès concurrents à un même élément de la base ;
  • comprendre les notions de Lazy Loading / Eager Loading ;
  • découvrir que la mise à jour de la base par le contexte de persistance se fait dans une transaction.

III-E-1. Suppression d'éléments du contexte de persistance

Nous avons une base remplie. Nous allons la vider. Nous créons une nouvelle classe [Erase.cs] dans le projet actuel [1] :

Image non disponible

La classe [Erase] est la suivante :

[Erase]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Erase
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // on vide la base actuelle
        // les clients
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        // les médecins
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // on sauve le contexte de persistance
        context.SaveChanges();
      }
    }
  }
}
  • ligne 9 : les opérations sur un contexte de persistance se font toujours dans une clause [using]. Cela assure qu'à la sortie du [using], le contexte a été fermé ;
  • ligne 13 : on parcourt le contexte des clients [context.Clients]. Tous les clients de la base vont être mis dans le contexte de persistance ;
  • ligne 15 : pour chacun d'eux on fait l'opération [Remove] qui les supprime du contexte. En fait, ils sont toujours dans le contexte mais dans un état " supprimé " ;
  • lignes 18-21 : on fait la même chose pour les médecins ;
  • ligne 23 : on sauvegarde le contexte de persistance en base.

Lors de la sauvegarde du contexte en base, les entités du contexte qui :

  • ont une clé primaire null font l'objet d'une opération SQL INSERT ;
  • sont dans un état " supprimé " font l'objet d'une opération SQL DELETE ;
  • sont dans un état " modifié " font l'objet d'une opération SQL UPDATE ;

Comme nous le constaterons ultérieurement, ces opérations SQL se font à l'intérieur d'une transaction. Si l'une d'elles échoue, tout ce qui a été fait précédemment est défait.

Faisons du programme [Erase] le nouvel objet de démarrage du projet [1] puis exécutons le projet.

Image non disponible

Vérifions la base. On constatera que toutes les tables sont vides [2]. C'est étonnant, car nous avions demandé simplement la suppression des médecins et des clients. C'est par le jeu des clés étrangères que les autres tables ont été vidées en cascade.

La définition de la clé étrangère de la table [CRENEAUX] vers la table [MEDECINS] a été définie comme suit par le provider d'EF 5 :

Image non disponible
  • en [1], on sélectionne la table [CRENEAUX] ;
  • en [2], on sélectionne l'onglet des clés étrangères ;
  • en [3], on édite l'unique clé étrangère ;
Image non disponible
  • en [4], dans l'onglet DDL , la définition SQL de la contrainte de clé étrangère ;
  • en [5], la clause ON DELETE CASCADE fait que la suppression d'un médecin entraîne la suppression des créneaux qui lui sont associés.

Les contraintes de clé étrangères de la table [RVS] sont définies de façon analogue :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
ALTER TABLE [dbo].[RVS]
ADD CONSTRAINT [FK_dbo.RVS_dbo.CLIENTS_CLIENT_ID] FOREIGN KEY ([CLIENT_ID]) 
  REFERENCES [dbo].[CLIENTS] ([ID]) 
  ON UPDATE NO ACTION
  ON DELETE CASCADE
GO
  • lignes 1-6 : supprimer un client supprimera là également les rendez-vous qui lui sont associés ;
 
Sélectionnez
1.
2.
3.
4.
5.
6.
ALTER TABLE [dbo].[RVS]
ADD CONSTRAINT [FK_dbo.RVS_dbo.CRENEAUX_CRENEAU_ID] FOREIGN KEY ([CRENEAU_ID]) 
  REFERENCES [dbo].[CRENEAUX] ([ID]) 
  ON UPDATE NO ACTION
  ON DELETE CASCADE
GO
  • lignes 1-6 : supprimer un créneau supprimera également tous les rendez-vous qui lui sont associés.

III-E-2. Ajout d'éléments au contexte de persistance

Maintenant que nous avons vidé la base, nous allons la remplir de nouveau. Nous ajoutons au projet le programme [Fill.cs] [1].

Image non disponible

Le programme [Fill.cs] est le suivant :

[Fill.cs]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Fill
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // on vide la base actuelle
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // on la réinitialise
        // les clients
        Client[] clients ={
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
        foreach (Client client in clients)
        {
          context.Clients.Add(client);
        }
        // les médecins
        Medecin[] medecins ={
        new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
        new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
        new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
        new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
     };
        foreach (Medecin medecin in medecins)
        {
          context.Medecins.Add(medecin);
        }
        // les créneaux horaires
        Creneau[] creneaux ={
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
      };
        foreach (Creneau creneau in creneaux)
        {
          context.Creneaux.Add(creneau);
        }
        // les Rdv
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // on sauve le contexte de persistance
        context.SaveChanges();
      }
    }
  }
}
  • ligne 10 : on ouvre le contexte de persistance ;
  • lignes 13-20 : les lignes des tables [CLIENTS] et [MEDECINS] sont mises dans le contexte puis supprimées de celui-ci. Nous venons de voir que cela vidait totalement la base ;
  • lignes 22-88 : des éléments sont ajoutés au contexte de persistance. Ils ont tous leur clé primaire à null . Ils seront donc insérés dans la base ;
  • ligne 90 : les changements opérés sur le contexte sont synchronisés avec la base. Celle-ci va faire l'objet d'une série d'opérations SQL DELETE suivie d'une série d'opérations SQL INSERT ;

On fait du programme [Fill], le nouvel objet de démarrage du projet [1] puis on exécute ce dernier.

Image non disponible

On constate en [2] que les tables ont été remplies.

III-E-3. Affichage du contenu de la base

Nous allons maintenant afficher le contenu de la base à l'aide de requêtes LINQ to Entity . LINQ ( L anguage IN tegrated Q uery) est apparu avec le framework .NET 3.5 en 2007. Il apparaît comme une extension des langages .NET, c.a.d qu'il est intégré au langage et sa syntaxe est vérifiée par le compilateur. Il permet de requêter différentes collections avec une syntaxe présentant des similitudes avec le langage SQL (Structured Query Language) de requêtage des bases de données. Il existe différentes moutures de LINQ :

  • LINQ to Object, pour requêter des collections en mémoire ;
  • LINQ to XML, pour requêter du XML ;
  • LINQ to Entity, pour requêter des bases de données ;

Pour exister, LINQ s'appuie sur de nombreuses extensions faites aux langages .NET. Celles-ci peuvent être utilisées en-dehors de LINQ. Nous n'allons pas les présenter mais simplement donner deux références où le lecteur trouvera une description approfondie de LINQ :

  • LINQ in Action , Fabrice Marguerie, Steve Eichert, Jim Wooley aux éditions Manning ;
  • LINQ pocket reference , Joseph et Ben Albahari aux éditions O'Reilly.

J'ai lu le premier et l'ai trouvé excellent. Je n'ai pas lu le second mais ai lu des mêmes auteurs "  C# 3.0 in a nutshell  " à la sortie de LINQ. J'ai trouvé ce livre très au-dessus de la moyenne des livres que j'ai l'habitude de lire. Il semble que les autres livres de ces deux auteurs soient du même niveau. Nous allons par ailleurs utiliser LINQPad , un outil d'apprentissage de LINQ écrit par Joseph Albahari.

Nous allons afficher les entités présentes dans la base. Pour cela, nous ajoutons à leurs classes deux méthodes d'affichage. Commençons par l'entité [Medecin] :

[Medecin]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
// un médecin
  public class Medecin
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // les créneaux horaires du médecin
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // signature
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // signature courte
    public string ShortIdentity()
    {
      return ToString();
    }

    // utilitaire
    private string dump(byte[] timestamp){
      string str = "";
      foreach (byte b in timestamp)
      {
        str += b;
      }
      return str;
    }
  }
  • lignes 27-30 : la méthode ToString de la classe. On notera qu'elle n'affiche pas la collection de la ligne 21 ;
  • lignes 32-37 : la méthode ShortIdentity qui fait la même chose.

Il nous faut ici expliquer les notions de Lazy et Eager Loading pour mesurer l'impact des deux méthodes précédentes. Nous avons vu qu'une entité pouvait avoir des dépendances sur une autre entité. Elles sont de deux natures :

  • de un à plusieurs , comme ci-dessus où un médecin est relié à plusieurs créneaux horaires ;
  • de plusieurs à un , comme dans l'entité [Creneau] ci-dessous où un plusieurs créneaux sont reliés au même médecin ;
[Creneau]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
public class Creneau
  {
    // data
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
  }

Lorsque les dépendances sont chargées en même temps que les entités auxquelles elles sont attachées, on parle d' Eager Loading . Sinon, on parle de Lazy Loading  : les dépendances ne sont chargées que lorsqu'elles sont référencées la première fois. Par défaut, EF 5 utilise le Lazy Loading  : les dépendances ne sont pas chargées en même temps que l'entité.

Voyons notre méthode [ToString] ci-dessus :

[ToString]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
// les créneaux horaires du médecin
    public ICollection<Creneau> Creneaux { get; set; }
    
    // signature
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // signature courte
    public string ShortIdentity()
    {
      return ToString();
}

La méthode [ToString] n'affiche pas la dépendance [Creneaux] de la ligne 2. Si elle l'avait fait, elle aurait alors forcé le chargement de tous les créneaux du médecin avant son exécution. C'est pour éviter ce chargement coûteux que la dépendance n'a pas été incluse dans la signature de l'entité. De façon générale, nous allons inclure deux signatures dans chaque entité :

  • une méthode ToString qui affichera l'entité et ses éventuelles dépendances plusieurs à un . Comme il vient d'être expliqué cela provoquera le chargement de la dépendance ;
  • une méthode ShortIdentity qui ne référencera aucune dépendance. Il n'y aura donc aucun chargement de dépendance ;

Les méthodes d'affichage des autres entités seront les suivantes :

L'entité [Client] :

[Client]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
public class Client
  {
    // data
    ...
    // les Rvs du client
    public ICollection<Rv> Rvs { get; set; }
    
    // signature
    public override string ToString()
    {
      return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // signature courte
    public string ShortIdentity()
    {
      return ToString();
    }

}
  • lignes 9-12 : la méthode [ToString] n'affiche pas la dépendance de la ligne 6 ;

L'entité [Creneau] :

[Creneau]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
public class Creneau
  {
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    // les Rvs du créneau
    public ICollection<Rv> Rvs { get; set; }
    
    // signature
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
    // signature courte
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
    }
  }
  • ligne 16 : la méthode [ToString] référence la dépendance de la ligne 9. Cela va forcer son chargement ;
  • ligne 11 : la dépendance [Rvs] n'est pas référencée. Elle ne sera pas chargée ;
  • lignes 21-22 : la méthode [ShortIdentity] ne référence plus la référence [Medecin] de la ligne 9. Celle-ci ne sera donc pas chargée.

L'entité [Rv] :

[Rv]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
public class Rv
  {
    // data
    ...
    [Column("CLIENT_ID")]
    public int ClientId { get; set; }
    [ForeignKey("ClientId")]
    [Required]
    public virtual Client Client { get; set; }
    [Column("CRENEAU_ID")]
    public int CreneauId { get; set; }
    [ForeignKey("CreneauId")]
    [Required]
    public virtual Creneau Creneau { get; set; }

    // signature
    public override string ToString()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
    }
    // signature courte
    public string ShortIdentity()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, ClientId, CreneauId, dump(Timestamp));
    }

  }
  • lignes 17-20 : la méthode [ToString] référence les dépendances des lignes 9 et 14. Cela va forcer leur chargement ;
  • lignes 17-20 : la méthode [ShortIdentity] évite cela et donc les dépendances ne seront pas chargées.

En conclusion, on prêtera attention aux méthodes [ToString] des entités. Si on n'y prête pas attention, afficher une table peut charger la moitié de la base si la table a de nombreuses dépendances.

Ceci expliqué, on écrit le nouveau code [Dump.cs] suivant :

[Dump.cs]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;

namespace RdvMedecins_01
{
  class Dump
  {
    static void Main(string[] args)
    {
      // dump de la base
      using (var context = new RdvMedecinsContext())
      {
        // les clients
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // les médecins
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // les créneaux horaires
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // les Rdvs
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}

Nous allons expliquer les lignes 17-21 qui affiche les entités [Client]. L'explication donnée vaudra pour les autres entités.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
// les clients
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
}
  • ligne 3 : le mot clé var a été introduit avec C# 3.0. Il permet d'éviter d'indiquer le type précis d'une variable. Le compilateur déduit alors celui-ci du type de l'expression affectée à la variable ;
  • ligne 3 : l'expression affectée à la variable clients est une requête LINQ to Entity . On y reconnaît des mots clés du langage SQL portés dans LINQ. La syntaxe utilisée ici est la suivante :

from variable in DbSet select variable

Une syntaxe plus générale de LINQ est

from variable in collection select variable

La collection va être parcourue et pour chaque élément de celle-ci, la variable va être évaluée. Ceci n'est fait que lorsque la variable [clients] de la ligne 3 va être énumérée par le for / each des lignes 4-7. Tant que ceci n'est pas fait, la variable [clients] n'est qu'une requête non évaluée ;

  • ligne 4 : la requête [clients] est énumérée. Cela va forcer l'évaluation de la requête. Les lignes de la table [CLIENTS] vont être amenées tour à tour dans le contexte de persistance ;
  • ligne 6 : la méthode [ToString] de l'entité [Client] est utilisée pour l'affichage. Il n'y a aucun chargement de dépendances ;

Passons aux lignes suivantes du code :

  • lignes 24-28 : les lignes de la table [MEDECINS] sont amenées dans le contexte de persistance et affichées. Il n'y a aucun chargement de dépendances ;
  • lignes 31-35 : les lignes de la table [CRENEAUX] sont amenées dans le contexte de persistance et affichées. Nous avons vu que la méthode [ToString] de cette entité affichait la dépendance [Medecin]. Or celle-ci est déjà chargée. Il n'y aura donc pas de nouveau chargement ;
  • lignes 38-42 : les lignes de la table [RVS] sont amenées dans le contexte de persistance et affichées. Nous avons vu que la méthode [ToString] de cette entité affichait les dépendances [Client] et [Creneau]. Or celles-ci sont déjà chargées. Il n'y aura donc pas de nouveaux chargements.

On notera que l'ordre d'affichage n'est pas neutre. Si on avait voulu afficher d'abord les entités [Rv], la méthode [ToString] de celle-ci aurait provoqué le chargement des entités [Client] et [Creneau] liées à ces rendez-vous. Les autres n'auraient pas été chargées. Elles l'auraient été plus tard dans un autre affichage. Cela a un impact sur les performances. Le code précédent a besoin de quatre ordres SQL pour faire afficher toutes les entités. Supposons maintenant qu'on exploite d'abord la tables [RVS] des rendez-vous. Une première requête SQL est nécessaire pour la table [RVS]. Ensuite, la méthode [ToString] de l'entité [Rv] va provoquer le chargement éventuel des entités [Client] et [Creneau] associées. Il faut une requête SQL pour chacune. En supposant qu'il y ait N2 clients et N3 créneaux et que toutes ces entités soient référencées dans la table [RVS], l'affichage de celle-ci nécessitera 1+N2+N3 requêtes SQL. Donc, on n'est moins performant que dans la version étudiée. Pour afficher la table [RVS] avec ses dépendances, une jointure entre tables serait nécessaire. Il est possible de la réaliser avec LINQ. Nous y reviendrons sur un exemple. Pour l'instant, nous nous rappellerons que nous devons prêter attention aux requêtes SQL sous-jacentes à notre code LINQ.

On paramètre le projet pour exécuter ce nouveau code [1] et [2] puis on l'exécute :

Image non disponible

L'affichage console est le suivant :

[Console]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
Clients--------------------------------------
Client[9,Mr,Jules,Martin,000000844]
Client[10,Mme,Christine,German,000000845]
Client[11,Mr,Jules,Jacquard,000000846]
Client[12,Melle,Brigitte,Bistrou,000000847]
Médecins--------------------------------------
Medecin[9,Mme,Marie,Pelissier,000000848]
Medecin[10,Mr,Jacques,Bromard,000000873]
Medecin[11,Mr,Philippe,Jandot,000000886]
Medecin[12,Melle,Justine,Jacquemot,000000887]
Créneaux horaires--------------------------------------
Creneau[73,8,0,8,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000849]
Creneau[74,8,20,8,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000850]
Creneau[75,8,40,9,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000851]
Creneau[76,9,0,9,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000852]
Creneau[77,9,20,9,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000853]
Creneau[78,9,40,10,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000854]
Creneau[79,10,0,10,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000855]
Creneau[80,10,20,10,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000856]
Creneau[81,10,40,11,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000857]
Creneau[82,11,0,11,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000858]
Creneau[83,11,20,11,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000859]
Creneau[84,11,40,12,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000860]
Creneau[85,14,0,14,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000861]
Creneau[86,14,20,14,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000862]
Creneau[87,14,40,15,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000863]
Creneau[88,15,0,15,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000864]
Creneau[89,15,20,15,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000865]
Creneau[90,15,40,16,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000866]
Creneau[91,16,0,16,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000867]
Creneau[92,16,20,16,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000868]
Creneau[93,16,40,17,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000869]
Creneau[94,17,0,17,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000870]
Creneau[95,17,20,17,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000871]
Creneau[96,17,40,18,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000872]
Creneau[97,8,0,8,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000874]
Creneau[98,8,20,8,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000875]
Creneau[99,8,40,9,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000876]
Creneau[100,9,0,9,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000877]
Creneau[101,9,20,9,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000878]
Creneau[102,9,40,10,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000879]
Creneau[103,10,0,10,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000880]
Creneau[104,10,20,10,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000881]
Creneau[105,10,40,11,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000882]
Creneau[106,11,0,11,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000883]
Creneau[107,11,20,11,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000884]
Creneau[108,11,40,12,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000885]
Rendez-vous--------------------------------------
Rv[3,08/10/2012 00:00:00,Client[9,Mr,Jules,Martin,000000844],Creneau[73,8,0,8,20
, Medecin[9,Mme,Marie,Pelissier,000000848],000000849],000000888]
Appuyez sur une touche pour continuer...

III-E-4. Apprentissage de LINQ avec LINQPad

Nous avons utilisé ci-dessus, des requêtes LINQ to Entity pour afficher le contenu des tables de la base de données. Joseph Albahari a écrit un programme d'apprentissage des différentes formes de LINQ. Nous le présentons maintenant.

LINQPad est disponible à l'URL suivante [ http://www.linqpad.net/ ]. Une fois installé, nous le lançons [1] :

Image non disponible

Le débutant LINQ pourra s'initier avec les exemples de l'onglet [Samples] [2] qui montrent de très nombreux exemples. Sélectionnons l'exemple [3] qui s'affiche alors dans une autre fenêtre [4]. Le code complet de l'exemple est celui-ci :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
// Now for a simple LINQ-to-objects query expression (notice no semicolon):

from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word


// Feel free to edit this... (no-one's watching!) You'll be prompted to save any
// changes to a separate file.
//
// Tip:  You can execute part of a query by highlighting it, and then pressing F5.

Les lignes 3-5 sont un exemple de requête LINQ to Object. La requête LINQ suit la syntaxe :

from variable in collection orderby élément1 select élément2

  • variable désigne l'élément courant de la collection. Dans notre exemple, cette collection est la liste de mots résultats de la chaîne splittée ;
  • la collection est ordonnée selon le paramètre élément1 de orderby . Dans notre exemple, la collection de mots sera ordonnée selon leur longueur ;
  • le mot clé select désigne ce qu'on veut retirer de l'élément courant variable de la collection. Dans notre exemple, ce sera le mot.

Exécutons cette requête LINQ :

Image non disponible
  • en [1] : une expression LINQ est exécutée par [F5] ou bien via le bouton d'exécution ;
  • en [2] : l'affichage. Les mots sont affichés dans l'ordre de leur longueur. Ce simple exemple montre la puissance de LINQ ;
  • en [3], il est possible de télécharger d'autres exemples, notamment ceux du livre " LINQ in action " [4] ;
Image non disponible
  • en [5], nous choisissons un exemple du livre ;
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };

// Group words by length
var groups =
  from word in words
  orderby word ascending
  group word by word.Length into lengthGroups
  orderby lengthGroups.Key descending
  select new { Length = lengthGroups.Key, Words = lengthGroups };

// Print each group out
foreach (var group in groups)
{
  Console.WriteLine("Words of length " + group.Length);
  foreach (string word in group.Words)
    Console.WriteLine("  " + word);
}
  • ligne 4 : une nouvelle requête LINQ avec de nouveaux mots clés ;
  • ligne 5 : la collection requêtée est le tableau de mots de la ligne 1 ;
  • ligne 6 : la collection est triée dans l'ordre alphabétique des mots ;
  • ligne 7 : la collection est regroupée dans (mot clé into ) une nouvelle collection lengthGroups . lengthGroups.Key représente le facteur de regroupement (mot clé by ), ici la longueur des mots. lengthGroups rassemble les mots ayant le même facteur de regroupement donc la même longueur ;
  • ligne 8 : la collection lengthGroups est ordonnée par clé de regroupement descendant, donc ici par taille décroissante des mots ;
  • ligne 9 : de cette collection, on produit de nouveaux objets (classes anonymes) ayant deux champs :
  • Length  : la longueur des mots,
  • Words  : les mots ayant cette longueur ;

Ici, on voit particulièrement l'intérêt du mot clé var de la ligne 4. Parce qu'on a utilisé une classe anonyme ligne 9, on ne sait pas désigner le type de la variable groups . Le compilateur lui, va donner un nom interne à la classe anonyme et va typer avec, la variable groups . Il sera capable ensuite de dire si la variable groups est utilisée correctement

  • ligne 12 : parcours de la requête de la ligne 4. Ce n'est qu'à ce moment qu'elle est évaluée. On se rappelle que son exécution va produire une collection d'objets, précisés ligne 9 ;
  • ligne 14 : on affiche la propriété Length de l'élément courant, donc une longueur de mots ;
  • lignes 15-17 : on affiche chaque élément de la collection de la propriété Words , donc l'ensemble des mots ayant la longueur affichée précédemment.

Lorsque nous exécutons cette requête, nous obtenons le résultat suivant dans LINQPad :

Image non disponible

Maintenant que nous avons vu quelques exemples de requêtes [LINQ to Object], voyons des requêtes [LINQ to Entity] qui vont nous permettre de requêter des bases de données. Nous allons tout d'abord nous connecter à la base de données SQL Server que nous avons créée et remplie :

Image non disponible
  • en [1], on ajoute une connexion à une base de données ;
  • en [2], les moyens d'accès à la source de données. Pour accéder à la base SQL Server, nous utiliserons [LINQPad Driver] ;
  • en [3], il est également possible de récupérer un contexte de persistance [DbContext] défini dans un assembly .exe ou .dll (option 3). Malheureusement, à ce jour (8 octobre 2012), Entity Framework 5 n'est pas supporté ;
  • en [4], il est possible de télécharger des pilotes pour d'autres SGBD que SQL Server ;
  • en [5], on téléchargera le driver pour les SGBD MySQL et Oracle ;
Image non disponible
  • en [6], le pilote téléchargé ;
  • en [7], nous nous connectons à une base SQL Server ;
Image non disponible
  • en [8], la base est sur le serveur de nom (local) ;
  • en [9], on se connecte avec l'authentification sa / sqlserver2012 ;
  • en [10], à la base [rdvmedecins-ef] que nous avons créée ;
  • en [11], on peut tester la connexion ;
  • en [12], on termine l'assistant ;
  • en [13], la connexion apparaît dans LINQPad.

Les entités ont été créées à partir de la table [rdvmedecins-ef]. Ce sont les suivantes :

Image non disponible
  • en [1], [CLIENTS] représente l'ensemble des entités [Client]. Chaque entité a :
  • les propriétés (ID, TITRE, NOM, PRENOM, TIMESTAMP),
  • une relation 1 à plusieurs [CLIENTRVS] ;
  • en [2], [CRENEAUXes] représente l'ensemble des entités [Creneau]. Chaque entité a :
  • les propriétés (ID, HDEBUT, MDEBUT, HFIN, MFIN, MEDECIN_ID, TIMESTAMP),
  • une relation 1 à plusieurs [CRENEAURVS],
  • une relation plusieurs à 1 [MEDECIN] ;
  • en [3], l'entité [MEDECINS] représente l'ensemble des entités [Medecin]. Chaque entité a :
  • les propriétés (ID, TITRE, NOM, PRENOM, TIMESTAMP),
  • une relation 1 à plusieurs [MEDECINCRENEAUXes] ;
  • en [4], l'entité [RVS] représente l'ensemble des entités [Rv]. Chaque entité a :
  • les propriétés (ID, JOUR, CLIET_ID, CRENEAU_ID, TIMESTAMP),
  • une relation plusieurs à 1 [CLIENT],
  • une relation plusieurs à 1 [CRENEAU].

On notera que les noms des propriétés ci-dessus sont différentes des noms que nous avons utilisés jusqu'à maintenant. Cela importe peu. Nous voulons juste apprendre les principes de base du requêtage sur base de données.

Voyons comment nous pouvons requêter cette base d'entités. Par exemple, nous voulons la liste des médecins ordonnée par leur TITRE et NOM :

Image non disponible
  • en [1], on crée une nouvelle requête ;
  • en [2], le texte de la requête ;
Image non disponible
  • en [3], le résultat de la requête ;
  • en [4], la même requête avec des expressions lambda . Une requête avec des expressions lambda est moins lisible qu'une requête texte et on pourrait vouloir s'en passer. Elles sont cependant parfois indispensables car elles permettent certaines choses que les requêtes texte ne permettent pas. Une expression lambda désigne une fonction à un paramètre d'entrée a et un paramètre de sortie b , sous la forme a=>b . La méthode OrderBy ci-dessus admet une fonction lambda comme unique paramètre. Celle-ci lui fournit le paramètre selon lequel doit être ordonnée une collection. Ainsi MEDECINS.OrderBy(m=>m.TITRE) est la liste des médecins ordonnée par les titres. Il faut lire l'instruction comme un pipe-line sur une collection. La collection des médecins est fournie en entrée à la méthode OrderBy . Celle-ci va exploiter les entités [Medecin] une par une. Dans l'expression lambda m=>m.TITRE, m représente l'entrée de la fonction lambda. On peut la nommer comme on veut. Ici, l'entrée de la fonction lambda sera une entité [Medecin]. La fonction m=>m.TITRE se lit comme suit : si j'appelle m mon entrée (une entité [Medecin]) alors ma sortie est m.TITRE , donc le titre du médecin. MEDECINS.OrderBy(m=>m.TITRE) est a son tour une collection, la collection des médecins ordonnée par les titres. Cette nouvelle collection peut alimenter une autre méthode, dans l'exemple la méthode ThenBy . Celle-ci fonctionne sur le même principe. Elle sert à indiquer des paramètres supplémentaires pour le tri de la collection.

Lire le code lambda équivalent au code texte que nous tapons habituellement est une bonne façon de l'apprendre ;

Image non disponible
  • en [5], l'ordre SQL émis sur la base. Là encore, on lira attentivement ce code. Il permet d'évaluer le coût réel d'une requête LINQ.

Dans la suite, nous présentons quelques exemples de requête LINQ. A chaque fois, nous montrons les résultats affichés et les codes lambda et SQL équivalents. Pour comprendre ces requêtes, il faut rappeler les relations plusieurs à un qui relient les entités les unes aux autres. C'est par elles qu'on navigue d'une entité à l'autre. On les appelle des propriétés navigationnelles .

Image non disponible

// les clients dont le titre est Mr classés par ordre décroissant des noms

Résultats  :

Image non disponible
LINQ
Sélectionnez
from client in CLIENTS where client.TITRE=="Mr" orderby client.NOM descending  select client
Lambda
Sélectionnez
CLIENTS
.Where (client => (client.TITRE == "Mr"))
.OrderByDescending (client => client.NOM)
SQL
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'Mr'
-- EndRegion
SELECT [t0].[ID], [t0].[TITRE], [t0].[NOM], [t0].[PRENOM], [t0].[TIMESTAMP]
FROM [CLIENTS] AS [t0]
WHERE [t0].[TITRE] = @p0
ORDER BY [t0].[NOM] DESC

// tous les créneaux horaires avec le médecin associé

Résultats (partiels)  :

Image non disponible
LINQ
Sélectionnez
from creneau in CRENEAUXes select new { hd=creneau.HDEBUT, md=creneau.MDEBUT, hf=creneau.HFIN, mf=creneau.MFIN, medecin=creneau.MEDECIN}

Lambda

Image non disponible
SQL
Sélectionnez
1.
2.
3.
SELECT [t0].[HDEBUT] AS [hd], [t0].[MDEBUT] AS [md], [t0].[HFIN] AS [hf], [t0].[MFIN] AS [mf], [t1].[ID], [t1].[TITRE], [t1].[NOM], [t1].[PRENOM], [t1].[TIMESTAMP]
FROM [CRENEAUX] AS [t0]
INNER JOIN [MEDECINS] AS [t1] ON [t1].[ID] = [t0].[MEDECIN_ID]

// tous les rv avec le client et le médecin associés

Résultats  :

Image non disponible
LINQ
Sélectionnez
from rv in RVS select new { rv=rv.CLIENT, medecin=rv.CRENEAU.MEDECIN}

Lambda

Image non disponible
SQL
Sélectionnez
1.
2.
3.
4.
5.
SELECT [t1].[ID], [t1].[TITRE], [t1].[NOM], [t1].[PRENOM], [t1].[TIMESTAMP], [t3].[ID] AS [ID2], [t3].[TITRE] AS [TITRE2], [t3].[NOM] AS [NOM2], [t3].[PRENOM] AS [PRENOM2], [t3].[TIMESTAMP] AS [TIMESTAMP2]
FROM [RVS] AS [t0]
INNER JOIN [CLIENTS] AS [t1] ON [t1].[ID] = [t0].[CLIENT_ID]
INNER JOIN [CRENEAUX] AS [t2] ON [t2].[ID] = [t0].[CRENEAU_ID]
INNER JOIN [MEDECINS] AS [t3] ON [t3].[ID] = [t2].[MEDECIN_ID]

// les médecins n'ayant pas de Rdv

Résultats  :

Image non disponible

Lambda

Image non disponible
SQL
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
SELECT [t0].[ID], [t0].[TITRE], [t0].[NOM], [t0].[PRENOM], [t0].[TIMESTAMP]
FROM [MEDECINS] AS [t0]
WHERE NOT (EXISTS(
    SELECT NULL AS [EMPTY]
    FROM [RVS] AS [t1]
    INNER JOIN [CRENEAUX] AS [t2] ON [t2].[ID] = [t1].[CRENEAU_ID]
    INNER JOIN [MEDECINS] AS [t3] ON [t3].[ID] = [t2].[MEDECIN_ID]
    WHERE [t3].[ID] = [t0].[ID]
    ))

Il n'y a pas de requête LINQ pour cette demande. Il faut passer par des expressions lambda. Celle-ci se lit de la façon suivante : je prends la collection des médecins (MEDECINS) et je ne garde (Where) que les médecins (m) tels que je ne suis pas capable de trouver dans la collection des rendez-vous (RVS) un rendez-vous (rv) avec ce médecin (m).

// créneaux horaires de Mme Pélissier

Résultats (partiels)  :

Image non disponible
LINQ
Sélectionnez
from creneau in CRENEAUXes where creneau.MEDECIN.NOM=="Pelissier" select creneau
SQL
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
-- Region Parameters

DECLARE @p0 NVarChar(1000) = 'Pelissier'

DECLARE @p1 DateTime = '2012-10-08 00:00:00.000'

-- EndRegion

SELECT COUNT(*) AS [value]

FROM [RVS] AS [t0]

INNER JOIN [CRENEAUX] AS [t1] ON [t1].[ID] = [t0].[CRENEAU_ID]

INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]

WHERE ([t2].[NOM] = @p0) AND ([t0].[JOUR] = @p1)

Lambda

Image non disponible

// nombre de Rdv de Mme Pélissier le 08/10/2012

Résultats  :

Image non disponible
LINQ
Sélectionnez
(from rv in RVS where rv.CRENEAU.MEDECIN.NOM=="Pelissier" && rv.JOUR==new DateTime(2012,10,08)  select rv).Count()
SQL
Sélectionnez
-- Region Parameters

DECLARE @p0 NVarChar(1000) = 'Pelissier'

DECLARE @p1 DateTime = '2012-10-08 00:00:00.000'

-- EndRegion

SELECT COUNT(*) AS [value]

FROM [RVS] AS [t0]

INNER JOIN [CRENEAUX] AS [t1] ON [t1].[ID] = [t0].[CRENEAU_ID]

INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]

WHERE ([t2].[NOM] = @p0) AND ([t0].[JOUR] = @p1)

// liste des clients ayant pris Rdv avec Mme Pélissier le 08/10/2012

Résultats  :

Image non disponible
LINQ
Sélectionnez
from rv in RVS where (rv.JOUR==new DateTime(2012,10,08) && rv.CRENEAU.MEDECIN.NOM=="Pelissier") select rv.CLIENT
SQL
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
-- Region Parameters

DECLARE @p0 DateTime = '2012-10-08 00:00:00.000'

DECLARE @p1 NVarChar(1000) = 'Pelissier'

-- EndRegion

SELECT [t3].[ID], [t3].[TITRE], [t3].[NOM], [t3].[PRENOM], [t3].[TIMESTAMP]

FROM [RVS] AS [t0]

INNER JOIN [CRENEAUX] AS [t1] ON [t1].[ID] = [t0].[CRENEAU_ID]

INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]

INNER JOIN [CLIENTS] AS [t3] ON [t3].[ID] = [t0].[CLIENT_ID]

WHERE ([t0].[JOUR] = @p0) AND ([t2].[NOM] = @p1)

Lambda

Image non disponible

// nombre de créneaux horaires par médecin

Résultats  :

Image non disponible
LINQ
Sélectionnez
from creneau in CRENEAUXes 

group creneau by creneau.MEDECIN into creneauxMedecin 

select new { nom=creneauxMedecin.Key.NOM, prenom=creneauxMedecin.Key.PRENOM, nbRv=creneauxMedecin.Count()}
SQL
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
SELECT [t2].[NOM] AS [nom], [t2].[PRENOM] AS [prenom], [t1].[value] AS [nbRv]

FROM (

    SELECT COUNT(*) AS [value], [t0].[MEDECIN_ID]

    FROM [CRENEAUX] AS [t0]

    GROUP BY [t0].[MEDECIN_ID]

    ) AS [t1]

INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]

Lambda

Image non disponible

III-E-5. Modification d'une entité attachée au contexte de persistance

Nous avons vu les opérations suivantes sur le contexte de persistance :

  • ajouter un élément au contexte ( [dbContext].[DbSet].Add ) ;
  • supprimer un élément du contexte ( [dbContext].[DbSet].Remove ) ;
  • requêter un contexte avec des requêtes LINQ .

Lorsqu'on veut synchroniser le contexte avec la base, on écrit [dbContext].SaveChanges() .

Image non disponible

Le code [ModifyAttachedEntity] illustre la modification d'une entité attachée au contexte :

[ModifyAttachedEntity]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
using System;
using System.Data;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class ModifyAttachedEntity
  {
    static void Main(string[] args)
    {
      Client client1, client2, client3;
      // 1er contexte
      using (var context = new RdvMedecinsContext())
      {
        // on vide la base actuelle
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // on ajoute un client
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // suivi
        Console.WriteLine("client1--avant");
        Console.WriteLine(client1);
        // sauvegarde contexte
        context.SaveChanges();
        // suivi
        Console.WriteLine("client1--après");
        Console.WriteLine(client1);
      }
      // 2ième contexte
      using (var context = new RdvMedecinsContext())
      {
        // on récupère client1 dans client2
        client2 = context.Clients.Find(client1.Id);
        // suivi
        Console.WriteLine("client2");
        Console.WriteLine(client2);
        // on modifie client2
        client2.Nom = "yy";
        // sauvegarde contexte
        context.SaveChanges();
      }
      // 3ième contexte
      using (var context = new RdvMedecinsContext())
      {
        // on récupère client2 dans client3
        client3 = context.Clients.Find(client2.Id);
        // suivi
        Console.WriteLine("client3");
        Console.WriteLine(client3);
      }
    }
  }
}
  • ligne 15 : ouverture contexte de l'application ;
  • lignes 18-25 : le contexte est vidé. Très exactement, toutes les entités sont amenées dans le contexte à partir de la base de données puis passent dans un état " supprimé ". On notera qu'à ce stade la base n'a pas bougé. Tant que le contexte n'est pas synchronisé avec la base, celle-ci ne change pas. On se rappelle que supprimer les entités [Medecin] et [Client] suffit à vider la base par le jeu des suppressions en cascade ;
  • lignes 27-28 : un nouveau client est ajouté à la base ;
  • lignes 30-31 : on l'affiche avant sa sauvegarde dans la base ;
  • ligne 33 : on synchronise le contexte avec la base. Les entitées marquées dans un état " supprimé " vont faire l'objet d'une opération SQL DELETE, l'entité ajoutée d'une opération SQL INSERT ;
  • lignes 35-36 : on affiche le client après la synchronisation avec la base ;

Le résultat obtenu à la console est le suivant :

[Console]
Sélectionnez
1.
2.
3.
4.
client1--avant
Client[,xx,xx,xx,]
client1--après
Client[16,xx,xx,xx,000000132209]

On notera les points suivants :

  • avant la synchronisation avec la base, le client n'a ni clé primaire, ni timestamp,
  • après la synchronisation, il les a. On rappelle ici, que la clé primaire a été configurée pour être générée par SQL Server. De même ce SGBD génère automatiquement le timestamp ;
  • ligne 37 : le contexte de persistance est fermé. Les entités qu'il contenait deviennent "  détachées  ". Elles existent en tant qu'objets mais pas en tant qu'entités attachées à un contexte de persistance ;
  • ligne 39 : on redémarre un nouveau contexte vide ;
  • ligne 42 : on récupère le client directement dans la base via sa clé primaire. Il est alors amené dans le contexte. S'il n'est pas trouvé, la méthode Find rend le pointeur null  ;
  • lignes 48-49 : on l'affiche ;

Cela donne le résultat suivant :

 
Sélectionnez
1.
2.
client2
Client[16,xx,xx,xx,000000132209]
  • ligne 47 : on le modifie ;
  • ligne 49 : on synchronise le contexte avec la base. EF va détecter que certains éléments du contexte ont été modifiés depuis qu'ils y ont été amenés. Pour ces éléments, il va générer des ordres SQL UPDATE sur la base. Donc ici, la synchronisation va consister en un unique ordre UPDATE ;
  • ligne 50 : le deuxième contexte est fermé. L'entité client2 qui était attachée au contexte devient maintenant détachée de celui-ci ;
  • ligne 52 : on ouvre un troisième contexte vide ;
  • ligne 55 : on y amène de nouveau l'unique client de la base. On veut voir si la modification faite sur lui dans le contexte précédent a été répercutée dans la base ;
  • lignes 57-58 : on affiche le client. Cela donne le résultat suivant :
 
Sélectionnez
1.
2.
client3
Client[16,xx,xx,yy,000000132210]

Le nom du client a bien été modifié en base. On notera avec intérêt que son timestamp a été mis à jour.

  • ligne 59 : on ferme le contexte. Au passage, on notera que contrairement aux deux fois précédentes, on n'a pas eu besoin auparavant de synchroniser le contexte avec la base (SaveChanges) car le contexte n'avait pas été modifié.

III-E-6. Gestion des entités détachées

Revenons à l'architecture en couches d'une application telle que celle de l'étude de cas :

Image non disponible

La couche [DAO] utilise l'ORM EF5 pour accéder aux données. Nous avons les briques de base de cette couche. Chaque méthode ouvrira un contexte de persistance, fera dessus les opérations nécessaires (insertion, modification, suppression, requêtage) puis le fermera. Les entités gérées par la couche [DAO] vont remonter jusqu'à la couche web ASP.NET. Dans cette couche, elles sont hors contexte de persistance donc détachées. Dans la couche web, un utilisateur peut modifier ces entités (ajout, modification, suppression). Lorsqu'elles reviennent à la couche [DAO], elles sont toujours détachées. Or la couche [DAO] va devoir répercuter les modifications faites par l'utilisateur dans la base. Il va donc devoir travailler avec des entités détachées. Voyons les trois cas possibles :

Ajouter une entité détachée

C'est le cas normal pour un ajout. Il suffit d'ajouter (Add) l'entité détachée au contexte en s'assurant qu'elle a une clé primaire égale à null .

Modifier une entité détachée

On peut utiliser le code suivant :

[DbContext].Entry(entité-détachée).State=EntityState.Modified ;

  • la méthode [DbContext].Entry(entité-détachée) va mettre l'entité dans le contexte ;
  • l'état de cette entité est mis à " modifié " afin qu'elle fasse l'objet d'un ordre SQL UPDATE.

Supprimer une entité détachée

On peut utiliser le code suivant :

 
Sélectionnez
1.
2.
Entity e=[DbContext].[DbSet].Find(clé primaire de l'entité détachée) ;
[DbContext].[DbSet].Remove(e) ;
  • ligne 1 : on met dans le contexte l'entité de même clé primaire que l'entité détachée ;
  • ligne 2 : on la supprime :

On notera que cela nécessite en base un SELECT suivi d'un DELETE alors que normalement le seul DELETE est suffisant. On peut suivre également l'exemple de la modification d'une entité détachée et écrire :

[DbContext].Entry(entité-détachée).State=EntityState.Deleted ;

Comme je n'ai pas pu mettre en œuvre de logs sur les opérations SQL faites sur la base, je ne sais pas si une méthode est à conseiller plutôt que l'autre.

Voici un exemple :

Image non disponible

Le code du programme [ModifyDetachedEntities] est le suivant :

[ModifyDetachedEntities]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class ModifyDetachedEntities
  {
    static void Main(string[] args)
    {
      Client client1;

      // on vide la base actuelle
      Erase();
      // on ajoute un client
      using (var context = new RdvMedecinsContext())
      {
        // création client
        client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
        // ajout du client au contexte
        context.Clients.Add(client1);
        // on sauvegarde le contexte
        context.SaveChanges();
      }
      // affichage base
      Dump("1-----------------------------");
      // client1 n'est pas dans le contexte - on le modifie
      client1.Nom = "y";
      // nouveau contexte
      using (var context = new RdvMedecinsContext())
      {
        // ici, on a un contexte vide
        // on met client1 dans le contexte dans un état modifié
        context.Entry(client1).State = EntityState.Modified;
        // on sauvegarde le contexte
        context.SaveChanges();
      }
      // affichage base
      Dump("2-----------------------------");
      // suppression entité hors contexte
      using (var context = new RdvMedecinsContext())
      {
        // ici, on a un nouveau contexte vide
        // on met client1 dans le contexte dans un état supprimé
        context.Entry(client1).State = EntityState.Deleted;
        // on sauvegarde le contexte
        context.SaveChanges();
      }
      // affichage base
      Dump("3-----------------------------");
    }

    static void Erase()
    {
      // vide la base
      using (var context = new RdvMedecinsContext())
      {
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // on sauvegarde le contexte
        context.SaveChanges();
      }
    }

    static void Dump(string str)
    {
      Console.WriteLine(str);
      // affiche la base
      using (var context = new RdvMedecinsContext())
      {
        foreach (var rv in context.Rvs)
        {
          Console.WriteLine(rv);
        }
        foreach (var creneau in context.Creneaux)
        {
          Console.WriteLine(creneau);
        }
        foreach (var client in context.Clients)
        {
          Console.WriteLine(client);
        }
        foreach (var medecin in context.Medecins)
        {
          Console.WriteLine(medecin);
        }
      }
    }
  }
}
  • ligne 15 : la base est effacée ;
  • lignes 17-25 : un client est ajouté en base ;
  • ligne 27 : affiche le contenu de la base ;

1-----------------------------

Client[20,x,x,x,0000011209]

  • après la ligne 25, le contexte de persistance n'existe plus. Il n'y a donc plus d'entités attachées. L'entité client1 est passée dans l'état "  détaché  " ;
  • ligne 29 : on modifie le nom de l'entité détachée ;
  • ligne 31 : on ouvre un nouveau contexte vide ;
  • ligne 35 : l'entité détachée client1 est mise dans le contexte dans un état "  modifié  " ;
  • ligne 37 : le contexte est synchronisé avec la base ;
  • ligne 38 : il est fermé ;
  • ligne 40 : la base est affichée ;

2-----------------------------

Client[20,x,x,y,0000011210]

Le nom du client a bien été modifié en base. On notera que le timestamp a été mis à jour ;

  • ligne 42 : ouverture d'un nouveau contexte vide ;
  • ligne 46 : l'entité détachée client1 est mise dans le contexte dans un état " supprimé " ;
  • ligne 48 : le contexte est synchronisé avec la base ;
  • ligne 49 : il est fermé ;
  • ligne 51 : la base est affichée ;

3-----------------------------

L'entité a bien été supprimée en base.

Maintenant, nous voyons les deux modes de chargement des dépendances d'une entité : Lazy et Eager Loading .

III-E-7. Lazy et Eager Loading

Reprenons le schéma des dépendances plusieurs à un de nos quatre entités :

Image non disponible

Ci-dessus, l'entité [Creneau] a une propriété navigationnelle [Creneau.Medecin] vers l'entité [Medecin]. On appelle cela une dépendance. Nous avons vu qu'il y avait également des dépendances un à plusieurs . Le principe qui va être expliqué s'applique également à elles.

Par défaut, EF 5 est en mode Lazy Loading  : lorsqu'il amène une entité dans le contexte de persistance depuis la base, il n'amène pas ses dépendances. Celles-ci seront amenées lorsqu'elles seront utilisées la première fois. C'est une mesure de bon sens. Si ce n'était pas le cas, amener dans le contexte les rendez-vous amènerait d'après les dépendances ci-dessus :

  • les entités [Creneau] liées aux rendez-vous ;
  • les entités [Medecin] liées à ces créneaux ;
  • les entités [Clients] liées aux rendez-vous.

Parfois cependant, on a besoin d'une entité et de ses dépendances. Nous allons illustrer les deux modes de chargement.

Image non disponible

Le code de [LazyEagerLoading] est le suivant :

[LazyEagerLoading]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;

namespace RdvMedecins_01
{
  class LazyEagerLoading
  {
    // les entités
    static Medecin[] medecins;
    static Client[] clients;
    static Creneau[] creneaux;

    static void Main(string[] args)
    {
      // on initialise la base      
      InitBase();
      Console.WriteLine("Initialisation terminée");
      // eager loading
      Creneau creneau;
      int idCreneau = (int)creneaux[0].Id;
      using (var context = new RdvMedecinsContext())
      {
        // creneau  0
        creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // affichage dépendance
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
      }
      // lazy loading - mode par défaut
      using (var context = new RdvMedecinsContext())
      {
        // creneau  0
        creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // affichage dépendance
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 2 suivante s'est produite : {0}", e);
      }

    }

    static void InitBase()
    {
      // on initialise la base
      using (var context = new RdvMedecinsContext())
      {
        // on vide la base actuelle
        ...
        // on initialise la base
        // les clients
        clients = new Client[] {
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
...
        // les Rdv
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // on sauve le contexte de persistance
        context.SaveChanges();
      }
    }
  }
}
  • ligne 18 : on part d'une base connue, celle utilisée jusqu'à maintenant. Après cette opération, les tableaux des lignes 11-13 sont remplis d'entités détachées ;
  • lignes 21-22 : on s'intéresse au premier créneau et au médecin associé ;
  • ligne 23 : nouveau contexte ;
  • ligne 26 : on met le créneau dans le contexte avec sa dépendance (eager loading). Parce que ce n'est pas le mode par défaut, il faut demander explicitement cette dépendance. C'est la méthode Include qui permet cela. Son paramètre est le nom de la dépendance dans l'entité amenée dans le contexte. La requête qui amène l'entité dans le contexte utilise des expressions lambda. La méthode Single permet de préciser une condition permettant de ramener une unique entité. Ici, on recherche en base l'entité [Creneau] qui a la clé primaire du créneau n° 0 ;
  • ligne 27 : on affiche l'entité ramenée. Rappelons les deux méthodes d'écriture utilisées dans les entités :
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
// signature
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
 
   // signature courte
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
    }
  • lignes 2-5 : la méthode [ToString] affiche la dépendance [Medecin]. Si celle-ci n'est pas déjà dans le contexte, elle sera cherchée en base pour l'y mettre ;
  • lignes 8-11 : la méthode [ShortIdentity] n'affiche pas la dépendance [Medecin]. Elle ne sera donc pas recherchée en base si elle n'est pas dans le contexte ;

A ce stade, l'affichage console est le suivant :

Initialisation terminée

Creneau[181,8,0,8,20, 21, 00000195150]

  • ligne 28 : le contexte est fermé ;
  • lignes 30-37 : on essaie d'écrire la dépendance [Medecin] de l'entité. On rappelle le fonctionnement en Lazy Loading  : une dépendance est chargée lors de sa première utilisation si elle n'est pas présente. Ici, normalement elle est présente. L'affichage est le suivant :

Médecin=Medecin[21,Mme,Marie,Pelissier,00000195149]

  • lignes 39-44 : dans le cadre d'un nouveau contexte, le créneau n° 0 est de nouveau cherché en base et amené dans le contexte. Ici, la dépendance [Medecin] n'est pas demandée explicitement. Elle ne sera donc pas amenée (Lazy Loading) ;
  • ligne 43 : l'affichage de l'identité courte du créneau est la suivante :

Creneau[181,8,0,8,20, 21, 00000195150]

Ici, il est important d'utiliser ShortIdentity au lieu de ToString pour afficher l'entité. Si on utilise ToString , la dépendance [Medecin] va être affichée et pour cela elle va être cherchée en base. Or on ne veut pas cela.

  • ligne 44 : le contexte est fermé ;
  • lignes 46-53 : on essaie d'afficher la dépendance de l'entité. Il est important de faire cela hors contexte sinon elle sera recherchée en base et trouvée. Ici on est hors contexte. L'entité [Creneau] est détachée et sa dépendance [Medecin] est absente (Lazy Loading). Que va-t-il se passer ? L'affichage écran est le suivant :
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
L'erreur 2 suivante s'est produite : System.ObjectDisposedException: L'instance ObjectContext a été supprimée et ne peut plus être utilisée pour les opérations qui requièrent une connexion.
   à System.Data.Objects.ObjectContext.EnsureConnection()
   à System.Data.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)
   à System.Data.Objects.ObjectQuery`1.Execute(MergeOption mergeOption)
   à System.Data.Objects.DataClasses.EntityReference`1.Load(MergeOption mergeOption)
   à System.Data.Objects.DataClasses.RelatedEnd.Load()
   à System.Data.Objects.DataClasses.RelatedEnd.DeferredLoad()
   à System.Data.Objects.Internal.LazyLoadBehavior.LoadProperty[TItem](TItem propertyValue, String relationshipName, String targetRoleName, Boolean mustBeNull,Object wrapperObject)
   à System.Data.Objects.Internal.LazyLoadBehavior.<>c__DisplayClass7`2.<GetInterceptorDelegate>b__2(TProxy proxy, TItem item)
   à System.Data.Entity.DynamicProxies.Creneau_AF14A89855AD9B7E5ABA4A877B4989B2F8B3F7ECA154E3FEC02BA722002773E4.get_Medecin()
   à RdvMedecins_01.LazyEagerLoading.Main(String[] args) dans d:\data\istia-1213\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\LazyEagerLoading.cs:ligne 48

EF a trouvé la dépendance [Medecin] absente. Il a voulu la charger mais le contexte étant fermé, cette opération n'était plus possible. On mémorisera cette exception [System.ObjectDisposedException] car est elle caractéristique du chargement d'une dépendance en-dehors d'un contexte ouvert.

Maintenant examinons la concurrence d'accès aux entités.

III-E-8. Concurrence d'accès aux entités

Revenons sur la définition de l'entité [Client] :

[Client]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
public class Client
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // les Rvs du client
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // signature
    ...
  }

Nous allons nous intéresser au champ [Timestamp] de la ligne 23. Nous savons que sa valeur est générée par le SGBD. Nous avons dit également que l'annotation [Timestamp] de la ligne 22 faisait que EF 5 utilisait le champ annoté pour gérer les concurrences d'accès aux entités. Rappelons ce qu'est une gestion de concurrence d'accès :

  • un processus P1 lit une ligne L de la table [MEDECINS] au temps T1. La ligne a le timestamp TS1 ;
  • un processus P2 lit la même ligne L de la table [MEDECINS] au temps T2. La ligne a le timestamp TS1 parce que le processus P1 n'a pas encore validé sa modification ;
  • le processus P1 valide sa modification de la ligne L. Le timestamp de la ligne L passe alors à TS2 ;
  • le processus P2 valide sa modification de la ligne L. L'ORM lance alors une exception car le processus P2 a un timestamp TS1 de la ligne L différent du timestamp TS2 trouvé en base.

On appelle cela la gestion optimiste des accès concurrents. Avec EF 5, un champ jouant ce rôle doit avoir l'un des deux attributs [Timestamp] ou [ConcurrencyCheck]. SQL server a un type [timestamp]. Une colonne ayant ce type voit sa valeur automatiquement générée par SQL Server à toute insertion / modification d'une ligne. Une telle colonne peut alors servir à gérer la concurrence d'accès.

Nous allons illustrer cette concurrence d'accès avec deux threads qui vont modifier en même temps une même entité [Client] en base. Le projet évolue comme suit :

Image non disponible

Le code du programme [AccèsConcurrents] est le suivant :

[AccèsConcurrents]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{

  // objet échangé avec les threads
  class Data
  {
    public int Duree { get; set; }
    public string Nom { get; set; }
    public Client Client { get; set; }
  }

  // programme de test
  class AccèsConcurrents
  {

    static void Main(string[] args)
    {
      Client client1;
      using (var context = new RdvMedecinsContext())
      {
        // thread principal
        Thread.CurrentThread.Name = "main";
        // on vide la base actuelle
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // on ajoute un client
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // suivi
        Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
        // sauvegarde
        context.SaveChanges();
        // suivi
        Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
      }
      // on va modifier client1 avec deux threads
      // thead t1
      Thread t1 = new Thread(Modifie);
      t1.Name = "t1";
      t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
      // thread t2
      Thread t2 = new Thread(Modifie);
      t2.Name = "t2";
      t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
      // on attend la fin des 2 threads
      Console.WriteLine("Thread {0} -- début attente fin des deux threads", Thread.CurrentThread.Name);
      t1.Join();
      t2.Join();
      Console.WriteLine("Thread {0} -- fin attente fin des deux threads", Thread.CurrentThread.Name);
      // on affiche la modification - une seule a du réussir
      using (var context = new RdvMedecinsContext())
      {
        // on récupère client1 dans client2
        Client client2 = context.Clients.Find(client1.Id);
        Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
      }
    }

    // thread
    static void Modifie(object infos)
    {
 ...
}
  • ligne 26 : on démarre un contexte vide ;
  • ligne 29 : on donne un nom au thread courant pour le différentier des deux threads qui vont être créés ultérieurement ;
  • lignes 31-38 : les entités [Medecin] et [Client] sont mises dans l'état " supprimé " ;
  • lignes 40-41: on met un client dans le contexte ;
  • lignes 43-44 : on l'affiche avant la synchronisation du contexte ;
  • ligne 46 : synchronisation du contexte avec la base : les entités dans l'état " supprimé " vont être supprimées de la base. L'entité [Client] mise dans le contexte va être insérée dans la base. Ce sera le seul élément de la base ;
  • lignes 47-49 : on affiche le client après synchronisation du contexte. A ce stade, les affichages écran sont les suivants :
 
Sélectionnez
1.
2.
3.
4.
main client1--avant sauvegarde du contexte
Client[,xx,xx,xx,]
main client1--après sauvegarde du contexte
Client[33,xx,xx,xx,000001126209]

On remarquera qu'après synchronisation du contexte, le client a une clé primaire et un timestamp  ;

  • ligne 50 : le contexte est fermé ;
  • ligne 53 : un thread t1 est associé à la méthode [Modifie] de la ligne 84. Cela signifie que lorsqu'il sera lancé, il exécutera la méthode [Modifie] ;
  • ligne 54 : on donne un nom au thread t1 ;
  • ligne 55 : le thread t1 est lancé. On lui passe des paramètres sous la forme d'une structure [Data] définie lignes 12-17 :
  • Durée  : le thread s'arrêtera Durée secondes avant de terminer son exécution,
  • Client  : une référence sur le client à mettre à jour dans la base,
  • Nom  : nom à donner à ce client ;
  • lignes 57-59 : même chose avec un deuxième thread. Au final, deux threads vont essayer de changer en base, le nom du même client ;
  • lignes 60-63 : après avoir lancé les deux threads, le thread principal attend leur fin d'exécution ;
  • ligne 62 : attente de la fin du thread t1 ;
  • ligne 63 : attente de la fin du thread t2 ;
  • ligne 64 : on se sait pas dans quel ordre les deux threads vont se terminer. Ce qui est sûr, c'est qu'en ligne 64 ils sont terminés ;
  • lignes 66-72 : dans un nouveau contexte, on va chercher le client en base pour voir dans quel état il est.

Voyons maintenant, ce que font les deux threads t1 et t2. Ils exécutent la méthode [Modifie] suivante :

[Modifie]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
static void Modifie(object infos)
    {
      // on récupère le paramètre
      Data data = (Data)infos;
      try
      {
        using (var context = new RdvMedecinsContext())
        {
          Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
          // on récupère client1 dans client2
          Client client2 = context.Clients.Find(data.Client.Id);
          Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
          Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
          // on modifie client2
          client2.Nom = data.Nom;
          // on attend un peu
          Thread.Sleep(data.Duree);
          // on sauvegarde les changements
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        // exception
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
      }
      // fin du thread
      Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
    }
  • ligne 4 : on récupère les paramètres du thread (Durée, Nom, Client) ;
  • ligne 7 : nouveau contexte ;
  • ligne 11 : le client est amené dans le contexte ;
  • lignes 12-13 : suivi pour voir l'état du client ;
  • ligne 15 : on change son nom ;
  • ligne 17 : le thread s'arrête Duree milli-secondes. Cela a un effet intéressant. Le thread lâche le processeur qui l'exécutait laissant la place à un autre thread. Dans notre exemple, nous avons trois threads : main, t1 , t2. Le thread main est à l'arrêt attendant la fin des threads t1 et t2. En supposant que le thread t1 ait le processeur le premier, il le laisse désormais au thread t2. Cela va avoir pour effet, que le thread t2 va lire exactement la même chose que le thread t1, le même client avec le même timestamp  ;
  • ligne 19 : le contexte est synchronisé avec la base. Admettons de nouveau que le thread t1 se réveille le premier. Il va sauvegarder le client avec le nom "  yy  ". Il va pouvoir le faire parce qu'il a le même timestamp qu'en base. A cause de cette mise à jour, le SGBD va modifier le timestamp . Lorsque le thread t2 va se réveiller à son tour, il aura un client avec un timestamp différent de celui qui est maintenant en base. Sa mise à jour va être refusée.

Les affichages écran sont les suivants :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
main client1--avant sauvegarde du contexte
Client[,xx,xx,xx,]
main client1--après sauvegarde du contexte
Client[33,xx,xx,xx,000001126209]
Thread main -- début attente fin des deux threads
Début Thread t1
Début Thread t2
Thread t2 client2
Thread t2 Client[33,xx,xx,xx,000001126209]
Thread t1 client2
Thread t1 Client[33,xx,xx,xx,000001126209]
Fin Thread t2
Thread t1 System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: Une instruction de mise à jour, d'insertion ou de suppression dans le magasin a affecté un nombre inattendu de lignes (0). Des entités ont peut-être été modifiées ou supprimées depuis leur chargement. Actualisez les entrées ObjectStateManager. ---> System.Data.OptimisticConcurrencyException: Une instruction de mise à jour, d'insertion ou de suppression dans le magasin a affecté un nombre inattendu de lignes (0). Des entités ont peut-être été modifiées ou supprimées depuis leur char
gement. Actualisez les entrées ObjectStateManager.
   à System.Data.Mapping.Update.Internal.UpdateTranslator.ValidateRowsAffected(I
nt64 rowsAffected, UpdateCommand source)
   ...
   à RdvMedecins_01.AccèsConcurrents.Modifie(Object infos) dans d:\data\istia-12
13\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\AccèsConcurrents
.cs:ligne 102
Fin Thread t1
Thread main -- fin attente fin des deux threads
Thread main client2
Thread main Client[33,xx,xx,zz,000001126210]
  • ligne 4 : le client dans la base ;
  • ligne 9 : le client tel qu'il est lu par le thread t2 ;
  • ligne 11 : le client tel qu'il est lu par le thread t1. Les deux threads ont donc lu la même chose ;
  • ligne 12 : le thread t2 se termine le premier. Il a donc pu faire sa mise à jour. Le nom a du passer à " zz " ;
  • ligne 13 : le thread t1 lance une exception de type [System.Data.OptimisticConcurrencyException]. EF a détecté qu'il n'avait pas le bon timestamp  ;
  • ligne 21 : le thread t1 se termine à son tour ;
  • ligne 22 : le thread principal a terminé son attente ;
  • ligne 24 : le thread principal affiche le client en base. C'est bien le thread t2 qui a gagné. Le nom est " zz ". On notera que le timestamp a changé.

Maintenant, examinons un autre aspect : la transaction qui encadre la synchronisation du contexte de persistance avec la base.

III-E-9. Synchronisation dans une transaction

La table [CRENEAUX] a une contrainte d'unicité que nous avons ajoutée à la main (cf paragraphe , page ) :

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Nous allons procéder de la façon suivante : nous allons ajouter en même temps deux rendez-vous pour le même médecin, le même jour et le même créneau horaire. On va voir ce qui se passe.

Le projet évolue comme suit :

Image non disponible

Le code du programme [SynchronisationTransaction] est le suivant :

[SynchronisationTransaction]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{

  // programme de test
  class SynchronisationTransaction
  {

    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // on vide la base actuelle
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        context.SaveChanges();
      }

      // on crée un client
      Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // on crée un médecin
      Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // on crée un créneau pour ce médecin
      Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
      // on crée deux Rv pour ce médecin et ce client, même jour, même créneau
      Rv rv1 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
      Rv rv2 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
      try
      {
        // on met tout ce petit monde dans le contexte de persistance
        using (var context = new RdvMedecinsContext())
        {
          context.Clients.Add(client1);
          context.Creneaux.Add(creneau1);
          context.Medecins.Add(medecin1);
          context.Rvs.Add(rv1);
          context.Rvs.Add(rv2);
          // on sauvegarde le contexte - on doit avoir une exception
          // car la BD sous-jacente a une contrainte d'unicité empêchant
          // d'avoir deux RDV même jour, même créneau
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("Erreur : {0}", e);
      }
      // si la sauvegarde se passe dans une transaction alors rien n'a du être inséré dans la base
      // à cause de l'exception précédente - on vérifie

      using (var context = new RdvMedecinsContext())
      {
        // les clients
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // les médecins
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // les créneaux horaires
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // les Rdvs
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}
  • lignes 15-27 : on utilise un contexte de persistance pour vider la base ;
  • ligne 30 : création d'un objet [Client] ;
  • ligne 32 : création d'un objet [Medecin] ;
  • ligne 34 : création d'un objet [Creneau] ;
  • ligne 36 : création d'un objet [Rv] ;
  • ligne 37 : création d'un second objet [Rv] identique au précédent ;
  • ligne 41 : ouverture d'un nouveau contexte ;
  • lignes 43-47 : les objets créés précédemment sont attachés au nouveau contexte. Notez ici, qu'en tenant compte des dépendances, nous aurions pu minimiser le nombre d'opération Add . Mais EF lui optimisera les ordres SQL INSERT à émettre sur la base ;
  • ligne 51 : le contexte est synchronisé avec la base. Comme l'indique le commentaire, l'insertion d'un des deux rendez-vous doit échouer à cause de la contrainte d'unicité sur la table [RVS]. Mais davantage que cela, si la synchronisation se passe dans une transaction, tout doit être défait. Donc aucune insertion ne doit avoir lieu. La base doit rester vide ;
  • ligne 53 : le contexte est fermé ;
  • lignes 61-90 : affichage du contenu de la base. Elle doit être vide.

L'affichage écran est le suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
Erreur : System.Data.Entity.Infrastructure.DbUpdateException: Une erreur s'est produite lors de la mise à jour des entrées. Pour plus d'informations, consultezl'exception interne. ---> System.Data.UpdateException: Une erreur s'est produite lors de la mise à jour des entrées. Pour plus d'informations, consultez l'exception interne. ---> System.Data.SqlClient.SqlException: Violation de la contrainte UNIQUE KEY « RVS_uq ». Impossible d'insérer une clé en double dans l'objet « dbo.RVS ». Valeur de clé dupliquée : (oct 18 2012 12:00AM, 34).
L'instruction a été arrêtée.
   à System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   à System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)...
   --- Fin de la trace de la pile d'exception interne ---
   ...
   à System.Data.Entity.DbContext.SaveChanges()
   à RdvMedecins_01.SynchronisationTransaction.Main(String[] args) dans d:\data\istia-1213\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\SynchronisationTransaction.cs:ligne 59
Clients--------------------------------------
Médecins--------------------------------------
Créneaux horaires--------------------------------------
Rendez-vous--------------------------------------
  • ligne 1 : exception due à la violation de la contrainte d'unicité sur la table [RVS] ;
  • lignes 9-12 : la base est bien vide. La synchronisation du contexte avec la base s'est donc passée dans une transaction.

Il y aurait sans doute d'autres choses à explorer dans EF 5. Mais nous en savons assez pour revenir sur notre étude d'une architecture multicouche. Le lecteur trouvera au début de ce document des références d'articles et de livres lui permettant d'approfondir sa connaissance de EF 5.

III-F. Etude d'une architecture multicouche s'appuyant sur EF 5

Nous revenons à notre étude de cas décrite au paragraphe , page . Il s'agit d'une application web ASP.NET structurée comme suit :

Image non disponible

Nous allons commencer par construire la couche [DAO] d'accès aux données. Cette couche s'appuiera sur EF5.

III-F-1. Le nouveau projet

Nous créons un nouveau projet console VS 2012 [RdvMedecins-SqlServer-02] dans la solution courante [1] :

Image non disponible

Nous lui ajoutons quatre dossiers [2] dans lesquels nous allons répartir nos codes. Le dossier [Entites] est une recopie du dossier [Entites] du projet précédent. Après cette recopie, apparaissent des erreurs dues au fait que nous n'avons pas les bonnes références. Il nous faut ajouter une référence à Entity Framework 5. On suivra pour cela, la méthode expliquée au paragraphe , page . La liste des références devient la suivante [3] :

Image non disponible

A ce stade, le projet ne doit plus présenter d'erreurs de compilation. Du projet précédent, nous recopions également le fichier [App.config] qui configure la connexion à la base de données :

[App.config]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>

  <!-- chaîne de connexion sur la base -->
  <connectionStrings>
    <add name="monContexte"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- le factory provider -->
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider"
       invariant="System.Data.SqlClient"
       description=".Net Framework Data Provider for SqlServer"
       type="System.Data.SqlClient.SqlClientFactory, System.Data,
     Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    />
    </DbProviderFactories>
  </system.data>

</configuration>

III-F-2. La classe Exception

Nous allons utiliser une classe d'exception propre au projet. C'est celle qui sortira de la couche [DAO] :

Image non disponible

La couche [DAO] arrêtera toutes les exceptions qui remonteront jusqu'à elles et les encapsulera dans une exception de type [RdvMedecinsException]. Cette exception sera la suivante :

[RdvMedecinsException]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
using System;

namespace RdvMedecins.Exceptions
{
  public class RdvMedecinsException : Exception
  {

    // propriétés
    public int Code { get; set; }

    // constructeurs
    public RdvMedecinsException()
      : base()
    {
    }

    public RdvMedecinsException(string message)
      : base(message)
    {
    }

    public RdvMedecinsException(int code, string message)
      : base(message)
    {
      Code = code;
    }

    public RdvMedecinsException(int code, string message, Exception ex)
      : base(message, ex)
    {
      Code = code;
    }

    // identité
    public override string ToString()
    {
      if (InnerException == null)
      {
        return string.Format("RdvMedecinsException[{0},{1}]", Code, base.Message);
      }
      else
      {
        return string.Format("RdvMedecinsException[{0},{1},{2}]", Code, base.Message, base.InnerException.Message);
      }
    }
  }
}
  • ligne 5 : la classe dérive de la classe [Exception] ;
  • ligne 9 : elle ajoute à sa classe de base un code d'erreur ;
  • lignes 12-32 : les différents constructeurs intègrent la présence du champ [Code].

Le projet évolue comme suit :

Image non disponible

III-F-3. La couche [DAO]

Image non disponible

La couche [DAO] offre une interface à la couche [ASP.NET]. Pour identifier celle-ci, il faut regarder les pages web de l'application :

Image non disponible
  • en [1] ci-dessus, la liste déroulante a été remplie avec la liste des médecins. La couche [DAO] fournira cette liste ;
  • en [2], la couche [DAO] fournira ;
  • la liste des rendez-vous d'un médecin pour tel jour,
  • la liste des créneaux horaires d'un médecin,
  • des informations complémentaires sur le médecin sélectionné ;
Image non disponible
  • en [3], la liste déroulante des clients sera fournie par la couche [DAO] ;
Image non disponible
  • en [4], l'utilisateur valide un rendez-vous. La couche [DAO] doit pouvoir l'ajouter à la base. Elle doit pouvoir également donner des informations complémentaires sur le client sélectionné ;
Image non disponible
  • en [5], l'utilisateur supprime un rendez-vous. La couche [DAO] doit permettre cela.

Avec ces informations, l'interface [IDao] de la couche [DAO] pourrait être la suivante :

[IDao]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
using System;
using System.Collections.Generic;
using RdvMedecins.Entites;

namespace RdvMedecins.Dao
{
  public interface IDao
  {
    // liste des clients
    List<Client> GetAllClients();
    // liste des médecins
    List<Medecin> GetAllMedecins();
    // liste des créneaux horaires d'un médecin
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // liste des RV d'un médecin donné, un jour donné
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // ajouter un RV
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // supprimer un RV
    void SupprimerRv(int idRv);
    // trouver une entité T via sa clé primaire
    T Find<T>(int id) where T : class;
  }
}

Les méthodes des lignes 10-20 découlent de l'étude qui vient d'être faite. La méthodes de la ligne 22 est là pour remédier au fait qu'on travaille en Lazy Loading . Si dans la couche [ASP.NET] on a besoin d'une dépendance d'une entité, on ira la chercher en base avec cette méthode.

L'implémentation [Dao] de cette interface sera la suivante :

[Dao]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
using System;
using System.Collections.Generic;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using RdvMedecins.Models;

namespace RdvMedecins.Dao
{
  public class Dao : IDao
  {

    //liste des clients
    public List<Client> GetAllClients()
    {
      // liste des clients
      List<Client> clients = null;
      try
      {
        // ouverture contexte de persistance
        using (var context = new RdvMedecinsContext())
        {
          // liste des clients
          clients = context.Clients.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(1, "GetAllClients", ex);
      }
      // on rend le résultat
      return clients;
    }

    // liste des médecins
    public List<Medecin> GetAllMedecins()
    {
      // liste des médecins
      List<Medecin> medecins = null;
      try
      {
        // ouverture contexte de persistance
        using (var context = new RdvMedecinsContext())
        {
          // liste des médecins
          medecins = context.Medecins.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(2, "GetAllMedecins", ex);
      }
      // on rend le résultat
      return medecins;
    }

    // liste des créneaux horaires d'un médecin donné
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
   ...
    }

    // liste des RV d'un médecin pour un jour donné
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
 ...
    }

    // ajouter un RV
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
 ...
    }

    // supprimer un RV
    public void SupprimerRv(int idRv)
    {
...
    }

    // trouver un client
    public Client FindClient(int id)
    {
...
    }

    // trouver un créneau
    public Creneau FindCreneau(int id)
    {
 ...
    }

    // trouver un médecin
    public Medecin FindMedecin(int id)
    {
....
    }

    // trouver un Rv
    public Rv FindRv(int id){
...
    }

  }
}

Explicitons la méthode [GetAllClients] qui doit rendre la liste de tous les clients :

  • lignes 18-31 : la recherche des clients se fait dans un try / catch. Il en sera de même de toutes les méthodes à suivre ;
  • ligne 21 : ouverture d'un nouveau contexte ;
  • ligne 24 : les entités [Client] sont chargées dans le contexte et mises dans une liste.

La méthode [GetAllMedecins] qui doit rendre la liste de tous les médecins est analogue (lignes 37-57).

La méthode [GetCreneauxMedecin] est la suivante :

[GetCreneauxMedecin]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
// liste des créneaux horaires d'un médecin donné
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
      // liste des créneaux
      try
      {
        // ouverture contexte de persistance
        using (var context = new RdvMedecinsContext())
        {
          // on récupère le médecin avec ses créneaux
          Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
          // liste des créneaux du médecin
          return medecin.Creneaux.ToList<Creneau>();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
      }
    }
  • ligne 9 : ouverture d'un nouveau contexte de persistance ;
  • ligne 11 : on recherche le médecin dont on a la clé primaire. On demande d'y inclure la dépendance [Creneaux] qui est une collection des créneaux du médecin. Si le médecin n'existe pas, la méthode Single lance une exception ;
  • ligne 13 : on rend la liste des créneaux.

La méthode [GetRvMedecinJour] doit rendre la liste des rendez-vous d'un médecin pour un jour donné. Son code pourrait être le suivant :

[GetRvMedecinJour]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
// liste des RV d'un médecin pour un jour donné
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
      // liste des Rv
      List<Rv> rvs = null;

      try
      {
        // ouverture contexte de persistance
        using (var context = new RdvMedecinsContext())
        {
          // on récupère le médecin
          Medecin medecin = context.Medecins.Find(idMedecin);
          if (medecin == null)
          {
            throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
          }
          // liste des rv
          rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
      }
      // on rend le résultat
      return rvs;
    }
  • ligne 13 : on amène dans le contexte le médecin dont on a la clé primaire ;
  • lignes 14-17 : s'il n'existe pas, on lance une exception ;
  • ligne 19 : la requête LINQ pour récupérer les rendez-vous pour ce médecin ;

La méthode [AjouterRv] doit ajouter un rendez-vous en base et doit rendre la clé primaire de l'élément inséré. Son code pourrait être le suivant :

[AjouterRv]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
// ajouter un RV
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
      //  du Rdv ajouté
      int idRv;
      try
      {
        // ouverture contexte de persistance
        using (var context = new RdvMedecinsContext())
        {
          // on récupère le créneau
          Creneau creneau = context.Creneaux.Find(idCreneau);
          if (creneau == null)
          {
            throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
          }
          // on récupère le client
          Client client = context.Clients.Find(idClient);
          if (client == null)
          {
            throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
          }
          // création créneau
          Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
          // ajout dans le contexte
          context.Rvs.Add(rv);
          // sauvegarde du contexte
          context.SaveChanges();
          // on récupère la clé primaire du rv ajouté
          idRv = (int)rv.Id;
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(7, "AjouterRv", ex);
      }
      // résultat
      return idRv;
    }
  • ligne 12 : on cherche le créneau du rendez-vous en base ;
  • lignes 13-16 : si on ne le trouve pas, on lance une exception ;
  • ligne 18 : on cherche le client du rendez-vous en base ;
  • lignes 19-22 : si on ne le trouve pas, on lance une exception ;
  • ligne 24 : on construit un objet [Rv] avec les informations nécessaires ;
  • ligne 26 : on l'ajoute au contexte de persistance ;
  • ligne 28 : on synchronise le contexte de persistance avec la base. Le rendez-vous va alors être mis en base ;
  • ligne 30 : on sait qu'après synchronisation de la base, les clés primaires des éléments insérés sont disponibles. On récupère celle du rendez-vous ajouté ;
  • ligne 31 : on ferme le contexte de persistance.

La méthode [SupprimerRv] doit supprimer un rendez-vous dont on lui passe la clé primaire.

[SupprimerRv]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
// supprimer un RV
    public void SupprimerRv(int idRv)
    {
      try
      {
        // ouverture contexte de persistance
        using (var context = new RdvMedecinsContext())
        {
          // on récupère le Rv
          Rv rv = context.Rvs.Find(idRv);
          if (rv == null)
          {
            throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
          }
          // suppression Rv
          context.Rvs.Remove(rv);
          // sauvegarde du contexte
          context.SaveChanges();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(8, "SupprimerRv", ex);
      }
    }
  • ligne 7 : nouveau contexte de persistance ;
  • ligne 10 : on amène dans le contexte, le rendez-vous à supprimer ;
  • lignes 11-15 : s'il n'existe pas, on lance une exception ;
  • ligne 16 : on le supprime du contexte ;
  • ligne 18 : on synchronise le contexte avec la base ;
  • ligne 19 : on ferme le contexte.

La méthode [Find<T>] permet de rechercher en base une entité de type T, via sa clé primaire. Son code pourrait être le suivant :

[Find<T>]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
public T Find<T>(int id)  where T : class
    {
      try
      {
        // ouverture contexte de persistance
        using (var context = new RdvMedecinsContext())
        {
          return context.Set<T>().Find(id);
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(20, "Find<T>", ex);
      }
    }
  • ligne 8 : la méthode Set<T> permet de récupérer un DbSet<T> sur lequel on peut appliquer les méthodes habituelles.

Le projet évolue comme suit :

Image non disponible

III-F-4. Test de la couche [DAO]

Nous allons créer un programme de test de la couche [DAO]. L'architecture du test sera la suivante :

Image non disponible

Un programme console demande à [Spring.net] d'instancier la couche [DAO]. Ceci fait, il teste les différentes fonctionnalités de l'interface de la couche [DAO]. Plutôt qu'un programme console, il aurait été préférable d'écrire un programme de test de type NUnit. Un programme de test de la couche [DAO] pourrait être le suivant :

Test
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
using System;
using System.Collections.Generic;
using RdvMedecins.Dao;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using Spring.Context.Support;

namespace RdvMedecins.Tests
{
  class Program
  {
    public static void Main()
    {
      IDao dao = null;
      try
      {
        // instanciation couche [DAO] via Spring
        dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

        // affichage clients
        List<Client> clients = dao.GetAllClients();
        DisplayClients("Liste des clients :", clients);

        // affichage médecins
        List<Medecin> medecins = dao.GetAllMedecins();
        DisplayMedecins("Liste des médecins :", medecins);

        // liste des créneaux horaires du médecin  0
        List<Creneau> creneaux = dao.GetCreneauxMedecin((int)medecins[0].Id);
        DisplayCreneaux(string.Format("Liste des créneaux horaires du médecin {0}", medecins[0]), creneaux);

        // liste des Rv d'un médecin pour un jour donné
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));

        // ajouter un RV au médecin n°1 dans créneau  0
        Console.WriteLine(string.Format("Ajout d'un RV au médecin {0} avec client {1} le 23/11/2013", medecins[0], clients[0]));
        int idRv1 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
        Console.WriteLine("Rdv ajouté");
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));

        // ajouter un Rv dans un créneau déjà occupé - doit provoquer une exception
        int idRv2;
        Console.WriteLine("Ajout d'un RV dans un créneau déjà occupé");
        try
        {
          idRv2 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
          Console.WriteLine("Rdv ajouté");
          DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
        }
        catch (RdvMedecinsException ex)
        {
          Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
        }

        // supprimer un Rv
        Console.WriteLine(string.Format("Suppression du RV  {0}", idRv1));
        dao.SupprimerRv(idRv1);
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
      }
      catch (Exception ex)
      {
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
      }
      //pause 
      Console.ReadLine();
    }

    // méthodes utilitaires - affiche des listes
    public static void DisplayClients(string Message, List<Client> clients)
    {
      Console.WriteLine(Message);
      foreach (Client c in clients)
      {
        Console.WriteLine(c.ShortIdentity());
      }
    }
    public static void DisplayMedecins(string Message, List<Medecin> medecins)
    {
...
    }
    public static void DisplayCreneaux(string Message, List<Creneau> creneaux)
    {
...
    }
    public static void DisplayRvs(string Message, List<Rv> rvs)
    {
...
    }
  }
}
  • ligne 14 : la référence sur la couche [DAO]. Pour rendre le test indépendant de l'implémentation réelle de celle-ci, cette référence est du type de l'interface [IDao] et non du type de la classe [Dao] ;
  • ligne 18 : la couche [DAO] est instanciée par Spring. Nous reviendrons sur la configuration nécessaire pour que cela soit possible. Nous castons la référence d'objet rendue par Spring en une référence du type de l'interface [IDao] ;
  • lignes 21-22 : affichent les clients ;
  • lignes 25-26 : affichent les médecins ;
  • lignes 29-30 : affichent la liste des créneaux du médecin n° 0 ;
  • ligne 33 : affiche les rendez-vous du médecin n° 0 à la date du 23/11/2013. On doit en avoir aucun ;
  • ligne 37 : ajoute un rendez-vous au médecin n° 0 pour le 23/11/2013 ;
  • ligne 39 : affiche les rendez-vous du médecin n° 0 à la date du 23/11/2013. On doit en avoir un ;
  • ligne 46 : on ajoute une seconde fois le même rendez-vous. On doit avoir une exception ;
  • ligne 57 : on supprime l'unique rendez-vous ajouté ;
  • ligne 58 : affiche les rendez-vous du médecin n° 0 à la date du 23/11/2013. On doit en avoir aucun.

III-F-5. Configuration de Spring.net

Dans le programme de test ci-dessus, nous sommes passés rapidement sur l'instruction qui instancie la couche [DAO] :

dao = ContextRegistry .GetContext().GetObject( "rdvmedecinsDao" ) as IDao ;

La classe [ContextRegistry] est une classe de Spring dans l'espace de noms [Spring.Context.Support]. Pour pouvoir utiliser Spring, il nous faut ajouter sa DLL dans les références du projet. Nous procédons de la façon suivante :

Image non disponible
  • en [1], on recherche des paquetages avec l'outil [NuGet] ;
Image non disponible
  • en [2], on cherche des paquetages en ligne ;
  • en [3], on met le mot clé spring dans la zone de recherche ;
  • en [4], les paquetages dont la description contient ce mot clé sont affichées. Ici, c'est [Spring.Core] qui nous convient. On l'installe.

Les références du projet évoluent comme suit :

Image non disponible

Le paquetage [Spring.Core] avait une dépendance sur le paquetage [Common.Logging]. Celui-ci a été chargé également. A ce stade, le projet ne doit plus présenter d'erreurs.

Ce n'est pas pour cela qu'il va marcher. Il nous faut configurer d'abord Spring dans le fichier [App.config]. C'est la partie la plus délicate du projet. Le nouveau fichier [App.config] est le suivant :

[App.config]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    <!-- spring -->
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
    <!-- common logging-->
    <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <!-- Entity Framework -->
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="v11.0" />
      </parameters>
    </defaultConnectionFactory>
  </entityFramework>
  <!-- Chaînes de connexion -->
  <connectionStrings>
    <add name="monContexte" connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </DbProviderFactories>
  </system.data>
  <!-- configuration Spring -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
  </spring>
  <!-- configuration common.logging -->
  <logging>
    <factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
      <arg key="showLogName" value="true" />
      <arg key="showDataTime" value="true" />
      <arg key="level" value="DEBUG" />
      <arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
    </factoryAdapter>
  </logging>
</configuration>

Commençons par enlever tout ce qui est déjà connu : Entity Framework, chaînes de connexion, ProviderFactory. Le fichier évolue comme suit :

[App.config]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" ... />
    <!-- spring -->
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
    <!-- common logging-->
    <sectionGroup name="common">
      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
    </sectionGroup>
  </configSections>
...
  <!-- configuration Spring -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
  </spring>
  <!-- configuration common.logging -->
  <common>
    <logging>
      <factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
        <arg key="showLogName" value="true" />
        <arg key="showDataTime" value="true" />
        <arg key="level" value="DEBUG" />
        <arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
      </factoryAdapter>
    </logging>
  </common>
</configuration>
  • lignes 3-15 : définissent des sections de configuration ;
  • ligne 8 : définit la classe qui va gérer la section <spring><context> du fichier XML (lignes 19-21) ;
  • ligne 9 : définit la classe qui va gérer la section <spring><objects> du fichier XML (lignes 22-24) ;
  • ligne 13 : définit la classe qui va gérer la section <common><logging> du fichier XML (lignes 27-36) ;
  • lignes 7-14 : sont stables. N'ont pas à être changées dans un autre projet ;
  • lignes 18-25 : configuration Spring. Est stable sauf pour les lignes 22-24 qui définissent les objets que Spring sera amené à instancier ;
  • ligne 23 : définition d'un objet. L'attribut id est libre. C'est l'identifiant de l'objet. L'attribut type désigne la classe à instancier sous la forme " nom complet de la classe, Assembly qui contient la classe". La classe ici est celle qui implémente la couche [DAO] : [RdvMedecins.Dao.Dao]. Pour connaître son assembly, il faut regarder les propriétés du projet :
Image non disponible

En [1], le nom de l'assembly à fournir ;

  • lignes 27-36 : la configuration de " Common Logging " est stable. On peut être amenés à modifier le niveau d'information, ligne 32. Après la phase de débogage, on peut passer le niveau à INFO.

Au final, complexe au premier abord, le fichier de configuration de Spring s'avère simple. Il n'y a à modifier que :

  • les lignes 22-24 qui définissent les objets à instancier ;
  • ligne 32 : le niveau de logs.

Dans le programme de test l'instruction qui instancie la couche [DAO]est la suivante :

dao = ContextRegistry .GetContext().GetObject( "rdvmedecinsDao" ) as IDao ;

[ContextRegistry] est une classe de Spring qui exploite la configuration de Spring faite dans un fichier [Web.config] ou [App.config]. Ici, elle va exploiter la section suivante du fichier [App.config] :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
</spring>
  • ContextRegistry .GetContext() exploite le contexte des lignes 2-4. La ligne 3 signifie que les objets Spring sont définis dans la section [spring/objects] du fichier de configuration. Cette section est lignes 5-7 ;
  • ContextRegistry .GetContext().GetObject( "rdvmedecinsDao" ) exploite la section des lignes 5-7. Elle ramène une référence sur l'objet qui a l'attribut id= "rdvmedecinsDao " . C'est l'objet défini ligne 6. Spring va alors instancier la classe définie par l'attribut type en utilisant son constructeur sans paramètres. Celui-ci doit donc exister. Ceci fait, la référence de l'objet créé est rendue au code appelant. Si l'objet est demandé une seconde fois dans le code, Spring se contente de rendre une référence sur le premier objet créé. C'est le modèle de conception (Design Pattern) appelé singleton .

La construction de l'objet peut être plus complexe. On peut utiliser un constructeur avec paramètres ou préciser l'initialisation de certains champs de l'objet une fois celui-ci créé. Pour plus d'informations sur ce sujet, on pourra lire l'article " Tutoriel Spring IOC pour .NET ", à l'URL [http://tahe.developpez.com/dotnet/springioc/].

Ceci fait, nous pouvons exécuter l'application. Les résultats écran sont les suivants :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
Liste des clients :
Client[35,Mr,Jules,Martin,00000118981]
Client[36,Mme,Christine,German,00000118982]
Client[37,Mr,Jules,Jacquard,00000118983]
Client[38,Melle,Brigitte,Bistrou,00000118984]
Liste des médecins :
Medecin[26,Mme,Marie,Pelissier,00000118985]
Medecin[27,Mr,Jacques,Bromard,000001189110]
Medecin[28,Mr,Philippe,Jandot,000001189123]
Medecin[29,Melle,Justine,Jacquemot,000001189124]
Liste des créneaux horaires du médecin Medecin[26,Mme,Marie,Pelissier,00000118985]
Creneau[218,8,0,8,20, 26, 00000118986]
Creneau[219,8,20,8,40, 26, 00000118987]
Creneau[220,8,40,9,0, 26, 00000118988]
Creneau[221,9,0,9,20, 26, 00000118989]
Creneau[222,9,20,9,40, 26, 00000118990]
Creneau[223,9,40,10,0, 26, 00000118991]
Creneau[224,10,0,10,20, 26, 00000118992]
Creneau[225,10,20,10,40, 26, 00000118993]
Creneau[226,10,40,11,0, 26, 00000118994]
Creneau[227,11,0,11,20, 26, 00000118995]
Creneau[228,11,20,11,40, 26, 00000118996]
Creneau[229,11,40,12,0, 26, 00000118997]
Creneau[230,14,0,14,20, 26, 00000118998]
Creneau[231,14,20,14,40, 26, 00000118999]
Creneau[232,14,40,15,0, 26, 000001189100]
Creneau[233,15,0,15,20, 26, 000001189101]
Creneau[234,15,20,15,40, 26, 000001189102]
Creneau[235,15,40,16,0, 26, 000001189103]
Creneau[236,16,0,16,20, 26, 000001189104]
Creneau[237,16,20,16,40, 26, 000001189105]
Creneau[238,16,40,17,0, 26, 000001189106]
Creneau[239,17,0,17,20, 26, 000001189107]
Creneau[240,17,20,17,40, 26, 000001189108]
Creneau[241,17,40,18,0, 26, 000001189109]
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :
Ajout d'un RV au médecin Medecin[26,Mme,Marie,Pelissier,00000118985] avec client  Client[35,Mr,Jules,Martin,00000118981] le 23/11/2013
Rdv ajouté
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :
Rv[28,23/11/2013 00:00:00,35,218,00000289145]
Ajout d'un RV dans un créneau déjà occupé
L'erreur suivante s'est produite : RdvMedecinsException[7,AjouterRv,Une erreur s'est produite lors de la mise à jour des entrées. Pour plus d'informations, consultez l'exception interne.]
Suppression du RV n° 28
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :

Les résultats sont conformes à ce qui était attendu. On considèrera désormais que notre couche [DAO] est valide. Le tutoriel pourrait s'arrêter là. Nous avons montré jusqu'à maintenant :

  • les bases de l'ORM Entity Framework 5 ;
  • une couche [DAO] utilisant cet ORM.

Rappelons notre étude de cas décrite au début de ce document. Nous partons d'une application existante à l'architecture suivante :

Image non disponible

que nous voulons transformer en celle-ci :

Image non disponible

où EF5 a remplacé NHibernate. Nous venons de construire la couche [DAO2]. En fait elle ne présente pas la même interface que la couche [DAO1] dont l'interface était plus réduite :

[DAO1]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
  public interface IDao
  {
    // liste des clients
    List<Client> GetAllClients();
    // liste des médecins
    List<Medecin> GetAllMedecins();
    // liste des créneaux horaires d'un médecin
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // liste des RV d'un médecin donné, un jour donné
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // ajouter un RV
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // supprimer un RV
    void SupprimerRv(int idRv);
  }

La couche [DAO2] a rajouté à cette interface la méthode :

 
Sélectionnez
1.
2.
// trouver une entité T via sa clé primaire
T Find<T>(int id) where T : class;

L'ajout de cette méthode vient du fait que l'ORM EF 5 travaille par défaut en mode Lazy Loading. Les entités arrivent dans la couche [ASP.NET] sans leurs dépendances. La méthode ci-dessus nous permet de les récupérer si on en a besoin et dans certains cas on en a besoin. NHibernate travaille également par défaut en mode Lazy Loading mais je l'avais utilisé en mode Eager Loading. Les entités arrivaient dans la couche [ASP.NET] avec leurs dépendances.

Nous allons terminer le portage de l'application ASP.NET / NHibernate vers l'application ASP.NET / EF 5. Mais comme cela ne concerne plus EF5, nous ne commenterons pas le code web. Nous expliquerons simplement comment mettre en place l'application web et la tester. Celle-ci est disponible sur le site de ce tutoriel.

III-F-6. Génération de la DLL de la couche [DAO]

Dans l'architecture suivante :

Image non disponible

la couche [ASP.NET] aura à sa disposition les couches à sa droite sous la forme de DLL. Nous construisons donc la DLL de la couche [DAO].

Image non disponible
  • en [1], on sélectionne le programme de test et en [2] on ne l'inclut pas dans la DLL qui va être générée ;
  • en [3], dans les propriétés du projet, on indique que l'assembly à créer est une DLL ;
  • en [4], dans le menu de VS, on indique qu'on va générer un assembly de type [Release] qui contient moins d'informations qu'un assembly de type [Debug] ;
Image non disponible
  • en [5], on régénère l'assembly du projet. La DLL va être générée ;
  • en [6], on fait afficher tous les fichiers du projet ;
Image non disponible
  • en [7], la DLL du projet de la couche [DAO]. C'est celle-ci que le projet web ASP.NET utilisera ;
  • en [8], nous rafraîchissons l'affichage du projet ;
Image non disponible
  • en [9], les DLL du dossier [Release] sont rassemblées dans un dossier [lib] externe [10]. C'est là que le projet web ira chercher ses références.

III-F-7. La couche [ASP.NET]

Nous allons ici expliquer le portage de l'application [ASP.NET / NHibernate] vers l'application [ASP.NET / EF 5]. Nous allons travailler avec Visual Studio Express 2012 pour le web disponible gratuitement à l'URL [http://www.microsoft.com/visualstudio/fra/downloads].

Nous allons travailler à partir du projet web existant créé avec VS 2010.

Image non disponible
  • en [1], on ouvre le projet existant :
  • en [2], le projet chargé a les références suivantes [3] :
  • [NHibernate] est la DLL du framework NHibernate,
  • [Spring.Core] est la DLL du framework Spring.net,
  • [log4net] est la DLL du framework de logs log4net. Ce framework est utilisé par Spring.net,
  • [MySql.Data] est le pilote ADO.NET du SGBD MySQL,
  • [rdvmedecins] est la DLL de la couche [DAO] construite avec NHibernate ;
  • en [4], nous changeons le nom du projet et en [5], nous supprimons les références précédentes ;
Image non disponible
  • en [6], nous ajoutons des références au projet ;
  • en [7], dans l'assistant nous utilisons l'option [Parcourir] ;
Image non disponible
  • en [8], nous sélectionnons toutes les DLL du projet n° 2 mises précédemment dans le dossier [lib] ;
  • en [9], un récapitulatif que nous validons ;
  • en [10], le projet web avec ses nouvelles références.

Ceci fait, le projet se présente de la façon suivante :

Image non disponible
  • en [1], le code de gestion des pages web est réparti sur les deux fichiers [Global.asax] et [Default.aspx]. Du code utilitaire a été placé dans le dossier [Entites]. Enfin l'application est configurée par le fichier [Web.config] ;
  • en [2], nous générons l'assembly du projet ;
  • en [3], des erreurs apparaissent.

Examinons les erreurs, par exemple la suivante :

Image non disponible

et son explication :

Image non disponible

Le type de [medecin.Id] est int? alors que la méthode [GetCreneauxMedecin] est de type int . Il faut donc un cast . Cette erreur est récurrente dans tout le code car les entités du projet ASP.NET / NHibernate avaient des clés primaires de type int alors que celles du projet ASP.NET / EF 5 sont de type int? . On corrige toutes les erreurs de ce type et on régénère le projet. Il n'y en alors plus.

Il nous reste un détail à régler avant d'exécuter le projet : l'instanciation de la couche [DAO] par le framework Spring. Celle-ci est faite dans [Global.asax] :

[Global.asax]
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
protected void Application_Start(object sender, EventArgs e)
    {
      // on met en cache certaines données de la base de données
      try
      {
        // instanciation couche [dao]
        Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
        ...
      }
      catch (Exception ex)
      {...
      }
    }

Dans le programme de test de la couche [DAO], celui-ci instanciait la couche [DAO] de la façon suivante :

dao = ContextRegistry .GetContext().GetObject( "rdvmedecinsDao" ) as IDao ;

Les deux méthodes sont identiques. On se rappelle que cette instanciation de la couche [DAO] s'appuyait sur une configuration faite dans [App.config]. On remplace alors le contenu actuel [Web.config] du projet web par celui de [App.config] du projet de la couche [DAO] afin d'avoir la même configuration.

Nous sommes prêts pour une première exécution. La page d'accueil est affichée [1] :

Image non disponible
  • en [2], on entre un jour de rendez-vous et on valide ;
Image non disponible
  • en [3], une erreur.

Lorsqu'on examine le texte d'erreur affiché par la page, on s'aperçoit que l'exception signalée est celle du Lazy Loading  : on a essayé de charger une dépendance d'un objet alors que le contexte de persistance qui le gère a été fermé. L'objet est maintenant dans un état "  détaché  ". Cette erreur est due au fait que par que NHibernate avait été utilisé en mode Eager Loading alors que EF 5 travaille par défaut en Lazy Loading . Sur la ligne en rouge ci-dessus :

  • rdv représente un objet [Rv] qui a été chargé sans ses dépendances ;
  • pour évaluer rdv.Creneau.Id , l'application essaie de charger la dépendance rdv.Creneau . Mais comme on n'est plus dans le contexte, ce n'est pas possible, d'où l'exception.

Ici, la solution est simple. Ligne 108, On crée une entrée dans un dictionnaire avec pour clé la clé primaire du créneau d'un rendez-vous. Or il se trouve que l'entité [Rv] encapsule la clé primaire du créneau associé. On écrit donc :

dicoRvPris[( int )rdv.CreneauId] = rdv;

Nous réessayons l'exécution. Cette fois-ci l'erreur est la suivante :

Image non disponible

L'erreur est analogue. Ligne 132, on essaie de charger la dépendance [Client] d'un objet [Rv] dans la couche ASP.NET, donc hors contexte. Il faut aller chercher l'objet [Client] en base. C'est pour remédier à ce problème, que l'interface [IDao] a été enrichie de la méthode suivante :

// trouver une entité T via sa clé primaire

T Find<T>( int id) where T : class ;

Elle va permettre d'aller chercher les dépendances. Ainsi la ligne erronée ci-dessus va être réécrite de la façon suivante :

Client client = Global .Dao.Find< Client >(agenda.Creneaux[i].Rdv.ClientId);

De nouveau on notera l'intérêt que les entités embarquent leurs clés étrangères. Ici, l'entité [Rv] nous donne accès à la clé étrangère de la dépendance [Creneau] associée. Ces deux corrections faites, l'application marche. Le lecteur est invité à tester l'application [RdvMedecins-SqlServer-03] présente dans les téléchargements des exemples du site web de cet article.

III-G. Conclusion

Nous avons mené à bien le portage d'une application ASP.NET / NHibernate :

Image non disponible

vers une application ASP.NET / EF 5 :

Image non disponible

Alors que cette architecture aurait du nous permettre de garder intacte la couche [ASP.NET], nous avons du la modifier pour deux raisons :

  • les entités n'étaient pas exactement les mêmes. Le type des clés primaires des entités NHibernate était int alors que celui de EF 5 était int? . Cela nous a amenés à introduire des cast dans le code web ;
  • le mode de chargement des entités n'était pas le même pour les deux ORM : Eager Loading pour NHibernate, Lazy loading pour EF 5. Cela nous a amenés à enrichir l'interface de la couche [DAO] avec une méthode générique permettant d'aller chercher une entité via sa clé primaire.

Néanmoins, le portage s'est révélé plutôt simple justifiant de nouveau, si besoin était, l'architecture en couches et l'injection de dépendances avec Spring ou un autre framework d'injection de dépendances.

Nous allons mesurer maintenant l'impact d'un changement de SGBD sur l'architecture précédente. Nous allons porter l'ensemble des projets précédents vers quatre autres SGBD :

  • Oracle Database Express Edition 11 g Release 2 ;
  • MySQL 5.5.28 ;
  • PostgreSQL 9.2.1 ;
  • Firebird 2.1.

Les codes ne vont plus changer. Seuls les éléments suivants vont changer :

  • la définition dans les entités du champ utilisé pour contrôler la concurrence d'accès à une entité ;
  • les fichiers de configuration [App.config] ou [Web.config] ;

Nous ne commenterons que les éléments qui changent.


précédentsommairesuivant

  

Licence Creative Commons
Le contenu de cet article est rédigé par Serge Tahé et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2012 Developpez.com.