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 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 encontrase problemas que no pudiera resolver, descargue el capítulo completo e intente reproducir el problema. Por lo general, es posible encontrar la solución al problema comparando el propio 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:
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 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 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
.
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.
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.
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 el proyecto antes de terminar de crear todas estas clases de entidad, es posible que se produzcan errores del compilador.
Crear 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 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
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 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
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ó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; }
Creación de 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)]
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 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 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 propiedadInstructorID
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
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ó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 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.
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
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.
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).
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
.
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.