3. Caso práctico 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. Es bastante normal. Es probable que sea el SGBD más extendido 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 apartado 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 quedarían obsoletas con bastante rapidez. Es una tarea (que, hay que reconocerlo, no siempre es fácil) que dejamos en manos del lector.
Debemos instalar las siguientes herramientas:
- SGBD, SQL y Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. Descarga la versión «With Tools», que incluye, junto con SGBD, una herramienta de administración:
Una vez instalado el SGBD, lo ejecutamos:
![]() |
![]() |
- [1]: en el menú Inicio, ejecuta el «Administrador de configuración del servidor SQL»;
- [2]: en este gestor, inicia el servidor;
- [3]: ya se ha iniciado.
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.
Vamos a conectarnos al servidor:
![]() |
- en [1], abrimos el explorador de objetos;
- en [2], se introducen los parámetros de conexión:
- [3]: el servidor (local) (atención a los paréntesis necesarios) hace referencia al servidor instalado en el equipo,
- [4]: se elige la autenticación de Windows. Es necesario ser administrador del equipo para que esta conexión se realice correctamente,
- [5]: se establece la conexión;
![]() |
- [6]: ya estamos conectados;
- [7]: se desea modificar algunas propiedades del servidor;
![]() |
- [8]: se solicita que haya dos modos de autenticación:
- autenticación de Windows, tal y como se acaba de utilizar. De este modo, un usuario de Windows con los permisos adecuados puede iniciar sesión,
- autenticación del servidor SQL. El usuario debe figurar entre 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 contraseña es «sqlserver2012»;
![]() |
- en [10], se le concede permiso para conectarse;
- en [11], se activa la conexión. Una vez hecho esto, se puede validar el asistente;
- en [12], nos desconectamos del servidor.
Ahora, volvemos a conectarnos con el nombre de usuario sa/sqlserver2012:
![]() |
- en [1], nos volvemos a conectar;
- en [2], en la autenticación de 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 de datos [demo], hay que actualizar la base de datos;
- en [12], la tabla [PERSONNES] se ha creado correctamente.
Por ahora sabemos lo suficiente sobre el uso de la herramienta de administración de SQL Server.
3.3. El servidor integrado (localdb)\v11.0
VS Express 2012 incluye un servidor integrado SQL. 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 vamos a ver 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 e DLL de Entity Framework 5. Lo añadimos:
![]() |
- en [1], la herramienta NuGet permite descargar las dependencias;
![]() |
- en [2], se descarga la dependencia de Entity Framework;
- en [3], la referencia se ha añadido al proyecto.
Para obtener más información, consulta las propiedades de la referencia añadida:
![]() |
- en [1], la versión de DLL. Se necesita la versión 5;
- en [2], su ubicación en el sistema de archivos: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll donde <solution> es la carpeta de la solución VS. Todos los paquetes añadidos por NuGet se guardarán en la carpeta <solution>/packages;
- en [3], se ha creado 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>
En él se enumeran los paquetes importados por NuGet.
Volvamos al proyecto VS y creemos una carpeta [Models] dentro del proyecto:
![]() |
- en [1], añadir una carpeta al proyecto;
- en [2], se llamará [Models].
Seguiremos con esta costumbre 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. Empecemos por la tabla [MEDECINS]:
3.4.1. La entidad [Medecin]
Contiene información sobre los médicos gestionados por 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 línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
- NOM: el apellido del médico
- PRENOM: su nombre
- TITRE: su tratamiento (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 denominado «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 gestionadas 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 haya en la base de datos, uno por tabla;
- línea 15: se define una clase [RdvMedecinsInitializer] para inicializar la base de datos creada. En este caso, deriva de la clase [DropCreateDataBaseAlways] que, como su nombre indica, elimina la base de datos si ya existe y, a continuación, 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]: vuelve a crear 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 gestionar 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. En este caso, también 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 figuran 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 han registrado en [App.config] al añadir la dependencia de Entity Framework a las referencias del proyecto.
Ejecutemos el proyecto (Ctrl-F5) tras haber iniciado SQL Server Express (esto es importante):
![]() | ![]() |
La ejecución debe completarse sin errores. Ahora abramos la herramienta de administración de SQL Server y actualicemos la vista:
![]() |
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 recogen los nombres de los campos de la entidad [Medecin]. Si el código se ha ejecutado correctamente y la base de datos anterior no aparece, hay que consultar el servidor integrado (localdb)\v11.0 (véase la página 19). Con VS 2012 Pro, se utiliza este servidor si el servidor SQL no está activo en el momento de la ejecución del código. Con VS 2012 Express, no.
Analicemos la estructura de la tabla [MEDECINS]:
- recoge los nombres de los campos de la entidad [Medecin];
- la columna [Id] es la clave primaria. Se trata de 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, Apellidos y Nombre, se ha utilizado un tipo [nvarchar(max)]. Se podría ser más preciso: 5 caracteres para el título y 30 para los apellidos y el nombre;
- las columnas «Título», «Apellidos» y «Nombre» pueden tener el valor NULL. Vamos a cambiar esto.
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 lo genera automáticamente el servidor SQL. Adoptaremos esta estrategia con todos los SGBD.
Vamos a dar menos importancia a las convenciones de EF utilizando 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, y [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 ha traducido 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 línea L de la tabla [MEDECINS] en el momento T2. La línea 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. A continuación, el proceso ORM lanza 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 denomina gestión optimista de accesos concurrentes. Con EF 5, un campo que desempeñe esta función debe tener uno de los dos atributos: [Timestamp] o [ConcurrencyCheck]. SQL Server tiene un tipo [timestamp]. En una columna de este tipo, SQL Server genera automáticamente su valor cada vez que se inserta o modifica una fila. Dicha columna puede servir entonces para gestionar la concurrencia de acceso. Retomando el ejemplo anterior, el proceso P2 encontrará un timestamp diferente al que había leído, ya que, entretanto, 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], ya que 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 evoluciona 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 asignará un valor a la clave primaria. En este caso, el tipo int asignado a la clave primaria [Id] no es adecuado, ya que este tipo no admite el valor null. ¿Se le asigna entonces el tipo «int?» que acepta los valores int más el puntero null? La entidad [Medecin] utilizada será, por tanto, la siguiente:
public class Medecin
{
// datos
[Key]
[Column("ID")]
public int? Id { get; set; }
...
Ahora nos queda ver cómo representar en una entidad el concepto de clave foránea entre tablas.
3.4.2. La entidad [Creneau]
La tabla [CRENEAUX] enumera los intervalos horarios en los que son posibles los RV:
![]() |
![]() |
- ID: número que identifica el intervalo horario —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 de la franja horaria
- HFIN: hora de finalización de la franja horaria
- MFIN: minutos de fin de franja
La segunda línea de la tabla [CRENEAUX] (véase [1] más arriba) indica, por ejemplo, que la franja n.º 2 comienza a las 8:20 y termina a las 8:40, y corresponde 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-21. El hecho de que la tabla [CRENEAUX] tenga una clave foránea 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, solo es importante 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 ha generado a partir del nombre del campo correspondiente en la entidad (Médico), al que se le ha añadido 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 ya existente, la columna de clave externa no se llamará necesariamente [Medecin_Id]. En el caso de las demás columnas, ya habíamos visto que la anotación [Column] resolvía este problema. Curiosamente, en el caso de una clave externa resulta más complicado. 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 añade 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, la columna de clave externa lleva el nombre que le hemos asignado. 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]. No obstante, 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 resultar útil.
El campo [Medecin] anterior refleja la relación «muchos 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 añadido el campo [Creneaux], que es una colección de objetos [Creneau]. Este campo nos dará acceso a todas las franjas horarias del médico.
Al volver a ejecutar el proyecto, se observa que la tabla [MEDECINS] no ha cambiado:
![]() |
No se ha añadido 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 gestionados 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 línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
- 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 prácticamente idéntica a la clase [Medecin]. Se podrían derivar de una misma clase padre. La novedad se encuentra 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 forma única a RV – clave primaria
- JOUR: día del RV
- ID_CRENEAU: franja horaria del RV —clave externa en la columna [ID] de la tabla [CRENEAUX]—; determina tanto la franja horaria como el médico correspondiente.
- ID_CLIENT: número del cliente para el que se realiza la reserva – 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: la franja horaria 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 «muchos a uno»: a un intervalo 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: el conjunto de citas concertadas en ese intervalo 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. En este caso, lo ha generado EF;
- llenar la base de datos con datos.
3.4.4. Fijar el nombre de la base de datos
Para fijar 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 tenerlo en cuenta. Esta restricción se puede eludir en el constructor del contexto:
// constructor
public RdvMedecinsContext()
: base("monContexte")
{
}
En este caso, tendremos name= «monContexte». Esto es lo que aparecerá 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 tratarse de 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 añadirlas para los demás SGBD. Así que las pongo aquí para recordarlas. No suponen 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:
![]() |
Ya está. Estamos listos. Ejecutamos el proyecto y obtenemos la base [rdvmedecins-ef] siguiente:
![]() |
Esta será nuestra base definitiva. Ahora solo nos queda introducir datos en ella.
3.4.5. Relleno de la base de datos
La clase de inicialización de la base de datos se puede utilizar para introducir 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);
}
// las franjas horarias
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 añaden al contexto de persistencia, más concretamente a los médicos del mismo. Cabe destacar el método [Add], que permite realizar esta operación. Hay que recordar aquí 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 los gestiona EF. Antes no estaban vinculados. Existían como objetos, pero no los gestionaba 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 franjas horarias. 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 franjas horarias;
- líneas 71-74: se añaden estos intervalos horarios al contexto de persistencia;
- línea 76: creación de una cita para el primer cliente con la primera franja horaria y su inclusión en el contexto de persistencia.
Al ejecutar el proyecto, se obtiene la base de datos siguiente:
![]() | ![]() |
Arriba se puede ver 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 añadidos para la gestión de la persistencia con EF 5, resultan idénticas. Vamos a hacer que deriven de una clase [Personne]. Estas dos entidades pasan a ser entonces las siguientes:
// 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
{
// horarios 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. Añadir 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, una franja horaria de un médico solo puede reservarse una vez para una cita. En términos de 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 comprobado. Vamos a seguir otro enfoque. Utilizaremos un cliente de administración de SQL Server para añadir esta restricción.
Con «SQL Server Management Studio», no he encontrado una forma sencilla de añadir 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 ha creado un nuevo índice en la tabla [RVS].
Existen otras herramientas de administración de SQL Server. Aquí vamos a utilizar 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], se guarda una base de datos;
- en [2], nos conectamos al servidor (local);
- en [3], con autenticación SQL Server;
- en [4], con el nombre de usuario «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], se finaliza el asistente;
- en [9], la base de datos aparece en el árbol de bases de datos. Nos conectamos a ella [10];
- en [11], ya estamos conectados.
«SQL Manager Lite for SQL Server» permite crear la restricción de unicidad en la tabla [RVS].
![]() |
- En [1], vemos la restricción de unicidad que hemos creado 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], se trata de una restricción de unicidad;
- en [4], sobre las columnas JOUR y CRENEAU_ID;
La pestaña DDL nos proporciona el código SQL que se va a 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 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 sobre 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:

3.4.8. La base definitiva
Ya tenemos nuestra base definitiva. Exportamos su script SQL para poder regenerarla si fuera necesario.
![]() |
- en [1], inicio del asistente;
- en [2], el servidor;
- en [3], la base de datos que se va a exportar;
![]() |
- en [4], especifica el nombre del archivo en el que 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 va a generar;
- en [8], finaliza el asistente.
El script se ha generado y se ha 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 rellenan;
![]() |
- en [14], volvemos a encontrar la restricción de unicidad que habíamos creado para la tabla [RVS].
A partir de ahora trabajaremos con esta base de datos existente. Si se destruye o se daña, sabemos cómo regenerarla.
3.5. Explotación de la base de datos con Entity Framework
Vamos a:
- añadir, eliminar y modificar elementos de la base de datos;
- realizar consultas en 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 «Lazy Loading» y «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. Vamos a vaciarla. 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 en 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 incluirá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 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, a continuación, ejecutemos el proyecto.
![]() |
Comprobemos la base de datos. Veremos que todas las tablas están vacías ([2]). Es sorprendente, ya que solo habíamos solicitado la eliminación de los médicos y los clientes. Es gracias al funcionamiento de las claves externas que las demás tablas se han vaciado en cadena.
La definición de la clave externa de la tabla [CRENEAUX] hacia la tabla [MEDECINS] ha sido 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 las franjas horarias asociadas a él.
Las restricciones de clave externa de la tabla [RVS] se definen de forma análoga:
- líneas 1-6: al eliminar un cliente, también se eliminarán las citas asociadas a él;
- líneas 1-6: al eliminar un intervalo de tiempo, también se eliminarán todas las citas asociadas a él.
3.5.2. Añadir elementos al contexto de persistencia
Ahora que hemos vaciado la base de datos, vamos a volver a llenarla. Añadimos 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] });
// se guarda 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 introducen en el contexto y, a continuación, se eliminan de él. Acabamos de ver que esto vaciaba por completo la base de datos;
- líneas 22-88: se añaden elementos al contexto de persistencia. Todos ellos tienen la clave primaria en null. 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 base de datos será objeto de una serie de operaciones SQL y DELETE, seguidas de una serie de operaciones SQL y INSERT;
Se establece el programa [Fill] como nuevo objeto de inicio del proyecto [1] y, a continuación, se ejecuta este último.
![]() |
En [2] se comprueba que las tablas se han rellenado.
3.5.3. Visualización del contenido de la base de datos
Ahora vamos a visualizar el contenido de la base de datos mediante la consulta LINQ to Entity. LINQ (Language INtegrated Query) apareció con el framework .NET 3.5 en 2007. Se presenta como una extensión de los lenguajes .NET y c.a.d, de los que forma parte, 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) de consulta de bases de datos. Existen diferentes variantes de LINQ:
- LINQ to Object, para consultar colecciones en memoria;
- LINQ to XML, para realizar consultas en XML;
- LINQ a Entity, para realizar consultas en bases de datos;
Para funcionar, LINQ se basa en numerosas extensiones realizadas en los lenguajes .NET. Estas pueden utilizarse fuera de LINQ. No vamos a presentarlas, sino que nos limitaremos a proporcionar dos referencias en las que 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 ha parecido excelente. No he leído el segundo, pero sí he leído de los mismos autores «C# 3.0 in a nutshell» cuando salió a la venta LINQ. Este libro me ha parecido muy por encima de la media de los libros que suelo leer. Parece que los demás 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, añadiremos a sus clases dos métodos de visualización. Empecemos por 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 abreviada
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 «Lazy Loading» y «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 varias franjas horarias;
- de varios a uno, como en la entidad [Creneau] que se muestra a continuación, donde varios turnos 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:
// 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 todas las franjas horarias del médico antes de su ejecución. Para evitar esta costosa carga, la dependencia no se ha incluido en la firma de la entidad. En general, vamos a incluir dos firmas en cada entidad:
- un método ToString que mostrará la entidad y sus posibles dependencias varias a una. 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 obligará a 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 obligará a que se carguen;
- 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 tiene en cuenta 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 nuevo código [Dump.cs] de la siguiente manera:
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);
}
// franjas horarias
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-21, que muestran las entidades [Client]. La explicación dada será válida para 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 concreto de una variable. El compilador lo deduce entonces 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 se enumera mediante el bucle «for / each» de las líneas 4-7. Mientras esto no ocurra, la variable [clients] no es más que una consulta no evaluada;
- línea 4: se itera sobre la consulta [clients]. Esto forzará la evaluación de la consulta. Las líneas de la tabla [CLIENTS] se incorporarán una a 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 líneas 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 se realizará 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 neutro. Si se hubiera 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 estas citas. Las demás no se habrían cargado. Se habrían cargado más tarde en otra visualización. Esto afecta al 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 necesita 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 necesita una consulta SQL para cada una de ellas. Suponiendo que haya N2 clientes y N3 franjas horarias, y que todas estas entidades estén referenciadas en la tabla [RVS], su visualización requerirá 1 + N2 + N3 consultas SQL. Por lo tanto, el rendimiento es inferior al de la versión analizada. Para visualizar la tabla [RVS] con sus dependencias, sería necesaria una unión entre tablas. Es posible realizarla con LINQ. Volveremos sobre 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, a continuación, lo ejecutamos:
![]() |
La salida de la consola es la siguiente:
3.5.4. Aprendizaje de LINQ con LINQPad
Anteriormente hemos utilizado consultas de tipo «LINQ to Entity» para mostrar el contenido de las tablas de la base de datos. Joseph Albahari ha escrito 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 en LINQ podrán iniciarse con los ejemplos de la pestaña [Samples] [2], que muestran numerosos ejemplos. Seleccionemos el ejemplo [3], que se mostrará entonces en otra ventana [4]. El código completo del ejemplo es el siguiente:
// Ahora, una sencilla expresión de consulta LINQ-to-objects (fíjate en que no hay punto y coma):
from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word
// No dudes en editar esto... (¡nadie te está mirando!) Se te pedirá que guardes cualquier
// cambios en un archivo aparte.
//
// Conse jo: Puedes ejecutar parte de una consulta seleccionándola y pulsando F5.
Las líneas 3-5 son un ejemplo de consulta LINQ to 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 lo que queremos extraer del elemento actual 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 ordenadas por longitud. Este sencillo ejemplo muestra la potencia 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 agrupa 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. Como se ha utilizado una clase anónima en la línea 9, no se puede especificar el tipo de la variable groups. El compilador, por su parte, asignará un nombre interno a la clase anónima y utilizará ese nombre para tipificar la variable groups. A continuación, 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 generará una colección de objetos, tal y como se especifica 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 rellenado:
![]() |
- en [1], añadimos 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 día de hoy (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 se encuentra 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 comprobar la conexión;
- en [12], se cierra el asistente;
- en [13], la conexión aparece en LINQPad.
Las entidades se han creado 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» con [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 «uno a varios» con 1 [CLIENT],
- una relación «muchos a uno» [CRENEAU].
Cabe señalar que los nombres de las propiedades anteriores son diferentes de los que hemos utilizado hasta ahora. No tiene importancia. 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 quizá prefieras prescindir de ellas. Sin embargo, a veces son imprescindibles, ya que permiten hacer ciertas cosas que las consultas de texto no permiten. Una expresión lambda designa una función con un parámetro de entrada a y un parámetro de salida b, con la forma a=>b. El método OrderBy anterior 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 así: si denomino m a mi entrada (una entidad [Medecin]), entonces mi salida es m.TITRE, es decir, el nombre 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 la ordenación de 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 de datos. Una vez más, leeremos atentamente este código. Permite evaluar el coste real de una consulta LINQ.
A continuación, presentamos algunos ejemplos de consultas LINQ. En cada caso, mostramos los resultados visualizados 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 como se navega de una entidad a otra. Se denominan propiedades de navegación.
![]() |
// los clientes cuyo título es «Sr.» ordenados por orden descendente de los nombres
Resultados:
![]() |
LINQ | |
Lambda | |
SQL | |
// todas las franjas horarias con el médico asignado
Resultados (parciales):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// Todas las citas con el cliente y el médico asociados
Resultados:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// médicos sin citas
Resultados:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
No existe ninguna 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 conservo (Where) 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).
// franjas horarias de la Sra. Pélissier
Resultados (parciales):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// Número de citas de la Sra. Pélissier el 08/10/2012
Resultados:
![]() |
LINQ | |
Lambda | |
SQL | |
// Lista de clientes que han concertado una cita con la Sra. Pélissier el 08/10/2012
Resultados:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// número de franjas horarias por médico
Resultados:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
3.5.5. Modificación de una entidad vinculada al contexto de persistencia
Hemos visto las siguientes operaciones en el contexto de persistencia:
- añadir 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;
// 1.º 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);
}
// Añadir 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 cliente 1 en el cliente 2
client2 = context.Clients.Find(client1.Id);
// seguimiento
Console.WriteLine("client2");
Console.WriteLine(client2);
// se modifica el cliente 2
client2.Nom = "yy";
// guardar contexto
context.SaveChanges();
}
// tercer contexto
using (var context = new RdvMedecinsContext())
{
// se recupera el cliente 2 en el cliente 3
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 concretamente, todas las entidades se transfieren al contexto desde la base de datos y pasan a un estado «eliminado». Cabe señalar que, en esta fase, la base de datos no ha sufrido cambios. Mientras el contexto no se sincronice con la base de datos, esta no cambia. Recordemos que basta con eliminar las entidades [Medecin] y [Client] para vaciar la base de datos mediante el mecanismo de eliminaciones en cascada;
- líneas 27-28: se añade un nuevo cliente a la base de datos;
- 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 añadida a una operación SQL INSERT;
- líneas 35-36: se muestra el cliente tras la sincronización con la base de datos;
El resultado obtenido en la consola es el siguiente:
Cabe destacar lo siguiente:
- antes de la sincronización con la base de datos, el cliente no tiene ni clave primaria ni timestamp,
- tras la sincronización, sí los tiene. Recordemos aquí que la clave primaria se ha configurado para que la genere 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 reinicia 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:
- línea 47: se modifica;
- línea 49: se sincroniza el contexto con la base. EF detectará que algunos elementos del contexto se han modificado desde que se introdujeron en él. Para estos elementos, generará órdenes SQL y UPDATE en la base. Por lo tanto, en este caso, la sincronización consistirá en una única orden UPDATE;
- línea 50: se cierra el segundo contexto. La entidad client2, que estaba vinculada al contexto, queda ahora desvinculada de este;
- línea 52: se abre un tercer contexto vacío;
- línea 55: se vuelve a introducir en él el único cliente de la base de datos. Queremos comprobar si la modificación realizada en él en el contexto anterior se ha reflejado en la base de datos;
- líneas 57-58: se muestra el cliente. El resultado es el siguiente:
El nombre del cliente se ha modificado correctamente en la base de datos. Cabe destacar que su timestamp se ha actualizado.
- línea 59: se cierra el contexto. Por cierto, cabe señalar que, a diferencia de las dos ocasiones anteriores, no ha sido necesario sincronizar previamente el contexto con la base de datos (SaveChanges), ya que el contexto no se había modificado.
3.5.6. Gestión de entidades independientes
Volvamos a la arquitectura por capas de una aplicación como la del caso práctico:
![]() |
La capa [DAO] utiliza ORM y EF5 para acceder a los datos. Tenemos 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, a continuación, 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 (añadir, modificar, eliminar). Cuando vuelven a la capa [DAO], siguen estando desvinculadas. Sin embargo, la capa [DAO] tendrá que 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:
Añadir una entidad desvinculada
Este es el caso habitual para una adición. Basta con añadir (Add) la entidad separada al contexto, asegurándose de que tenga una clave primaria igual a null.
Modificar una entidad independiente
Se puede utilizar el siguiente código:
- 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:
- línea 1: se introduce 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:
Como no he podido implementar registros de las operaciones SQL realizadas en la base de datos, no sé si es recomendable un método en lugar de 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 añade un cliente
using (var context = new RdvMedecinsContext())
{
// creación de cliente
client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
// se añade el cliente al contexto
context.Clients.Add(client1);
// se guarda el contexto
context.SaveChanges();
}
// Visualización de la base
Dump("1-----------------------------");
// El cliente 1 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 una entidad fuera del contexto
using (var context = new RdvMedecinsContext())
{
// aquí tenemos un nuevo contexto vacío
// se coloca «client1» en el contexto en estado eliminado
context.Entry(client1).State = EntityState.Deleted;
// se guarda el contexto
context.SaveChanges();
}
// Visualización de la base de datos
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 añade un cliente a la base de datos;
- línea 27: muestra el contenido de la base de datos;
- tras la línea 25, el contexto de persistencia ya no existe. Por lo tanto, ya no hay entidades asociadas. 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 de datos;
- línea 38: se cierra;
- línea 40: se muestra la base;
El nombre del cliente se ha modificado correctamente en la base de datos. Cabe destacar que se ha actualizado el timestamp;
- línea 42: se abre un nuevo contexto vacío;
- línea 46: la entidad separada client1 se introduce en el contexto con el 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 de datos;
La entidad se ha eliminado correctamente de la base de datos.
Ahora veremos los dos modos de carga de las dependencias de una entidad: Lazy y Eager Loading.
3.5.7. Carga diferida (Lazy) y carga anticipada (Eager)
Volvamos al 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 va a explicar se aplica igualmente a ellas.
Por defecto, EF 5 está en modo «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 se traerían, según las dependencias anteriores:
- las entidades [Creneau] vinculadas a las citas;
- las entidades [Medecin] vinculadas a esas franjas horarias;
- 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 por defecto
using (var context = new RdvMedecinsContext())
{
// franja 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 de datos
// 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 utilizada hasta ahora. Tras esta operación, las tablas de las líneas 11-13 se rellenan con entidades independientes;
- líneas 21-22: nos centramos en la primera franja horaria y en el médico asociado;
- línea 23: nuevo contexto;
- línea 26: se introduce la franja horaria en el contexto con su dependencia (eager loading). Dado que 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 introduce la entidad en el contexto utiliza expresiones lambda. El método Single permite especificar una condición para recuperar una única entidad. En este caso, se busca en la base de datos la entidad [Creneau], que tiene como clave primaria el slot 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 añadirla;
- 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:
- 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 la primera vez que se utiliza si no está presente. En este caso, normalmente sí está presente. La salida es la siguiente:
- líneas 39-44: en el marco de un nuevo contexto, se vuelve a buscar el intervalo n.º 0 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 identificación abreviada del intervalo es la siguiente:
En este caso, 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 hacerlo 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:
EF ha detectado que la dependencia [Medecin] no está presente. Ha intentado cargarla, pero, al estar cerrado el contexto, 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 examinemos 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 lo genera el SGBD. También hemos dicho que la anotación [Timestamp] de la línea 22 hacía que EF 5 utilizara el campo anotado para gestionar los conflictos de acceso a las entidades. Recordemos en qué consiste la gestión de conflictos de acceso:
- un proceso P1 lee una línea L de la tabla [MEDECINS] en el momento T1. La línea tiene el timestamp TS1;
- un proceso P2 lee la misma línea L de la tabla [MEDECINS] en el momento T2. La línea 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 lanza 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 denomina gestión optimista de los accesos concurrentes. Con EF 5, un campo que desempeñe 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 de 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 accesos.
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 subprocesos
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 añade 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 «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 subprocesos
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 «client1» en «client2»
Client client2 = context.Clients.Find(client1.Id);
Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
}
}
// hilo
static void Modifie(object infos)
{
...
}
- línea 26: se inicia un contexto vacío;
- línea 29: se 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] pasan al estado «eliminado»;
- líneas 40-41: se añade 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] incluida 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 tras la sincronización del contexto. En este momento, las pantallas se muestran de la siguiente manera:
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 da 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 finalizar su ejecución,
- Cliente: una referencia al cliente que se va a actualizar en la base de datos,
- Nombre: nombre que se le va a dar a este cliente;
- líneas 57-59: lo mismo con un segundo hilo. Al final, dos hilos intentarán modificar en la base de datos el nombre del mismo cliente;
- líneas 60-63: tras iniciar los dos hilos, el hilo principal espera a que finalicen su ejecución;
- línea 62: espera a que finalice el hilo t1;
- línea 63: espera a que finalice 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 el 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 introduce el cliente en el 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 estaba ejecutando, dejando espacio para otro hilo. En nuestro ejemplo, tenemos tres hilos: main, t1 y t2. El hilo main está detenido, a la espera de que finalicen los hilos t1 y t2. Suponiendo que el hilo t1 sea el primero en disponer del procesador, ahora se lo cede al hilo t2. Esto tendrá como consecuencia 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 de nuevo que el hilo t1 se reactiva primero. Guardará el 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 hay ahora en la base de datos. Su actualización será rechazada.
Las pantallas son las siguientes:
- línea 4: el cliente en la base de datos;
- línea 9: el cliente tal y como lo lee el hilo t2;
- línea 11: el cliente tal y 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, ha podido realizar su actualización. El nombre debe de haber cambiado a «zz»;
- línea 13: el hilo t1 lanza una excepción de tipo [System.Data.OptimisticConcurrencyException]. EF ha detectado que no tenía el timestamp correcto;
- línea 21: el hilo t1 también finaliza;
- línea 22: el hilo principal ha finalizado su espera;
- línea 24: el hilo principal muestra el cliente en la base de datos. Efectivamente, ha ganado el hilo t2. 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 hemos añadido manualmente (véase el apartado 2.2.4, página 12):
Vamos a proceder de la siguiente manera: añadiremos al mismo tiempo dos citas para el mismo médico, el mismo día y en la misma franja horaria. Veremos qué ocurre.
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 una franja horaria 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; debería producirse una excepción
// ya que la tabla subyacente BD tiene una restricción de unicidad que impide
// que haya dos RDV el mismo día y en la misma franja horaria
context.SaveChanges();
}
}
catch (Exception e)
{
Console.WriteLine("Erreur : {0}", e);
}
// si el almacenamiento se realiza en una transacción, entonces no se debe haber insertado nada en la base de datos
// debido a la excepción anterior; se comprueba
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);
}
// las franjas horarias
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 asocian al nuevo contexto. Cabe señalar aquí que, teniendo en cuenta las dependencias, podríamos haber minimizado el número de operaciones Add. Pero EF optimizará las órdenes SQL y INSERT que se deben enviar a la base;
- línea 51: el contexto se sincroniza con la base de datos. Tal y como indica el comentario, la inserción de una de las dos citas debe fallar debido a la restricción de unicidad de la tabla [RVS]. Pero, además, si la sincronización se produce 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:
- línea 1: excepción debida a la violación de la restricción de unicidad en la tabla [RVS];
- líneas 9-12: la base de datos está efectivamente vacía. Por lo tanto, la sincronización del contexto con la base de datos se ha realizado en una transacción.
Sin duda, habría otros aspectos que explorar en EF 5. Pero sabemos lo suficiente como para volver a 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 en su conocimiento de EF 5.
3.6. Estudio de una arquitectura multicapa basada en EF 5
Volvemos a nuestro caso práctico descrito en el apartado 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 añadimos cuatro carpetas [2] en las que vamos a distribuir nuestros códigos. La carpeta [Entites] es una copia de la carpeta [Entites] del proyecto anterior. Tras esta copia, aparecen errores debidos a que no disponemos de las referencias correctas. Tenemos que añadir una referencia a Entity Framework 5. Para ello, seguiremos el método explicado en el apartado 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, copiamos también 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: añade 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, el menú desplegable se ha rellenado 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 franjas horarias 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 añadirla 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 franjas horarias de un médico
List<Creneau> GetCreneauxMedecin(int idMedecin);
// lista de RV de un médico concreto, en un día concreto
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// añadir 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-20 se derivan del análisis que acabamos de realizar. El método de la línea 22 sirve para solucionar el hecho de que se trabaja con «Lazy Loading». Si en la capa [ASP.NET] se necesita una dependencia de una entidad, se recuperará de la base de datos mediante 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 franjas horarias de un médico concreto
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
...
}
// lista de RV de un médico para un día determinado
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
...
}
// añadir 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 una franja horaria
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 añaden a una lista.
El método [GetAllMedecins], que debe devolver la lista de todos los médicos, es análogo (líneas 37-57).
El método [GetCreneauxMedecin] es el siguiente:
// lista de franjas horarias de un médico concreto
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
// lista de franjas horarias
try
{
// abrir contexto de persistencia
using (var context = new RdvMedecinsContext())
{
// se recupera el médico con sus franjas horarias
Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
// lista de franjas horarias 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 el médico cuya clave primaria se conoce. Se solicita que se incluya la dependencia [Creneaux], que es una colección de las franjas horarias 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 franjas horarias.
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 introduce en el contexto el médico cuya clave primaria se conoce;
- 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 añadir una cita a la base de datos y debe devolver la clave primaria del elemento insertado. Su código podría ser el siguiente:
// Añadir un RV
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
// N.º de la cita añadida
int idRv;
try
{
// apertura del contexto de persistencia
using (var context = new RdvMedecinsContext())
{
// se recupera la franja horaria
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 };
// Añadido al contexto
context.Rvs.Add(rv);
// guardado del contexto
context.SaveChanges();
// se recupera la clave primaria de la cita añadida
idRv = (int)rv.Id;
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(7, "AjouterRv", ex);
}
// resultado
return idRv;
}
- línea 12: se busca la franja horaria 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 añade al contexto de persistencia;
- línea 28: se sincroniza el contexto de persistencia con la base de datos. La cita se guardará entonces 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 de la cita añadida;
- 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 introduce en el 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 solicita a [Spring.net] que instancie la capa [DAO]. Una vez hecho esto, comprueba las diferentes funcionalidades de la interfaz de la capa [DAO]. En lugar de un programa de consola, habría 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 franjas horarias 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)));
// añadir una cita (RV) al médico n.º 1 en la franja horaria 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)));
// Añadir una cita en un horario ya ocupado; debe provocar 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 veremos la configuración necesaria para que esto sea posible. Convertimos la referencia al objeto devuelta por Spring a una referencia del tipo de la interfaz [IDao];
- líneas 21-22: muestran los clientes;
- líneas 25-26: muestran a los pacientes;
- líneas 29-30: muestran la lista de franjas horarias 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: añade 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 añade por segunda vez la misma cita. Debe producirse una excepción;
- línea 57: se elimina la única cita añadida;
- línea 58: muestra las citas del médico n.º 0 con fecha del 23/11/2013. No debe haber ninguna.
3.6.5. Configuración de Spring.net
En el programa de prueba anterior, hemos pasado por alto rápidamente 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 añadir su DLL a las referencias del proyecto. Procedemos de la siguiente manera:
![]() |
- en [1], buscamos paquetes con la herramienta [NuGet];
![]() |
- en [2], se buscan paquetes en línea;
- en [3], se introduce 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 quedan así:
![]() |
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 tenemos que 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 gestionará la sección <spring><context> del archivo XML (líneas 19-21);
- línea 9: define la clase que gestionará la sección <spring><objects> del archivo XML (líneas 22-24);
- línea 13: define la clase que gestionará 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, salvo 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» designa la clase que se va a instanciar en el formato «nombre completo de la clase, Assembly que contiene la clase». La clase en este caso es la que implementa la capa [DAO]: [RdvMedecins.Dao.Dao]. Para conocer su assembly, hay que consultar las propiedades del proyecto:
![]() |
En [1], el nombre del ensamblado que hay que proporcionar;
- líneas 27-36: la configuración de «Common Logging» es estable. Es posible que haya que modificar el nivel de información, en la línea 32. Tras la fase de depuración, se puede cambiar el nivel a INFO.
En definitiva, 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 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». Se trata del 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 realiza la llamada. Si se solicita el objeto por segunda vez en el código, Spring se limita a devolver una referencia al primer objeto creado. Se trata del 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 determinados campos del objeto una vez creado este. 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:
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 fundamentos de ORM en Entity Framework 5;
- una capa [DAO] que utiliza este ORM.
Recordemos nuestro caso práctico descrito al principio de este documento. Partimos de una aplicación existente con la siguiente arquitectura:
![]() |
que queremos transformar en esta:
![]() |
donde EF5 ha sustituido 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 franjas horarias de un médico
List<Creneau> GetCreneauxMedecin(int idMedecin);
// lista de RV de un médico concreto, en un día determinado
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// añadir un RV
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// eliminar un RV
void SupprimerRv(int idRv);
}
La capa [DAO2] ha añadido 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 «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 algunos casos las necesitamos. NHibernate también funciona por defecto en modo «Lazy Loading», pero yo lo había utilizado en modo «Eager Loading». Las entidades llegaban a la capa [ASP.NET] con sus dependencias.
Vamos a finalizar la migración de la aplicación ASP.NET / NHibernate a la aplicación ASP.NET / EF 5. Pero, dado que esto ya no afecta a EF5, no comentaremos el código web. Simplemente explicaremos cómo configurar la aplicación web y probarla. Esta está disponible en la página web de este tutorial.
3.6.6. Generación de la DLL a partir de la capa [DAO]
En la siguiente arquitectura:
![]() |
la capa [ASP.NET] tendrá a su disposición las capas situadas a su derecha en forma de DLL. Por lo tanto, creamos 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 vuelve a generar 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]
A continuación 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].
Partiremos 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], añadimos 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 habían guardado 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 queda así:
![]() |
- En [1], el código de gestió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:
![]()
y su explicación:
![]()
¿El tipo de [medecin.Id] es «int»? Sin embargo, 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. Entonces 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 framework Spring. Esto se realiza en [Global.asax]:
protected void Application_Start(object sender, EventArgs e)
{
// se almacenan en caché determinados 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, sustituimos 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], introducimos una fecha de cita y confirmamos;
![]() |
- en [3], se produce un error.
Al examinar el texto del error que muestra la página, se observa que la excepción señalada es la de «Lazy Loading»: se ha intentado cargar una dependencia de un objeto cuando el contexto de persistencia que lo gestiona se ha cerrado. El objeto se encuentra ahora en estado «desvinculado». Este error se debe a que NHibernate se había utilizado en modo «Eager Loading», mientras que EF funciona por defecto en modo «Lazy Loading». En la línea marcada en rojo anterior:
- rdv representa un objeto [Rv] que se ha cargado 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 de la franja horaria de una cita como clave. Ahora bien, resulta que la entidad [Rv] encapsula la clave primaria de la franja horaria asociada. 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. Así, 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 de la página 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 ORM: «Eager Loading» para NHibernate y «Lazy Loading» para EF 5. Esto nos llevó a ampliar la interfaz de la capa [DAO] con un método genérico que permite recuperar una entidad mediante su clave primaria.
No obstante, la migración resultó bastante sencilla, lo que justifica una vez más, por si fuera necesario, la arquitectura por 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.


















































































































































