Tutorial: crear un modelo de datos más complejo para una aplicación MVC de ASP.NET
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 mediante 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:
En este tutorial ha:
- Personalizar el modelo de datos
- Actualizar la entidad Student
- Crea la entidad Instructor
- Crea la entidad OfficeAssignment
- Modificar la entidad Course
- Crear la entidad Department
- Modificar la entidad Enrollment
- Agregar código al contexto de la base de datos
- Inicializa la base de datos con datos de prueba
- Agregar una migración
- Actualizar la base de datos
Requisitos previos
Personalizar el modelo de datos
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 agregando atributos a las clases que ya ha creado y creando 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 ID { 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)]
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, para mostrar un control de calendario, el símbolo de divisa adecuado según la configuración regional, vínculos de correo electrónico, validación de entradas del lado cliente, 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 elegir la plantilla de campo adecuada para representar los datos (DisplayFormat utiliza la plantilla de cadena). Para más información, consulte Plantillas de MVC 2 de ASP.NET 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, consulte esta conversación de StackOverflow.
Para obtener más información sobre cómo controlar otros formatos de fecha en MVC, vaya a Introducción a MVC 5: examinar los métodos de edición y editar vista y busque en la página "internacionalización".
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
.
StringLengthAttribute
También puede especificar reglas de validación de datos y mensajes de error de validación mediante atributos. 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 MVC de ASP.NET. 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.
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 ID { 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
crea un archivo denominado <timeStamp>_MaxLengthOnNames.cs. Este archivo contiene código en el método Up
que actualizará la base de datos para que coincida con el modelo de datos actual. El comando update-database
ejecutó ese código.
Entity Framework usa la marca de tiempo que precede al nombre de archivo de migraciones para ordenar las migraciones. Puede crear varias migraciones antes de ejecutar el comando update-database
y, después, todas las migraciones se aplican en el orden en el que se hayan creado.
Ejecute la página Create y escriba un nombre de más de 50 caracteres. Al hacer clic en Crear, la validación del lado cliente muestra un mensaje de error: El campo LastName debe ser una cadena con una longitud máxima de 50.
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.
En el archivo Student.cs, agregue una instrucción using
para System.ComponentModel.DataAnnotations.Schema y agregue 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 ID { 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 la consola de administración de paquetes (PMC) para crear otra migración:
add-migration ColumnFirstName
update-database
En el Explorador de servidores, abra el diseñador de tablas Student haciendo doble clic en la tabla Student.
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.
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 antes de terminar de crear todas las clases de entidad en las secciones siguientes, es posible que se produzcan errores del compilador.
Actualizar la entidad Student
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 ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
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; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
El atributo Required
El atributo Required hace que las propiedades de nombre sean campos obligatorios. El Required attribute
no es necesario para tipos de valor como DateTime, int, double y float. A los tipos de valor no se les puede asignar un valor null, por lo que son tratados de forma inherente como campos obligatorios.
El atributo Required
se debe usar con MinimumLength
para que se aplique MinimumLength
.
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
y Required
permiten que el espacio en blanco satisfaga la validación. Utilice el atributo RegularExpression
para un control total sobre la cadena.
El atributo Display
El atributo Display
especifica que el título de los cuadros de texto debe ser "First Name" (Nombre), "Last Name" (Apellidos), "Full Name" (Nombre completo) y "Enrollment Date" (Fecha de inscripción) en lugar del nombre de propiedad de cada instancia (que no tiene ningún espacio para dividir las palabras).
La 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.
Crea la entidad Instructor
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 ID { 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; }
[Display(Name = "Full Name")]
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, deberá refactorizar este código para eliminar la redundancia.
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 ID { 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; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
Las 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 Agregar.
Un instructor puede impartir cualquier número de cursos, por lo que Courses
se define como una colección de entidades Course
.
public virtual ICollection<Course> Courses { get; set; }
Nuestras reglas de negocio 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 OfficeAssignment OfficeAssignment { get; set; }
Crea la entidad OfficeAssignment
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 classname ID
. 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 configurarse 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.
La 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
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; }
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ónDepartment
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; }
Crear la entidad Department
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)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
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. Para obtener más información sobre los tipos de datos CLR y cómo coinciden con los tipos de datos de SQL Server, consulte SqlClient para Entity FrameworkTypes.
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 entidadInstructor
y se agrega un signo de interrogación después de la designación del tipoint
para marcar la propiedad como que admite un valor NULL. El nombre de la propiedad de navegación esAdministrator
pero contiene una entidadInstructor
: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 producirá una excepción al intentar agregar una migración. Por ejemplo, si no ha definido la propiedad
Department.InstructorID
como que admite un valor NULL, obtendrá el siguiente mensaje de excepción: "La relación referencial dará lugar a una referencia cíclica que no está permitida". Si las reglas de negocio requerían que la propiedadInstructorID
no acepte valores NULL, tendría que usar la siguiente instrucción de la API fluida para deshabilitar la eliminación en cascada en la relación:
modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);
Modificar la 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ónCourse
: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ónStudent
: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).
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:
Pero en la base de datos se necesita una tabla de combinación, como se muestra en el diagrama de base de datos siguiente:
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 relación entre entidades
En la siguiente ilustración se muestra el diagrama creado por Entity Framework Power Tools para el modelo School completado.
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.
Agregar 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 encadenando una serie de llamadas de método entre sí en una única instrucción, como en el siguiente ejemplo:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
En este tutorial solo usará la API fluida para la asignación de base de datos que no pueda 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
yCourse
, 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 comoInstructorInstructorID
para la columnaInstructorID
.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.
Inicializa 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 ContosoUniversity.Models;
using ContosoUniversity.DAL;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
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").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").ID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID }
};
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").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
Location = "Thompson 304" },
};
officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, 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").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").ID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};
foreach (Enrollment e in enrollments)
{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == 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,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
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 podría agregar la inicialización de lista al constructor.
Agregar una migración
Desde la PMC, escriba el comando add-migration
(aún no haga el comando update-database
):
add-Migration ComplexDataModel
Si ahora intentara ejecutar el comando update-database
(no lo haga todavía), obtendría el error siguiente:
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".
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 en el método Up
de ComplexDataModel agrega una clave externa DepartmentID
que no acepta valores NULL a la tabla Course
. 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, hay que cambiar el código para asignar un valor predeterminado a la nueva columna y crear un departamento de código auxiliar denominado "Temp" para que actúe como el departamento predeterminado. Como resultado, las filas Course
existentes estarán relacionadas con el departamento "Temp" después de ejecutar el método Up
. Puede relacionarlas con los departamentos correctos en el método Seed
.
Edite el archivo <timestamp>_ComplexDataModel.cs, comente la línea de código que agrega la columna DepartmentID a la tabla Course y agregue el código resaltado siguiente (la línea comentada también está resaltada):
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));
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 la posibilidad de 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".
Actualizar la base de datos
Una vez que haya terminado de editar el archivo <timestamp>_ComplexDataModel.cs, escriba el comando update-database
en la PMC para ejecutar la migración.
update-database
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 se pueden resolver, puede cambiar el nombre de la base de datos en la cadena de conexión 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. En el ejemplo siguiente se muestra el nombre cambiado a CU_Test:
<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;"
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, consulte Procedimiento para quitar una base de datos de Visual Studio 2012.
Si se produce un error, otra cosa que puede intentar es volver a inicializar la base de datos escribiendo el siguiente comando en la PMC:
update-database -TargetMigration:0
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).
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
.
Obtención del código
Descargar el proyecto completado
Recursos adicionales
Puede encontrar vínculos a otros recursos de Entity Framework en el Acceso a datos de ASP.NET: recursos recomendados.
Pasos siguientes
En este tutorial ha:
- Se personalizó el modelo de datos.
- Se actualizó la entidad Student.
- Creado la entidad Instructor
- Creado la entidad OfficeAssignment
- Se modificó la entidad Course.
- Se creó la entidad Department.
- Se modificó la entidad Enrollment.
- Se agregó código al contexto de base de datos.
- Inicializado la base de datos con datos de prueba
- Agregado una migración
- Actualizado la base de datos
Pase al siguiente artículo para aprender a leer y mostrar datos relacionados que Entity Framework carga en las propiedades de navegación.