Compartir a través de


Creación de un modelo de datos más complejo para una aplicación ASP.NET MVC (4 de 10)

Por Tom Dykstra

En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones de ASP.NET MVC 4 con Code First de Entity Framework 5 y Visual Studio 2012. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial de la serie.

Nota:

Si se encuentra con un problema que no puede resolver, descargue el capítulo completado e intente reproducir el problema. Por lo general, puede encontrar la solución al problema si compara el código con el código completado. Para obtener algunos errores comunes y cómo resolverlos, vea Errores y soluciones alternativas.

En los tutoriales anteriores, ha trabajado con un modelo de datos simple que se componía de tres entidades. En este tutorial agregará más entidades y relaciones, y personalizará el modelo de datos mediante la especificación de reglas de formato, validación y asignación de base de datos. Verá dos maneras de personalizar el modelo de datos: mediante la adición de atributos a clases de entidad y la adición de código a la clase de contexto de base de datos.

Cuando haya terminado, las clases de entidad conformarán el modelo de datos completo que se muestra en la ilustración siguiente:

School_class_diagram

Personalizar el modelo de datos mediante el uso de atributos

En esta sección verá cómo personalizar el modelo de datos mediante el uso de atributos que especifican reglas de formato, validación y asignación de base de datos. Después, en varias de las secciones siguientes, creará el modelo de datos School completo mediante la adición de atributos a las clases que ya ha creado y la creación de clases para los demás tipos de entidad del modelo.

El atributo DataType

Para las fechas de inscripción de estudiantes, en todas las páginas web se muestra actualmente la hora junto con la fecha, aunque todo lo que le interesa para este campo es la fecha. Mediante los atributos de anotación de datos, puede realizar un cambio de código que fijará el formato de presentación en cada vista en la que se muestren los datos. Para ver un ejemplo de cómo hacerlo, deberá agregar un atributo a la propiedad EnrollmentDate en la clase Student.

En Models/Student.cs, agregue una instrucción using para el espacio de nombres System.ComponentModel.DataAnnotations y los atributos DataType y DisplayFormat a la propiedad EnrollmentDate, como se muestra en el ejemplo siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

El atributo DataType se usa para especificar un tipo de datos más específico que el tipo intrínseco de base de datos. En este caso solo se quiere realizar el seguimiento de la fecha, no de la fecha y la hora. La enumeración DataType proporciona muchos tipos de datos, comoDate, Time, PhoneNumber, Currency, EmailAddress, etc. El atributo DataType también puede permitir que la aplicación proporcione automáticamente características específicas del tipo. Por ejemplo, se puede crear un vínculo mailto: para DataType.EmailAddress y se puede proporcionar un selector de fechas para DataType.Date en exploradores compatibles con HTML5. Los atributos DataType emiten atributos data- de HTML 5 que los exploradores HTML 5 pueden comprender. Los atributos DataType no proporcionan ninguna validación.

DataType.Date no especifica el formato de la fecha que se muestra. De manera predeterminada, el campo de datos se muestra según los formatos predeterminados basados en el elemento CultureInfo del servidor.

El atributo DisplayFormat se usa para especificar el formato de fecha de forma explícita:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

El valor ApplyFormatInEditMode especifica que el formato indicado se debe aplicar también cuando el valor se muestra en un cuadro de texto para su edición. (Es posible que no le interese ese comportamiento para algunos campos, por ejemplo, para los valores de moneda, es posible que no quiera que el símbolo de la moneda se incluya en el cuadro de texto para modificarlo).

Puede usar el atributo DisplayFormat por sí solo, pero normalmente se recomienda usar también el atributo DataType. El atributo DataType transmite la semántica de los datos en contraposición a cómo se representan en una pantalla y ofrece las siguientes ventajas que no se obtienen con DisplayFormat:

  • El explorador puede habilitar características de HTML5 (por ejemplo, mostrar un control de calendario, el símbolo de moneda adecuado según la configuración regional, vínculos de correo electrónico, etc.).
  • De manera predeterminada, el explorador representa los datos con el formato correcto según la configuración regional.
  • El atributo DataType puede habilitar MVC para que elija la plantilla de campo adecuada para representar los datos (DisplayFormat, si se usa por sí solo, utiliza la plantilla de cadena). Para más información, vea Plantillas de ASP.NET MVC 2 de Brad Wilson. (Aunque está escrito para MVC 2, este artículo todavía se aplica a la versión actual de ASP.NET MVC).

Si usa el atributo DataType con un campo de fecha, también debe especificar el atributo DisplayFormat para asegurarse de que el campo se representa correctamente en los exploradores Chrome. Para más información, vea esta conversación de StackOverflow.

Ejecute la página Students Index y verá que ya no se muestran las horas para las fechas de inscripción. Lo mismo sucede para cualquier vista en la que se use el modelo Student.

Students_index_page_with_formatted_date

StringLengthAttribute

También puede especificar reglas de validación de datos y mensajes mediante atributos. Imagine que quiere asegurarse de que los usuarios no escriban más de 50 caracteres para un nombre. Para agregar esta limitación, agregue atributos StringLength a las propiedades LastName y FirstMidName, como se muestra en el ejemplo siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

El atributo StringLength no impedirá que un usuario escriba un espacio en blanco en un nombre. Puede usar el atributo RegularExpression para aplicar restricciones a la entrada. Por ejemplo, en el código siguiente es necesario que el primer carácter sea una letra mayúscula y el resto de caracteres sean alfabéticos:

[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]

El atributo MaxLength proporciona una funcionalidad similar a la del atributo StringLength pero no proporciona validación del lado cliente.

Ejecute la aplicación y haga clic en la pestaña Students. Se muestra el siguiente error:

El modelo que respalda al contexto "SchoolContext" ha cambiado desde que se creó la base de datos. Considere la posibilidad de usar Migraciones de Code First para actualizar la base de datos (https://go.microsoft.com/fwlink/?LinkId=238269).

Ahora el modelo de base de datos ha cambiado de tal forma que se necesita un cambio en el esquema de la base de datos y Entity Framework lo ha detectado. Usará migraciones para actualizar el esquema sin perder los datos que ha agregado a la base de datos mediante la interfaz de usuario. Si ha cambiado los datos creados por el método Seed, se volverán a cambiar a su estado original debido al método AddOrUpdate que usa en el método Seed. (AddOrUpdate es equivalente a una operación "upsert" de la terminología de la base de datos).

En la Consola del Administrador de paquetes (PMC), escriba los comandos siguientes:

add-migration MaxLengthOnNames
update-database

El comando add-migration MaxLengthOnNames crea un archivo denominado <marca_de_tiempo>_MaxLengthOnNames.cs. Este archivo contiene código que actualizará la base de datos para que coincida con el modelo de datos actual. Entity Framework usa la marca de tiempo que precede al nombre de archivo de migraciones para ordenar las migraciones. Después de crear varias migraciones, si quita la base de datos o si implementa el proyecto mediante Migraciones, todas las migraciones se aplican en el orden en que se hayan creado.

Ejecute la página Create y escriba un nombre de más de 50 caracteres. Tan pronto como supere los 50 caracteres, la validación del lado cliente muestra inmediatamente un mensaje de error.

client side val error

El atributo Column

También puede usar atributos para controlar cómo se asignan las clases y propiedades a la base de datos. Imagine que hubiera usado el nombre FirstMidName para el nombre de campo por la posibilidad de que el campo contenga también un segundo nombre. Pero quiere que la columna de base de datos se denomine FirstName, ya que los usuarios que van a escribir consultas ad hoc en la base de datos están acostumbrados a ese nombre. Para realizar esta asignación, puede usar el atributo Column.

El atributo Column especifica que, cuando se cree la base de datos, la columna de la tabla Student que se asigna a la propiedad FirstMidName se denominará FirstName. En otras palabras, cuando el código hace referencia a Student.FirstMidName, los datos procederán o se actualizarán en la columna FirstName de la tabla Student. Si no especifica nombres de columna, se les asigna el mismo nombre que el de la propiedad.

Agregue una instrucción using para System.ComponentModel.DataAnnotations.Schema y el atributo de nombre de columna a la propiedad FirstMidName, como se muestra en el código resaltado siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { get; set; }
        [StringLength(50)]       
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

La adición del atributo Column cambia el modelo de respaldo de SchoolContext, por lo que no coincidirá con la base de datos. Escriba los siguientes comandos en PMC para crear otra migración:

add-migration ColumnFirstName
update-database

En el Explorador de servidores (Explorador de bases de datos si usa Express para Web) haga doble clic en la tabla Student.

Screenshot that shows the Student table in Server Explorer.

En la imagen siguiente se muestra el nombre de columna original tal y como estaba antes de aplicar las dos primeras migraciones. Además del nombre de columna que cambia de FirstMidName a FirstName, las dos columnas de nombre han cambiado de una longitud MAX a 50 caracteres.

Screenshot that shows the Student table in Server Explorer. The First Name line in the previous screenshot has changed to read as First Mid Name.

También puede realizar cambios en la asignación de bases de datos mediante la API fluida, como verá más adelante en este tutorial.

Nota:

Si intenta compilar el proyecto antes de terminar de crear todas estas clases de entidad, es posible que se produzcan errores del compilador.

Crear la entidad Instructor

Instructor_entity

Cree Models/Instructor.cs y reemplace el código de plantilla con el código siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int InstructorID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public virtual ICollection<Course> Courses { get; set; }
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}

Tenga en cuenta que varias propiedades son las mismas en las entidades Student y Instructor. En el tutorial Implementación de la herencia más adelante en esta serie, refactorizará mediante la herencia para eliminar la redundancia.

Atributos Required y Display

Los atributos de la propiedad LastName especifican que es un campo obligatorio, que el título del cuadro de texto debe ser "Last Name" (en lugar del nombre de propiedad, que sería "LastName" sin espacio) y que el valor no puede tener más de 50 caracteres.

[Required]
[Display(Name="Last Name")]
[StringLength(50)]
public string LastName { get; set; }

El atributo StringLength establece la longitud máxima de la base de datos y proporciona la validación del lado cliente y el lado servidor para ASP.NET MVC. En este atributo también se puede especificar la longitud mínima de la cadena, pero el valor mínimo no influye en el esquema de la base de datos. El atributo Required no es necesario para tipos de valor como DateTime, int, double y float. Los tipos de valor no se pueden asignar a un valor null, por lo que son inherentemente obligatorios. Puede quitar el atributo Required y reemplazarlo por un parámetro de longitud mínima para el atributo StringLength:

[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }

Puede colocar varios atributos en una línea, por lo que también podría escribir la clase instructor como se indica a continuación:

public class Instructor
{
   public int InstructorID { get; set; }

   [Display(Name = "Last Name"),StringLength(50, MinimumLength=1)]
   public string LastName { get; set; }

   [Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
   public string FirstMidName { get; set; }

   [DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
   public DateTime HireDate { get; set; }

   public string FullName
   {
      get { return LastName + ", " + FirstMidName; }
   }

   public virtual ICollection<Course> Courses { get; set; }
   public virtual OfficeAssignment OfficeAssignment { get; set; }
}

Propiedad calculada FullName

FullName es una propiedad calculada que devuelve un valor que se crea mediante la concatenación de otras dos propiedades. Por tanto, solo tiene un descriptor de acceso get y no se generará ninguna columna FullName en la base de datos.

public string FullName
{
    get { return LastName + ", " + FirstMidName; }
}

Propiedades de navegación Courses y OfficeAssignment

Courses y OfficeAssignment son propiedades de navegación. Como se ha explicado antes, normalmente se definen como virtuales para que puedan aprovechar las ventajas de una característica de Entity Framework denominada carga diferida. Además, si una propiedad de navegación puede contener varias entidades, su tipo debe implementar la interfaz ICollection<T>. (Por ejemplo, IList<T> califica pero no IEnumerable<T> porque IEnumerable<T> no implementa Add).

Un instructor puede impartir cualquier número de cursos, por lo que Courses se define como una colección de entidades Course. Las reglas de negocios establecen que un instructor solo puede tener como máximo una oficina, por lo que OfficeAssignment se define como una sola entidad OfficeAssignment (que puede ser null si no se asigna ninguna oficina).

public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }

Creación de la entidad OfficeAssignment

OfficeAssignment_entity

Cree Models/OfficeAssignment.cs con el código siguiente:

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

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        [ForeignKey("Instructor")]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public virtual Instructor Instructor { get; set; }
    }
}

Compile el proyecto, que guarda los cambios y comprueba que no ha cometido ningún error de copia y pegado que el compilador puede detectar.

Atributo Key

Hay una relación de uno a cero o uno entre Instructor y las entidades OfficeAssignment. Una asignación de oficina solo existe en relación con el instructor al que se asigna y, por tanto, su clave principal también es su clave externa para la entidad Instructor. Pero Entity Framework no reconoce automáticamente InstructorID como la clave principal de esta entidad porque su nombre no sigue la convención de nomenclatura de ID o nombre_de_claseID. Por tanto, se usa el atributo Key para identificarla como la clave:

[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }

También puede usar el atributo Key si la entidad tiene su propia clave principal, pero querrá asignar un nombre a la propiedad que no sea classnameID o ID. De manera predeterminada, en EF la clave se trata como no generada por la base de datos porque la columna es para una relación de identificación.

Atributo ForeignKey

Cuando hay una relación de uno a cero o de uno a uno entre dos entidades (por ejemplo, entre OfficeAssignment y Instructor), EF no puede resolver qué extremo de la relación es la entidad de seguridad y cuál es el extremo dependiente. Las relaciones uno a uno tienen una propiedad de navegación de referencia en cada clase a la otra clase. El atributo ForeignKey se puede aplicar a la clase dependiente para establecer la relación. Si omite el atributo ForeignKey, se producirá el siguiente error al intentar crear la migración:

No se puede determinar el extremo principal de una asociación entre los tipos "ContosoUniversity.Models.OfficeAssignment" y "ContosoUniversity.Models.Instructor". El extremo principal de esta asociación se debe configurar explícitamente mediante la API fluida de la relación o con anotaciones de datos.

Más adelante en el tutorial se muestra cómo configurar esta relación con la API fluida.

Propiedad de navegación Instructor

La entidad Instructor tiene una propiedad de navegación OfficeAssignment que admite un valor NULL (porque es posible que no se asigne una oficina a un instructor), y la entidad OfficeAssignment tiene una propiedad de navegación Instructor que no admite un valor NULL (porque una asignación de oficina no puede existir sin un instructor; InstructorID no admite valores NULL). Cuando una entidad Instructor tiene una entidad OfficeAssignment relacionada, cada entidad tiene una referencia a la otra en su propiedad de navegación.

Podría incluir un atributo [Required] en la propiedad de navegación Instructor para especificar que debe haber un instructor relacionado, pero no es necesario hacerlo porque la clave externa InstructorID (que también es la clave para esta tabla) no acepta valores NULL.

Modificar la entidad Course

Course_entity

En Models/Course.cs, reemplace el código que ha agregado antes con el código siguiente:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
   public class Course
   {
      [DatabaseGenerated(DatabaseGeneratedOption.None)]
      [Display(Name = "Number")]
      public int CourseID { get; set; }

      [StringLength(50, MinimumLength = 3)]
      public string Title { get; set; }

      [Range(0, 5)]
      public int Credits { get; set; }

      [Display(Name = "Department")]
      public int DepartmentID { get; set; }

      public virtual Department Department { get; set; }
      public virtual ICollection<Enrollment> Enrollments { get; set; }
      public virtual ICollection<Instructor> Instructors { get; set; }
   }
}

La entidad Course tiene una propiedad de clave externa DepartmentID que apunta a la entidad Department relacionada y tiene una propiedad de navegación Department. Entity Framework no requiere que agregue una propiedad de clave externa al modelo de datos cuando tenga una propiedad de navegación para una entidad relacionada. EF Core crea automáticamente claves externas en la base de datos siempre que se necesiten. Pero tener la clave externa en el modelo de datos puede hacer que las actualizaciones sean más sencillas y eficaces. Por ejemplo, al recuperar una entidad course para modificarla, la entidad Department es NULL si no la carga, por lo que cuando se actualiza la entidad course, primero tendrá que capturar la entidad Department. Cuando la propiedad de clave externa DepartmentID se incluye en el modelo de datos, no es necesario capturar la entidad Department antes de la actualización.

Atributo DatabaseGenerated

El atributo DatabaseGenerated con el parámetro None en la propiedad CourseID especifica que los valores de clave principal los proporciona el usuario, en lugar de que los genere la base de datos.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

De manera predeterminada, Entity Framework da por supuesto que la base de datos genera los valores de clave principal. Es lo que le interesa en la mayoría de los escenarios. Pero para las entidades Course, usará un número de curso especificado por el usuario, por ejemplo, una serie 1000 para un departamento, una serie 2000 para otro y así sucesivamente.

Propiedades de clave externa y de navegación

Las propiedades de clave externa y las de navegación de la entidad Course reflejan las relaciones siguientes:

  • Un curso se asigna a un departamento, por lo que hay una clave externa DepartmentID y una propiedad de navegación Department por las razones mencionadas anteriormente.

    public int DepartmentID { get; set; }
    public virtual Department Department { get; set; }
    
  • Un curso puede tener cualquier número de alumnos inscritos en él, por lo que la propiedad de navegación Enrollments es una colección:

    public virtual ICollection<Enrollment> Enrollments { get; set; }
    
  • Un curso puede ser impartido por varios instructores, por lo que la propiedad de navegación Instructors es una colección:

    public virtual ICollection<Instructor> Instructors { get; set; }
    

Creación de la entidad Department

Department_entity

Cree Models/Department.cs con el código siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
   public class Department
   {
      public int DepartmentID { get; set; }

      [StringLength(50, MinimumLength=3)]
      public string Name { get; set; }

      [DataType(DataType.Currency)]
      [Column(TypeName = "money")]
      public decimal Budget { get; set; }

      [DataType(DataType.Date)]
      public DateTime StartDate { get; set; }

      [Display(Name = "Administrator")]
      public int? InstructorID { get; set; }

      public virtual Instructor Administrator { get; set; }
      public virtual ICollection<Course> Courses { get; set; }
   }
}

El atributo Column

Anteriormente ha usado el atributo Column para cambiar la asignación de nombres de columna. En el código de la entidad Department, se usa el atributo Column para cambiar la asignación de tipos de datos de SQL para que la columna se defina con el tipo money de SQL Server en la base de datos:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Por lo general, la asignación de columnas no es necesaria, ya que Entity Framework elige el tipo de datos de SQL Server adecuado en función del tipo CLR que defina para la propiedad. El tipo CLR decimal se asigna a un tipo decimal de SQL Server. Pero en este caso sabe que la columna va a contener cantidades de moneda, y el tipo de datos money es más adecuado para eso.

Propiedades de clave externa y de navegación

Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:

  • Un departamento puede tener o no un administrador, y un administrador es siempre un instructor. Por tanto, la propiedad InstructorID se incluye como la clave externa de la entidad Instructor y se agrega un signo de interrogación después de la designación del tipo int para marcar la propiedad como que admite un valor NULL. El nombre de la propiedad de navegación es Administrator pero contiene una entidad Instructor:

    public int? InstructorID { get; set; }
    public virtual Instructor Administrator { get; set; }
    
  • Un departamento puede tener varios cursos, por lo que hay una propiedad de navegación Courses:

    public virtual ICollection<Course> Courses { get; set; }
    

    Nota:

    Por convención, Entity Framework permite la eliminación en cascada para las claves externas que no aceptan valores NULL y para las relaciones de varios a varios. Esto puede dar lugar a reglas de eliminación en cascada circulares, lo que iniciará una excepción cuando se ejecute el código del inicializador. Por ejemplo, si no ha definido la propiedad Department.InstructorID como que admite un valor NULL, obtendrá el siguiente mensaje de excepción cuando se ejecute el inicializador: "La relación referencial dará lugar a una referencia cíclica que no está permitida". Si las reglas de negocio exigían que la propiedad InstructorID no aceptara valores NULL, tendría que usar la siguiente API fluida para deshabilitar la eliminación en cascada en la relación:

modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);

Modificación de la entidad Student

Student_entity

En Models/Student.cs, reemplace el código que ha agregado antes con el código siguiente. Los cambios aparecen resaltados.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
   public class Student
   {
      public int StudentID { get; set; }

      [StringLength(50, MinimumLength = 1)]
      public string LastName { get; set; }

      [StringLength(50, MinimumLength = 1, ErrorMessage = "First name cannot be longer than 50 characters.")]
      [Column("FirstName")]
      public string FirstMidName { get; set; }

      [DataType(DataType.Date)]
      [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
      [Display(Name = "Enrollment Date")]
      public DateTime EnrollmentDate { get; set; }

      public string FullName
      {
         get { return LastName + ", " + FirstMidName; }
      }

      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }
}

Entidad Enrollment

En Models/Enrollment.cs, reemplace el código que ha agregado antes con el código siguiente

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public virtual Course Course { get; set; }
        public virtual Student Student { get; set; }
    }
}

Propiedades de clave externa y de navegación

Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:

  • Un registro de inscripción es para un solo curso, por lo que hay una propiedad de clave externa CourseID y una propiedad de navegación Course:

    public int CourseID { get; set; }
    public virtual Course Course { get; set; }
    
  • Un registro de inscripción es para un solo estudiante, por lo que hay una propiedad de clave externa StudentID y una propiedad de navegación Student:

    public int StudentID { get; set; }
    public virtual Student Student { get; set; }
    

Relaciones Varios a Varios

Hay una relación de varios a varios entre las entidades Student y Course, y la entidad Enrollment funciona como una tabla de combinación de varios a varios con carga en la base de datos. Esto significa que la tabla Enrollment contiene datos adicionales además de las claves externas para las tablas combinadas (en este caso, una clave principal y una propiedad Grade).

En la ilustración siguiente se muestra el aspecto de estas relaciones en un diagrama de entidades. (Este diagrama se ha generado mediante Entity Framework Power Tools; la creación del diagrama no forma parte del tutorial, simplemente se usa aquí como una ilustración).

Student-Course_many-to-many_relationship

Cada línea de relación tiene un 1 en un extremo y un asterisco (*) en el otro, para indicar una relación uno a varios.

Si la tabla Enrollment no incluyera información de calificaciones, solo tendría que contener las dos claves externas CourseID y StudentID. En ese caso, se correspondería a una tabla de combinación de varios a varios sin carga (o una tabla de combinación pura) en la base de datos, y no tendría que crear una clase de modelo para ella en absoluto. Las entidades Instructor y Course tienen ese tipo de relación de varios a varios y, como puede ver, no hay ninguna clase de entidad entre ellas:

Instructor-Course_many-to-many_relationship

Pero en la base de datos se necesita una tabla de combinación, como se muestra en el diagrama de base de datos siguiente:

Instructor-Course_many-to-many_relationship_tables

Entity Framework crea automáticamente la tabla CourseInstructor, y la lee y actualiza indirectamente al leer y actualizar las propiedades de navegación Instructor.Courses y Course.Instructors.

Diagrama de entidades en el que se muestran las relaciones

En la siguiente ilustración se muestra el diagrama creado por Entity Framework Power Tools para el modelo School completado.

School_data_model_diagram

Además de las líneas de relaciones de varios a varios (* a *) y las de relaciones de uno a varios (1 a *), aquí puede ver la línea de relación de uno a cero o uno (1 a 0..1) entre las entidades Instructor y OfficeAssignment, y la línea de relación de cero o uno a varios (0..1 a *) entre las entidades Instructor y Department.

Personalización del modelo de datos mediante la adición de código al contexto de la base de datos

A continuación, agregará las nuevas entidades a la clase SchoolContext y personalizará parte de la asignación mediante llamadas a la API fluida. (La API se denomina "fluida" porque a menudo se usa para encadenar una serie de llamadas de método en una única instrucción).

En este tutorial, solo usará la API fluida para la asignación de base de datos que no puede realizar con atributos. Pero también se puede usar la API fluida para especificar casi todas las reglas de formato, validación y asignación que se pueden realizar mediante el uso de atributos. Algunos atributos como MinimumLength no se pueden aplicar con la API fluida. Como se ha mencionado antes, MinimumLength no cambia el esquema, solo aplica una regla de validación del lado cliente y del lado servidor

Algunos desarrolladores prefieren usar la API fluida de forma exclusiva para así mantener las clases de entidad "limpias". Si quiere, puede mezclar atributos y la API fluida, y hay algunas personalizaciones que solo se pueden realizar mediante la API fluida, pero en general el procedimiento recomendado es elegir uno de estos dos enfoques y usarlo de forma constante siempre que sea posible.

Para agregar las nuevas entidades al modelo de datos y realizar la asignación de base de datos que no ha hecho mediante atributos, reemplace el código de DAL\SchoolContext.cs por el código siguiente:

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      public DbSet<Course> Courses { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("InstructorID")
                 .ToTable("CourseInstructor"));
      }
   }
}

La nueva instrucción del método OnModelCreating configura la tabla de combinación de varios a varios:

  • Para la relación de varios a varios entre las entidades Instructor y Course, el código especifica los nombres de tabla y columna para la tabla de combinación. Code First puede configurar la relación de varios a varios sin este código, pero si no lo llama, obtendrá nombres predeterminados como InstructorInstructorID para la columna InstructorID.

    modelBuilder.Entity<Course>()
        .HasMany(c => c.Instructors).WithMany(i => i.Courses)
        .Map(t => t.MapLeftKey("CourseID")
            .MapRightKey("InstructorID")
            .ToTable("CourseInstructor"));
    

En el código siguiente se proporciona un ejemplo de cómo podría haber usado la API fluida en lugar de atributos para especificar la relación entre las entidades Instructor y OfficeAssignment:

modelBuilder.Entity<Instructor>()
    .HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);

Para obtener información sobre lo que hacen las instrucciones de "API fluida" en segundo plano, vea la entrada de blog de la API fluida.

Inicialización de la base de datos con datos de prueba

Reemplace el código del archivo Migrations\Configuration.cs con el código siguiente a fin de proporcionar datos de inicialización para las nuevas entidades que ha creado.

namespace ContosoUniversity.Migrations
{
   using System;
   using System.Collections.Generic;
   using System.Data.Entity;
   using System.Data.Entity.Migrations;
   using System.Linq;
   using ContosoUniversity.Models;
   using ContosoUniversity.DAL;

   internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
   {
      public Configuration()
      {
         AutomaticMigrationsEnabled = false;
      }

      protected override void Seed(SchoolContext context)
      {
         var students = new List<Student>
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander", 
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",    
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",     
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas", 
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",        
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",   
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",    
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",  
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

         students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
         context.SaveChanges();

         var instructors = new List<Instructor>
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie", 
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",    
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",       
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",      
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",      
                    HireDate = DateTime.Parse("2004-02-12") }
            };
         instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
         context.SaveChanges();

         var departments = new List<Department>
            {
                new Department { Name = "English",     Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").InstructorID },
                new Department { Name = "Mathematics", Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").InstructorID },
                new Department { Name = "Engineering", Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").InstructorID },
                new Department { Name = "Economics",   Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").InstructorID }
            };
         departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
         context.SaveChanges();

         var courses = new List<Course>
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
            };
         courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
         context.SaveChanges();

         var officeAssignments = new List<OfficeAssignment>
            {
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").InstructorID, 
                    Location = "Smith 17" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Harui").InstructorID, 
                    Location = "Gowan 27" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").InstructorID, 
                    Location = "Thompson 304" },
            };
         officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.Location, s));
         context.SaveChanges();

         AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
         AddOrUpdateInstructor(context, "Chemistry", "Harui");
         AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
         AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");

         AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
         AddOrUpdateInstructor(context, "Trigonometry", "Harui");
         AddOrUpdateInstructor(context, "Composition", "Abercrombie");
         AddOrUpdateInstructor(context, "Literature", "Abercrombie");

         context.SaveChanges();

         var enrollments = new List<Enrollment>
            {
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID, 
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, 
                    Grade = Grade.A 
                },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, 
                    Grade = Grade.C 
                 },                            
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, 
                    Grade = Grade.B
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").StudentID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").StudentID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B         
                 },
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Barzdukas").StudentID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Li").StudentID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Justice").StudentID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B         
                 }
            };

         foreach (Enrollment e in enrollments)
         {
            var enrollmentInDataBase = context.Enrollments.Where(
                s =>
                     s.Student.StudentID == e.StudentID &&
                     s.Course.CourseID == e.CourseID).SingleOrDefault();
            if (enrollmentInDataBase == null)
            {
               context.Enrollments.Add(e);
            }
         }
         context.SaveChanges();
      }

      void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
      {
         var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
         var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
         if (inst == null)
            crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
      }
   }
}

Como ha visto en el primer tutorial, la mayor parte de este código simplemente crea objetos de entidad y carga los datos de ejemplo en propiedades según sea necesario para las pruebas. Pero observe cómo se controla la entidad Course, que tiene una relación de varios a varios con la entidad Instructor:

var courses = new List<Course>
{
     new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
       Department = departments.Single( s => s.Name == "Engineering"),
       Instructors = new List<Instructor>() 
     },
     ...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();

Al crear un objeto Course, inicializa la propiedad de navegación Instructors como una colección vacía mediante el código Instructors = new List<Instructor>(). Esto permite agregar entidadesInstructor relacionadas con Course mediante el método Instructors.Add. Si no hubiera creado una lista vacía, no podría agregar estas relaciones, ya que la propiedad Instructors sería null y no tendría un método Add. También puede agregar la inicialización de lista al constructor.

Adición de una migración y actualización de la base de datos

Desde PMC, escriba el comando add-migration:

PM> add-Migration Chap4

Si intenta actualizar la base de datos en este momento, obtendrá el siguiente error:

Instrucción ALTER TABLE en conflicto con la restricción FOREIGN KEY "FK_dbo.Course_dbo.Department_DepartmentID". El conflicto ha aparecido en la base de datos "ContosoUniversity", tabla "dbo.Department", columna "DepartmentID".

Edite el archivo <marca_de_tiempo>_Chap4.cs y realice los siguientes cambios de código (agregará una instrucción SQL y modificará una instrucción AddColumn):

CreateTable(
        "dbo.CourseInstructor",
        c => new
            {
                CourseID = c.Int(nullable: false),
                InstructorID = c.Int(nullable: false),
            })
        .PrimaryKey(t => new { t.CourseID, t.InstructorID })
        .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
        .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
        .Index(t => t.CourseID)
        .Index(t => t.InstructorID);

    // Create  a department for course to point to.
    Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    //  default value for FK points to department created above.
    AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1)); 
    //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));

    AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));
    AddForeignKey("dbo.Course", "DepartmentID", "dbo.Department", "DepartmentID", cascadeDelete: true);
    CreateIndex("dbo.Course", "DepartmentID");
}

public override void Down()
{

(Asegúrese de marcar como comentario o eliminar la línea AddColumn existente al agregar la nueva o recibirá un error al escribir el comando update-database).

En ocasiones, al ejecutar migraciones con datos existentes, debe insertar datos de código auxiliar en la base de datos para satisfacer las restricciones de clave externa, como hará ahora. El código generado agrega a la tabla Course una clave externa DepartmentID que no acepta valores NULL. Si ya hay filas en la tabla Course cuando se ejecuta el código, se produce un error en la operación AddColumn porque SQL Server no sabe qué valor incluir en la columna que no puede ser NULL. Por tanto, ha cambiado el código para asignar un valor predeterminado a la nueva columna y ha creado un departamento de código auxiliar denominado "Temp" para que actúe como el predeterminado. Como resultado, si hay filas Course existentes cuando se ejecuta este código, todas estarán relacionadas con el departamento "Temp".

Cuando se ejecute el método Seed, insertará filas en la tabla Department y relacionará las filas Course existentes con esas filas Department nuevas. Si todavía no ha agregado cursos en la interfaz de usuario, ya no necesitará el departamento "Temp" ni el valor predeterminado en la columna Course.DepartmentID. Para permitir que alguien haya agregado cursos mediante la aplicación, también querrá actualizar el código del método Seed para asegurarse de que todas las filas Course (no solo las insertadas por ejecuciones anteriores del método Seed) tienen valores DepartmentID válidos antes de quitar el valor predeterminado de la columna y eliminar el departamento "Temp".

Una vez que haya terminado de editar el archivo <marca_de_tiempo>_Chap4.cs, escriba el comando update-database en PMC para ejecutar la migración.

Nota:

Al migrar datos y hacer cambios en el esquema, es posible que se generen otros errores. Si se producen errores de migración que no puede resolver, puede cambiar la cadena de conexión en el archivo Web.config o eliminar la base de datos. El enfoque más sencillo consiste en cambiar el nombre de la base de datos en el archivo Web.config. Por ejemplo, cambie el nombre de la base de datos a CU_test como se muestra a continuación:

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;
      Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\CU_Test.mdf" 
      providerName="System.Data.SqlClient" />

Con una base de datos nueva, no hay ningún dato para migrar y es mucho más probable que el comando update-database se complete sin errores. Para obtener instrucciones sobre cómo eliminar la base de datos, vea Procedimiento para quitar una base de datos de Visual Studio 2012.

Abra la base de datos en el Explorador de servidores como hizo antes y expanda el nodo Tablas para ver que se han creado todas las tablas. (Si el Explorador de servidores sigue abierto de la vez anterior, haga clic en el botón Actualizar).

Screenshot that shows the Server Explorer database. The Tables node is expanded.

No ha creado una clase de modelo para la tabla CourseInstructor. Como se ha explicado antes, se trata de una tabla de combinación para la relación de varios a varios entre las entidades Instructor y Course.

Haga clic con el botón derecho en la tabla CourseInstructor y seleccione Mostrar datos de tabla para comprobar que tiene datos en ella como resultado de las entidades Instructor que ha agregado a la propiedad de navegación Course.Instructors.

Table_data_in_CourseInstructor_table

Resumen

Ahora tiene un modelo de datos más complejo y la base de datos correspondiente. En el siguiente tutorial, obtendrá más información sobre las distintas formas de acceder a datos relacionados.

En el mapa de contenido de acceso a datos de ASP.NET pueden encontrar vínculos a otros recursos de Entity Framework.