Skip to content

3. Caso de estudio con SQL Server Express 2012

3.1. Introduction

La mayoría de los ejemplos que se encuentran en Internet sobre Entity Framework son ejemplos con SQL Server. Esto es bastante normal. Es probable que sea el SGBD más utilizado en el mundo .NET de las empresas. Vamos a seguir esta tendencia. Posteriormente, los ejemplos se ampliarán a todas las bases de datos mencionadas en el párrafo 1.2.

3.2. Instalación de las herramientas

No describiremos la instalación de las herramientas. De hecho, esto requeriría una gran cantidad de capturas de pantalla que se vuelven obsoletas con bastante rapidez. Es una tarea (que, es cierto, no siempre es fácil) que dejamos en manos del lector.

Debemos instalar las siguientes herramientas:

  • el SGBD SQL Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. Descargue la versión «With Tools», que incluye, junto con el SGBD, una herramienta de administración:
 

Una vez instalado el SGBD, lo ejecutamos:

  • [1]: en el menú Inicio, inicia el «Administrador de configuración del servidor SQL»;
  • [2]: en este administrador, inicia el servidor;
  • [3]: ya está en ejecución.

Ahora iniciamos la herramienta de administración de SQL Server:

  • [1]: en el menú Inicio, inicia «SQL Server Management Studio»;
  • [2]: la herramienta de administración.

Nos conectaremos al servidor:

  • en [1], abrimos el explorador de objetos;
  • en [2], se ingresan los parámetros de conexión:
  • [3]: el servidor (local) (cuidado con los paréntesis necesarios) se refiere al servidor instalado en la máquina,
  • [4]: se elige la autenticación de Windows. Es necesario ser administrador de la computadora para que esta conexión sea exitosa,
  • [5]: se inicia sesión;
  • [6]: ya estamos conectados;
  • [7]: se desea modificar ciertas propiedades del servidor;
  • [8]: se solicita que haya dos modos de autenticación:
  • autenticación de Windows, tal como se acaba de utilizar. Un usuario de Windows con los derechos adecuados puede entonces conectarse,
  • autenticación del servidor SQL. El usuario debe formar parte de los usuarios registrados en el SGBD;

Una vez hecho esto, se pueden validar las propiedades del servidor;

  • [9]: se editan las propiedades del usuario «sa» (administrador del sistema);
  • en [10], se le asigna una contraseña. En el resto del documento, esta es sqlserver2012;
  • en [10], se le otorga permiso para conectarse;
  • en [11], se activa la conexión. Con esto, se puede validar el asistente;
  • en [12], nos desconectamos del servidor.

Ahora, nos volvemos a conectar con el nombre de usuario sa/sqlserver2012:

  • en [1], nos volvemos a conectar;
  • en [2], en la autenticación SQL Server;
  • en [3], el usuario es sa;
  • en [4], su contraseña es sqlserver2012;
  • en [5], se inicia sesión;
  • en [6], ya estamos conectados.

Ahora vamos a crear una base de datos de demostración:

  • en [1], creamos una nueva BD;
  • en [2], se llamará «demo»;
  • en [3], validamos;
  • en [4], se crea la base de datos;
  • en [5], se crea una nueva tabla en la base de datos demo;
  • en [6], se define una tabla con dos columnas: ID y NOM;
  • en [7], se establece la columna [ID] como clave primaria;
  • en [8], la clave primaria se representa con un símbolo de llave;
  • en [9], se guarda la tabla;
  • en [10], se le asigna un nombre;
  • en [11], para que la tabla aparezca en la base [demo], hay que actualizar la base;
  • en [12], la tabla [PERSONNES] se ha creado correctamente.

Por ahora, ya sabemos lo suficiente sobre cómo usar la herramienta de administración de SQL Server.

3.3. El servidor integrado (localdb)\v11.0

VS Express 2012 viene con un servidor SQL integrado. Se supone aquí que se ha instalado VS Express 2012 [http://www.microsoft.com/visualstudio/fra/downloads]. Se inicia VS 2012 [1]:

Se inicia la herramienta de administración de SQL Server 2012 [2] y se inicia sesión en [3].

  • en [4], nos conectamos al servidor (localdb)\v11.0;
  • en [5], con autenticación de Windows;
  • en [6], una vez establecida la conexión, se muestran las bases de datos del servidor. Al igual que antes, se podría crear una nueva base de datos.

No utilizaremos este servidor integrado en VS 2012.

3.4. Creación de la base de datos a partir de las entidades

Entity Framework 5 Code First permite crear una base de datos a partir de entidades. Eso es lo que veremos ahora. Con VS Express 2012, creamos un primer proyecto de consola en C#:

  • en [1], la definición del proyecto;
  • en [2], el proyecto creado.

Todos nuestros proyectos necesitarán el archivo e DLL de Entity Framework 5. Lo agregamos:

  • en [1], la herramienta NuGet permite descargar las dependencias;
  • en [2], se descarga la dependencia de Entity Framework;
  • en [3], la referencia se ha agregado al proyecto.

Para obtener más información, consulta las propiedades de la referencia agregada:

  • en [1], la versión de DLL. Se requiere la versión 5;
  • en [2], su ubicación en el sistema de archivos: <solución>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll donde <solución> es la carpeta de la solución VS. Todos los paquetes agregados por NuGet se guardarán en la carpeta <solución>/packages;
  • en [3], se creó un archivo [packages.config]. Su contenido es el siguiente:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>

Enumera los paquetes importados por NuGet.

Volvamos al proyecto VS y creemos una carpeta [Models] dentro del proyecto:

  • en [1], se agrega una carpeta al proyecto;
  • en [2], se llamará [Models].

Seguiremos con esta práctica de colocar la definición de nuestras entidades en la carpeta [Models].

Para crear nuestras entidades, nos basaremos en la definición de la base de datos MySQL 5 utilizada en el proyecto NHibernate. Recordemos la función de las entidades EF:

Las entidades deben reflejar las tablas de la base de datos. La capa de acceso a los datos utiliza estas entidades en lugar de trabajar directamente con las tablas. Comencemos con la tabla [MEDECINS]:

3.4.1. La entidad [Medecin]

Contiene información sobre los médicos que administra la aplicación [RdvMedecins].

  • ID: número que identifica al médico —clave primaria de la tabla
  • VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
  • NOM: el apellido del médico
  • PRENOM: su nombre
  • TITRE: su título (Srta., Sra., Sr.)

Podríamos partir de la siguiente clase [Medecin]:


using System;

[Table("MEDECINS", Schema = "dbo")]
  namespace RdvMedecins.Entites
{
  public class Medecin
  {
    // datos
    public int Id { get; set; }
    public string Titre { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
}
  • línea 3: la clase [Medecin] está asociada a la tabla [MEDECINS] de la base de datos. Esta se encontrará en un esquema llamado «dbo».

Colocamos esta clase en un archivo [Entites.cs] [1]. Ahí es donde colocaremos todas nuestras entidades.

También en la carpeta [Models], creamos el siguiente archivo [Context.cs]:


using System.Data.Entity;
using RdvMedecins.Entites;

namespace RdvMedecins.Models
{

  // el contexto
  public class RdvMedecinsContext : DbContext
  {
    // los médicos
    public DbSet<Medecin> Medecins { get; set; }
  }

  // inicialización de la base de datos
  public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
  {
  }
}
  • línea 8: la clase [RdvMedecinsContext] representará el contexto de persistencia, c.-à-d. el conjunto de entidades administradas por ORM. Debe derivarse de la clase [System.Data.Entity.DbContext];
  • línea 11: el campo [Medecins] representará las entidades de tipo [Medecin] del contexto de persistencia. Es de tipo DbSet<Médico>. Por lo general, habrá tantos [DbSet] como tablas en la base de datos, uno por tabla;
  • línea 15: se define una clase [RdvMedecinsInitializer] para inicializar la base de datos creada. Aquí deriva de la clase [DropCreateDataBaseAlways], que, como su nombre lo indica, elimina la base de datos si ya existe y luego la vuelve a crear. Esto resulta práctico durante la fase de desarrollo de la clase BD. El parámetro de la clase [DropCreateDataBaseAlways] es el tipo de contexto de persistencia asociado a la base de datos. Se pueden utilizar otras clases padre distintas de [DropCreateDataBaseAlways] para la clase de inicialización:
  • [DropCreateDatabaseIfModelChanges]: recrea la base de datos si las entidades han cambiado,
  • [CreateDatabaseIfNotExists]: crea la base de datos si no existe;

Ahora nos queda crear un programa principal. Será el siguiente: [CreateDB_01.cs]:


using System;
using System.Data.Entity;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class CreateDB_01
  {
    static void Main(string[] args)
    {
      // se crea la base de datos
      Database.SetInitializer(new RdvMedecinsInitializer());
      using (var context = new RdvMedecinsContext())
      {
        context.Database.Initialize(false);
      }
    }
  }
}
  • línea 12: [System.Data.Entity.DataBase] es una clase que ofrece métodos estáticos para administrar la base de datos asociada a un contexto de persistencia. El método estático [SetInitializer] permite especificar la clase de inicialización de la base de datos. Esto no inicia la inicialización;
  • línea 13: para trabajar con un contexto de persistencia, es necesario instanciarlo. Eso es lo que se hace aquí. Se utiliza una cláusula «using» para que el contexto se cierre automáticamente al salir de la cláusula. Por lo tanto, en la línea 17, el contexto se cierra;
  • línea 15: se inicia explícitamente la generación de la base de datos asociada al contexto de persistencia [RdvMedecinsContext]. El parámetro false indica que esta operación no debe realizarse si ya se ha llevado a cabo para este contexto. Aquí, igualmente se podría haber utilizado true.

Cuando se trabaja con una base de datos, los parámetros de conexión suelen registrarse en el archivo [App.config]. Observemos que, por el momento, no se encuentran allí:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Para obtener más información sobre la configuración de Entity Framework, visita 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>

Los elementos anteriores se registraron en [App.config] cuando se agregó la dependencia de Entity Framework a las referencias del proyecto.

Ejecutemos el proyecto (Ctrl-F5) después de haber iniciado SQL Server Express (esto es importante):

La ejecución debe finalizar sin errores. Ahora abramos la herramienta de administración de SQL Server y actualicemos la pantalla:

Se observa que se ha creado una base de datos con el nombre completo de la clase [RdvMedecinsContext] y que contiene una tabla [dbo.MEDECINS] (ese es el nombre que le habíamos dado) con columnas que reflejan los nombres de los campos de la entidad [Medecin]. Si el código se ejecutó correctamente y la base de datos mencionada anteriormente no aparece, hay que revisar el servidor integrado (localdb)\v11.0 (véase la página 19). Con VS 2012 Pro, este servidor se utiliza si el servidor SQL no está activo en el momento de ejecutar el código. Con VS 2012 Express, no.

Analicemos la estructura de la tabla [MEDECINS]:

  • incluye los nombres de los campos de la entidad [Medecin];
  • la columna [Id] es la clave primaria. Esta es una convención de EF: si la entidad E tiene un campo Id o Eid (MedecinId), entonces esta columna es la clave primaria en la tabla asociada;
  • los tipos de las columnas de la tabla son los mismos que los de los campos de la entidad;
  • para las columnas Título, Apellido y Nombre, se utilizó un tipo [nvarchar(max)]. Se podría ser más preciso: 5 caracteres para el título, 30 para el apellido y el nombre;
  • las columnas Título, Apellido y Nombre pueden tener el valor NULL. Vamos a cambiar eso.

Veamos las propiedades de la clave primaria [Id]:

En [1], vemos que la clave primaria es de tipo [Identité], lo que significa que su valor es generado automáticamente por el servidor SQL. Adoptaremos esta estrategia con todos los SGBD.

Vamos a reducir la dependencia de las convenciones de EF mediante el uso de anotaciones. El código de la entidad en [Entites.cs] queda así:


using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RdvMedecins.Entites
{
  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin
  {
    // datos
    [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; }
    [Required]
    [Column("VERSION")]
    public int Version { get; set; }
  }
}
  • líneas 2 y 3: las anotaciones se encuentran en los espacios de nombres [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength) y [System.ComponentModel.DataAnnotations.Schema] (Column). Se pueden encontrar otras anotaciones en URL y [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
  • línea 11: [Key] designa la clave primaria;
  • línea 12: [Column] establece el nombre de la columna correspondiente al campo;
  • línea 14: [Required] indica que el campo es obligatorio (SQL, NOT, NULL);
  • línea 15: [MaxLength] establece el tamaño máximo de la cadena de caracteres, [MinLength] su tamaño mínimo;

Ejecutemos el proyecto con esta nueva definición de la entidad [Medecin]. La base de datos creada es entonces la siguiente:

 
  • las columnas tienen el nombre que les hemos asignado;
  • la anotación [Required] se ha traducido como SQL, NOT y NULL;
  • la anotación [MaxLength(N)] se tradujo a un tipo SQL nvarchar(N).

En la aplicación NHibernate, la columna [VERSION] servía para evitar accesos concurrentes a una misma fila de una tabla. El principio es el siguiente:

  • un proceso P1 lee una fila L de la tabla [MEDECINS] en el momento T1. La fila tiene la versión V1;
  • un proceso P2 lee la misma fila L de la tabla [MEDECINS] en el momento T2. La fila tiene la versión V1 porque el proceso P1 aún no ha validado su modificación;
  • el proceso P1 valida su modificación de la línea L. La versión de la línea L pasa entonces a V2 = V1 + 1;
  • el proceso P2 valida su modificación de la línea L. El proceso ORM lanza entonces una excepción porque el proceso P2 tiene una versión V1 de la línea L diferente de la versión V2 encontrada en la base de datos.

A esto se le llama gestión optimista de accesos concurrentes. Con EF 5, un campo que desempeña esta función debe tener uno de los dos atributos: [Timestamp] o [ConcurrencyCheck]. SQL Server tiene un tipo [timestamp]. En una columna con este tipo, SQL Server genera automáticamente su valor cada vez que se inserta o modifica una fila. Dicha columna puede utilizarse entonces para gestionar la concurrencia de acceso. Retomando el ejemplo anterior, el proceso P2 encontrará un timestamp diferente al que leyó, ya que, mientras tanto, la modificación realizada por el proceso P1 lo habrá alterado.

Nuestra entidad [Medecin] evoluciona de la siguiente manera:


using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
  {
    // datos
    [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; }
  }
}
  • líneas 26-28: la nueva columna con el atributo [Timestamp] de la línea 27. El tipo del campo debe ser byte[] (línea 28). El nombre del campo puede ser cualquiera. No se le asigna el atributo [Required] porque no es la aplicación la que proporcionará este valor, sino el propio SGBD.

Si se ejecuta el proyecto con esta nueva entidad, la base de datos se modifica de la siguiente manera:

Nos queda por resolver un último punto. El contexto de persistencia “sabe” que una entidad debe ser insertada en la base de datos porque, en ese momento, su clave primaria es nula. Es la inserción en la base de datos la que le dará un valor a la clave primaria. En este caso, el tipo int asignado a la clave primaria [Id] no es adecuado porque este tipo no acepta el valor null. ¿Entonces se le asigna el tipo int?, que acepta los valores int más el puntero null? La entidad [Medecin] que se utilizará será, por lo tanto, la siguiente:


public class Medecin
  {
    // datos
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    ...

Nos queda por ver cómo representar en una entidad el concepto de clave externa entre tablas.

3.4.2. La entidad [Creneau]

La tabla [CRENEAUX] enumera los intervalos de tiempo en los que son posibles los RV:

  • ID: número que identifica el intervalo de tiempo —clave primaria de la tabla
  • VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
  • ID_MEDECIN: número que identifica al médico al que pertenece este horario – clave externa en la columna MEDECINS (ID).
  • HDEBUT: hora de inicio de la franja horaria
  • MDEBUT: minutos de inicio del horario
  • HFIN: hora de finalización del horario
  • MFIN: minutos de fin de la franja

La segunda línea de la tabla [CRENEAUX] (véase [1] más arriba) indica, por ejemplo, que el horario n.º 2 comienza a las 8:20 y termina a las 8:40, y pertenece a la doctora n.º 1 (la Sra. Marie PELISSIER).

Con lo que sabemos, podemos definir la entidad [Creneau] de la siguiente manera en [Entites.cs]:


[Table("CRENEAUX", Schema = "dbo")]
  public class Creneau
  {
    // datos
    [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 única novedad se encuentra en las líneas 20 y 21. El hecho de que la tabla [CRENEAUX] tenga una clave externa en la tabla [MEDECINS] se refleja en la entidad [Creneau] mediante la presencia de una referencia a la entidad [Medecin], línea 21. El nombre del campo no importa; lo único importante es el tipo. La propiedad debe declararse virtual con la palabra clave virtual. De hecho, EF debe redefinir todas las propiedades denominadas «navegacionales», es decir, aquellas que corresponden a una clave externa y que permiten pasar de una tabla a otra.

Para probar la nueva entidad, debemos realizar algunas modificaciones en [Context.cs]:


using System.Data.Entity;
using RdvMedecins.Entites;

namespace RdvMedecins.Models
{

  // el contexto
  public class RdvMedecinsContext : DbContext
  {
    // las entidades
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
  }

  // inicialización de la base
  public class RdvMedecinsInitializer :  DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
  }
}

La línea 12 refleja que el contexto tiene una entidad más que gestionar. Al ejecutar el proyecto, obtenemos la siguiente base de datos nueva:

La tabla [CRENEAUX] se ha creado correctamente y la novedad es la presencia de una clave externa [1] y [2]. Su nombre se generó a partir del nombre del campo correspondiente en la entidad (Médico), al que se le añadió el sufijo «_Id». Para conocer las propiedades de esta clave externa, intentamos modificarla a [3].

La captura de pantalla anterior muestra que [Medecin_Id] es una clave externa de la tabla [CRENEAUX] y que hace referencia a la clave primaria [ID] de la tabla [MEDECINS].

Si creamos las entidades para una base de datos existente, la columna de clave externa no se llamará necesariamente [Medecin_Id]. Para las demás columnas, ya habíamos visto que la anotación [Column] resolvía este problema. Curiosamente, es más complicado en el caso de una clave externa. Hay que proceder de la siguiente manera:


public class Creneau
  {
    // datos
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
}
  • líneas 5-7: se crea un campo del tipo de la clave externa (int). Mediante el atributo [Column], se especifica el nombre de la columna que será la clave externa en la tabla asociada a la entidad;
  • línea 9: se agrega la anotación [ForeignKey] al campo de tipo [Medecin]. El argumento de esta anotación es el nombre del campo (no de la columna) que está asociado a la columna de clave externa de la tabla.

Al ejecutar el proyecto, esta vez se crea la siguiente tabla:

Como se puede ver arriba, la columna de clave externa lleva el nombre que le hemos dado. Cabe señalar que los campos:


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

solo han dado lugar a una única columna, la columna [MEDECIN_ID]. Sin embargo, la presencia del campo [MedecinId] es importante. Al leer una fila de la tabla [CRENEAUX], esta recibirá el valor de la columna [MEDECIN_ID], es decir, el valor de la clave externa de la tabla [MEDECINS]. Esto suele ser útil.

El campo [Medecin] anterior refleja la relación de varios a uno que vincula la entidad [Creneau] con la entidad [Medecin]. Varios objetos [Creneau] están vinculados a un mismo [Medecin]. La relación inversa, en la que un objeto [Medecin] está asociado a varios objetos [Creneau], puede modelarse mediante un campo adicional en la entidad [Medecin]:


public class Medecin
  {
    // datos
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    ...
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

En la línea 8, se ha agregado el campo [Creneaux], que es una colección de objetos [Creneau]. Este campo nos dará acceso a todos los horarios disponibles del médico.

Al volver a ejecutar el proyecto, observamos que la tabla [MEDECINS] no ha cambiado:

 

No se ha agregado ninguna columna. La relación de clave externa que existe entre la tabla [CRENEAUX] y la tabla [MEDECINS] es suficiente para que EF pueda generar los campos relacionados con ella:


  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; }
    ...
  }

Ya sabemos lo esencial. Podemos terminar con la creación de las otras dos entidades.

3.4.3. Las entidades [Client] y [Rv]

Con lo que hemos aprendido, podemos escribir las entidades [Client] y [Rv]. La entidad [Client] contiene información sobre los clientes administrados por la aplicación [RdvMedecins].

  • ID: número que identifica al cliente —clave primaria de la tabla
  • VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la fila.
  • NOM: el nombre del cliente
  • PRENOM: su nombre
  • TITRE: su tratamiento (Srta., Sra., Sr.)

La entidad [Client] podría ser la siguiente:


  [Table("CLIENTS", Schema = "dbo")]
  public class Client
  {
    // datos
    [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; }
    // las citas del cliente
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

La clase [Client] es casi idéntica a la clase [Medecin]. Se podrían derivar de una misma clase padre. La novedad está en la línea 21. Refleja el hecho de que un cliente puede tener varias citas y se deriva de la presencia de una clave externa de la tabla [RVS] a la tabla [CLIENTS].

La entidad [Rv] representa una cita:

  • ID: número que identifica de manera única a RV – clave primaria
  • JOUR: día del RV
  • ID_CRENEAU: horario de la cita del RV —clave externa en la columna [ID] de la tabla [CRENEAUX]—; determina tanto el horario de la cita como el médico correspondiente.
  • ID_CLIENT: número del cliente para quien se realiza la reservación – clave externa en la columna [ID] de la tabla [CLIENTS]

La entidad [Rv] podría ser la siguiente:


[Table("MEDECINS", Schema = "dbo")]
  public class Rv
  {
    // datos
    [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; }
}
  • líneas 5-7: clave primaria;
  • líneas 8-10: fecha de la cita;
  • líneas 11-12: la clave externa de la tabla [RVS] hacia la tabla [CLIENTS];
  • líneas 13-15: el cliente que tiene la cita;
  • líneas 16-17: la clave externa de la tabla [RVS] hacia la tabla [CRENEAUX];
  • líneas 18-20: el horario de la cita;
  • líneas 21-23: el campo de gestión de accesos concurrentes.

En la línea 17, se observa una relación de varios a uno: a un horario pueden corresponder varias citas (no en el mismo día). La relación inversa puede reflejarse en la entidad [Creneau]:


public class Creneau
  {
    // las citas de la franja horaria
    public ICollection<Rv> Rvs { get; set; }
    ...
}

Línea 4: la colección de citas programadas en ese horario.

Al ejecutar el proyecto, la base de datos generada es la siguiente:

 

Las tablas [MEDECINS] y [CRENEAUX] no han cambiado. Las tablas [CLIENTS] y [RVS] son las siguientes:

Esto es lo que se esperaba. Nos quedan algunos detalles por resolver:

  • gestionar el nombre de la base de datos. Aquí fue generado por EF;
  • llenar la base de datos con información.

3.4.4. Establecer el nombre de la base de datos

Para establecer el nombre de la base de datos generada por EF, utilizaremos una cadena de conexión definida en [App.config]. Este archivo de configuración queda así:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Para obtener más información sobre la configuración de Entity Framework, visita 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>

  <!-- cadena de conexión a la base de datos -->
  <connectionStrings>
    <add name="RdvMedecinsContext"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- el proveedor de fábrica -->
  <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>
  • líneas 15-19: la cadena de conexión a la base de datos;
  • línea 16: el atributo [name] toma el nombre de la clase [RdvMedecinsContext] utilizada para el contexto de persistencia. Es importante recordar esto. Esta restricción se puede eludir en el constructor del contexto:

    // constructor
    public RdvMedecinsContext()
      : base("monContexte")
    {
    }

En este caso, podríamos tener name= "monContexte". Esto es lo que veremos en el resto del documento.

  • línea 17: la cadena de conexión. [Data Source]: el nombre del servidor en el que se encuentra el SGBD, [Initial Catalog]: el nombre de la base de datos, es decir, en este caso [rdvmedecins-ef]; [User Id]: el propietario de la conexión; [Password]: su contraseña. El lector deberá adaptar esta cadena a su entorno;
  • líneas 21-29: definen un [DbProviderFactory]. No sé qué es esto. A juzgar por el nombre, podría ser una clase que permite generar la capa [ADO.NET] que separa EF de SGBD:

De hecho, estas líneas son innecesarias para SQL Server, pero tuve que agregarlas para los otros SGBD. Así que las pongo aquí para recordarlas. No causan ningún problema. Lo único importante es la versión de la línea 27. Es la de DLL y [System.Data] que aparecen en las referencias del proyecto:

Listo. Ya estamos listos. Ejecutamos el proyecto y obtenemos la base [rdvmedecins-ef] siguiente:

 

Esta será nuestra base definitiva. Ahora solo nos queda ingresar datos en ella.

3.4.5. Llenado de la base de datos

La clase de inicialización de la base de datos se puede utilizar para insertar datos en ella:


public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
    // inicialización de la base
    public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
    {
      protected override void Seed(RdvMedecinsContext context)
      {
        base.Seed(context);
        // se inicializa la base de datos
        // los clientes
        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);
        }
        // los médicos
        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);
        }
        // los horarios
        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);
        }
        // las citas
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
      }

    }
  }
  • línea 6: la inicialización se lleva a cabo en el método [Seed]. Este método existe en la clase padre. Aquí se redefine. El argumento es el contexto de persistencia [RdvMedecinsContext] de la aplicación;
  • línea 8: el argumento se pasa a la clase padre; es probable que esta abra el contexto de persistencia que se le ha pasado, ya que dicha apertura ya no es necesaria posteriormente;
  • líneas 11-16: creación de 4 clientes;
  • líneas 17-20: estos se agregan al contexto de persistencia, más precisamente a los médicos de este. Cabe destacar el método [Add] que permite esto. Aquí hay que recordar la definición del contexto:

  public class RdvMedecinsContext : DbContext
  {
    // las entidades
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
    public DbSet<Client> Clients { get; set; }
public DbSet<Rv> Rvs { get; set; }
...

También se dice que los clientes se han vinculado al contexto, es decir, que ahora son administrados por EF. Antes no estaban vinculados. Existían como objetos, pero no eran administrados por EF;

  • líneas 21-27: creación de 4 médicos;
  • líneas 28-31: se colocan en el contexto de persistencia;
  • líneas 33-70: creación de horarios. Líneas 34-57, para el médico medecins[0]; líneas 58-69, para el médico medecins[1]. Los demás médicos no tienen horarios;
  • líneas 71-74: se colocan estos horarios en el contexto de persistencia;
  • línea 76: creación de una cita para el primer cliente con el primer horario disponible y su inclusión en el contexto de persistencia.

Al ejecutar el proyecto, se obtiene la base de datos siguiente:

Arriba se ve la tabla [CLIENTS] completada.

3.4.6. Modificación de las entidades

Actualmente, las clases [Medecin] y [Client] son prácticamente idénticas. De hecho, si eliminamos los campos agregados para la gestión de la persistencia con EF 5, son idénticas. Las haremos derivar de una clase [Personne]. Estas dos entidades quedan entonces así:


// una persona
  public abstract class Personne
  {
    // datos
    [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; }

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

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

  }

  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin : Personne
  {
    // los horarios de atención del médico
    public ICollection<Creneau> Creneaux { get; set; }
    // firma
    public override string ToString()
    {
      return String.Format("Medecin {0}", base.ToString());
    }
  }

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

Al ejecutar el proyecto, se obtiene la misma base de datos. EF 5 ha mapeado las clases más bajas de la herencia, cada una en una tabla. De hecho, EF 5 cuenta con diferentes estrategias de generación de tablas para representar la herencia de entidades. No las presentaremos aquí. Se puede consultar, por ejemplo, « : Entity Framework Code First Inheritance: Table Per Hierarchy and Table Per Type», en URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].

A partir de ahora utilizaremos esta versión de las entidades.

3.4.7. Agregar restricciones a la base de datos

Nos queda un detalle por resolver. La tabla [RVS] de las citas es la siguiente:

 

Esta tabla debe tener una restricción de unicidad: para un día determinado, un horario de un médico solo puede reservarse una vez para una cita. En términos de la tabla, esto significa que el par (JOUR,CRENEAU_ID) debe ser único. No sé si esta restricción se puede expresar directamente en el código, ya sea en las entidades o en el contexto. Es probable, pero no lo he verificado. Vamos a seguir otro enfoque. Utilizaremos un cliente de administración de SQL Server para agregar esta restricción.

Con el «SQL Server Management Studio», no he encontrado una forma sencilla de agregar esta restricción, salvo ejecutar el comando SQL que la crea:

  • en [1] se crea una consulta SQL para la base de datos [rdvmedecins-ef];
  • en [2], la consulta SQL que crea la restricción de unicidad;
  • en [3], la ejecución de esta consulta creó un nuevo índice en la tabla [RVS].

Existen otras herramientas de administración de SQL Server. Aquí utilizaremos la herramienta EMS SQL Manager for SQL Server Freeware [http://www.sqlmanager.net/fr/products/mssql/manager/download]. Una vez instalada, la ejecutamos:

  • en [1], guardamos una base de datos;
  • en [2], nos conectamos al servidor (local);
  • en [3], con una autenticación SQL Server;
  • en [4], con la identidad «sa»;
  • en [5], y la contraseña sqlserver2012;
  • en [6], pasamos al siguiente paso;
  • en [7], se selecciona la base de datos [rdvmedecins-ef];
  • en [8], finalizamos el asistente;
  • en [9], la base de datos aparece en el árbol de bases de datos. Nos conectamos a ella en [10];
  • en [11], ya estamos conectados.

«SQL Manager Lite para SQL Server» permite crear la restricción de unicidad en la tabla [RVS].

  • En [1], vemos la restricción de unicidad que creamos anteriormente;
  • en [2], la eliminamos;
  • en [3], el índice correspondiente a esta restricción de unicidad ha desaparecido.

Volvemos a crear la restricción eliminada:

  • en [1], creamos un nuevo índice para la tabla [RVS];
  • en [2], se le asigna un nombre;
  • en [3], es una restricción de unicidad;
  • en [4], sobre las columnas JOUR y CRENEAU_ID;

La pestaña DDL nos da el código SQL que se ejecutará:

  • en [6], se compila la orden SQL;
  • en [7], se confirma;
  • en [8], ha aparecido el nuevo índice.

La interfaz que ofrece «SQL Manager Lite for SQL server» es similar a la que ofrece «SQL Server Management Studio». Se pueden encontrar interfaces similares para los servidores SGBD Oracle, PostgreSQL, Firebird y MySQL. Por lo tanto, a partir de ahora continuaremos con esta familia de herramientas de administración de SGBD.

Para acceder a la información de una tabla, basta con hacer doble clic en ella:

La información sobre la tabla seleccionada está disponible en pestañas. Arriba, se ve la pestaña [Fields] de la tabla [CLIENTS]. La pestaña [Data] muestra el contenido de la tabla:

Image

3.4.8. La base definitiva

Ya tenemos nuestra base definitiva. Exportamos su script SQL para poder regenerarla si es necesario.

  • en [1], inicio del asistente;
  • en [2], el servidor;
  • en [3], la base de datos que se va a exportar;
  • en [4], especifique el nombre del archivo donde se guardará el script SQL;
  • en [5], especifica su codificación;
  • en [6], especifique qué desea extraer (tablas, restricciones, datos);
  • en [7], puede ajustar el script que se generará;
  • en [8], finalice el asistente.

El script se ha generado y cargado en el editor de scripts. Puede consultar el código SQL generado. Vamos a reconstruir la base de datos a partir de este script.

  • en [1], se elimina la base de datos;
  • en [2] y [3], la recreamos;
  • en [4], se realiza la autenticación;
  • en [5], se ejecuta el script SQL para crear la base de datos;
  • en [6], se guarda en «SQL Manager»;
  • en [7], nos conectamos a la base de datos que acabamos de crear;
  • en [8], la base de datos aún no tiene tablas;
  • en [9a], se abre un editor de scripts SQL;
  • en [9b], se abre el script SQL creado anteriormente;
  • en [10], lo ejecutamos;
  • en [11], se han creado las tablas;
  • en [12], se han llenado;
  • en [14], volvemos a encontrar la restricción de unicidad que habíamos creado para la tabla [RVS].

Ahora trabajaremos con esta base de datos existente. Si se destruye o se daña, sabemos cómo regenerarla.

3.5. Uso de la base de datos con Entity Framework

Vamos a:

  • agregar, eliminar y modificar elementos de la base de datos;
  • consultar la base de datos con LINQ to Entities;
  • gestionar los accesos simultáneos a un mismo elemento de la base de datos;
  • comprender los conceptos de carga diferida (Lazy Loading) y carga inmediata (Eager Loading);
  • descubrir que la actualización de la base de datos mediante el contexto de persistencia se realiza en una transacción.

3.5.1. Eliminación de elementos del contexto de persistencia

Tenemos una base de datos llena. La vamos a vaciar. Creamos una nueva clase [Erase.cs] en el proyecto actual [1]:

La clase [Erase] es la siguiente:


using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Erase
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // se vacía la base de datos actual
        // los clientes
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        // los médicos
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // se guarda el contexto de persistencia
        context.SaveChanges();
      }
    }
  }
}
  • línea 9: las operaciones en un contexto de persistencia siempre se realizan dentro de una cláusula [using]. Esto garantiza que, al salir de [using], el contexto se haya cerrado;
  • línea 13: se recorre el contexto de los clientes [context.Clients]. Todos los clientes de la base de datos se colocarán en el contexto de persistencia;
  • línea 15: para cada uno de ellos se realiza la operación [Remove], que los elimina del contexto. De hecho, siguen estando en el contexto, pero en un estado «eliminado»;
  • líneas 18-21: se hace lo mismo con los médicos;
  • línea 23: se guarda el contexto de persistencia en la base de datos.

Al guardar el contexto en la base de datos, las entidades del contexto que:

  • tienen una clave primaria nula son objeto de una operación SQL INSERT;
  • se encuentran en estado «eliminado» son objeto de una operación SQL DELETE;
  • se encuentran en estado «modificado», son objeto de una operación SQL UPDATE;

Como veremos más adelante, estas operaciones SQL se realizan dentro de una transacción. Si alguna de ellas falla, todo lo que se haya hecho anteriormente se deshace.

Hagamos del programa [Erase] el nuevo objeto de inicio del proyecto [1] y luego ejecutemos el proyecto.

Revisemos la base de datos. Veremos que todas las tablas están vacías en [2]. Esto es sorprendente, ya que solo habíamos solicitado la eliminación de los médicos y los clientes. Es por el funcionamiento de las claves externas que las demás tablas se vaciaron en cascada.

La definición de la clave externa de la tabla [CRENEAUX] hacia la tabla [MEDECINS] fue definida de la siguiente manera por el proveedor de EF 5:

  • en [1], se selecciona la tabla [CRENEAUX];
  • en [2], se selecciona la pestaña de claves externas;
  • en [3], se edita la única clave externa;
  • en [4], en la pestaña DDL, la definición SQL de la restricción de clave externa;
  • En [5], la cláusula ON DELETE CASCADE hace que, al eliminar a un médico, se eliminen también los horarios asociados a él.

Las restricciones de clave externa de la tabla [RVS] se definen de manera análoga:

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
  • líneas 1-6: al eliminar a un cliente, también se eliminarán las citas asociadas a él;
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
  • líneas 1-6: al eliminar un horario, también se eliminarán todas las citas asociadas a él.

3.5.2. Agregar elementos al contexto de persistencia

Ahora que hemos vaciado la base de datos, la volveremos a llenar. Agregamos al proyecto el programa [Fill.cs] [1].

El programa [Fill.cs] es el siguiente:


using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Fill
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // se vacía la base de datos actual
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // se reinicia
        // los clientes
        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);
        }
        // los médicos
        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);
        }
        // los horarios
        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);
        }
        // las citas
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // guardamos el contexto de persistencia
        context.SaveChanges();
      }
    }
  }
}
  • línea 10: se abre el contexto de persistencia;
  • líneas 13-20: las filas de las tablas [CLIENTS] y [MEDECINS] se insertan en el contexto y luego se eliminan de él. Acabamos de ver que esto vaciaba por completo la base de datos;
  • líneas 22-88: se agregan elementos al contexto de persistencia. Todos tienen su clave primaria en nulo. Por lo tanto, se insertarán en la base de datos;
  • línea 90: los cambios realizados en el contexto se sincronizan con la base de datos. Esta será objeto de una serie de operaciones SQL DELETE, seguidas de una serie de operaciones SQL INSERT;

Se establece el programa [Fill] como el nuevo objeto de inicio del proyecto [1] y luego se ejecuta este último.

En [2] se observa que las tablas se han llenado.

3.5.3. Visualización del contenido de la base de datos

Ahora vamos a mostrar el contenido de la base de datos mediante la consulta LINQ to Entity. LINQ (Lenguaje INtegrated Query) apareció con el marco .NET 3.5 en 2007. Se presenta como una extensión de los lenguajes .NET y c.a.d, ya que está integrado en el lenguaje y su sintaxis es verificada por el compilador. Permite realizar consultas en diferentes colecciones con una sintaxis que presenta similitudes con el lenguaje SQL (Structured Query Language) para consultas en bases de datos. Existen diferentes versiones de LINQ:

  • LINQ to Object, para consultar colecciones en memoria;
  • LINQ to XML, para realizar consultas en XML;
  • LINQ a Entidad, para realizar consultas en bases de datos;

Para funcionar, LINQ se basa en numerosas extensiones realizadas a los lenguajes .NET. Estas pueden utilizarse fuera de LINQ. No las presentaremos aquí, sino que simplemente daremos dos referencias donde el lector encontrará una descripción detallada de LINQ:

  • LINQ in Action, de Fabrice Marguerie, Steve Eichert y Jim Wooley, publicado por Manning;
  • LINQ pocket reference, de Joseph y Ben Albahari, publicado por O’Reilly.

He leído el primero y me pareció excelente. No he leído el segundo, pero sí leí de los mismos autores «C# 3.0 in a nutshell» cuando salió LINQ. Este libro me pareció muy por encima del promedio de los libros que suelo leer. Parece que los otros libros de estos dos autores están al mismo nivel. Además, vamos a utilizar LINQPad, una herramienta de aprendizaje de LINQ escrita por Joseph Albahari.

Vamos a mostrar las entidades presentes en la base de datos. Para ello, agregaremos a sus clases dos métodos de visualización. Comencemos con la entidad [Medecin]:


// un médico
  public class Medecin
  {
    // datos
    [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; }
    // los horarios del médico
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

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

    // utilidad
    private string dump(byte[] timestamp){
      string str = "";
      foreach (byte b in timestamp)
      {
        str += b;
      }
      return str;
    }
  }
  • líneas 27-30: el método ToString de la clase. Cabe señalar que no muestra la colección de la línea 21;
  • líneas 32-37: el método ShortIdentity, que hace lo mismo.

Aquí debemos explicar los conceptos de carga diferida (Lazy Loading) y carga inmediata (Eager Loading) para evaluar el impacto de los dos métodos anteriores. Hemos visto que una entidad puede tener dependencias con respecto a otra entidad. Estas dependencias pueden ser de dos tipos:

  • de uno a varios, como en el ejemplo anterior, donde un médico está vinculado a varios horarios;
  • de varios a uno, como en la entidad [Creneau] que se muestra a continuación, donde varios horarios están vinculados al mismo médico;

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

Cuando las dependencias se cargan al mismo tiempo que las entidades a las que están vinculadas, se habla de «Eager Loading». De lo contrario, se habla de «Lazy Loading»: las dependencias solo se cargan cuando se hace referencia a ellas por primera vez. Por defecto, EF 5 utiliza el «Lazy Loading»: las dependencias no se cargan al mismo tiempo que la entidad.

Veamos nuestro método [ToString] anterior:


    // los horarios del médico
    public ICollection<Creneau> Creneaux { get; set; }
    
    // firma
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // firma abreviada
    public string ShortIdentity()
    {
      return ToString();
}

El método [ToString] no muestra la dependencia [Creneaux] de la línea 2. Si lo hubiera hecho, habría forzado la carga de todos los horarios del médico antes de su ejecución. Para evitar esta carga costosa, la dependencia no se incluyó en la firma de la entidad. En general, incluiremos dos firmas en cada entidad:

  • un método ToString que mostrará la entidad y sus posibles dependencias de tipo «uno a varios». Como se acaba de explicar, esto provocará la carga de la dependencia;
  • un método ShortIdentity que no hará referencia a ninguna dependencia. Por lo tanto, no se cargará ninguna dependencia;

Los métodos de visualización de las demás entidades serán los siguientes:

La entidad [Client]:


  public class Client
  {
    // datos
    ...
    // las citas del cliente
    public ICollection<Rv> Rvs { get; set; }
    
    // firma
    public override string ToString()
    {
      return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // firma corta
    public string ShortIdentity()
    {
      return ToString();
    }

}
  • líneas 9-12: el método [ToString] no muestra la dependencia de la línea 6;

La entidad [Creneau]:


public class Creneau
  {
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    // las citas de la franja horaria
    public ICollection<Rv> Rvs { get; set; }
    
    // firma
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
    // firma corta
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
    }
  }
  • línea 16: el método [ToString] hace referencia a la dependencia de la línea 9. Esto forzará su carga;
  • línea 11: la dependencia [Rvs] no está referenciada. No se cargará;
  • líneas 21-22: el método [ShortIdentity] ya no hace referencia a la referencia [Medecin] de la línea 9. Por lo tanto, esta no se cargará.

La entidad [Rv]:


public class Rv
  {
    // datos
    ...
    [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; }

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

  }
  • líneas 17-20: el método [ToString] hace referencia a las dependencias de las líneas 9 y 14. Esto forzará su carga;
  • líneas 17-20: el método [ShortIdentity] evita esto y, por lo tanto, las dependencias no se cargarán.

En conclusión, hay que prestar atención a los métodos [ToString] de las entidades. Si no se presta atención a esto, mostrar una tabla puede cargar la mitad de la base de datos si la tabla tiene muchas dependencias.

Una vez explicado esto, escribimos el siguiente código nuevo [Dump.cs]:


using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;

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

Vamos a explicar las líneas 17 a 21, que muestran las entidades [Client]. La explicación dada se aplicará a las demás entidades.


        // los clientes
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
}
  • línea 3: la palabra clave var se introdujo con C# 3.0. Permite evitar indicar el tipo específico de una variable. El compilador deduce entonces este a partir del tipo de la expresión asignada a la variable;
  • línea 3: la expresión asignada a la variable clients es una consulta LINQ to Entity. En ella se reconocen palabras clave del lenguaje SQL incorporadas en LINQ. La sintaxis utilizada aquí es la siguiente:

from variable in DbSet select variable

Una sintaxis más general de LINQ es


from variable in collection select variable

Se recorrerá la colección y, para cada elemento de la misma, se evaluará la variable. Esto solo se hace cuando la variable [clients] de la línea 3 sea enumerada por el bucle «for / each» de las líneas 4 a 7. Mientras esto no suceda, la variable [clients] es solo una consulta no evaluada;

  • línea 4: se itera sobre la consulta [clients]. Esto forzará la evaluación de la consulta. Las filas de la tabla [CLIENTS] se incorporarán una por una al contexto de persistencia;
  • línea 6: se utiliza el método [ToString] de la entidad [Client] para la visualización. No se cargan dependencias;

Pasemos a las siguientes líneas del código:

  • líneas 24-28: las filas de la tabla [MEDECINS] se incorporan al contexto de persistencia y se visualizan. No se cargan dependencias;
  • líneas 31-35: las filas de la tabla [CRENEAUX] se incorporan al contexto de persistencia y se muestran. Hemos visto que el método [ToString] de esta entidad mostraba la dependencia [Medecin]. Sin embargo, esta ya está cargada. Por lo tanto, no habrá una nueva carga;
  • líneas 38-42: las filas de la tabla [RVS] se incorporan al contexto de persistencia y se muestran. Hemos visto que el método [ToString] de esta entidad mostraba las dependencias [Client] y [Creneau]. Sin embargo, estas ya están cargadas. Por lo tanto, no habrá nuevas cargas.

Cabe señalar que el orden de visualización no es arbitrario. Si hubiéramos querido mostrar primero las entidades [Rv], el método [ToString] de esta habría provocado la carga de las entidades [Client] y [Creneau] vinculadas a esas citas. Las demás no se habrían cargado. Se habrían cargado más tarde en otra visualización. Esto afecta el rendimiento. El código anterior necesita cuatro órdenes SQL para mostrar todas las entidades. Supongamos ahora que primero se consulta la tabla [RVS] de citas. Se requiere una primera consulta SQL para la tabla [RVS]. A continuación, el método [ToString] de la entidad [Rv] provocará la posible carga de las entidades asociadas [Client] y [Creneau]. Se requiere una consulta SQL para cada una. Suponiendo que haya N2 clientes y N3 horarios, y que todas estas entidades estén referenciadas en la tabla [RVS], la visualización de esta requerirá 1 + N2 + N3 consultas SQL. Por lo tanto, el rendimiento es menor que en la versión analizada. Para mostrar la tabla [RVS] con sus dependencias, se necesitaría una unión entre tablas. Es posible realizarla con LINQ. Volveremos a esto con un ejemplo. Por ahora, recordaremos que debemos prestar atención a las consultas SQL subyacentes a nuestro código LINQ.

Configuramos el proyecto para ejecutar este nuevo código [1] y [2] y luego lo ejecutamos:

La salida de la consola es la siguiente:

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...

3.5.4. Aprendizaje de LINQ con LINQPad

Anteriormente utilizamos consultas de tipo LINQ to Entity para mostrar el contenido de las tablas de la base de datos. Joseph Albahari escribió un programa para aprender las diferentes formas de LINQ. A continuación lo presentamos.

LINQPad está disponible en la siguiente URL [http://www.linqpad.net/]. Una vez instalado, lo ejecutamos [1]:

Los principiantes podrán iniciarse con los ejemplos de la pestaña [Samples] [2], que muestran una gran cantidad de ejemplos. Seleccionemos el ejemplo [3], que se mostrará en otra ventana [4]. El código completo del ejemplo es el siguiente:


// Ahora, una expresión de consulta sencilla de LINQ-to-objects (fíjate que no hay punto y coma):

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


// Siéntete libre de editar esto... (¡nadie te está viendo!) Se te pedirá que guardes cualquier
// cambios en un archivo aparte.
//
// Conse jo: Puedes ejecutar parte de una consulta resaltándola y luego presionando F5.

Las líneas 3 a 5 son un ejemplo de consulta LINQ a Object. La consulta LINQ sigue la sintaxis:


from variable in collection orderby élément1 select élément2
  • La variable designa el elemento actual de la colección. En nuestro ejemplo, esta colección es la lista de palabras resultantes de la cadena dividida;
  • la colección está ordenada según el parámetro élément1 de orderby. En nuestro ejemplo, la colección de palabras se ordenará según su longitud;
  • la palabra clave select indica qué se quiere extraer del elemento actual </span>*<span style="color: #000000">variable *de la colección. En nuestro ejemplo, será la palabra.

Ejecutemos esta consulta LINQ:

  • en [1]: una expresión LINQ se ejecuta mediante [F5] o bien a través del botón de ejecución;
  • en [2]: la visualización. Las palabras se muestran en orden de longitud. Este sencillo ejemplo muestra el potencial de LINQ;
  • en [3], es posible descargar otros ejemplos, en particular los del libro «LINQ in action» [4];
  • en [5], elegimos un ejemplo del libro;

string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };

// Agrupa las palabras por longitud
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 };

// Imprimir cada grupo
foreach (var group in groups)
{
  Console.WriteLine("Words of length " + group.Length);
  foreach (string word in group.Words)
    Console.WriteLine("  " + word);
}
  • línea 4: una nueva consulta LINQ con nuevas palabras clave;
  • línea 5: la colección solicitada es la tabla de palabras de la línea 1;
  • línea 6: la colección se ordena alfabéticamente por palabras;
  • línea 7: la colección se agrupa (palabra clave «into») en una nueva colección lengthGroups. lengthGroups.Key representa el factor de agrupación (palabra clave «by»), en este caso la longitud de las palabras. lengthGroups reúne las palabras que tienen el mismo factor de agrupación, es decir, la misma longitud;
  • línea 8: la colección lengthGroups se ordena por clave de agrupación en orden descendente, es decir, en este caso por tamaño decreciente de las palabras;
  • línea 9: a partir de esta colección, se generan nuevos objetos (clases anónimas) con dos campos:
    • Length: la longitud de las palabras,
    • Words: las palabras que tienen esa longitud;

Aquí se aprecia especialmente la utilidad de la palabra clave var de la línea 4. Debido a que se utilizó una clase anónima en la línea 9, no se puede especificar el tipo de la variable groups. El compilador, por su parte, le asignará un nombre interno a la clase anónima y utilizará ese nombre para tipificar la variable groups. Luego podrá determinar si la variable groups se utiliza correctamente

  • línea 12: recorrido de la consulta de la línea 4. Solo en este momento se evalúa. Recordemos que su ejecución producirá una colección de objetos, especificados en la línea 9;
  • línea 14: se muestra la propiedad Length del elemento actual, es decir, la longitud de las palabras;
  • líneas 15-17: se muestra cada elemento de la colección de la propiedad Words, es decir, el conjunto de palabras que tienen la longitud mostrada anteriormente.

Al ejecutar esta consulta, obtenemos el siguiente resultado en LINQPad:

 

Ahora que hemos visto algunos ejemplos de consultas [LINQ to Object], veamos algunas consultas [LINQ to Entity] que nos permitirán realizar consultas en bases de datos. En primer lugar, nos conectaremos a la base de datos SQL Server que hemos creado y llenado:

  • en [1], agregamos una conexión a una base de datos;
  • en [2], los medios de acceso a la fuente de datos. Para acceder a la base de datos SQL Server, utilizaremos [LINQPad Driver];
  • en [3], también es posible recuperar un contexto de persistencia [DbContext] definido en un archivo .exe o .dll de assembly (opción 3). Lamentablemente, a la fecha (8 de octubre de 2012), Entity Framework 5 no es compatible;
  • en [4], es posible descargar controladores para otros SGBD distintos de SQL Server;
  • en [5], se descargará el controlador para los SGBD, MySQL y Oracle;
  • en [6], el controlador descargado;
  • en [7], nos conectamos a una base de datos SQL Server;
  • en [8], la base de datos está en el servidor de nombres (local);
  • en [9], nos conectamos con la autenticación sa / sqlserver2012;
  • en [10], a la base de datos [rdvmedecins-ef] que hemos creado;
  • en [11], podemos probar la conexión;
  • en [12], finalizamos el asistente;
  • en [13], la conexión aparece en LINQPad.

Las entidades se crearon a partir de la tabla [rdvmedecins-ef]. Son las siguientes:

  • en [1], [CLIENTS] representa el conjunto de entidades de [Client]. Cada entidad tiene:
    • las propiedades (ID, TITRE, NOM, PRENOM, TIMESTAMP),
    • una relación de uno a varios con [CLIENTRVS];
  • en [2], [CRENEAUXes] representa el conjunto de entidades [Creneau]. Cada entidad tiene:
    • las propiedades (ID, HDEBUT, MDEBUT, HFIN, MFIN, MEDECIN_ID, TIMESTAMP),
    • una relación de 1 a varios [CRENEAURVS],
  • una relación de varios a uno [MEDECIN];
  • en [3], la entidad [MEDECINS] representa el conjunto de entidades [Medecin]. Cada entidad tiene:
    • las propiedades (ID, TITRE, NOM, PRENOM, TIMESTAMP),
    • una relación de uno a varios [MEDECINCRENEAUXes];
  • en [4], la entidad [RVS] representa el conjunto de entidades [Rv]. Cada entidad tiene:
    • las propiedades (ID, JOUR, CLIET_ID, CRENEAU_ID, TIMESTAMP),
    • una relación de varios a uno [CLIENT],
    • una relación de varios a uno [CRENEAU].

Cabe señalar que los nombres de las propiedades anteriores son diferentes de los que hemos utilizado hasta ahora. Eso no importa. Solo queremos aprender los principios básicos de las consultas en bases de datos.

Veamos cómo podemos realizar consultas en esta base de entidades. Por ejemplo, queremos la lista de médicos ordenada por su TITRE y NOM:

  • en [1], creamos una nueva consulta;
  • en [2], el texto de la consulta;
  • en [3], el resultado de la consulta;
  • en [4], la misma consulta con expresiones lambda. Una consulta con expresiones lambda es menos legible que una consulta de texto y tal vez prefieras evitarla. Sin embargo, a veces son indispensables porque permiten hacer ciertas cosas que las consultas de texto no permiten. Una expresión lambda se refiere a una función con un parámetro de entrada a y un parámetro de salida b, en la forma a=>b. El método OrderBy mencionado anteriormente admite una función lambda como único parámetro. Esta le proporciona el criterio según el cual debe ordenarse una colección. Así, MEDECINS.OrderBy(m=>m.TITRE) es la lista de médicos ordenada por títulos. Hay que interpretar la instrucción como una cadena de procesamiento sobre una colección. La colección de médicos se proporciona como entrada al método OrderBy. Este procesará las entidades [Medecin] una por una. En la expresión lambda m=>m.TITRE, m representa la entrada de la función lambda. Se le puede dar el nombre que se desee. En este caso, la entrada de la función lambda será una entidad [Medecin]. La función m=>m.TITRE se lee de la siguiente manera: si llamo a m mi entrada (una entidad [Medecin]), entonces mi salida es m.TITRE, es decir, el título del médico. MEDECINS.OrderBy(m=>m.TITRE) es, a su vez, una colección: la colección de médicos ordenada por títulos. Esta nueva colección puede alimentar otro método; en el ejemplo, el método ThenBy. Este funciona según el mismo principio. Sirve para indicar parámetros adicionales para ordenar la colección.

Leer el código lambda equivalente al código de texto que solemos escribir es una buena forma de aprenderlo;

  • en [5], la orden SQL emitida en la base. Una vez más, leeremos este código con atención. Permite evaluar el costo real de una consulta LINQ.

A continuación, presentamos algunos ejemplos de consultas LINQ. En cada caso, mostramos los resultados que se visualizan y los códigos lambda y SQL equivalentes. Para comprender estas consultas, hay que recordar las relaciones «muchos a uno» que conectan las entidades entre sí. Es a través de ellas que se navega de una entidad a otra. Se les llama propiedades de navegación.

// los clientes cuyo título es «Sr.» ordenados en orden descendente por nombre

Resultados:

 
LINQ

from client in CLIENTS where client.TITRE=="Mr" 
orderby client.NOM descending  select client
Lambda
CLIENTS
.Where (client => (client.TITRE == "Mr"))
.OrderByDescending (client => client.NOM)
SQL

-- 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

// todos los horarios con el médico asignado

Resultados (parciales):

 
LINQ

from creneau in CRENEAUXes 
select new { hd=creneau.HDEBUT, md=creneau.MDEBUT, hf=creneau.HFIN,
 mf=creneau.MFIN, medecin=creneau.MEDECIN}
Lambda
SQL

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]

// todas las citas con el cliente y el médico asociados

Resultados:

 
LINQ

from rv in RVS select new { rv=rv.CLIENT, medecin=rv.CRENEAU.MEDECIN}
Lambda
SQL

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]

// médicos sin citas

Resultados:

 
LINQ
 
Lambda
SQL

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]
    ))

No existe una consulta LINQ para esta solicitud. Hay que utilizar expresiones lambda. Esta se lee de la siguiente manera: tomo la colección de médicos (MEDECINS) y solo retengo (Where) a aquellos médicos (m) para los que no puedo encontrar en la colección de citas (RVS) una cita (rv) con ese médico (m).

// Horarios disponibles de la Sra. Pélissier

Resultados (parciales):

 
LINQ

from creneau in CRENEAUXes where creneau.MEDECIN.NOM=="Pelissier"
 select creneau
Lambda
SQL

-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'Pelissier'
-- EndRegion
SELECT [t0].[ID], [t0].[HDEBUT], [t0].[MDEBUT], [t0].[HFIN], [t0].[MFIN],
 [t0].[MEDECIN_ID], [t0].[TIMESTAMP]
FROM [CRENEAUX] AS [t0]
INNER JOIN [MEDECINS] AS [t1] ON [t1].[ID] = [t0].[MEDECIN_ID]
WHERE [t1].[NOM] = @p0

// Número de citas de la Sra. Pélissier el 08/10/2012

Resultados:

 
LINQ

(from rv in RVS where rv.CRENEAU.MEDECIN.NOM=="Pelissier"
 && rv.JOUR==new DateTime(2012,10,08)  select rv).Count()
Lambda
 
SQL

-- 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)

// Lista de clientes que concertaron una cita con la Sra. Pélissier el 08/10/2012

Resultados:

 
LINQ

from rv in RVS where (rv.JOUR==new DateTime(2012,10,08)
 && rv.CRENEAU.MEDECIN.NOM=="Pelissier") select rv.CLIENT
Lambda
SQL

-- 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)

// número de turnos por médico

Resultados:

 
LINQ

from creneau in CRENEAUXes 
group creneau by creneau.MEDECIN into creneauxMedecin 
select new { nom=creneauxMedecin.Key.NOM,
 prenom=creneauxMedecin.Key.PRENOM,
 nbRv=creneauxMedecin.Count()}
Lambda
SQL

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]

3.5.5. Modificación de una entidad vinculada al contexto de persistencia

Hemos visto las siguientes operaciones en el contexto de persistencia:

  • agregar un elemento al contexto ([dbContext].[DbSet].Add);
  • eliminar un elemento del contexto ([dbContext].[DbSet].Remove);
  • consultar un contexto con consultas LINQ.

Cuando se desea sincronizar el contexto con la base de datos, se escribe [dbContext].SaveChanges().

El código [ModifyAttachedEntity] ilustra la modificación de una entidad vinculada al contexto:


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;
      // Primer contexto
      using (var context = new RdvMedecinsContext())
      {
        // Se vacía la base de datos actual
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // Se agrega un cliente
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // Seguimiento
        Console.WriteLine("client1--avant");
        Console.WriteLine(client1);
        // Guardar contexto
        context.SaveChanges();
        // seguimiento
        Console.WriteLine("client1--après");
        Console.WriteLine(client1);
      }
      // segundo contexto
      using (var context = new RdvMedecinsContext())
      {
        // se recupera el cliente1 en el cliente2
        client2 = context.Clients.Find(client1.Id);
        // seguimiento
        Console.WriteLine("client2");
        Console.WriteLine(client2);
        // se modifica el cliente2
        client2.Nom = "yy";
        // guardar contexto
        context.SaveChanges();
      }
      // tercer contexto
      using (var context = new RdvMedecinsContext())
      {
        // se recupera el cliente2 en el cliente3
        client3 = context.Clients.Find(client2.Id);
        // seguimiento
        Console.WriteLine("client3");
        Console.WriteLine(client3);
      }
    }
  }
}
  • línea 15: apertura del contexto de la aplicación;
  • líneas 18-25: se vacía el contexto. Más precisamente, todas las entidades se transfieren al contexto desde la base de datos y luego pasan a un estado «eliminado». Cabe señalar que, en esta etapa, la base de datos no ha cambiado. Mientras el contexto no se sincronice con la base de datos, esta no cambia. Recordemos que eliminar las entidades [Medecin] y [Client] es suficiente para vaciar la base de datos mediante el mecanismo de eliminaciones en cascada;
  • líneas 27-28: se agrega un nuevo cliente a la base;
  • líneas 30-31: se muestra antes de guardarlo en la base de datos;
  • línea 33: se sincroniza el contexto con la base de datos. Las entidades marcadas como «eliminadas» serán objeto de una operación SQL DELETE, la entidad se agrega mediante una operación SQL INSERT;
  • líneas 35-36: se muestra el cliente después de la sincronización con la base de datos;

El resultado obtenido en la consola es el siguiente:

1
2
3
4
client1--avant
Client[,xx,xx,xx,]
client1--après
Client[16,xx,xx,xx,000000132209]

Cabe destacar lo siguiente:

  • antes de la sincronización con la base de datos, el cliente no tiene ni clave primaria ni timestamp,
  • después de la sincronización, sí los tiene. Cabe recordar aquí que la clave primaria se configuró para que la generara el servidor SQL. Del mismo modo, este SGBD genera automáticamente la marca de tiempo;
  • línea 37: se cierra el contexto de persistencia. Las entidades que contenía pasan a estar «desvinculadas». Existen como objetos, pero no como entidades vinculadas a un contexto de persistencia;
  • línea 39: se inicia un nuevo contexto vacío;
  • línea 42: se recupera el cliente directamente de la base de datos mediante su clave primaria. A continuación, se incorpora al contexto. Si no se encuentra, el método Find devuelve el puntero null;
  • líneas 48-49: se muestra;

Esto da el siguiente resultado:

client2
Client[16,xx,xx,xx,000000132209]
  • línea 47: se modifica;
  • línea 49: se sincroniza el contexto con la base. EF detectará que algunos elementos del contexto han sido modificados desde que fueron incorporados. Para estos elementos, generará órdenes SQL y UPDATE en la base. Por lo tanto, en este caso, la sincronización consistirá en una sola orden: UPDATE;
  • línea 50: se cierra el segundo contexto. La entidad client2, que estaba vinculada al contexto, ahora queda desvinculada de este;
  • línea 52: se abre un tercer contexto vacío;
  • línea 55: se vuelve a traer al único cliente de la base. Queremos ver si la modificación realizada en él en el contexto anterior se ha reflejado en la base;
  • líneas 57-58: se muestra el cliente. Esto da el siguiente resultado:
client3
Client[16,xx,xx,yy,000000132210]

El nombre del cliente sí se modificó en la base de datos. Cabe destacar que su timestamp se actualizó.

  • línea 59: se cierra el contexto. Cabe señalar que, a diferencia de las dos ocasiones anteriores, no fue necesario sincronizar previamente el contexto con la base de datos (SaveChanges) porque el contexto no se había modificado.

3.5.6. Manejo de entidades desligadas

Volvamos a la arquitectura en capas de una aplicación como la del caso de estudio:

La capa [DAO] utiliza ORM y EF5 para acceder a los datos. Contamos con los componentes básicos de esta capa. Cada método abrirá un contexto de persistencia, realizará en él las operaciones necesarias (inserción, modificación, eliminación, consulta) y luego lo cerrará. Las entidades gestionadas por la capa [DAO] se transmitirán hasta la capa web ASP.NET. En esta capa, se encuentran fuera del contexto de persistencia, por lo que están desvinculadas. En la capa web, un usuario puede modificar estas entidades (agregar, modificar, eliminar). Cuando regresan a la capa [DAO], siguen estando desvinculadas. Sin embargo, la capa [DAO] deberá reflejar en la base de datos las modificaciones realizadas por el usuario. Por lo tanto, tendrá que trabajar con entidades desvinculadas. Veamos los tres casos posibles:

Agregar una entidad desvinculada

Este es el caso habitual para una adición. Basta con agregar (Add) la entidad desvinculada al contexto, asegurándose de que tenga una clave primaria igual a null.

Modificar una entidad separada

Se puede utilizar el siguiente código:

[DbContext].Entry(entité-détachée).State=EntityState.Modified ;
  • el método [DbContext].Entry(entidad-separada) colocará la entidad en el contexto;
  • el estado de esta entidad se establece en «modificado» para que sea objeto de una orden SQL UPDATE.

Eliminar una entidad separada

Se puede utilizar el siguiente código:

Entity e=[DbContext].[DbSet].Find(clé primaire de l'entité détachée) ;
[DbContext].[DbSet].Remove(e) ;
  • línea 1: se coloca en el contexto la entidad con la misma clave primaria que la entidad separada;
  • línea 2: se elimina:

Cabe señalar que esto requiere, como base, un SELECT seguido de un DELETE, mientras que normalmente basta con el DELETE. También se puede seguir el ejemplo de la modificación de una entidad separada y escribir:

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

Como no he podido implementar registros de las operaciones SQL realizadas en la base de datos, no sé si se recomienda un método en lugar del otro.

He aquí un ejemplo:

El código del programa [ModifyDetachedEntities] es el siguiente:


using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;

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

      // se vacía la base de datos actual
      Erase();
      // se agrega un cliente
      using (var context = new RdvMedecinsContext())
      {
        // creación de cliente
        client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
        // se agrega el cliente al contexto
        context.Clients.Add(client1);
        // se guarda el contexto
        context.SaveChanges();
      }
      // visualización de la base
      Dump("1-----------------------------");
      // el cliente1 no está en el contexto; se modifica
      client1.Nom = "y";
      // nuevo contexto
      using (var context = new RdvMedecinsContext())
      {
        // aquí tenemos un contexto vacío
        // se coloca al cliente 1 en el contexto en un estado modificado
        context.Entry(client1).State = EntityState.Modified;
        // se guarda el contexto
        context.SaveChanges();
      }
      // visualización básica
      Dump("2-----------------------------");
      // eliminación de entidad fuera del contexto
      using (var context = new RdvMedecinsContext())
      {
        // aquí tenemos un nuevo contexto vacío
        // se coloca al cliente1 en el contexto en estado eliminado
        context.Entry(client1).State = EntityState.Deleted;
        // guardamos el contexto
        context.SaveChanges();
      }
      // visualización de la base
      Dump("3-----------------------------");
    }

    static void Erase()
    {
      // vacía 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);
        }
        // se guarda el contexto
        context.SaveChanges();
      }
    }

    static void Dump(string str)
    {
      Console.WriteLine(str);
      // muestra 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);
        }
      }
    }
  }
}
  • línea 15: se borra la base de datos;
  • líneas 17-25: se agrega un cliente a la base de datos;
  • línea 27: muestra el contenido de la base de datos;
1-----------------------------
Client[20,x,x,x,0000011209]
  • después de la línea 25, el contexto de persistencia ya no existe. Por lo tanto, ya no hay entidades vinculadas. La entidad client1 pasa al estado «desvinculado»;
  • línea 29: se modifica el nombre de la entidad desvinculada;
  • línea 31: se abre un nuevo contexto vacío;
  • línea 35: la entidad desvinculada client1 se coloca en el contexto en estado «modificado»;
  • línea 37: el contexto se sincroniza con la base;
  • línea 38: se cierra;
  • línea 40: se muestra la base;
2-----------------------------
Client[20,x,x,y,0000011210]

El nombre del cliente se ha modificado correctamente en la base de datos. Cabe señalar que el timestamp se ha actualizado;

  • línea 42: se abre un nuevo contexto vacío;
  • línea 46: la entidad separada client1 se coloca en el contexto en estado «eliminado»;
  • línea 48: el contexto se sincroniza con la base de datos;
  • línea 49: se cierra;
  • línea 51: se muestra la base;
3-----------------------------

La entidad se ha eliminado correctamente de la base de datos.

Ahora veremos los dos modos de carga de las dependencias de una entidad: carga diferida (Lazy Loading) y carga inmediata (Eager Loading).

3.5.7. Carga diferida (Lazy) y carga anticipada (Eager)

Retomemos el esquema de dependencias «muchos a uno» de una de nuestras cuatro entidades:

En el ejemplo anterior, la entidad [Creneau] tiene una propiedad de navegación [Creneau.Medecin] hacia la entidad [Medecin]. A esto se le llama una dependencia. Hemos visto que también existen dependencias de uno a varios. El principio que se explicará a continuación también se aplica a ellas.

Por defecto, EF 5 está en modo de carga diferida (Lazy Loading): cuando trae una entidad al contexto de persistencia desde la base de datos, no trae sus dependencias. Estas se traerán cuando se utilicen por primera vez. Es una medida de sentido común. Si no fuera así, al traer las citas al contexto, según las dependencias anteriores, se traerían:

  • las entidades [Creneau] vinculadas a las citas;
  • las entidades [Medecin] vinculadas a esos horarios;
  • las entidades [Clients] vinculadas a las citas.

Sin embargo, a veces se necesita una entidad y sus dependencias. Vamos a ilustrar los dos modos de carga.

El código de [LazyEagerLoading] es el siguiente:


using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;

namespace RdvMedecins_01
{
  class LazyEagerLoading
  {
    // las entidades
    static Medecin[] medecins;
    static Client[] clients;
    static Creneau[] creneaux;

    static void Main(string[] args)
    {
      // se inicializa la base      
      InitBase();
      Console.WriteLine("Initialisation terminée");
      // carga anticipada
      Creneau creneau;
      int idCreneau = (int)creneaux[0].Id;
      using (var context = new RdvMedecinsContext())
      {
        // franja n.º 0
        creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // visualización de dependencias
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
      }
      // carga diferida - modo predeterminado
      using (var context = new RdvMedecinsContext())
      {
        // intervalo n.º 0
        creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // visualización de dependencias
      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()
    {
      // se inicializa la base de datos
      using (var context = new RdvMedecinsContext())
      {
        // se vacía la base de datos actual
        ...
        // se inicializa la base
        // los clientes
        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" }
     };
...
        // las citas
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // se guarda el contexto de persistencia
        context.SaveChanges();
      }
    }
  }
}
  • línea 18: partimos de una base conocida, la que se ha utilizado hasta ahora. Tras esta operación, las tablas de las líneas 11 a 13 se llenan con entidades independientes;
  • líneas 21-22: nos enfocamos en el primer horario y en el médico asociado;
  • línea 23: nuevo contexto;
  • línea 26: se coloca el horario en el contexto junto con su dependencia (carga anticipada). Como este no es el modo predeterminado, hay que solicitar explícitamente esta dependencia. El método Include permite hacerlo. Su parámetro es el nombre de la dependencia en la entidad incorporada al contexto. La consulta que incorpora la entidad al contexto utiliza expresiones lambda. El método Single permite especificar una condición para recuperar una sola entidad. Aquí, se busca en la base de datos la entidad [Creneau], que tiene como clave primaria el horario n.º 0;
  • línea 27: se muestra la entidad recuperada. Recordemos los dos métodos de escritura utilizados en las entidades:

// firma
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
 
   // firma corta
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
    }
  • líneas 2-5: el método [ToString] muestra la dependencia [Medecin]. Si esta aún no se encuentra en el contexto, se buscará en la base de datos para agregarla;
  • líneas 8-11: el método [ShortIdentity] no muestra la dependencia [Medecin]. Por lo tanto, no se buscará en la base de datos si no se encuentra en el contexto;

En este punto, la salida de la consola es la siguiente:

Initialisation terminée
Creneau[181,8,0,8,20, 21, 00000195150]
  • línea 28: se cierra el contexto;
  • líneas 30-37: se intenta escribir la dependencia [Medecin] de la entidad. Recordemos cómo funciona el Lazy Loading: una dependencia se carga al usarla por primera vez si no está presente. Aquí, normalmente sí está presente. La salida es la siguiente:
Médecin=Medecin[21,Mme,Marie,Pelissier,00000195149]
  • líneas 39-44: en el marco de un nuevo contexto, el intervalo n.º 0 se busca nuevamente en la base de datos y se incorpora al contexto. En este caso, la dependencia [Medecin] no se solicita explícitamente. Por lo tanto, no se incorporará (carga diferida);
  • línea 43: la visualización de la identidad corta del horario es la siguiente:
Creneau[181,8,0,8,20, 21, 00000195150]

Aquí es importante utilizar ShortIdentity en lugar de ToString para mostrar la entidad. Si se utiliza ToString, se mostrará la dependencia [Medecin] y, para ello, se buscará en la base de datos. Pero eso no es lo que queremos.

  • línea 44: se cierra el contexto;
  • líneas 46-53: se intenta mostrar la dependencia de la entidad. Es importante hacer esto fuera del contexto; de lo contrario, se buscará en la base de datos y se encontrará. Aquí estamos fuera del contexto. La entidad [Creneau] está separada y su dependencia [Medecin] no está presente (carga diferida). ¿Qué va a pasar? La visualización en pantalla es la siguiente:
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\CitasConMédicos-SqlServer-01\LazyEagerLoading.cs:línea 48

EF detectó que la dependencia [Medecin] no estaba presente. Intentó cargarla, pero como el contexto estaba cerrado, esta operación ya no era posible. Recordaremos esta excepción [System.ObjectDisposedException], ya que es característica de la carga de una dependencia fuera de un contexto abierto.

Ahora analicemos la concurrencia en el acceso a las entidades.

3.5.8. Concurrencia en el acceso a las entidades

Volvamos a la definición de la entidad [Client]:


public class Client
  {
    // datos
    [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; }
    // los Rvs del cliente
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // firma
    ...
  }

Nos centraremos en el campo [Timestamp] de la línea 23. Sabemos que su valor es generado por el SGBD. También mencionamos que la anotación [Timestamp] de la línea 22 hacía que EF 5 utilizara el campo anotado para gestionar la concurrencia de acceso a las entidades. Recordemos en qué consiste la gestión de la concurrencia de acceso:

  • un proceso P1 lee una fila L de la tabla [MEDECINS] en el momento T1. La fila tiene el timestamp TS1;
  • un proceso P2 lee la misma fila L de la tabla [MEDECINS] en el momento T2. La fila tiene los valores timestamp y TS1 porque el proceso P1 aún no ha validado su modificación;
  • el proceso P1 valida su modificación de la línea L. El timestamp de la línea L pasa entonces a TS2;
  • el proceso P2 valida su modificación de la línea L. ElORM genera entonces una excepción porque el proceso P2 tiene un timestamp TS1 de la línea L diferente del timestamp TS2 encontrado en la base de datos.

A esto se le llama gestión optimista de accesos concurrentes. Con EF 5, un campo que desempeña esta función debe tener uno de los dos atributos: [Timestamp] o [ConcurrencyCheck]. El servidor SQL tiene un tipo [timestamp]. El valor de una columna con este tipo es generado automáticamente por el servidor SQL cada vez que se inserta o modifica una fila. Dicha columna puede utilizarse entonces para gestionar la concurrencia de acceso.

Ilustraremos esta concurrencia de acceso con dos subprocesos que modificarán al mismo tiempo una misma entidad [Client] en la base de datos. El proyecto evoluciona de la siguiente manera:

El código del programa [AccèsConcurrents] es el siguiente:


using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{

  // objeto intercambiado con los hilos
  class Data
  {
    public int Duree { get; set; }
    public string Nom { get; set; }
    public Client Client { get; set; }
  }

  // programa de prueba
  class AccèsConcurrents
  {

    static void Main(string[] args)
    {
      Client client1;
      using (var context = new RdvMedecinsContext())
      {
        // hilo principal
        Thread.CurrentThread.Name = "main";
        // se vacía la base de datos actual
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // se agrega un cliente
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // seguimiento
        Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
        // copia de seguridad
        context.SaveChanges();
        // seguimiento
        Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
      }
      // vamos a modificar al cliente1 con dos subprocesos
      // hilo t1
      Thread t1 = new Thread(Modifie);
      t1.Name = "t1";
      t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
      // hilo t2
      Thread t2 = new Thread(Modifie);
      t2.Name = "t2";
      t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
      // esperamos a que terminen los dos hilos
      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);
      // se muestra la modificación; solo una debe haber tenido éxito
      using (var context = new RdvMedecinsContext())
      {
        // se recupera el cliente1 en el cliente2
        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());
      }
    }

    // hilo
    static void Modifie(object infos)
    {
 ...
}
  • línea 26: se inicia un contexto vacío;
  • línea 29: se le asigna un nombre al hilo actual para diferenciarlo de los dos hilos que se crearán posteriormente;
  • líneas 31-38: las entidades [Medecin] y [Client] se ponen en estado «eliminado»;
  • líneas 40-41: se agrega un cliente al contexto;
  • líneas 43-44: se muestra antes de la sincronización del contexto;
  • línea 46: sincronización del contexto con la base de datos: las entidades en estado «eliminado» se eliminarán de la base de datos. La entidad [Client], colocada en el contexto, se insertará en la base de datos. Será el único elemento de la base de datos;
  • líneas 47-49: se muestra el cliente después de la sincronización del contexto. En esta etapa, las pantallas se ven así:
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]

Cabe destacar que, tras la sincronización del contexto, el cliente tiene una clave primaria y un timestamp;

  • línea 50: se cierra el contexto;
  • línea 53: se asocia un hilo t1 al método [Modifie] de la línea 84. Esto significa que, cuando se inicie, ejecutará el método [Modifie];
  • línea 54: se le asigna un nombre al hilo t1;
  • línea 55: se inicia el hilo t1. Se le pasan parámetros en forma de una estructura [Data] definida en las líneas 12-17:
    • Duración: el hilo se detendrá Durée segundos antes de terminar su ejecución,
    • Cliente: una referencia al cliente que se debe actualizar en la base de datos,
    • Nombre: nombre que se le dará a este cliente;
  • líneas 57-59: lo mismo con un segundo hilo. Al final, dos hilos intentarán cambiar en la base de datos el nombre del mismo cliente;
  • líneas 60-63: después de iniciar los dos hilos, el hilo principal espera a que terminen de ejecutarse;
  • línea 62: espera a que termine el hilo t1;
  • línea 63: espera a que termine el hilo t2;
  • línea 64: no sabemos en qué orden terminarán los dos subprocesos. Lo que sí es seguro es que, en la línea 64, ya han terminado;
  • líneas 66-72: en un nuevo contexto, se busca al cliente en la base de datos para ver en qué estado se encuentra.

Veamos ahora qué hacen los dos hilos t1 y t2. Ejecutan el siguiente método [Modifie]:


static void Modifie(object infos)
    {
      // se recupera el parámetro
      Data data = (Data)infos;
      try
      {
        using (var context = new RdvMedecinsContext())
        {
          Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
          // se recupera «cliente1» en «cliente2»
          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());
          // se modifica cliente2
          client2.Nom = data.Nom;
          // esperamos un poco
          Thread.Sleep(data.Duree);
          // se guardan los cambios
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        // excepción
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
      }
      // fin del hilo
      Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
    }
  • línea 4: se recuperan los parámetros del hilo (Duración, Nombre, Cliente);
  • línea 7: nuevo contexto;
  • línea 11: se incorpora al cliente al contexto;
  • líneas 12-13: seguimiento para ver el estado del cliente;
  • línea 15: se cambia su nombre;
  • línea 17: el hilo se detiene durante Duree milisegundos. Esto tiene un efecto interesante. El hilo libera el procesador que lo ejecutaba, dejando espacio para otro hilo. En nuestro ejemplo, tenemos tres hilos: main, t1 y t2. El hilo main está detenido, esperando a que terminen los hilos t1 y t2. Suponiendo que el hilo t1 sea el primero en obtener el procesador, ahora se lo cede al hilo t2. Esto tendrá como efecto que el hilo t2 lea exactamente lo mismo que el hilo t1: el mismo cliente con el mismo timestamp;
  • línea 19: el contexto se sincroniza con la base de datos. Supongamos nuevamente que el hilo t1 se activa primero. Guardará al cliente con el nombre «yy». Podrá hacerlo porque tiene el mismo timestamp que en la base de datos. Debido a esta actualización, el SGBD modificará el timestamp. Cuando el hilo t2 se active a su vez, tendrá un cliente con un timestamp diferente al que ahora se encuentra en la base de datos. Su actualización será rechazada.

Las pantallas son las siguientes:

main client1--antes de guardar el contexto
Client[,xx,xx,xx,]
main client1--después de guardar el contexto
Client[33,xx,xx,xx,000001126209]
Thread main -- comienzo de la espera hasta que terminen ambos hilos
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 é modifiées ou supprimées depuis leur chargement. Actualisez les entrées ObjectStateManager. ---> System.Data.OptimisticConcurrencyException: Una instrucción de actualización, inserción o eliminación en el almacén afectó a un número inesperado de filas (0). Es posible que se hayan modificado o eliminado entidades desde su creación
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 de la espera, fin de los dos hilos
Thread main client2
Thread main Client[33,xx,xx,zz,000001126210]
  • línea 4: el cliente en la base de datos;
  • línea 9: el cliente tal como lo lee el hilo t2;
  • línea 11: el cliente tal como lo lee el hilo t1. Por lo tanto, ambos hilos han leído lo mismo;
  • línea 12: el hilo t2 termina primero. Por lo tanto, pudo realizar su actualización. El nombre debió haber cambiado a «zz»;
  • línea 13: el hilo t1 lanza una excepción de tipo [System.Data.OptimisticConcurrencyException]. EF detectó que no tenía el timestamp correcto;
  • línea 21: el hilo t1, a su vez, finaliza;
  • línea 22: el hilo principal ha terminado su espera;
  • línea 24: el hilo principal muestra el cliente en la base de datos. Efectivamente, fue el hilo t2 el que ganó. El nombre es «zz». Cabe señalar que el timestamp ha cambiado.

Ahora, analicemos otro aspecto: la transacción que enmarca la sincronización del contexto de persistencia con la base de datos.

3.5.9. Sincronización en una transacción

La tabla [CRENEAUX] tiene una restricción de unicidad que agregamos manualmente (véase el párrafo 2.2.4, página 12):

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

Procederemos de la siguiente manera: agregaremos al mismo tiempo dos citas para el mismo médico, el mismo día y el mismo horario. Veamos qué sucede.

El proyecto evoluciona de la siguiente manera:

El código del programa [SynchronisationTransaction] es el siguiente:


using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{

  // programa de prueba
  class SynchronisationTransaction
  {

    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // se vacía la base de datos actual
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        context.SaveChanges();
      }

      // se crea un cliente
      Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // se crea un médico
      Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // se crea un horario para este médico
      Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
      // se crean dos citas para este médico y este cliente, el mismo día, en la misma franja horaria
      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
      {
        // se guarda todo esto en el contexto de persistencia
        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);
          // se guarda el contexto; debe producirse una excepción
          // porque la tabla subyacente BD tiene una restricción de unicidad que impide
          // que haya dos RDV el mismo día, en el mismo intervalo
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("Erreur : {0}", e);
      }
      // si el almacenamiento se realiza dentro de una transacción, entonces no se debe haber insertado nada en la base de datos
      // debido a la excepción anterior; se verifica

      using (var context = new RdvMedecinsContext())
      {
        // los clientes
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // los médicos
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // los horarios
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // las citas
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}
  • líneas 15-27: se utiliza un contexto de persistencia para vaciar la base de datos;
  • línea 30: creación de un objeto [Client];
  • línea 32: creación de un objeto [Medecin];
  • línea 34: creación de un objeto [Creneau];
  • línea 36: creación de un objeto [Rv];
  • línea 37: creación de un segundo objeto [Rv] idéntico al anterior;
  • línea 41: apertura de un nuevo contexto;
  • líneas 43-47: los objetos creados anteriormente se vinculan al nuevo contexto. Tenga en cuenta aquí que, al tomar en cuenta las dependencias, podríamos haber minimizado el número de operaciones Add. Pero EF optimizará las órdenes SQL y INSERT que se emitirán a la base;
  • línea 51: el contexto se sincroniza con la base de datos. Como indica el comentario, la inserción de una de las dos citas debe fallar debido a la restricción de unicidad en la tabla [RVS]. Pero más allá de eso, si la sincronización ocurre dentro de una transacción, todo debe revertirse. Por lo tanto, no debe realizarse ninguna inserción. La base de datos debe permanecer vacía;
  • línea 53: se cierra el contexto;
  • líneas 61-90: visualización del contenido de la base de datos. Debe estar vacía.

La visualización en pantalla es la siguiente:

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: Se produjo un error al actualizar las entradas. Para obtener más información, consulta la excepción interna. ---> System.Data.SqlClient.SqlException: Violación de la restricción UNIQUE KEY «RVS_uq». No se puede insertar una clave duplicada en el objeto «dbo.RVS». Valor de clave duplicado: (18 de oct. de 2012 12:00AM, 34).
L'instruction a é 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 del seguimiento de la pila de excepciones internas ---
   ...
   à 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:línea 59
Clients--------------------------------------
Médecins--------------------------------------
Créneaux horaires--------------------------------------
Rendez-vous--------------------------------------
  • línea 1: excepción debido a la violación de la restricción de unicidad en la tabla [RVS];
  • líneas 9-12: la base de datos está vacía. Por lo tanto, la sincronización del contexto con la base de datos se realizó dentro de una transacción.

Sin duda, habría otros aspectos que explorar en EF 5. Pero sabemos lo suficiente como para retomar nuestro estudio de una arquitectura multicapa. El lector encontrará al principio de este documento referencias a artículos y libros que le permitirán profundizar su conocimiento de EF 5.

3.6. Estudio de una arquitectura multicapa basada en EF 5

Volvemos a nuestro caso de estudio descrito en el párrafo 2. Se trata de una aplicación web ASP.NET estructurada de la siguiente manera:

Comenzaremos por construir la capa [DAO] de acceso a los datos. Esta capa se basará en EF5.

3.6.1. El nuevo proyecto

Creamos un nuevo proyecto de consola VS 2012 [RdvMedecins-SqlServer-02] en la solución actual [1]:

Le agregamos cuatro carpetas [2] en las que distribuiremos nuestros códigos. La carpeta [Entites] es una copia de la carpeta [Entites] del proyecto anterior. Tras esta copia, aparecen errores debido a que no contamos con las referencias correctas. Debemos agregar una referencia a Entity Framework 5. Para ello, seguiremos el método explicado en el párrafo 3.4, página 21. La lista de referencias queda así: [3]:

En este punto, el proyecto ya no debería presentar errores de compilación. Del proyecto anterior, también copiamos el archivo [App.config], que configura la conexión a la base de datos:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Para obtener más información sobre la configuración de Entity Framework, visita 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>

  <!-- cadena de conexión a la base de datos -->
  <connectionStrings>
    <add name="monContexte"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- el proveedor de fábrica -->
  <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>

3.6.2. La clase Exception

Vamos a utilizar una clase de excepción propia del proyecto. Es la que se generará desde la capa [DAO]:

La capa [DAO] interceptará todas las excepciones que lleguen hasta ella y las encapsulará en una excepción de tipo [RdvMedecinsException]. Esta excepción será la siguiente:


using System;

namespace RdvMedecins.Exceptions
{
  public class RdvMedecinsException : Exception
  {

    // propiedades
    public int Code { get; set; }

    // constructores
    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;
    }

    // identidad
    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);
      }
    }
  }
}
  • línea 5: la clase deriva de la clase [Exception];
  • línea 9: agrega un código de error a su clase base;
  • líneas 12-32: los distintos constructores incorporan el campo [Code].

El proyecto evoluciona de la siguiente manera:

3.6.3. La capa [DAO]

La capa [DAO] ofrece una interfaz a la capa [ASP.NET]. Para identificar esta última, hay que consultar las páginas web de la aplicación:

  • En [1], arriba, la lista desplegable se ha llenado con la lista de médicos. La capa [DAO] proporcionará esta lista;
  • en [2], la capa [DAO] proporcionará;
  • la lista de citas de un médico para ese día,
  • la lista de horarios disponibles de un médico,
  • información adicional sobre el médico seleccionado;
  • en [3], la lista desplegable de clientes será proporcionada por la capa [DAO];
  • en [4], el usuario confirma una cita. La capa [DAO] debe poder agregarla a la base de datos. También debe poder proporcionar información adicional sobre el cliente seleccionado;
  • En [5], el usuario elimina una cita. La capa [DAO] debe permitirlo.

Con esta información, la interfaz [IDao] de la capa [DAO] podría ser la siguiente:


using System;
using System.Collections.Generic;
using RdvMedecins.Entites;

namespace RdvMedecins.Dao
{
  public interface IDao
  {
    // lista de clientes
    List<Client> GetAllClients();
    // lista de médicos
    List<Medecin> GetAllMedecins();
    // lista de horarios de un médico
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // lista de RV de un médico específico, en un día específico
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // agregar un RV
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // eliminar un RV
    void SupprimerRv(int idRv);
    // buscar una entidad T mediante su clave primaria
    T Find<T>(int id) where T : class;
  }
}

Los métodos de las líneas 10 a 20 se derivan del análisis que acabamos de realizar. El método de la línea 22 está ahí para solucionar el hecho de que se trabaja con carga diferida (Lazy Loading). Si en la capa [ASP.NET] se necesita una dependencia de una entidad, se buscará en la base de datos con este método.

La implementación [Dao] de esta interfaz será la siguiente:


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
  {

    //lista de clientes
    public List<Client> GetAllClients()
    {
      // lista de clientes
      List<Client> clients = null;
      try
      {
        // abrir contexto de persistencia
        using (var context = new RdvMedecinsContext())
        {
          // lista de clientes
          clients = context.Clients.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(1, "GetAllClients", ex);
      }
      // se devuelve el resultado
      return clients;
    }

    // lista de médicos
    public List<Medecin> GetAllMedecins()
    {
      // lista de médicos
      List<Medecin> medecins = null;
      try
      {
        // apertura del contexto de persistencia
        using (var context = new RdvMedecinsContext())
        {
          // lista de médicos
          medecins = context.Medecins.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(2, "GetAllMedecins", ex);
      }
      // se devuelve el resultado
      return medecins;
    }

    // lista de horarios disponibles de un médico específico
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
   ...
    }

    // lista de RV de un médico para un día específico
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
 ...
    }

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

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

    // buscar un cliente
    public Client FindClient(int id)
    {
...
    }

    // buscar un horario disponible
    public Creneau FindCreneau(int id)
    {
 ...
    }

    // buscar un médico
    public Medecin FindMedecin(int id)
    {
....
    }

    // buscar una cita
    public Rv FindRv(int id){
...
    }

  }
}

Explicaremos el método [GetAllClients], que debe devolver la lista de todos los clientes:

  • líneas 18-31: la búsqueda de clientes se realiza en un try / catch. Lo mismo ocurrirá con todos los métodos siguientes;
  • línea 21: apertura de un nuevo contexto;
  • línea 24: las entidades [Client] se cargan en el contexto y se colocan en una lista.

El método [GetAllMedecins], que debe devolver la lista de todos los médicos, es similar (líneas 37-57).

El método [GetCreneauxMedecin] es el siguiente:


// lista de horarios de un médico específico
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
      // lista de horarios
      try
      {
        // abrir contexto de persistencia
        using (var context = new RdvMedecinsContext())
        {
          // se recupera el médico con sus horarios
          Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
          // lista de horarios del médico
          return medecin.Creneaux.ToList<Creneau>();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
      }
    }
  • línea 9: apertura de un nuevo contexto de persistencia;
  • línea 11: se busca al médico cuya clave primaria se conoce. Se solicita incluir la dependencia [Creneaux], que es una colección de los horarios disponibles del médico. Si el médico no existe, el método Single lanza una excepción;
  • línea 13: se devuelve la lista de horarios.

El método [GetRvMedecinJour] debe devolver la lista de citas de un médico para un día determinado. Su código podría ser el siguiente:


// lista de RV de un médico para un día determinado
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
      // lista de citas
      List<Rv> rvs = null;

      try
      {
        // apertura del contexto de persistencia
        using (var context = new RdvMedecinsContext())
        {
          // se recupera el médico
          Medecin medecin = context.Medecins.Find(idMedecin);
          if (medecin == null)
          {
            throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
          }
          // lista de citas
          rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
      }
      // se devuelve el resultado
      return rvs;
    }
  • línea 13: se incorpora al contexto el médico cuya clave primaria se tiene;
  • líneas 14-17: si no existe, se lanza una excepción;
  • línea 19: se ejecuta la consulta LINQ para recuperar las citas de ese médico;

El método [AjouterRv] debe agregar una cita a la base de datos y debe devolver la clave primaria del elemento insertado. Su código podría ser el siguiente:


// Agregar un RV
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
      // N.º de la cita agregada
      int idRv;
      try
      {
        // apertura del contexto de persistencia
        using (var context = new RdvMedecinsContext())
        {
          // se recupera el horario
          Creneau creneau = context.Creneaux.Find(idCreneau);
          if (creneau == null)
          {
            throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
          }
          // se recupera el cliente
          Client client = context.Clients.Find(idClient);
          if (client == null)
          {
            throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
          }
          // creación de la franja horaria
          Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
          // Agregación al contexto
          context.Rvs.Add(rv);
          // guardar el contexto
          context.SaveChanges();
          // se recupera la clave primaria de la cita agregada
          idRv = (int)rv.Id;
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(7, "AjouterRv", ex);
      }
      // resultado
      return idRv;
    }
  • línea 12: se busca el horario de la cita en la base de datos;
  • líneas 13-16: si no se encuentra, se lanza una excepción;
  • línea 18: se busca el cliente de la cita en la base de datos;
  • líneas 19-22: si no se encuentra, se lanza una excepción;
  • línea 24: se crea un objeto [Rv] con la información necesaria;
  • línea 26: se agrega al contexto de persistencia;
  • línea 28: se sincroniza el contexto de persistencia con la base de datos. De esta manera, la cita se guardará en la base de datos;
  • línea 30: sabemos que, tras la sincronización de la base de datos, las claves primarias de los elementos insertados están disponibles. Recuperamos la del evento agregado;
  • línea 31: se cierra el contexto de persistencia.

El método [SupprimerRv] debe eliminar una cita cuya clave primaria se le pase como parámetro.


// eliminar un RV
    public void SupprimerRv(int idRv)
    {
      try
      {
        // apertura del contexto de persistencia
        using (var context = new RdvMedecinsContext())
        {
          // se recupera el Rv
          Rv rv = context.Rvs.Find(idRv);
          if (rv == null)
          {
            throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
          }
          // eliminación de Rv
          context.Rvs.Remove(rv);
          // guardar el contexto
          context.SaveChanges();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(8, "SupprimerRv", ex);
      }
    }
  • línea 7: nuevo contexto de persistencia;
  • línea 10: se incorpora al contexto la cita que se va a eliminar;
  • líneas 11-15: si no existe, se lanza una excepción;
  • línea 16: se elimina del contexto;
  • línea 18: se sincroniza el contexto con la base de datos;
  • línea 19: se cierra el contexto.

El método [Find<T>] permite buscar en la base de datos una entidad de tipo T mediante su clave primaria. Su código podría ser el siguiente:


public T Find<T>(int id)  where T : class
    {
      try
      {
        // apertura del contexto de persistencia
        using (var context = new RdvMedecinsContext())
        {
          return context.Set<T>().Find(id);
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(20, "Find<T>", ex);
      }
    }
  • línea 8: el método Set<T> permite recuperar un DbSet<T> al que se le pueden aplicar los métodos habituales.

El proyecto evoluciona de la siguiente manera:

3.6.4. Prueba de la capa [DAO]

Vamos a crear un programa de prueba de la capa [DAO]. La arquitectura de la prueba será la siguiente:

Un programa de consola le pide a [Spring.net] que instancie la capa [DAO]. Una vez hecho esto, prueba las diferentes funcionalidades de la interfaz de la capa [DAO]. En lugar de un programa de consola, hubiera sido preferible escribir un programa de prueba del tipo NUnit. Un programa de prueba de la capa [DAO] podría ser el siguiente:


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
      {
        // instanciación de la capa [DAO] mediante Spring
        dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

        // visualización de clientes
        List<Client> clients = dao.GetAllClients();
        DisplayClients("Liste des clients :", clients);

        // visualización de médicos
        List<Medecin> medecins = dao.GetAllMedecins();
        DisplayMedecins("Liste des médecins :", medecins);

        // lista de horarios disponibles del médico n.º 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);

        // lista de citas de un médico para un día determinado
        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)));

        // agregar un RV al médico n.º 1 en el horario n.º 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)));

        // Agregar una cita en un horario ya ocupado — debe generar una excepción
        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));
        }

        // eliminar una cita
        Console.WriteLine(string.Format("Suppression du RV n° {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));
      }
      //pausa 
      Console.ReadLine();
    }

    // métodos de utilidad: muestra listas
    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)
    {
...
    }
  }
}
  • línea 14: la referencia a la capa [DAO]. Para que la prueba sea independiente de la implementación real de esta, dicha referencia es del tipo de la interfaz [IDao] y no del tipo de la clase [Dao];
  • línea 18: Spring instancia la capa [DAO]. Más adelante abordaremos la configuración necesaria para que esto sea posible. Convertimos la referencia de objeto devuelta por Spring a una referencia del tipo de la interfaz [IDao];
  • líneas 21-22: muestran a los clientes;
  • líneas 25-26: muestran a los pacientes;
  • líneas 29-30: muestran la lista de horarios del médico n.º 0;
  • línea 33: muestra las citas del médico n.º 0 para la fecha del 23/11/2013. No debería haber ninguna;
  • línea 37: agrega una cita al médico n.º 0 para el 23/11/2013;
  • línea 39: muestra las citas del médico n.º 0 para el 23/11/2013. Debe haber una;
  • línea 46: se agrega por segunda vez la misma cita. Debe producirse una excepción;
  • línea 57: se elimina la única cita agregada;
  • línea 58: muestra las citas del médico n.º 0 para la fecha del 23/11/2013. No debe haber ninguna.

3.6.5. Configuración de Spring.net

En el programa de prueba anterior, pasamos rápidamente por alto la instrucción que instancia la capa [DAO]:


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

La clase [ContextRegistry] es una clase de Spring en el espacio de nombres [Spring.Context.Support]. Para poder utilizar Spring, debemos agregar su DLL a las referencias del proyecto. Procedemos de la siguiente manera:

  • en [1], buscamos paquetes con la herramienta [NuGet];
  • en [2], buscamos paquetes en línea;
  • en [3], se ingresa la palabra clave spring en el campo de búsqueda;
  • en [4], se muestran los paquetes cuya descripción contiene esa palabra clave. En este caso, el que nos conviene es [Spring.Core]. Lo instalamos.

Las referencias del proyecto cambian de la siguiente manera:

El paquete [Spring.Core] dependía del paquete [Common.Logging]. Este último también se ha cargado. En este punto, el proyecto ya no debería presentar errores.

Pero eso no significa que vaya a funcionar. Primero debemos configurar Spring en el archivo [App.config]. Esta es la parte más delicada del proyecto. El nuevo archivo [App.config] es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Para obtener más información sobre la configuración de Entity Framework, visita 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>
    <!-- registro común-->
    <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>
  <!-- Cadenas de conexión -->
  <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>
  <!-- configuración de 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>
  <!-- configuración 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>

Empecemos por eliminar todo lo que ya conocemos: Entity Framework, cadenas de conexión, ProviderFactory. El archivo queda así:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Para obtener más información sobre la configuración de Entity Framework, visita 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>
    <!-- registro común-->
    <sectionGroup name="common">
      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
    </sectionGroup>
  </configSections>
...
  <!-- configuración de 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>
  <!-- configuración 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>
  • líneas 3-15: definen secciones de configuración;
  • línea 8: define la clase que administrará la sección <spring><context> del archivo XML (líneas 19-21);
  • línea 9: define la clase que administrará la sección <spring><objects> del archivo XML (líneas 22-24);
  • línea 13: define la clase que administrará la sección <common><logging> del archivo XML (líneas 27-36);
  • líneas 7-14: son fijas. No es necesario modificarlas en otro proyecto;
  • líneas 18-25: configuración de Spring. Es estable, excepto por las líneas 22-24, que definen los objetos que Spring deberá instanciar;
  • línea 23: definición de un objeto. El atributo id es libre. Es el identificador del objeto. El atributo type indica la clase que se instanciará en el formato «nombre completo de la clase, ensamblaje que contiene la clase». La clase en este caso es la que implementa la capa [DAO]: [RdvMedecins.Dao.Dao]. Para conocer su ensamblado, hay que consultar las propiedades del proyecto:

En [1], el nombre del ensamblado que se debe proporcionar;

  • líneas 27-36: la configuración de «Common Logging» es estable. Es posible que sea necesario modificar el nivel de información, en la línea 32. Tras la fase de depuración, se puede cambiar el nivel a INFO.

Al final, aunque a primera vista parezca complejo, el archivo de configuración de Spring resulta sencillo. Solo hay que modificar:

  • las líneas 22-24, que definen los objetos que se van a instanciar;
  • la línea 32: el nivel de registro.

En el programa de prueba, la instrucción que instancia la capa [DAO] es la siguiente:


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

[ContextRegistry] es una clase de Spring que utiliza la configuración de Spring definida en un archivo [Web.config] o [App.config]. En este caso, utilizará la siguiente sección del archivo [App.config]:


  <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() utiliza el contexto de las líneas 2 a 4. La línea 3 indica que los objetos de Spring se definen en la sección [spring/objects] del archivo de configuración. Esta sección abarca las líneas 5 a 7;
  • ContextRegistry.GetContext().GetObject("rdvmedecinsDao") utiliza la sección de las líneas 5 a 7. Devuelve una referencia al objeto que tiene el atributo id= "rdvmedecinsDao". Este es el objeto definido en la línea 6. A continuación, Spring instanciará la clase definida por el atributo type utilizando su constructor sin parámetros. Por lo tanto, este debe existir. Una vez hecho esto, la referencia del objeto creado se devuelve al código que lo invocó. Si el objeto se solicita por segunda vez en el código, Spring simplemente devuelve una referencia al primer objeto creado. Este es el patrón de diseño (Design Pattern) denominado «singleton».

La creación del objeto puede ser más compleja. Se puede utilizar un constructor con parámetros o especificar la inicialización de ciertos campos del objeto una vez que este se ha creado. Para obtener más información sobre este tema, puede consultarse el artículo «Tutorial de Spring IOC para .NET», en URL [http://tahe.developpez.com/dotnet/springioc/].

Una vez hecho esto, podemos ejecutar la aplicación. Los resultados en pantalla son los siguientes:

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 :

Los resultados coinciden con lo esperado. A partir de ahora consideraremos que nuestra capa [DAO] es válida. El tutorial podría terminar aquí. Hasta ahora hemos mostrado:

  • los conceptos básicos de ORM en Entity Framework 5;
  • una capa [DAO] que utiliza este ORM.

Recordemos nuestro caso de estudio descrito al inicio de este documento. Partimos de una aplicación existente con la siguiente arquitectura:

que queremos transformar en esta:

donde EF5 ha reemplazado a NHibernate. Acabamos de construir la capa [DAO2]. De hecho, no presenta la misma interfaz que la capa [DAO1], cuya interfaz era más reducida:


  public interface IDao
  {
    // lista de clientes
    List<Client> GetAllClients();
    // lista de médicos
    List<Medecin> GetAllMedecins();
    // lista de horarios de un médico
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // lista de RV de un médico específico, en un día específico
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // agregar un RV
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // eliminar un RV
    void SupprimerRv(int idRv);
  }

La capa [DAO2] ha agregado a esta interfaz el método:


// buscar una entidad T mediante su clave primaria
T Find<T>(int id) where T : class;

La incorporación de este método se debe a que la capa ORM EF 5 funciona por defecto en modo de carga diferida (Lazy Loading). Las entidades llegan a la capa [ASP.NET] sin sus dependencias. El método anterior nos permite recuperarlas si las necesitamos, y en ciertos casos sí las necesitamos. NHibernate también funciona por defecto en modo de carga diferida (Lazy Loading), pero yo lo había utilizado en modo de carga inmediata (Eager Loading). Las entidades llegaban a la capa [ASP.NET] con sus dependencias.

Vamos a terminar la migración de la aplicación ASP.NET / NHibernate a la aplicación ASP.NET / EF 5. Pero como esto ya no se aplica a EF5, no comentaremos el código web. Simplemente explicaremos cómo configurar la aplicación web y probarla. Esta está disponible en el sitio web de este tutorial.

3.6.6. Generación de DLL a partir de la capa [DAO]

En la siguiente arquitectura:

la capa [ASP.NET] tendrá a su disposición las capas a su derecha en forma de DLL. Por lo tanto, construimos la DLL a partir de la capa [DAO].

  • en [1], se selecciona el programa de prueba y en [2] no se incluye en la DLL que se va a generar;
  • en [3], en las propiedades del proyecto, se indica que el ensamblado que se va a crear es un DLL;
  • en [4], en el menú de VS, se indica que se va a generar un ensamblado de tipo [Release] que contiene menos información que un ensamblado de tipo [Debug];
  • en [5], se regenera el ensamblado del proyecto. Se generará el DLL;
  • en [6], se muestran todos los archivos del proyecto;
  • en [7], se genera el archivo DLL del proyecto de la capa [DAO]. Este es el que utilizará el proyecto web ASP.NET;
  • en [8], actualizamos la visualización del proyecto;
  • en [9], los archivos DLL de la carpeta [Release] se agrupan en una carpeta externa [lib], [10]. Ahí es donde el proyecto web obtendrá sus referencias.

3.6.7. La capa [ASP.NET]

Aquí explicaremos la migración de la aplicación [ASP.NET / NHibernate] a la aplicación [ASP.NET / EF 5]. Trabajaremos con Visual Studio Express 2012 para la web, disponible de forma gratuita en URL [http://www.microsoft.com/visualstudio/fra/downloads].

Trabajaremos a partir del proyecto web existente creado con VS 2010.

  • En [1], abrimos el proyecto existente:
  • en [2], el proyecto cargado tiene las siguientes referencias: [3]:
  • [NHibernate] es el DLL del marco NHibernate,
  • [Spring.Core] es el DLL del marco Spring.net,
  • [log4net] es el DLL del marco de trabajo de registros log4net. Este marco de trabajo es utilizado por Spring.net,
  • [MySql.Data] es el controlador ADO.NET del SGBD MySQL,
  • [rdvmedecins] es la DLL de la capa [DAO] construida con NHibernate;
  • en [4], cambiamos el nombre del proyecto y en [5], eliminamos las referencias anteriores;
  • en [6], agregamos referencias al proyecto;
  • en [7], en el asistente utilizamos la opción [Parcourir];
  • en [8], seleccionamos todos los DLL del proyecto n.º 2 que se colocaron previamente en la carpeta [lib];
  • en [9], un resumen que validamos;
  • en [10], el proyecto web con sus nuevas referencias.

Una vez hecho esto, el proyecto se presenta de la siguiente manera:

  • En [1], el código de administración de las páginas web se distribuye entre los dos archivos [Global.asax] y [Default.aspx]. El código de utilidades se ha colocado en la carpeta [Entites]. Por último, la aplicación se configura mediante el archivo [Web.config];
  • en [2], generamos el ensamblado del proyecto;
  • en [3], aparecen errores.

Analicemos los errores, por ejemplo, el siguiente:

Image

y su explicación:

Image

¿El tipo de [medecin.Id] es int?, mientras que el método [GetCreneauxMedecin] es de tipo int. Por lo tanto, se necesita un cast. Este error se repite en todo el código porque las entidades del proyecto ASP.NET / NHibernate tenían claves primarias de tipo int, mientras que las de los proyectos ASP.NET / EF son de tipo int?. Corregimos todos los errores de este tipo y regeneramos el proyecto. Ya no hay ninguno.

Nos queda un detalle por resolver antes de ejecutar el proyecto: la instanciación de la capa [DAO] por parte del marco Spring. Esto se realiza en [Global.asax]:


protected void Application_Start(object sender, EventArgs e)
    {
      // se almacenan en caché ciertos datos de la base de datos
      try
      {
        // instanciación de la capa [dao]
        Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
        ...
      }
      catch (Exception ex)
      {...
      }
    }

En el programa de prueba de la capa [DAO], esta instanciaba la capa [DAO] de la siguiente manera:


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

Ambos métodos son idénticos. Recordemos que esta instanciación de la capa [DAO] se basaba en una configuración realizada en [App.config]. A continuación, reemplazamos el contenido actual [Web.config] del proyecto web por el de [App.config] del proyecto de la capa [DAO] para tener la misma configuración.

Ya estamos listos para una primera ejecución. Se muestra la página de inicio [1]:

  • en [2], ingresamos una fecha de cita y confirmamos;
  • en [3], se produce un error.

Al revisar el texto del error que muestra la página, se observa que la excepción reportada es la de Lazy Loading: se intentó cargar una dependencia de un objeto cuando el contexto de persistencia que lo administra ya se había cerrado. El objeto se encuentra ahora en un estado «desvinculado». Este error se debe a que NHibernate se utilizó en modo Eager Loading, mientras que EF funciona por defecto en Lazy Loading. En la línea en rojo anterior:

  • rdv representa un objeto [Rv] que se cargó sin sus dependencias;
  • para evaluar rdv.Creneau.Id, la aplicación intenta cargar la dependencia rdv.Creneau. Pero como ya no estamos en el contexto, esto no es posible, de ahí la excepción.

En este caso, la solución es sencilla. En la línea 108, se crea una entrada en un diccionario con la clave primaria del horario de una cita como clave. Ahora bien, resulta que la entidad [Rv] encapsula la clave primaria del horario asociado. Por lo tanto, se escribe:


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

Volvemos a intentar la ejecución. Esta vez, el error es el siguiente:

El error es similar. En la línea 132, se intenta cargar la dependencia [Client] de un objeto [Rv] en la capa ASP.NET, es decir, fuera de contexto. Hay que buscar el objeto [Client] en la base de datos. Para solucionar este problema, se ha ampliado la interfaz [IDao] con el siguiente método:


    // buscar una entidad T mediante su clave primaria
    T Find<T>(int id) where T : class;

Esto permitirá buscar las dependencias. De este modo, la línea errónea anterior se reescribirá de la siguiente manera:


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

Una vez más, cabe destacar la importancia de que las entidades incluyan sus claves externas. En este caso, la entidad [Rv] nos da acceso a la clave externa de la dependencia asociada [Creneau]. Una vez realizadas estas dos correcciones, la aplicación funciona. Se invita al lector a probar la aplicación [RdvMedecins-SqlServer-03], disponible en la sección de descargas de ejemplos del sitio web de este artículo.

3.7. Conclusion

Hemos completado con éxito la adaptación de una aplicación ASP.NET / NHibernate:

a una aplicación ASP.NET / EF 5:

Aunque esta arquitectura debería habernos permitido mantener intacta la capa [ASP.NET], tuvimos que modificarla por dos razones:

  • las entidades no eran exactamente iguales. El tipo de las claves primarias de las entidades NHibernate era int, mientras que el de EF 5 era int?. Esto nos llevó a introducir cast en el código web;
  • el modo de carga de las entidades no era el mismo para las dos: carga anticipada (Eager Loading) para NHibernate y carga diferida (Lazy Loading) para EF 5. Esto nos llevó a ampliar la interfaz de la capa [DAO] con un método genérico que permite buscar una entidad mediante su clave primaria.

Sin embargo, la migración resultó bastante sencilla, lo que justifica una vez más, si es que hacía falta, la arquitectura en capas y la inyección de dependencias con Spring u otro marco de inyección de dependencias.

Ahora vamos a evaluar el impacto de un cambio de SGBD en la arquitectura anterior. Vamos a migrar todos los proyectos anteriores a otros cuatro SGBD:

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

Los códigos ya no cambiarán. Solo cambiarán los siguientes elementos:

  • la definición en las entidades del campo utilizado para controlar la concurrencia de acceso a una entidad;
  • los archivos de configuración [App.config] o [Web.config];

Solo comentaremos los elementos que cambian.