Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En los tutoriales anteriores, trabajó con un modelo de datos simple compuesto por tres entidades. En este tutorial, agregará más entidades y relaciones y personalizará el modelo de datos especificando reglas de formato, validación y asignación 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, usted hará lo siguiente:
- Personalizar el modelo de datos
- Modificar la entidad Estudiante
- Crea la entidad Instructor
- Crear entidad OfficeAssignment
- Modifica la entidad Course
- Crea la entidad Department
- Modificar la entidad de inscripción
- Actualización del contexto de la base de datos
- Inicializar la base de datos con datos de prueba
- Agregar una migración
- Cambiar el cadena de conexión
- Actualizar la base de datos
Prerequisites
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. A continuación, en varias de las secciones siguientes, creará el modelo de datos school completo agregando atributos a las clases que ya creó y creando nuevas clases para los tipos de entidad restantes 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 using instrucción para el System.ComponentModel.DataAnnotations espacio de nombres y agregue 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 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 DataType enumeración proporciona muchos tipos de datos, como Date, 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 datos para DataType.Date en exploradores compatibles con HTML5. El atributo DataType emite atributos HTML 5 data- (pronunciado data dash) que los exploradores HTML 5 pueden entender. Los DataType atributos no proporcionan ninguna validación.
DataType.Date no especifica el formato de la fecha que se muestra. De forma predeterminada, el campo de datos se muestra según los formatos predeterminados basados en 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 se debe aplicar también cuando el valor se muestra en un cuadro de texto para su edición. (Es posible que no desee eso para algunos campos; por ejemplo, para los valores de moneda, es posible que no desee el símbolo de moneda en el cuadro de texto de edición.)
Puede usar el DisplayFormat atributo por sí mismo, pero generalmente es una buena idea usar el DataType atributo también. El DataType atributo transmite la semántica de los datos en lugar de cómo representarlos en una pantalla y proporciona las siguientes ventajas que no se obtienen con DisplayFormat:
El explorador puede habilitar las características HTML5 (por ejemplo, para mostrar un control de calendario, el símbolo de moneda adecuado para la configuración regional, vínculos de correo electrónico, alguna validación de entrada del lado cliente, etc.).
De manera predeterminada, el explorador representa los datos con el formato correcto según la configuración regional.
Para obtener más información, consulte la documentación del <Tag Helper input>.
Ejecute la aplicación, vaya a la página Índice de estudiantes y observe que las horas ya no se muestran para las fechas de inscripción. Lo mismo será para cualquier vista que use el modelo Student.
El atributo StringLength
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 en la base de datos y proporciona validación del lado cliente y del lado servidor para ASP.NET Core 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.
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)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
El StringLength atributo no impedirá que un usuario escriba espacios en blanco para un nombre. Puede usar el RegularExpression atributo para aplicar restricciones a la entrada. Por ejemplo, el código siguiente requiere que el primer carácter sea una letra mayúscula y el resto de caracteres sean alfabéticos:
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
El MaxLength atributo proporciona una funcionalidad similar al StringLength atributo, pero no proporciona validación del lado cliente.
El modelo de base de datos ha cambiado de una manera que requiere un cambio en el esquema de la base de datos. Usará migraciones para actualizar el esquema sin perder los datos que pueda haber agregado a la base de datos mediante la interfaz de usuario de la aplicación.
Guarde los cambios y compile el proyecto. A continuación, abra la ventana de comandos en la carpeta del proyecto y escriba los siguientes comandos:
dotnet ef migrations add MaxLengthOnNames
dotnet ef database update
El migrations add comando advierte que puede producirse una pérdida de datos, ya que el cambio reduce la longitud máxima para dos columnas. Las migraciones crean 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 database update comando ejecutó ese código.
Entity Framework usa la marca de tiempo antepuesta al nombre del archivo de migraciones para ordenar las migraciones. Puede crear varias migraciones antes de ejecutar el comando update-database y, a continuación, todas las migraciones se aplican en el orden en que se crearon.
Ejecute la aplicación, seleccione la pestaña Estudiantes , haga clic en Crear nuevo e intente escribir cualquiera de los nombres de más de 50 caracteres. La aplicación debe impedir que hagas esto.
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 campo del nombre, ya que el campo podría contener 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 hacer este mapeo, 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 nombre de la propiedad.
En el Student.cs archivo, agregue una using instrucción para System.ComponentModel.DataAnnotations.Schema y agregue el atributo de nombre de columna a la FirstMidName propiedad , 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)]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
La adición del atributo Column cambia el modelo que respalda SchoolContext, por lo que no coincidirá con la base de datos.
Guarde los cambios y compile el proyecto. A continuación, abra la ventana de comandos en la carpeta del proyecto y escriba los siguientes comandos para crear otra migración:
dotnet ef migrations add ColumnFirstName
dotnet ef database update
En SQL Server Explorador de objetos, abra el diseñador de la tabla Student haciendo doble clic en la tabla Student.
Antes de aplicar las dos primeras migraciones, las columnas de nombre eran de tipo nvarchar(MAX). Ahora son nvarchar(50) y el nombre de columna ha cambiado de FirstMidName a FirstName.
Note
Si intenta compilar antes de terminar de crear todas las clases de entidad en las secciones siguientes, puede obtener errores del compilador.
Cambios en la entidad Estudiante
En Models/Student.cs, reemplace el código que agregó anteriormente por 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)]
[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 ICollection<Enrollment> Enrollments { get; set; }
}
}
El atributo Required
El atributo Required hace que las propiedades de nombre sean campos obligatorios. El Required atributo no es necesario para tipos que no aceptan valores NULL, como tipos de valor (DateTime, int, double, float, etc.). Los tipos que no aceptan valores NULL se tratan automáticamente como campos obligatorios.
El atributo Required se debe usar con MinimumLength para que MinimumLength se aplique.
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
El atributo Display
El Display atributo especifica que el título de los cuadros de texto debe ser "First Name", "Last Name", "Full Name" y "Enrollment Date" en lugar del nombre de propiedad de cada instancia (que no tiene espacio que divida 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, reemplazando el código de plantilla por 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 ICollection<CourseAssignment> CourseAssignments { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
Observe que varias propiedades son las mismas en las entidades Student e Instructor. En el tutorial Implementación de herencia más adelante en esta serie, refactorizarás este código para eliminar la redundancia.
Puede colocar varios atributos en una línea, por lo que también puede escribir los HireDate atributos de la siguiente manera:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Las propiedades de navegación CourseAssignments y OfficeAssignment
CourseAssignments y OfficeAssignment son propiedades de navegación.
Un instructor puede impartir cualquier número de cursos, por lo que CourseAssignments se define como una colección.
public ICollection<CourseAssignment> CourseAssignments { get; set; }
Si una propiedad de navegación puede contener varias entidades, su tipo debe ser una lista en la que se pueden agregar, eliminar y actualizar las entradas. Puede especificar ICollection<T> o un tipo como List<T> o HashSet<T>. Si especifica ICollection<T>, EF crea una colección HashSet<T> de forma predeterminada.
La razón por la que estas son entidades CourseAssignment se explica a continuación en la sección sobre las relaciones de varios a varios.
Las reglas de negocios de Contoso University establecen que un instructor solo puede tener como máximo una oficina, por lo que la OfficeAssignment propiedad contiene una sola entidad OfficeAssignment (que puede ser null si no se asigna ninguna oficina).
public OfficeAssignment OfficeAssignment { get; set; }
Crear la entidad OfficeAssignment
Cree el archivo Models/OfficeAssignment.cs con el siguiente código:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
El atributo Key
Hay una relación de uno a cero o uno entre la entidad Instructor y la entidad OfficeAssignment. Una asignación de oficina solo existe en relación con el instructor al que está asignada y, por lo tanto, su clave primaria es también su clave foránea para la entidad Instructor. Pero Entity Framework no puede reconocer InstructorID automáticamente como clave principal de esta entidad porque su nombre no sigue las convenciones de nomenclatura ID o classnameID. Por tanto, se usa el atributo Key para identificarla como la clave:
[Key]
public int InstructorID { get; set; }
También puede usar el atributo Key si la entidad sí tiene su propia clave principal, pero desea dar a la propiedad un nombre distinto de classnameID o ID.
De forma predeterminada, EF trata la clave como no generada por la base de datos porque la columna es para una relación de identificación.
La propiedad de navegación Instructor
La entidad Instructor tiene una propiedad de navegación que acepta valores NULL OfficeAssignment (porque es posible que un instructor no tenga una asignación de oficina) y la entidad OfficeAssignment tiene una propiedad de navegación que no acepta valores NULL Instructor (porque una asignación de office no puede existir sin un instructor -- InstructorID no admite valores NULL). Cuando una entidad Instructor tiene una entidad OfficeAssignment relacionada, cada entidad tendrá una referencia a la otra en su propiedad de navegación.
Puede colocar un [Required] atributo en la propiedad de navegación Instructor para especificar que debe haber un instructor relacionado, pero no tiene que hacerlo porque la InstructorID clave externa (que también es la clave para esta tabla) no admite valores NULL.
Modifica la entidad Course
En Models/Course.cs, reemplace el código que agregó anteriormente por el código siguiente. Los cambios aparecen resaltados.
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 Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}
La entidad curso tiene una propiedad DepartmentID de clave externa que apunta a la entidad Departamento 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 crea automáticamente claves externas en la base de datos donde sea necesario y crea shadow properties para ellas. Pero tener la clave externa en el modelo de datos puede hacer que las actualizaciones sean más sencillas y eficaces. Por ejemplo, al capturar una Course entidad para editarla, la Department entidad es null si no la carga, por lo que al actualizar la Course entidad, primero tendrá que capturar la Department entidad. 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.
El 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 forma predeterminada, Entity Framework supone 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.
El DatabaseGenerated atributo también se puede usar para generar valores predeterminados, como en el caso de las columnas de base de datos usadas para registrar la fecha en que se creó o actualizó una fila. Para obtener más información, vea Propiedades generadas.
Propiedades de clave externa y de navegación
Las propiedades de clave externa y las de navegación de la entidad Course reflejan las relaciones siguientes:
Un curso se asigna a un departamento, por lo que hay una clave externa DepartmentID y una propiedad de navegación Department por las razones mencionadas anteriormente.
public int DepartmentID { get; set; }
public 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 ICollection<Enrollment> Enrollments { get; set; }
Un curso puede ser enseñado por varios instructores, por lo que la propiedad de CourseAssignments navegación es una colección (el tipo CourseAssignment se explica más adelante):
public ICollection<CourseAssignment> CourseAssignments { get; set; }
Crea la entidad Department
Cree el archivo Models/Department.cs con el siguiente código:
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 Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
El atributo Column
Anteriormente usó el atributo Column para cambiar la asignación de nombres de columnas. En el código de la Department entidad, el Column atributo se usa para cambiar la asignación de tipos de datos SQL para que la columna se defina mediante el tipo SQL Server money en la base de datos:
[Column(TypeName="money")]
public decimal Budget { get; set; }
La asignación de columnas no suele ser necesaria, ya que Entity Framework elige el tipo de datos SQL Server adecuado en función del tipo CLR que defina para la propiedad. El tipo de CLR decimal se mapea a un tipo de SQL Server decimal. Pero en este caso sabes que la columna contendrá importes 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 lo tanto, la InstructorID propiedad se incluye como clave externa a la entidad Instructor y se agrega un signo de interrogación después de la designación de int tipo para marcar la propiedad como que acepta valores NULL. La propiedad de navegación se denomina Administrator pero contiene una entidad Instructor:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Un departamento puede tener varios cursos, por lo que hay una propiedad de navegación Courses:
public ICollection<Course> Courses { get; set; }
Note
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 circulares de eliminación en cascada, lo que provocará una excepción al intentar agregar una migración. Por ejemplo, si no definió la Department.InstructorID propiedad como que acepta valores NULL, EF configuraría una regla de eliminación en cascada para eliminar el departamento al eliminar el instructor, lo que no es lo que quiere que ocurra. Si las reglas de negocio requerían que la InstructorID propiedad no aceptase valores NULL, tendría que usar la siguiente instrucción de API fluida para deshabilitar la eliminación en cascada en la relación:
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Modifica la entidad Enrollment
En Models/Enrollment.cs, reemplace el código que agregó anteriormente por el código siguiente:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
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 Course Course { get; set; }
public Student Student { get; set; }
}
}
Propiedades de clave externa y de navegación
Las propiedades de clave externa y de navegación reflejan las relaciones siguientes:
Un registro de inscripción es para un solo curso, por lo que hay una propiedad de clave externa CourseID y una propiedad de navegación Course:
public int CourseID { get; set; }
public Course Course { get; set; }
Un registro de inscripción es para un solo estudiante, por lo que hay una propiedad de clave externa StudentID y una propiedad de navegación Student:
public int StudentID { get; set; }
public Student Student { get; set; }
Relaciones de muchos a muchos
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. "Con carga" significa que la Enrollment tabla contiene datos adicionales además de claves externas para las tablas combinadas (en este caso, una clave principal y una Grade propiedad).
En la ilustración siguiente se muestra el aspecto de estas relaciones en un diagrama de entidades. (Este diagrama se generó con Entity Framework Power Tools para EF 6.x; la creación del diagrama no forma parte del tutorial, solo se usa aquí como 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, sería una tabla de unión de varios a varios sin datos adicionales (o una tabla de unión pura) en la base de datos. Las entidades Instructor y Course tienen ese tipo de relación de varios a varios y el siguiente paso es crear una clase de entidad para que funcione como una tabla de combinación sin carga.
EF Core admite tablas de combinación implícitas para relaciones de varios a varios, pero este tutorial no se ha actualizado para usar una tabla de combinación implícita. Vea Relaciones de varios a varios, la versión de Razor Pages de este tutorial que se ha actualizado.
La entidad CourseAssignment
Cree el archivo Models/CourseAssignment.cs con el siguiente código:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class CourseAssignment
{
public int InstructorID { get; set; }
public int CourseID { get; set; }
public Instructor Instructor { get; set; }
public Course Course { get; set; }
}
}
Unir nombres de entidades
Se requiere una tabla de combinación en la base de datos para la relación de varios a varios entre Instructor y Courses, y se tiene que representar mediante un conjunto de entidades. Es habitual asignar un nombre a una entidad EntityName1EntityName2de combinación , que en este caso sería CourseInstructor. Sin embargo, se recomienda elegir un nombre que describa la relación. Los modelos de datos empiezan siendo simples y crecen, con las uniones sin payload que suelen obtener payloads más adelante. Si empieza con un nombre de entidad descriptivo, no tendrá que cambiar el nombre más adelante. Idealmente, la entidad asociativa tendrá su propio nombre natural (posiblemente una sola palabra) en el dominio empresarial. Por ejemplo, los libros y los clientes podrían vincularse a través de valoraciones. Para esta relación, CourseAssignment es una opción mejor que CourseInstructor.
Clave compuesta
Dado que las claves externas no admiten valores NULL y juntas identifican de forma única cada fila de la tabla, no es necesario usar una clave principal independiente. Las propiedades InstructorID y CourseID deben funcionar como una clave principal compuesta. La única manera de identificar las claves principales compuestas en EF es usar la API fluida (no se puede realizar mediante atributos). Verá cómo configurar la clave principal compuesta en la sección siguiente.
La clave compuesta garantiza que, aunque puede tener varias filas para un curso y varias filas para un instructor, no puede tener varias filas para el mismo instructor y curso. La Enrollment entidad join define su propia clave principal, por lo que los duplicados de este tipo son posibles. Para evitar estos duplicados, puede agregar un índice único en los campos de clave foránea o configurar Enrollment con una clave primaria compuesta similar a CourseAssignment. Para obtener más información, vea Índices.
Actualización del contexto de la base de datos
Agregue el código resaltado siguiente al Data/SchoolContext.cs archivo:
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<CourseAssignment> CourseAssignments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
Este código agrega las nuevas entidades y configura la clave principal compuesta de la entidad CourseAssignment.
Acerca de una alternativa de la API fluida
El código en el método OnModelCreating de la clase DbContext usa la API fluida para configurar el comportamiento de EF. La API se denomina "fluent" porque a menudo se usa encadenando una serie de llamadas a métodos en una sola instrucción, como en este ejemplo de la EF Core documentación:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
En este tutorial, está usando la API fluida solo para la asignación de bases de datos que no puede hacer 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 mencionó anteriormente, MinimumLength no cambia el esquema, solo aplica una regla de validación del lado cliente y 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. Si usa ambos enfoques, tenga en cuenta que siempre que hay un conflicto, la API fluida invalida los atributos.
Para obtener más información sobre la diferencia entre los atributos y la API fluida, vea Métodos de configuración.
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 relación uno a varios (1 a *), aquí se 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.
Inicializar la base de datos con datos de prueba
Reemplace el código en el archivo Data/DbInitializer.cs por el código siguiente para proporcionar datos de inicialización para las nuevas entidades que ha creado.
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
//context.Database.EnsureCreated();
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}
var students = new 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") }
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();
var instructors = new 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") }
};
foreach (Instructor i in instructors)
{
context.Instructors.Add(i);
}
context.SaveChanges();
var departments = new 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 }
};
foreach (Department d in departments)
{
context.Departments.Add(d);
}
context.SaveChanges();
var courses = new Course[]
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();
var officeAssignments = new 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" },
};
foreach (OfficeAssignment o in officeAssignments)
{
context.OfficeAssignments.Add(o);
}
context.SaveChanges();
var courseInstructors = new CourseAssignment[]
{
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
};
foreach (CourseAssignment ci in courseInstructors)
{
context.CourseAssignments.Add(ci);
}
context.SaveChanges();
var enrollments = new 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();
}
}
}
Como ha visto en el primer tutorial, la mayoría de este código simplemente crea nuevos objetos de entidad y carga datos de ejemplo en propiedades según sea necesario para las pruebas. Observe cómo se gestionan las relaciones de varios a varios: el código crea relaciones mediante la creación de entidades en los conjuntos de entidades de combinación Enrollments y CourseAssignment.
Agregar una migración
Guarde los cambios y compile el proyecto. A continuación, abra la ventana de comandos en la carpeta del proyecto y escriba el migrations add comando (aún no haga el comando update-database):
dotnet ef migrations add ComplexDataModel
Recibirá una advertencia sobre la posible pérdida de datos.
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'
Si intentara ejecutar el comando database update en este punto (no lo haga todavía), obtendría 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".
A veces, al ejecutar migraciones con datos existentes, debe insertar datos ficticios en la base de datos para satisfacer las restricciones de clave externa. El código generado en el método Up agrega una clave externa DepartmentID no anulable a la tabla Course. Si ya hay filas en la tabla Course cuando se ejecuta el código, se produce un error en la AddColumn operación porque SQL Server no sabe qué valor colocar en la columna que no puede ser NULL. En este tutorial ejecutará la migración en una nueva base de datos, pero en una aplicación de producción tendría que hacer que la migración controle los datos existentes, por lo que en las siguientes instrucciones se muestra un ejemplo de cómo hacerlo.
Para que esta migración funcione con los datos existentes, debe cambiar el código para asignar a la nueva columna un valor predeterminado y crear un departamento provisional denominado "Temp" para que actúe como departamento predeterminado. Como resultado, las filas course existentes estarán relacionadas con el departamento "Temp" después de que se ejecute el Up método.
Abra el archivo
{timestamp}_ComplexDataModel.cs.Convierta en comentario la línea de código que agrega la columna DepartmentID a la tabla Course.
migrationBuilder.AlterColumn<string>( name: "Title", table: "Course", maxLength: 50, nullable: true, oldClrType: typeof(string), oldNullable: true); //migrationBuilder.AddColumn<int>( // name: "DepartmentID", // table: "Course", // nullable: false, // defaultValue: 0);Agregue el código resaltado siguiente después del código que crea la tabla Department:
migrationBuilder.CreateTable( name: "Department", columns: table => new { DepartmentID = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Budget = table.Column<decimal>(type: "money", nullable: false), InstructorID = table.Column<int>(nullable: true), Name = table.Column<string>(maxLength: 50, nullable: true), StartDate = table.Column<DateTime>(nullable: false) }, constraints: table => { table.PrimaryKey("PK_Department", x => x.DepartmentID); table.ForeignKey( name: "FK_Department_Instructor_InstructorID", column: x => x.InstructorID, principalTable: "Instructor", principalColumn: "ID", onDelete: ReferentialAction.Restrict); }); migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())"); // Default value for FK points to department created above, with // defaultValue changed to 1 in following AddColumn statement. migrationBuilder.AddColumn<int>( name: "DepartmentID", table: "Course", nullable: false, defaultValue: 1);
En una aplicación de producción, debería escribir código o scripts para agregar filas Department y filas Course relacionadas a las nuevas filas Department. Después, ya no necesitaría el departamento "Temp" ni el valor predeterminado en la columna Course.DepartmentID.
Guarde los cambios y compile el proyecto.
Cambiar el cadena de conexión
Ahora tiene código nuevo en la DbInitializer clase que agrega datos de inicialización para las nuevas entidades a una base de datos vacía. Para que EF cree una nueva base de datos vacía, cambie el nombre de la base de datos en el cadena de conexión de appsettings.json a ContosoUniversity3 o a algún otro nombre que no haya usado en el equipo que esté usando.
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
},
Guarde los cambios en appsettings.json .
Note
Como alternativa a cambiar el nombre de la base de datos, puede eliminar la base de datos. Use el Explorador de objetos de SQL Server (SSOX) o el comando de la CLI database drop:
dotnet ef database drop
Actualizar la base de datos
Después de cambiar el nombre de la base de datos o eliminar la base de datos, ejecute el database update comando en la ventana de comandos para ejecutar las migraciones.
dotnet ef database update
Ejecute la aplicación para que el DbInitializer.Initialize método se ejecute y rellene la nueva base de datos.
Abra la base de datos en SSOX como hizo anteriormente y expanda el nodo Tablas para ver que se han creado todas las tablas. (Si todavía tiene SSOX abierto desde la hora anterior, haga clic en el botón Actualizar ).
Ejecute la aplicación para desencadenar el código de inicializador que puebla la base de datos con datos iniciales.
Haga clic con el botón derecho en la tabla CourseAssignment y seleccione Ver datos para comprobar que tiene datos en ella.
Obtención del código
Descargue o vea la aplicación completa.
Pasos siguientes
En este tutorial, usted hará lo siguiente:
- Se personalizó el modelo de datos
- Cambios realizados en la entidad Student
- Entidad de instructor creada
- Entidad OfficeAssignment creada
- Entidad de curso modificada
- Creado la entidad Department
- Entidad de inscripción modificada
- Se ha actualizado el contexto de la base de datos
- Base de datos inicializado con datos de prueba
- Se ha agregado una migración
- Se cambió el cadena de conexión
- Se actualizó la base de datos
Pase al siguiente tutorial para obtener más información sobre cómo acceder a los datos relacionados.