Parte 5, Razor Pagine con EF Core in ASP.NET Core - Data Model
Di Tom Dykstra, Jeremy Likness e Jon P Smith
L'app Web Contoso University illustra come creare Razor app Web Pages usando EF Core e Visual Studio. Per informazioni sulla serie di esercitazioni, vedere la prima esercitazione.
Se si verificano problemi che non è possibile risolvere, scaricare l'app completata e confrontare tale codice con quello creato seguendo questa esercitazione.
Nelle esercitazioni precedenti è stato usato un modello di dati semplice costituito da tre entità. Contenuto dell'esercitazione:
- Vengono aggiunte altre entità e relazioni.
- Il modello di dati viene personalizzato specificando regole di formattazione, convalida e mapping del database.
Il modello di dati completato è illustrato nella figura seguente:
Il diagramma di database seguente è stato creato con Dataedo:
Per creare un diagramma di database con Dataedo:
- Distribuire l'app in Azure
- Scaricare e installare Dataedo nel computer.
- Seguire le istruzioni Generare la documentazione per database SQL di Azure in 5 minuti
Nel diagramma Dataedo precedente, CourseInstructor
è una tabella join creata da Entity Framework. Per altre informazioni, vedere Molti-a-molti
Entità Student (Studente)
Sostituire il codice in Models/Student.cs
con il codice seguente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Il codice precedente aggiunge una proprietà FullName
e aggiunge gli attributi seguenti alle proprietà esistenti:
Proprietà calcolata FullName
FullName
è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. FullName
non può essere impostata, quindi include solo una funzione di accesso get. Nel database non viene creata una colonna FullName
.
Attributo DataType
[DataType(DataType.Date)]
Per le date di iscrizione degli studenti, tutte le pagine visualizzano attualmente l'ora del giorno insieme alla data, anche se è pertinente solo la data. Mediante gli attributi di annotazione dei dati è possibile modificare il codice per correggere il formato di visualizzazione in tutte le pagine che visualizzano i dati.
L'attributo DataType indica un tipo di dati più specifico rispetto al tipo intrinseco del database. In questo caso deve essere visualizzata solo la data e non la data e l'ora. L'enumerazione DataType fornisce molti tipi di dati, ad esempio Data, Ora, PhoneNumber, Valuta, EmailAddress e così via. L'attributo DataType
può anche consentire all'app di fornire automaticamente funzionalità specifiche del tipo. Ad esempio:
- Il collegamento
mailto:
viene creato automaticamente perDataType.EmailAddress
. - Il selettore data viene incluso per
DataType.Date
nella maggior parte dei browser.
L'attributo DataType
genera attributi HTML 5 data-
. Gli attributi DataType
non garantiscono la convalida.
Attributo DisplayFormat
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
DataType.Date
non specifica il formato della data visualizzata. Per impostazione predefinita il campo data viene visualizzato in base ai formati predefiniti per il valore CultureInfo del server.
L'attributo DisplayFormat
viene usato per specificare in modo esplicito il formato della data. L'impostazione ApplyFormatInEditMode
specifica che la formattazione deve essere applicata anche all'interfaccia utente di modifica. Alcuni campi non devono usare ApplyFormatInEditMode
. Ad esempio il simbolo di valuta in genere non deve essere visualizzato in una casella di testo di modifica.
L'attributo DisplayFormat
può essere usato da solo. In genere l'uso dell'attributo DataType
con l'attributo DisplayFormat
è consigliato. L'attributo DataType
offre la semantica dei dati anziché specificarne il rendering in una schermata. L'attributo DataType
offre i vantaggi seguenti che non sono disponibili in DisplayFormat
:
- Il browser può abilitare le funzionalità HTML5. Ad esempio può visualizzare un controllo di calendario, il simbolo della valuta appropriato per le impostazioni locali, i collegamenti alla posta elettronica e la convalida dell'input sul lato client.
- Per impostazione predefinita, il browser esegue il rendering dei dati usando il formato corretto in base alle impostazioni locali.
Per altre informazioni, vedere la documentazione dell'helper< tag di input>.
Attributo StringLength
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
È possibile specificare regole di convalida dei dati e messaggi di errore di convalida usando gli attributi. L'attributo StringLength specifica il numero minimo e massimo di caratteri consentiti in un campo dati. Il codice precedente limita i nomi a un massimo di 50 caratteri. Un esempio che imposta la lunghezza minima della stringa è disponibile di seguito.
L'attributo StringLength
offre anche la convalida lato client e lato server. Il valore minimo non ha alcun effetto sullo schema del database.
L'attributo StringLength
non impedisce a un utente di immettere spazi vuoti per un nome. L'attributo RegularExpression può essere usato per applicare restrizioni all'input. Ad esempio il codice seguente richiede che il primo carattere sia maiuscolo e i caratteri rimanenti siano caratteri alfabetici:
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
In Esplora oggetti di SQL Server (SSOX) aprire il designer della tabella Student (Studente) facendo doppio clic sulla tabella.
L'immagine precedente visualizza lo schema per la tabella Student
. I campi del nome sono di tipo nvarchar(MAX)
. Quando una migrazione viene creata e applicata più avanti in questa esercitazione, i campi del nome diventano nvarchar(50)
come risultato degli attributi di lunghezza della stringa.
Attributo Column
[Column("FirstName")]
public string FirstMidName { get; set; }
Gli attributi possono controllare il mapping delle classi e delle proprietà nel database. Nel modello Student
l'attributo Column
viene usato per il mapping del nome della proprietà FirstMidName
su "FirstName" nel database.
Quando viene creato il database, i nomi delle proprietà nel modello vengono usati per i nomi di colonna (tranne quando viene usato l'attributo Column
). Il modello Student
usa il nome FirstMidName
per il campo first-name (Nome) perché il campo potrebbe contenere anche un secondo nome.
Con l'attributo [Column]
, per Student.FirstMidName
nel modello di dati viene eseguito il mapping alla colonna FirstName
della tabella Student
. L'aggiunta dell'attributo Column
modifica il modello che supporta SchoolContext
. Il modello che supporta SchoolContext
non corrisponde più al database. Questa discrepanza verrà risolta aggiungendo una migrazione più avanti in questa esercitazione.
Attributo Required
[Required]
L'attributo Required
rende obbligatori i campi delle proprietà del nome. L'attributo Required
non è necessario per i tipi che non ammettono valori Null come i tipi valore (ad esempio, DateTime
, int
e double
). I tipi che non possono essere null vengono considerati automaticamente come campi obbligatori.
L'attributo Required
deve essere usato con MinimumLength
per l'applicazione di MinimumLength
.
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
e Required
consentono spazi vuoti per soddisfare la convalida. Usare l'attributo RegularExpression
per il controllo completo sulla stringa.
Attributo Display
[Display(Name = "Last Name")]
L'attributo Display
specifica che la didascalia delle caselle di testo deve essere "First Name", "Last Name", "Full Name" e "Enrollment Date". Le didascalie predefinite non hanno spazio che divide le parole, ad esempio "Lastname".
Creare una migrazione
Eseguire l'app e passare alla pagina Students (Studenti). Viene generata un'eccezione. Con l'attributo [Column]
EF si aspetta di trovare una colonna denominata FirstName
, ma il nome della colonna nel database è ancora FirstMidName
.
Il messaggio di errore è simile al seguente:
SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:
SchoolContext
Nella console di Gestione pacchetti immettere i comandi seguenti per creare una nuova migrazione e aggiornare il database:
Add-Migration ColumnFirstName Update-Database
Il primo di questi comandi genera il messaggio di avviso seguente:
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
L'avviso viene generato perché i campi nome ora sono limitati a 50 caratteri. Se un nome nel database ha più di 50 caratteri, i caratteri dal 51 all'ultimo andranno perduti.
Aprire la tabella Student (Studente) in SSOX:
Prima dell'applicazione della migrazione, le colonne del nome erano di tipo nvarchar(MAX). Ora le colonne del nome sono di tipo
nvarchar(50)
. Il nome della colonna è cambiato daFirstMidName
aFirstName
.
- Eseguire l'app e passare alla pagina Students (Studenti).
- Si noti che l'ora non viene inclusa nell'input o visualizzata insieme alla data.
- Selezionare Create New (Crea nuovo) e provare immettere un nome di lunghezza superiore a 50 caratteri.
Nota
Nelle sezioni seguenti la compilazione dell'app genera errori del compilatore in alcune fasi. Le istruzioni specificano quando compilare l'applicazione.
Entità Instructor
Creare Models/Instructor.cs
con il codice seguente:
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<Course> Courses { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
Un'unica riga può ospitare più attributi. Gli attributi HireDate
possono essere scritti come segue:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Proprietà di navigazione
Le proprietà Courses
e OfficeAssignment
sono proprietà di navigazione.
Un insegnante può tenere un numero qualsiasi di corsi, pertanto Courses
è definita come raccolta.
public ICollection<Course> Courses { get; set; }
Un insegnante può avere al massimo un ufficio, quindi la proprietà OfficeAssignment
contiene una singola entità OfficeAssignment
. OfficeAssignment
è null se non è assegnato nessun ufficio.
public OfficeAssignment OfficeAssignment { get; set; }
Entità OfficeAssignment
Creare Models/OfficeAssignment.cs
con il codice seguente:
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; }
}
}
Attributo Key
L'attributo [Key]
viene usato per identificare una proprietà come chiave primaria (PK) quando il nome della proprietà è diverso da classnameID
o ID
.
È una relazione uno-a-zero-o-uno tra le entità Instructor
e OfficeAssignment
. L'assegnazione di un ufficio esiste solo in relazione all'insegnante al quale viene assegnato l'ufficio. La chiave primaria OfficeAssignment
è anche la chiave esterna (FK, Foreign Key) per l'entità Instructor
. Una relazione uno-a-zero-o-uno si verifica quando un'infrastruttura a chiave pubblica in una tabella è sia un pk che un FK in un'altra tabella.
EF Core non può riconoscere InstructorID
automaticamente come pk of OfficeAssignment
perché InstructorID
non segue la convenzione di denominazione ID o classnameID. Di conseguenza l'attributo Key
viene usato per identificare l'entità InstructorID
come chiave primaria:
[Key]
public int InstructorID { get; set; }
Per impostazione predefinita, EF Core considera la chiave come non generata dal database perché la colonna è per una relazione di identificazione. Per altre informazioni, vedere Chiavi di Entity Framework.
Proprietà di navigazione Instructor
La proprietà di navigazione Instructor.OfficeAssignment
può essere Null perché potrebbe non essere presente una riga OfficeAssignment
per un determinato insegnante. Un insegnante potrebbe non avere un ufficio assegnato.
La proprietà di navigazione OfficeAssignment.Instructor
avrà sempre un'entità Instructor perché il tipo InstructorID
della chiave esterna è int
, ovvero un tipo valore che non ammette valori Null. Un'assegnazione di ufficio non può esistere senza un insegnante.
Quando un'entità Instructor
dispone di un'entità OfficeAssignment
correlata, ogni entità include un riferimento all'altra entità nella relativa proprietà di navigazione.
Entità Course
Aggiornare Models/Course.cs
con il codice seguente:
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<Instructor> Instructors { get; set; }
}
}
L'entità Course
dispone di una proprietà chiave esterna (FK) DepartmentID
. DepartmentID
fa riferimento all'entità Department
correlata. L'entità Course
dispone di una proprietà di navigazione Department
.
EF Core non richiede una proprietà di chiave esterna per un modello di dati quando il modello ha una proprietà di navigazione per un'entità correlata. EF Core crea automaticamente IK nel database ovunque siano necessari. EF Core crea proprietà shadow per gli FK creati automaticamente. Includere la chiave esterna in modo esplicito nel modello di dati, tuttavia, può rendere più semplici ed efficienti gli aggiornamenti. Si consideri ad esempio un modello in cui la proprietà chiave esterna DepartmentID
non è inclusa. Quando un'entità Course viene recuperata per la modifica:
- La
Department
proprietà ènull
se non viene caricata in modo esplicito. - Per aggiornare l'entità Course, è in primo luogo necessario recuperare l'entità
Department
.
Quando la proprietà chiave esterna DepartmentID
è inclusa nel modello di dati, non è necessario recuperare l'entità Department
prima di un aggiornamento.
Attributo DatabaseGenerated
L'attributo [DatabaseGenerated(DatabaseGeneratedOption.None)]
indica che la chiave primaria viene resa disponibile dall'applicazione anziché essere generata dal database.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Per impostazione predefinita, EF Core si presuppone che i valori PK vengano generati dal database. La generazione nel database è in genere l'approccio migliore. Per le entità Course
la chiave primaria viene specificata dall'utente. Un esempio può essere un numero di corso, quale la serie 1000 per il reparto di matematica o la serie 2000 per il reparto di lingua inglese.
L'attributo DatabaseGenerated
può essere usato anche per generare valori predefiniti. Ad esempio, il database può generare automaticamente un campo data per registrare la data di creazione o aggiornamento di una riga. Per altre informazioni, vedere Generated Properties (Proprietà generate).
Proprietà chiave esterna e di navigazione
Le proprietà chiave esterna (FK) e le proprietà di navigazione nell'entità Course
riflettono le relazioni seguenti:
Un corso viene assegnato a un solo reparto, pertanto è presente una chiave esterna DepartmentID
e una proprietà di navigazione Department
.
public int DepartmentID { get; set; }
public Department Department { get; set; }
Un corso può avere un numero qualsiasi di studenti iscritti, pertanto la proprietà di navigazione Enrollments
è una raccolta:
public ICollection<Enrollment> Enrollments { get; set; }
Un corso può essere impartito da più insegnanti, pertanto la proprietà di navigazione Instructors
è una raccolta:
public ICollection<Instructor> Instructors { get; set; }
Entità Department
Creare Models/Department.cs
con il codice seguente:
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; }
}
}
Attributo Column
In precedenza l'attributo Column
è stato usato per modificare il mapping del nome di colonna. Nel codice dell'entità Department
l'attributo Column
viene usato per modificare il mapping dei tipi di dati SQL. La colonna Budget
viene definita usando il tipo SQL Server money nel database:
[Column(TypeName="money")]
public decimal Budget { get; set; }
In genere il mapping di colonne non è necessario. EF Core sceglie il tipo di dati SQL Server appropriato in base al tipo CLR per la proprietà . Il tipo CLR decimal
esegue il mapping a un tipo SQL Server decimal
. Budget
è associato alla valuta e il tipo di dati money è più adatto per la valuta.
Proprietà chiave esterna e di navigazione
Le proprietà chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:
- Un reparto può avere o non avere un amministratore.
- Un amministratore è sempre un insegnante. Di conseguenza la proprietà
InstructorID
è inclusa come chiave esterna per l'entitàInstructor
.
La proprietà di navigazione è denominata Administrator
ma contiene un'entità Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Nel ?
codice precedente specifica che la proprietà è nullable.
Un reparto può avere molti corsi, pertanto è disponibile una proprietà di navigazione Courses:
public ICollection<Course> Courses { get; set; }
Per convenzione, EF Core abilita l'eliminazione a catena per gli FK non nullable e per le relazioni molti-a-molti. Questo comportamento predefinito può generare regole di eliminazione a catena circolari. Quando viene aggiunta una migrazione, le regole di eliminazione a catena circolari causano un'eccezione.
Ad esempio, se la Department.InstructorID
proprietà è stata definita come non nullable, EF Core configurare una regola di eliminazione a catena. In tal caso, il dipartimento verrebbe eliminato in seguito all'eliminazione dell'insegnante assegnato come amministratore. In questo scenario, una regola Restrict potrebbe essere più sensata. L'API Fluent seguente imposta una regola di limitazione e disabilita l'eliminazione a catena.
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Proprietà di registrazione chiave esterna e navigazione
Un record di iscrizione è relativo a un solo corso frequentato da un solo studente.
Aggiornare Models/Enrollment.cs
con il codice seguente:
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 Course Course { get; set; }
public Student Student { get; set; }
}
}
Le proprietà chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:
Un record di iscrizione è relativo a un solo corso, pertanto sono presenti una proprietà chiave esterna CourseID
e una proprietà di navigazione Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Un record di iscrizione è relativo a un solo studente, pertanto sono presenti una proprietà chiave esterna StudentID
e una proprietà di navigazione Student
:
public int StudentID { get; set; }
public Student Student { get; set; }
Relazioni molti-a-molti
Esiste una relazione molti-a-molti tra le entità Student
e Course
. L'entità Enrollment
funziona come una tabella di join molti-a-molti con payload nel database. Con payload si intende che la Enrollment
tabella contiene dati aggiuntivi oltre agli FK per le tabelle unite in join. Nell'entità Enrollment
, i dati aggiuntivi oltre agli FK sono pk e Grade
.
La figura seguente illustra l'aspetto di queste relazioni in un diagramma di entità. Questo diagramma è stato generato tramite EF Power Tools per EF 6.x. La creazione del diagramma non fa parte dell'esercitazione.
Ogni riga della relazione inizia con un 1 e termina con un asterisco (*), per indicare una relazione uno-a-molti.
Se la Enrollment
tabella non include informazioni sul grado, sarebbe necessario contenere solo i due FK CourseID
e StudentID
. Una tabella di join molti-a-molti senza payload è anche detta tabella di join pura (PJT, Pure Join Table).
Le Instructor
entità e Course
hanno una relazione molti-a-molti usando un PJT.
Aggiornare il contesto di database
Aggiornare Data/SchoolContext.cs
con il codice seguente:
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; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable(nameof(Course))
.HasMany(c => c.Instructors)
.WithMany(i => i.Courses);
modelBuilder.Entity<Student>().ToTable(nameof(Student));
modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
}
}
}
Il codice precedente aggiunge le nuove entità e configura la relazione molti-a-molti tra le Instructor
entità e Course
.
Alternativa API Fluent agli attributi
Il OnModelCreating
metodo nel codice precedente usa l'API Fluent per configurare EF Core il comportamento. L'API è denominata "API Fluent" perché viene spesso usata unendo una serie di chiamate di metodi in un'unica istruzione. Il codice seguente è un esempio di API Fluent:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
In questa esercitazione l'API Fluent viene usata solo per le operazioni di mapping del database che non possono essere eseguite con gli attributi. Tuttavia l'API Fluent può specificare la maggior parte delle regole di formattazione, convalida e mapping specificabili tramite gli attributi.
Alcuni attributi quali MinimumLength
non possono essere applicati con l'API Fluent. MinimumLength
non modifica lo schema, ma si limita ad applicare una regola di convalida per la lunghezza minima.
Alcuni sviluppatori preferiscono usare esclusivamente l'API Fluent in modo che possano mantenere pulite le classi di entità. È possibile combinare gli attributi e l'API Fluent. Esistono alcune configurazioni che possono essere eseguite solo con l'API Fluent, ad esempio specificando un'infrastruttura PK composita. Altre configurazioni possono essere eseguite solo con gli attributi (MinimumLength
). La procedura consigliata per l'uso dell'API Fluent o degli attributi è la seguente:
- Scegliere uno dei due approcci.
- Usare l'approccio scelto con la massima coerenza possibile.
Alcuni degli attributi usati in questa esercitazione vengono usati per:
- Solo convalida (ad esempio
MinimumLength
). - EF Core solo configurazione (ad esempio,
HasKey
). - Convalida e EF Core configurazione (ad esempio,
[StringLength(50)]
).
Per altre informazioni sul confronto tra attributi e API Fluent, vedere Metodi di configurazione.
Specificare il valore di inizializzazione del database
Aggiornare il codice in Data/DbInitializer.cs
:
using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}
var alexander = new Student
{
FirstMidName = "Carson",
LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2016-09-01")
};
var alonso = new Student
{
FirstMidName = "Meredith",
LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var anand = new Student
{
FirstMidName = "Arturo",
LastName = "Anand",
EnrollmentDate = DateTime.Parse("2019-09-01")
};
var barzdukas = new Student
{
FirstMidName = "Gytis",
LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var li = new Student
{
FirstMidName = "Yan",
LastName = "Li",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var justice = new Student
{
FirstMidName = "Peggy",
LastName = "Justice",
EnrollmentDate = DateTime.Parse("2017-09-01")
};
var norman = new Student
{
FirstMidName = "Laura",
LastName = "Norman",
EnrollmentDate = DateTime.Parse("2019-09-01")
};
var olivetto = new Student
{
FirstMidName = "Nino",
LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2011-09-01")
};
var students = new Student[]
{
alexander,
alonso,
anand,
barzdukas,
li,
justice,
norman,
olivetto
};
context.AddRange(students);
var abercrombie = new Instructor
{
FirstMidName = "Kim",
LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11")
};
var fakhouri = new Instructor
{
FirstMidName = "Fadi",
LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06")
};
var harui = new Instructor
{
FirstMidName = "Roger",
LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01")
};
var kapoor = new Instructor
{
FirstMidName = "Candace",
LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15")
};
var zheng = new Instructor
{
FirstMidName = "Roger",
LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12")
};
var instructors = new Instructor[]
{
abercrombie,
fakhouri,
harui,
kapoor,
zheng
};
context.AddRange(instructors);
var officeAssignments = new OfficeAssignment[]
{
new OfficeAssignment {
Instructor = fakhouri,
Location = "Smith 17" },
new OfficeAssignment {
Instructor = harui,
Location = "Gowan 27" },
new OfficeAssignment {
Instructor = kapoor,
Location = "Thompson 304" }
};
context.AddRange(officeAssignments);
var english = new Department
{
Name = "English",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = abercrombie
};
var mathematics = new Department
{
Name = "Mathematics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = fakhouri
};
var engineering = new Department
{
Name = "Engineering",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = harui
};
var economics = new Department
{
Name = "Economics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = kapoor
};
var departments = new Department[]
{
english,
mathematics,
engineering,
economics
};
context.AddRange(departments);
var chemistry = new Course
{
CourseID = 1050,
Title = "Chemistry",
Credits = 3,
Department = engineering,
Instructors = new List<Instructor> { kapoor, harui }
};
var microeconomics = new Course
{
CourseID = 4022,
Title = "Microeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};
var macroeconmics = new Course
{
CourseID = 4041,
Title = "Macroeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};
var calculus = new Course
{
CourseID = 1045,
Title = "Calculus",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { fakhouri }
};
var trigonometry = new Course
{
CourseID = 3141,
Title = "Trigonometry",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { harui }
};
var composition = new Course
{
CourseID = 2021,
Title = "Composition",
Credits = 3,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};
var literature = new Course
{
CourseID = 2042,
Title = "Literature",
Credits = 4,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};
var courses = new Course[]
{
chemistry,
microeconomics,
macroeconmics,
calculus,
trigonometry,
composition,
literature
};
context.AddRange(courses);
var enrollments = new Enrollment[]
{
new Enrollment {
Student = alexander,
Course = chemistry,
Grade = Grade.A
},
new Enrollment {
Student = alexander,
Course = microeconomics,
Grade = Grade.C
},
new Enrollment {
Student = alexander,
Course = macroeconmics,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = calculus,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = trigonometry,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = anand,
Course = chemistry
},
new Enrollment {
Student = anand,
Course = microeconomics,
Grade = Grade.B
},
new Enrollment {
Student = barzdukas,
Course = chemistry,
Grade = Grade.B
},
new Enrollment {
Student = li,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = justice,
Course = literature,
Grade = Grade.B
}
};
context.AddRange(enrollments);
context.SaveChanges();
}
}
}
Il codice precedente offre i dati di inizializzazione per le nuove entità. La maggior parte di questo codice crea nuovi oggetti entità e carica dati di esempio. I dati di esempio vengono usati per i test.
Applicare la migrazione o eliminare e ricreare
Con il database esistente, esistono due approcci per modificare il database:
- Eliminare e ricreare il database. Scegliere questa sezione quando si usa SQLite.
- Applicare la migrazione al database esistente. Le istruzioni riportate in questa sezione funzionano solo per SQL Server e non per SQLite.
Entrambe le scelte funzionano per SQL Server. Anche se il metodo che prevede l'applicazione della migrazione è più complesso e richiede più tempo, si tratta dell'approccio consigliato per gli ambienti di produzione reali.
Eliminare e ricreare il database
Per forzare EF Core la creazione di un nuovo database, eliminare e aggiornare il database:
- Eliminare la cartella Migrations.
- Nella console Gestione pacchetti eseguire i comandi seguenti:
Drop-Database
Add-Migration InitialCreate
Update-Database
Eseguire l'app. Quando si esegue l'app viene eseguito il metodo DbInitializer.Initialize
. DbInitializer.Initialize
popola il nuovo database.
Aprire il database in SSOX:
- Se SSOX è stato aperto in precedenza, fare clic sul pulsante Aggiorna.
- Espandere il nodo Tabelle. Vengono visualizzate le tabelle create.
Passaggi successivi
Nelle due esercitazioni successive viene illustrato come leggere e aggiornare i dati correlati.
Nelle esercitazioni precedenti è stato usato un modello di dati semplice costituito da tre entità. Contenuto dell'esercitazione:
- Vengono aggiunte altre entità e relazioni.
- Il modello di dati viene personalizzato specificando regole di formattazione, convalida e mapping del database.
Il modello di dati completato è illustrato nella figura seguente:
Entità Student (Studente)
Sostituire il codice in Models/Student.cs
con il codice seguente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Il codice precedente aggiunge una proprietà FullName
e aggiunge gli attributi seguenti alle proprietà esistenti:
[DataType]
[DisplayFormat]
[StringLength]
[Column]
[Required]
[Display]
Proprietà calcolata FullName
FullName
è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. FullName
non può essere impostata, quindi include solo una funzione di accesso get. Nel database non viene creata una colonna FullName
.
Attributo DataType
[DataType(DataType.Date)]
Per le date di iscrizione degli studenti, tutte le pagine visualizzano attualmente l'ora del giorno insieme alla data, anche se è pertinente solo la data. Mediante gli attributi di annotazione dei dati è possibile modificare il codice per correggere il formato di visualizzazione in tutte le pagine che visualizzano i dati.
L'attributo DataType indica un tipo di dati più specifico rispetto al tipo intrinseco del database. In questo caso deve essere visualizzata solo la data e non la data e l'ora. L'enumerazione DataType fornisce molti tipi di dati, ad esempio Data, Ora, PhoneNumber, Valuta, EmailAddress e così via. L'attributo DataType
può anche consentire all'app di fornire automaticamente funzionalità specifiche del tipo. Ad esempio:
- Il collegamento
mailto:
viene creato automaticamente perDataType.EmailAddress
. - Il selettore data viene incluso per
DataType.Date
nella maggior parte dei browser.
L'attributo DataType
genera attributi HTML 5 data-
. Gli attributi DataType
non garantiscono la convalida.
Attributo DisplayFormat
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
DataType.Date
non specifica il formato della data visualizzata. Per impostazione predefinita il campo data viene visualizzato in base ai formati predefiniti per il valore CultureInfo del server.
L'attributo DisplayFormat
viene usato per specificare in modo esplicito il formato della data. L'impostazione ApplyFormatInEditMode
specifica che la formattazione deve essere applicata anche all'interfaccia utente di modifica. Alcuni campi non devono usare ApplyFormatInEditMode
. Ad esempio il simbolo di valuta in genere non deve essere visualizzato in una casella di testo di modifica.
L'attributo DisplayFormat
può essere usato da solo. In genere l'uso dell'attributo DataType
con l'attributo DisplayFormat
è consigliato. L'attributo DataType
offre la semantica dei dati anziché specificarne il rendering in una schermata. L'attributo DataType
offre i vantaggi seguenti che non sono disponibili in DisplayFormat
:
- Il browser può abilitare le funzionalità HTML5. Ad esempio può visualizzare un controllo di calendario, il simbolo della valuta appropriato per le impostazioni locali, i collegamenti alla posta elettronica e la convalida dell'input sul lato client.
- Per impostazione predefinita, il browser esegue il rendering dei dati usando il formato corretto in base alle impostazioni locali.
Per altre informazioni, vedere la documentazione dell'helper< tag di input>.
Attributo StringLength
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
È possibile specificare regole di convalida dei dati e messaggi di errore di convalida usando gli attributi. L'attributo StringLength specifica il numero minimo e massimo di caratteri consentiti in un campo dati. Il codice precedente limita i nomi a un massimo di 50 caratteri. Un esempio che imposta la lunghezza minima della stringa è disponibile di seguito.
L'attributo StringLength
offre anche la convalida lato client e lato server. Il valore minimo non ha alcun effetto sullo schema del database.
L'attributo StringLength
non impedisce a un utente di immettere spazi vuoti per un nome. L'attributo RegularExpression può essere usato per applicare restrizioni all'input. Ad esempio il codice seguente richiede che il primo carattere sia maiuscolo e i caratteri rimanenti siano caratteri alfabetici:
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
In Esplora oggetti di SQL Server (SSOX) aprire il designer della tabella Student (Studente) facendo doppio clic sulla tabella.
L'immagine precedente visualizza lo schema per la tabella Student
. I campi del nome sono di tipo nvarchar(MAX)
. Quando una migrazione viene creata e applicata più avanti in questa esercitazione, i campi del nome diventano nvarchar(50)
come risultato degli attributi di lunghezza della stringa.
Attributo Column
[Column("FirstName")]
public string FirstMidName { get; set; }
Gli attributi possono controllare il mapping delle classi e delle proprietà nel database. Nel modello Student
l'attributo Column
viene usato per il mapping del nome della proprietà FirstMidName
su "FirstName" nel database.
Quando viene creato il database, i nomi delle proprietà nel modello vengono usati per i nomi di colonna (tranne quando viene usato l'attributo Column
). Il modello Student
usa il nome FirstMidName
per il campo first-name (Nome) perché il campo potrebbe contenere anche un secondo nome.
Con l'attributo [Column]
, per Student.FirstMidName
nel modello di dati viene eseguito il mapping alla colonna FirstName
della tabella Student
. L'aggiunta dell'attributo Column
modifica il modello che supporta SchoolContext
. Il modello che supporta SchoolContext
non corrisponde più al database. Questa discrepanza verrà risolta aggiungendo una migrazione più avanti in questa esercitazione.
Attributo Required
[Required]
L'attributo Required
rende obbligatori i campi delle proprietà del nome. L'attributo Required
non è necessario per i tipi che non ammettono valori Null come i tipi valore (ad esempio, DateTime
, int
e double
). I tipi che non possono essere null vengono considerati automaticamente come campi obbligatori.
L'attributo Required
deve essere usato con MinimumLength
per l'applicazione di MinimumLength
.
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
e Required
consentono spazi vuoti per soddisfare la convalida. Usare l'attributo RegularExpression
per il controllo completo sulla stringa.
Attributo Display
[Display(Name = "Last Name")]
L'attributo Display
specifica che la didascalia delle caselle di testo deve essere "First Name", "Last Name", "Full Name" e "Enrollment Date". Le didascalie predefinite non hanno spazio che divide le parole, ad esempio "Lastname".
Creare una migrazione
Eseguire l'app e passare alla pagina Students (Studenti). Viene generata un'eccezione. Con l'attributo [Column]
EF si aspetta di trovare una colonna denominata FirstName
, ma il nome della colonna nel database è ancora FirstMidName
.
Il messaggio di errore è simile al seguente:
SqlException: Invalid column name 'FirstName'.
Nella console di Gestione pacchetti immettere i comandi seguenti per creare una nuova migrazione e aggiornare il database:
Add-Migration ColumnFirstName Update-Database
Il primo di questi comandi genera il messaggio di avviso seguente:
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
L'avviso viene generato perché i campi nome ora sono limitati a 50 caratteri. Se un nome nel database ha più di 50 caratteri, i caratteri dal 51 all'ultimo andranno perduti.
Aprire la tabella Student (Studente) in SSOX:
Prima dell'applicazione della migrazione, le colonne del nome erano di tipo nvarchar(MAX). Ora le colonne del nome sono di tipo
nvarchar(50)
. Il nome della colonna è cambiato daFirstMidName
aFirstName
.
- Eseguire l'app e passare alla pagina Students (Studenti).
- Si noti che l'ora non viene inclusa nell'input o visualizzata insieme alla data.
- Selezionare Create New (Crea nuovo) e provare immettere un nome di lunghezza superiore a 50 caratteri.
Nota
Nelle sezioni seguenti la compilazione dell'app genera errori del compilatore in alcune fasi. Le istruzioni specificano quando compilare l'applicazione.
Entità Instructor
Creare Models/Instructor.cs
con il codice seguente:
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; }
}
}
Un'unica riga può ospitare più attributi. Gli attributi HireDate
possono essere scritti come segue:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Proprietà di navigazione
Le proprietà CourseAssignments
e OfficeAssignment
sono proprietà di navigazione.
Un insegnante può tenere un numero qualsiasi di corsi, pertanto CourseAssignments
è definita come raccolta.
public ICollection<CourseAssignment> CourseAssignments { get; set; }
Un insegnante può avere al massimo un ufficio, quindi la proprietà OfficeAssignment
contiene una singola entità OfficeAssignment
. OfficeAssignment
è null se non è assegnato nessun ufficio.
public OfficeAssignment OfficeAssignment { get; set; }
Entità OfficeAssignment
Creare Models/OfficeAssignment.cs
con il codice seguente:
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; }
}
}
Attributo Key
L'attributo [Key]
viene usato per identificare una proprietà come chiave primaria (PK, Primary Key) quando il nome della proprietà è diverso da classnameID o ID.
È una relazione uno-a-zero-o-uno tra le entità Instructor
e OfficeAssignment
. L'assegnazione di un ufficio esiste solo in relazione all'insegnante al quale viene assegnato l'ufficio. La chiave primaria OfficeAssignment
è anche la chiave esterna (FK, Foreign Key) per l'entità Instructor
.
EF Core non può riconoscere InstructorID
automaticamente come pk of OfficeAssignment
perché InstructorID
non segue la convenzione di denominazione ID o classnameID. Di conseguenza l'attributo Key
viene usato per identificare l'entità InstructorID
come chiave primaria:
[Key]
public int InstructorID { get; set; }
Per impostazione predefinita, EF Core considera la chiave come non generata dal database perché la colonna è per una relazione di identificazione.
Proprietà di navigazione Instructor
La proprietà di navigazione Instructor.OfficeAssignment
può essere Null perché potrebbe non essere presente una riga OfficeAssignment
per un determinato insegnante. Un insegnante potrebbe non avere un ufficio assegnato.
La proprietà di navigazione OfficeAssignment.Instructor
avrà sempre un'entità Instructor perché il tipo InstructorID
della chiave esterna è int
, ovvero un tipo valore che non ammette valori Null. Un'assegnazione di ufficio non può esistere senza un insegnante.
Quando un'entità Instructor
dispone di un'entità OfficeAssignment
correlata, ogni entità include un riferimento all'altra entità nella relativa proprietà di navigazione.
Entità Course
Aggiornare Models/Course.cs
con il codice seguente:
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; }
}
}
L'entità Course
dispone di una proprietà chiave esterna (FK) DepartmentID
. DepartmentID
fa riferimento all'entità Department
correlata. L'entità Course
dispone di una proprietà di navigazione Department
.
EF Core non richiede una proprietà di chiave esterna per un modello di dati quando il modello ha una proprietà di navigazione per un'entità correlata. EF Core crea automaticamente IK nel database ovunque siano necessari. EF Core crea proprietà shadow per gli FK creati automaticamente. Includere la chiave esterna in modo esplicito nel modello di dati, tuttavia, può rendere più semplici ed efficienti gli aggiornamenti. Si consideri ad esempio un modello in cui la proprietà chiave esterna DepartmentID
non è inclusa. Quando un'entità Course viene recuperata per la modifica:
- La proprietà
Department
è Null se non viene caricata in modo esplicito. - Per aggiornare l'entità Course, è in primo luogo necessario recuperare l'entità
Department
.
Quando la proprietà chiave esterna DepartmentID
è inclusa nel modello di dati, non è necessario recuperare l'entità Department
prima di un aggiornamento.
Attributo DatabaseGenerated
L'attributo [DatabaseGenerated(DatabaseGeneratedOption.None)]
indica che la chiave primaria viene resa disponibile dall'applicazione anziché essere generata dal database.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Per impostazione predefinita, EF Core si presuppone che i valori PK vengano generati dal database. La generazione nel database è in genere l'approccio migliore. Per le entità Course
la chiave primaria viene specificata dall'utente. Un esempio può essere un numero di corso, quale la serie 1000 per il reparto di matematica o la serie 2000 per il reparto di lingua inglese.
L'attributo DatabaseGenerated
può essere usato anche per generare valori predefiniti. Ad esempio, il database può generare automaticamente un campo data per registrare la data di creazione o aggiornamento di una riga. Per altre informazioni, vedere Generated Properties (Proprietà generate).
Proprietà chiave esterna e di navigazione
Le proprietà chiave esterna (FK) e le proprietà di navigazione nell'entità Course
riflettono le relazioni seguenti:
Un corso viene assegnato a un solo reparto, pertanto è presente una chiave esterna DepartmentID
e una proprietà di navigazione Department
.
public int DepartmentID { get; set; }
public Department Department { get; set; }
Un corso può avere un numero qualsiasi di studenti iscritti, pertanto la proprietà di navigazione Enrollments
è una raccolta:
public ICollection<Enrollment> Enrollments { get; set; }
Un corso può essere impartito da più insegnanti, pertanto la proprietà di navigazione CourseAssignments
è una raccolta:
public ICollection<CourseAssignment> CourseAssignments { get; set; }
CourseAssignment
viene illustrato più avanti.
Entità Department
Creare Models/Department.cs
con il codice seguente:
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; }
}
}
Attributo Column
In precedenza l'attributo Column
è stato usato per modificare il mapping del nome di colonna. Nel codice dell'entità Department
l'attributo Column
viene usato per modificare il mapping dei tipi di dati SQL. La colonna Budget
viene definita usando il tipo SQL Server money nel database:
[Column(TypeName="money")]
public decimal Budget { get; set; }
In genere il mapping di colonne non è necessario. EF Core sceglie il tipo di dati SQL Server appropriato in base al tipo CLR per la proprietà . Il tipo CLR decimal
esegue il mapping a un tipo SQL Server decimal
. Budget
è associato alla valuta e il tipo di dati money è più adatto per la valuta.
Proprietà chiave esterna e di navigazione
Le proprietà chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:
- Un reparto può avere o non avere un amministratore.
- Un amministratore è sempre un insegnante. Di conseguenza la proprietà
InstructorID
è inclusa come chiave esterna per l'entitàInstructor
.
La proprietà di navigazione è denominata Administrator
ma contiene un'entità Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Il punto interrogativo (?) nel codice precedente specifica che la proprietà è nullable.
Un reparto può avere molti corsi, pertanto è disponibile una proprietà di navigazione Courses:
public ICollection<Course> Courses { get; set; }
Per convenzione, EF Core abilita l'eliminazione a catena per gli FK non nullable e per le relazioni molti-a-molti. Questo comportamento predefinito può generare regole di eliminazione a catena circolari. Quando viene aggiunta una migrazione, le regole di eliminazione a catena circolari causano un'eccezione.
Ad esempio, se la Department.InstructorID
proprietà è stata definita come non nullable, EF Core configurare una regola di eliminazione a catena. In tal caso, il dipartimento verrebbe eliminato in seguito all'eliminazione dell'insegnante assegnato come amministratore. In questo scenario, una regola Restrict potrebbe essere più sensata. L'API Fluent seguente imposta una regola di limitazione e disabilita l'eliminazione a catena.
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Entità Enrollment (Iscrizione)
Un record di iscrizione è relativo a un solo corso frequentato da un solo studente.
Aggiornare Models/Enrollment.cs
con il codice seguente:
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; }
}
}
Proprietà chiave esterna e di navigazione
Le proprietà chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:
Un record di iscrizione è relativo a un solo corso, pertanto sono presenti una proprietà chiave esterna CourseID
e una proprietà di navigazione Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Un record di iscrizione è relativo a un solo studente, pertanto sono presenti una proprietà chiave esterna StudentID
e una proprietà di navigazione Student
:
public int StudentID { get; set; }
public Student Student { get; set; }
Relazioni molti-a-molti
Esiste una relazione molti-a-molti tra le entità Student
e Course
. L'entità Enrollment
funziona come una tabella di join molti-a-molti con payload nel database. "Con payload" significa che la tabella Enrollment
contiene dati aggiuntivi oltre alle chiavi esterne delle tabelle di join (in questo caso la chiave primaria e Grade
).
La figura seguente illustra l'aspetto di queste relazioni in un diagramma di entità. Questo diagramma è stato generato tramite EF Power Tools per EF 6.x. La creazione del diagramma non fa parte dell'esercitazione.
Ogni riga della relazione inizia con un 1 e termina con un asterisco (*), per indicare una relazione uno-a-molti.
Se la tabella Enrollment
non include informazioni sul livello, è sufficiente che contenga le due chiavi esterne CourseID
e StudentID
. Una tabella di join molti-a-molti senza payload è anche detta tabella di join pura (PJT, Pure Join Table).
Le entità Instructor
e Course
hanno una relazione molti-a-molti con una tabella di join pura.
Nota: EF 6.x supporta tabelle di join implicite per relazioni molti-a-molti, ma EF Core non. Per altre informazioni, vedere Relazioni molti-a-molti nella EF Core versione 2.0.
Entità CourseAssignment
Creare Models/CourseAssignment.cs
con il codice seguente:
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; }
}
}
La relazione molti-a-molti tra insegnanti e corsi richiede una tabella di join e l'entità per tale tabella di join è CourseAssignment.
È pratica comune assegnare a un'entità di join un nome EntityName1EntityName2
. Ad esempio la tabella di join istruttori-corsi che usa questa convenzione sarebbe CourseInstructor
. È tuttavia consigliabile usare un nome che descrive la relazione.
I modelli di dati iniziano come strutture semplici, quindi le loro dimensioni aumentano. Le tabelle di join senza payload (PJT) si evolvono spesso per includere il payload. Se si assegna inizialmente un nome di entità descrittivo, non sarà necessario modificarlo quando la tabella di join cambia. Idealmente l'entità di join dovrebbe avere il proprio nome naturale (se possibile composto da un'unica parola) nel dominio di business. Ad esempio Books (Documentazione) e Customers (Clienti) potrebbero essere collegati mediante un'entità di join Ratings (Valutazioni). Per la relazione molti-a-molti Instructor-to-Courses CourseAssignment
è preferibile a CourseInstructor
.
Chiave composta
Le due chiavi esterne in CourseAssignment
(InstructorID
e CourseID
) identificano insieme in modo univoco ogni riga della tabella CourseAssignment
. CourseAssignment
non richiede una chiave primaria dedicata. Le proprietà InstructorID
e CourseID
funzionano come una chiave primaria composta. L'unico modo per specificare i PK compositi da EF Core usare è l'API Fluent. La sezione successiva illustra come configurare la chiave primaria composta.
La chiave composta garantisce che:
- Sono consentite più righe per un corso.
- Sono consentite più righe per un insegnante.
- Non sono consentite più righe per lo stesso insegnante e lo stesso corso.
L'entità di join Enrollment
definisce la propria chiave primaria, pertanto sono possibili i duplicati di questo tipo. Per evitare tali duplicati:
- Aggiungere un indice univoco ai campi chiave esterna oppure
- Configurare
Enrollment
con una chiave primaria composta simile aCourseAssignment
. Per altre informazioni, vedere Indexes (Indici).
Aggiornare il contesto di database
Aggiornare Data/SchoolContext.cs
con il codice seguente:
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 });
}
}
}
Questo codice aggiunge le nuove entità e configura la chiave primaria composta dell'entità CourseAssignment
.
Alternativa API Fluent agli attributi
Il OnModelCreating
metodo nel codice precedente usa l'API Fluent per configurare EF Core il comportamento. L'API è denominata "API Fluent" perché viene spesso usata unendo una serie di chiamate di metodi in un'unica istruzione. Il codice seguente è un esempio di API Fluent:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
In questa esercitazione l'API Fluent viene usata solo per le operazioni di mapping del database che non possono essere eseguite con gli attributi. Tuttavia l'API Fluent può specificare la maggior parte delle regole di formattazione, convalida e mapping specificabili tramite gli attributi.
Alcuni attributi quali MinimumLength
non possono essere applicati con l'API Fluent. MinimumLength
non modifica lo schema, ma si limita ad applicare una regola di convalida per la lunghezza minima.
Alcuni sviluppatori preferiscono usare esclusivamente l'API Fluent in modo che possano mantenere "pulite" le classi di entità. Gli attributi e l'API Fluent possono essere misti. Alcune configurazioni possono essere eseguite solo con l'API Fluent (specificando una chiave primaria composta). Altre configurazioni possono essere eseguite solo con gli attributi (MinimumLength
). La procedura consigliata per l'uso dell'API Fluent o degli attributi è la seguente:
- Scegliere uno dei due approcci.
- Usare l'approccio scelto con la massima coerenza possibile.
Alcuni degli attributi usati in questa esercitazione vengono usati per:
- Solo convalida (ad esempio
MinimumLength
). - EF Core solo configurazione (ad esempio,
HasKey
). - Convalida e EF Core configurazione (ad esempio,
[StringLength(50)]
).
Per altre informazioni sul confronto tra attributi e API Fluent, vedere Metodi di configurazione.
Diagramma dell'entità
La figura seguente visualizza il diagramma creato da EF Power Tools per il modello School completato.
Il diagramma precedente mostra quanto segue:
- Diverse linee di relazione uno-a-molti (da 1 a *).
- La linea di relazione uno-a-zero-o-uno (da 1 a 0..1) tra le entità
Instructor
eOfficeAssignment
. - La linea di relazione zero-o-uno-a-molti (da 0..1 a *) tra le entità
Instructor
eDepartment
.
Specificare il valore di inizializzazione del database
Aggiornare il codice in Data/DbInitializer.cs
:
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("2016-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2019-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2017-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2019-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2011-09-01") }
};
context.Students.AddRange(students);
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") }
};
context.Instructors.AddRange(instructors);
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 }
};
context.Departments.AddRange(departments);
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
},
};
context.Courses.AddRange(courses);
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" },
};
context.OfficeAssignments.AddRange(officeAssignments);
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
},
};
context.CourseAssignments.AddRange(courseInstructors);
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();
}
}
}
Il codice precedente offre i dati di inizializzazione per le nuove entità. La maggior parte di questo codice crea nuovi oggetti entità e carica dati di esempio. I dati di esempio vengono usati per i test. Visualizzare Enrollments
e CourseAssignments
per alcuni esempi del modo in cui può essere impostato il valore di inizializzazione per le tabelle join molti-a-molti.
Aggiungere una migrazione
Compilare il progetto.
Nella console di Gestione pacchetti eseguire il comando seguente.
Add-Migration ComplexDataModel
Il comando precedente visualizza un avviso sulla possibile perdita di dati.
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
To undo this action, use 'ef migrations remove'
Se viene eseguito il comando database update
, viene generato l'errore seguente:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
Nella prossima sezione viene descritto come procedere per questo errore.
Applicare la migrazione o eliminare e ricreare
Ora che è disponibile un database esistente, è necessario preoccuparsi di come applicare eventuali modifiche. Questa esercitazione illustra due alternative:
- Eliminare e ricreare il database. Scegliere questa sezione se si usa SQLite.
- Applicare la migrazione al database esistente. Le istruzioni riportate in questa sezione funzionano solo per SQL Server e non per SQLite.
Entrambe le scelte funzionano per SQL Server. Anche se il metodo che prevede l'applicazione della migrazione è più complesso e richiede più tempo, si tratta dell'approccio consigliato per gli ambienti di produzione reali.
Eliminare e ricreare il database
Ignorare questa sezione se si usa SQL Server e si vuole adottare l'approccio con applicazione della migrazione nella sezione seguente.
Per forzare EF Core la creazione di un nuovo database, eliminare e aggiornare il database:
Nella console di Gestione pacchetti eseguire il comando seguente:
Drop-Database
Eliminare la cartella Migrations e quindi eseguire il comando seguente:
Add-Migration InitialCreate Update-Database
Eseguire l'app. Quando si esegue l'app viene eseguito il metodo DbInitializer.Initialize
. DbInitializer.Initialize
popola il nuovo database.
Aprire il database in SSOX:
Se SSOX è stato aperto in precedenza, fare clic sul pulsante Aggiorna.
Espandere il nodo Tabelle. Vengono visualizzate le tabelle create.
Esaminare la tabella CourseAssignment:
- Fare clic con il pulsante destro del mouse sulla tabella CourseAssignment e selezionare Visualizza dati.
- Verificare che la tabella CourseAssignment contenga dati.
Applicare la migrazione
Questa sezione è facoltativa. Questa procedura funziona solo per SQL Server Local DB e solo se è stata ignorata la sezione Eliminare e ricreare il database precedente.
Quando le migrazioni vengono eseguite con dati esistenti, possono essere presenti vincoli di chiave esterna che non vengono soddisfatti con i dati esistenti. Con i dati di produzione, è necessario eseguire passaggi per la migrazione dei dati esistenti. Questa sezione visualizza un esempio di correzione delle violazioni dei vincoli di chiave esterna. Non apportare queste modifiche al codice senza un backup. Non apportare queste modifiche al codice se è stata completata la sezione precedente Eliminare e ricreare il database.
Il {timestamp}_ComplexDataModel.cs
file contiene il codice seguente:
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
Il codice precedente aggiunge una chiave esterna non nullable DepartmentID
alla tabella Course
. Il database dell'esercitazione precedente contiene righe in Course
, pertanto la tabella non può essere aggiornata mediante le migrazioni.
Per far sì che la migrazione ComplexDataModel
funzioni con i dati esistenti:
- Modificare il codice per assegnare un valore predefinito alla nuova colonna (
DepartmentID
). - Creare un reparto fittizio denominato "Temp" che assume il ruolo di reparto predefinito.
Risolvere i vincoli della chiave esterna
Nella classe della migrazione ComplexDataModel
aggiornare il metodo Up
:
- Apri il file
{timestamp}_ComplexDataModel.cs
. - Impostare come commento la riga di codice che aggiunge la colonna
DepartmentID
alla tabellaCourse
.
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);
Aggiungere il codice evidenziato seguente. Il nuovo codice viene inserito dopo il blocco .CreateTable( name: "Department"
:
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);
Con le modifiche precedenti, le righe Course
esistenti saranno correlate al dipartimento "Temp" dopo l'esecuzione del metodo ComplexDataModel.Up
.
La modalità di gestione della situazione illustrata di seguito è semplificata per questa esercitazione. Un'app di produzione:
- Includerà codice o script per l'aggiunta di righe
Department
e righeCourse
correlate alle nuove righeDepartment
. - Non userà il reparto "Temp" o il valore predefinito per
Course.DepartmentID
.
Nella console di Gestione pacchetti eseguire il comando seguente:
Update-Database
Dato che il metodo DbInitializer.Initialize
è progettato per funzionare solo con un database vuoto, usare SSOX per eliminare tutte le righe nelle tabelle Student e Course. (L'eliminazione a catena si occuperà della tabella Enrollment.)
Eseguire l'app. Quando si esegue l'app viene eseguito il metodo DbInitializer.Initialize
. DbInitializer.Initialize
popola il nuovo database.
Passaggi successivi
Nelle due esercitazioni successive viene illustrato come leggere e aggiornare i dati correlati.
Nelle esercitazioni precedenti è stato usato un modello di dati semplice costituito da tre entità. Contenuto dell'esercitazione:
- Vengono aggiunte altre entità e relazioni.
- Il modello di dati viene personalizzato specificando regole di formattazione, convalida e mapping del database.
Le classi di entità per il modello di dati completato sono visualizzate nella figura seguente:
Se si verificano problemi che non si è in grado di risolvere, scaricare l'app completa.
Personalizzare il modello di dati usando gli attributi
In questa sezione il modello di dati viene personalizzato usando gli attributi.
Attributo DataType
Attualmente le pagine Student (Studente) visualizzano l'ora associata alla data di iscrizione. In genere i campi data visualizzano solo la data e non l'ora.
Eseguire l'aggiornamento Models/Student.cs
con il codice evidenziato seguente:
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; }
}
}
L'attributo DataType indica un tipo di dati più specifico rispetto al tipo intrinseco del database. In questo caso deve essere visualizzata solo la data e non la data e l'ora. L'enumerazione DataType fornisce molti tipi di dati, ad esempio Data, Ora, PhoneNumber, Valuta, EmailAddress e così via. L'attributo DataType
può anche consentire all'app di fornire automaticamente funzionalità specifiche del tipo. Ad esempio:
- Il collegamento
mailto:
viene creato automaticamente perDataType.EmailAddress
. - Il selettore data viene incluso per
DataType.Date
nella maggior parte dei browser.
L'attributo DataType
genera attributi HTML 5 data-
supportati dai browser HTML 5. Gli attributi DataType
non garantiscono la convalida.
DataType.Date
non specifica il formato della data visualizzata. Per impostazione predefinita il campo data viene visualizzato in base ai formati predefiniti per il valore CultureInfo del server.
L'attributo DisplayFormat
viene usato per specificare in modo esplicito il formato della data:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
L'impostazione ApplyFormatInEditMode
specifica che la formattazione deve essere applicata anche all'interfaccia utente di modifica. Alcuni campi non devono usare ApplyFormatInEditMode
. Ad esempio il simbolo di valuta in genere non deve essere visualizzato in una casella di testo di modifica.
L'attributo DisplayFormat
può essere usato da solo. In genere l'uso dell'attributo DataType
con l'attributo DisplayFormat
è consigliato. L'attributo DataType
offre la semantica dei dati anziché specificarne il rendering in una schermata. L'attributo DataType
offre i vantaggi seguenti che non sono disponibili in DisplayFormat
:
- Il browser può abilitare le funzionalità HTML5. Ad esempio può visualizzare un controllo di calendario, il simbolo della valuta appropriato per le impostazioni locali, i collegamenti alla posta elettronica, alcune istanze di convalida lato client e così via.
- Per impostazione predefinita, il browser esegue il rendering dei dati usando il formato corretto in base alle impostazioni locali.
Per altre informazioni, vedere la documentazione dell'helper< tag di input>.
Eseguire l'app. Passare alla pagina Students Index (Indice studenti). L'ora non viene più visualizzata. Ogni visualizzazione che usa il modello Student
visualizza la data senza l'ora.
Attributo StringLength
È possibile specificare regole di convalida dei dati e messaggi di errore di convalida usando gli attributi. L'attributo StringLength specifica il numero minimo e massimo di caratteri consentiti in un campo dati. L'attributo StringLength
offre anche la convalida lato client e lato server. Il valore minimo non ha alcun effetto sullo schema del database.
Aggiornare il modello Student
con il codice seguente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Il codice precedente limita i nomi a un massimo di 50 caratteri. L'attributo StringLength
non impedisce a un utente di immettere spazi vuoti per un nome. L'attributo RegularExpression viene usato per applicare restrizioni all'input. Ad esempio il codice seguente richiede che il primo carattere sia maiuscolo e i caratteri rimanenti siano caratteri alfabetici:
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
Eseguire l'app:
- Passare alla pagina Student (Studente).
- Selezionare Crea nuovo e immettere un nome di lunghezza superiore a 50 caratteri.
- Quando si fa clic su Crea la convalida lato client visualizza un messaggio di errore.
In Esplora oggetti di SQL Server (SSOX) aprire il designer della tabella Student (Studente) facendo doppio clic sulla tabella.
L'immagine precedente visualizza lo schema per la tabella Student
. I campi nome hanno il tipo nvarchar(MAX)
perché migrations non è stato eseguito nel database. Quando le istruzioni migrations verranno eseguite, più avanti in questa esercitazione, i campi nome diventeranno nvarchar(50)
.
Attributo Column
Gli attributi possono controllare il mapping delle classi e delle proprietà nel database. In questa sezione l'attributo Column
viene usato per il mapping del nome della proprietà FirstMidName
su "FirstName" nel database.
Quando viene creato il database, i nomi delle proprietà nel modello vengono usati per i nomi di colonna (tranne quando viene usato l'attributo Column
).
Il modello Student
usa il nome FirstMidName
per il campo first-name (Nome) perché il campo potrebbe contenere anche un secondo nome.
Aggiornare il Student.cs
file con il codice evidenziato seguente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Con la modifica precedente, Student.FirstMidName
nell'app esegue il mapping alla colonna FirstName
della tabella Student
.
L'aggiunta dell'attributo Column
modifica il modello che supporta SchoolContext
. Il modello che supporta SchoolContext
non corrisponde più al database. Se l'app viene eseguita prima di applicare migrations, viene generata l'eccezione seguente:
SqlException: Invalid column name 'FirstName'.
Per aggiornare il database:
- Compilare il progetto.
- Aprire una finestra di comando nella cartella di progetto. Immettere i comandi seguenti per creare una nuova migrazione e aggiornare il database:
Add-Migration ColumnFirstName
Update-Database
Il comando migrations add ColumnFirstName
genera il messaggio di avviso seguente:
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
L'avviso viene generato perché i campi nome ora sono limitati a 50 caratteri. Se un nome nel database ha più di 50 caratteri, i caratteri dal 51 all'ultimo andranno perduti.
- Test dell'app.
Aprire la tabella Student (Studente) in SSOX:
Prima dell'applicazione della migrazione, le colonne del nome erano di tipo nvarchar(MAX). Ora le colonne del nome sono di tipo nvarchar(50)
. Il nome della colonna è cambiato da FirstMidName
a FirstName
.
Nota
Nella sezione seguente la compilazione dell'applicazione genera errori del compilatore in alcune fasi. Le istruzioni specificano quando compilare l'applicazione.
Aggiornamento dell'entità Student
Aggiornare Models/Student.cs
con il codice seguente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Attributo Required
L'attributo Required
rende obbligatori i campi delle proprietà del nome. L'attributo Required
non è necessario per i tipi non nullable, ad esempio per i tipi valore (DateTime
, int
, double
e così via). I tipi che non possono essere null vengono considerati automaticamente come campi obbligatori.
L'attributo Required
può essere sostituito con un parametro di lunghezza minima nell'attributo StringLength
:
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
Attributo Display
L'attributo Display
specifica che la didascalia delle caselle di testo deve essere "First Name", "Last Name", "Full Name" e "Enrollment Date". Le didascalie predefinite non hanno spazio che divide le parole, ad esempio "Lastname".
Proprietà calcolata FullName
FullName
è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. FullName
non è impostabile e include solo una funzione di accesso get. Nel database non viene creata una colonna FullName
.
Creare l'entità Instructor
Creare Models/Instructor.cs
con il codice seguente:
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; }
}
}
Un'unica riga può ospitare più attributi. Gli attributi HireDate
possono essere scritti come segue:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Proprietà di navigazione CourseAssignments e OfficeAssignment
Le proprietà CourseAssignments
e OfficeAssignment
sono proprietà di navigazione.
Un insegnante può tenere un numero qualsiasi di corsi, pertanto CourseAssignments
è definita come raccolta.
public ICollection<CourseAssignment> CourseAssignments { get; set; }
Se una proprietà di navigazione contiene più entità:
- Deve essere un tipo di elenco in cui le voci possono essere aggiunte, eliminate e aggiornate.
I tipi di proprietà di navigazione includono:
ICollection<T>
List<T>
HashSet<T>
Se ICollection<T>
viene specificato, EF Core crea una HashSet<T>
raccolta per impostazione predefinita.
L'entità CourseAssignment
è illustrata nella sezione sulle relazioni molti-a-molti.
Le regole business di Contoso University specificano che un insegnante non può avere più di un ufficio. La proprietà OfficeAssignment
contiene un'unica entità OfficeAssignment
. OfficeAssignment
è null se non è assegnato nessun ufficio.
public OfficeAssignment OfficeAssignment { get; set; }
Creare l'entità OfficeAssignment
Creare Models/OfficeAssignment.cs
con il codice seguente:
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; }
}
}
Attributo Key
L'attributo [Key]
viene usato per identificare una proprietà come chiave primaria (PK, Primary Key) quando il nome della proprietà è diverso da classnameID o ID.
È una relazione uno-a-zero-o-uno tra le entità Instructor
e OfficeAssignment
. L'assegnazione di un ufficio esiste solo in relazione all'insegnante al quale viene assegnato l'ufficio. La chiave primaria OfficeAssignment
è anche la chiave esterna (FK, Foreign Key) per l'entità Instructor
. EF Core non è in grado di riconoscere InstructorID
automaticamente l'infrastruttura a chiave pubblica di OfficeAssignment
perché:
InstructorID
non segue la convenzione di denominazione ID o classnameID.
Di conseguenza l'attributo Key
viene usato per identificare l'entità InstructorID
come chiave primaria:
[Key]
public int InstructorID { get; set; }
Per impostazione predefinita, EF Core considera la chiave come non generata dal database perché la colonna è per una relazione di identificazione.
Proprietà di navigazione Instructor
La proprietà di navigazione OfficeAssignment
per l'entità Instructor
è nullable perché:
- I tipi di riferimento (ad esempio le classi) sono nullable.
- Un insegnante potrebbe non avere un ufficio assegnato.
L'entità OfficeAssignment
ha una proprietà di navigazione Instructor
non nullable perché:
InstructorID
è non nullable.- Un'assegnazione di ufficio non può esistere senza un insegnante.
Quando un'entità Instructor
dispone di un'entità OfficeAssignment
correlata, ogni entità include un riferimento all'altra entità nella relativa proprietà di navigazione.
L'attributo [Required]
può essere applicato alla proprietà di navigazione Instructor
:
[Required]
public Instructor Instructor { get; set; }
Il codice precedente specifica che deve essere presente un insegnante correlato. Il codice precedente non è necessario perché la chiave esterna InstructorID
(che è anche la chiave primaria) è non nullable.
Modificare l'entità Course
Aggiornare Models/Course.cs
con il codice seguente:
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; }
}
}
L'entità Course
dispone di una proprietà chiave esterna (FK) DepartmentID
. DepartmentID
fa riferimento all'entità Department
correlata. L'entità Course
dispone di una proprietà di navigazione Department
.
EF Core non richiede una proprietà FK per un modello di dati quando il modello ha una proprietà di navigazione per un'entità correlata.
EF Core crea automaticamente IK nel database ovunque siano necessari. EF Core crea proprietà shadow per gli FK creati automaticamente. Il fatto di avere la chiave esterna nel modello di dati può rendere più semplici ed efficienti gli aggiornamenti. Si consideri ad esempio un modello in cui la proprietà chiave esterna DepartmentID
non è inclusa. Quando un'entità Course viene recuperata per la modifica:
- L'entità
Department
è null se non viene caricata in modo esplicito. - Per aggiornare l'entità Course, è in primo luogo necessario recuperare l'entità
Department
.
Quando la proprietà chiave esterna DepartmentID
è inclusa nel modello di dati, non è necessario recuperare l'entità Department
prima di un aggiornamento.
Attributo DatabaseGenerated
L'attributo [DatabaseGenerated(DatabaseGeneratedOption.None)]
indica che la chiave primaria viene resa disponibile dall'applicazione anziché essere generata dal database.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Per impostazione predefinita, EF Core si presuppone che i valori PK vengano generati dal database. La generazione dei valori di chiave primaria nel database è in genere l'approccio migliore. Per le entità Course
la chiave primaria viene specificata dall'utente. Un esempio può essere un numero di corso, quale la serie 1000 per il reparto di matematica o la serie 2000 per il reparto di lingua inglese.
L'attributo DatabaseGenerated
può essere usato anche per generare valori predefiniti. Ad esempio il database può generare automaticamente un campo data per registrare la data di creazione o aggiornamento di una riga. Per altre informazioni, vedere Generated Properties (Proprietà generate).
Proprietà chiave esterna e di navigazione
Le proprietà chiave esterna (FK) e le proprietà di navigazione nell'entità Course
riflettono le relazioni seguenti:
Un corso viene assegnato a un solo reparto, pertanto è presente una chiave esterna DepartmentID
e una proprietà di navigazione Department
.
public int DepartmentID { get; set; }
public Department Department { get; set; }
Un corso può avere un numero qualsiasi di studenti iscritti, pertanto la proprietà di navigazione Enrollments
è una raccolta:
public ICollection<Enrollment> Enrollments { get; set; }
Un corso può essere impartito da più insegnanti, pertanto la proprietà di navigazione CourseAssignments
è una raccolta:
public ICollection<CourseAssignment> CourseAssignments { get; set; }
CourseAssignment
viene illustrato più avanti.
Creare l'entità Department
Creare Models/Department.cs
con il codice seguente:
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; }
}
}
Attributo Column
In precedenza l'attributo Column
è stato usato per modificare il mapping del nome di colonna. Nel codice dell'entità Department
l'attributo Column
viene usato per modificare il mapping dei tipi di dati SQL. La colonna Budget
viene definita usando il tipo SQL Server money nel database:
[Column(TypeName="money")]
public decimal Budget { get; set; }
In genere il mapping di colonne non è necessario. EF Core in genere sceglie il tipo di dati DI SQL Server appropriato in base al tipo CLR per la proprietà . Il tipo CLR decimal
esegue il mapping a un tipo SQL Server decimal
. Budget
è associato alla valuta e il tipo di dati money è più adatto per la valuta.
Proprietà chiave esterna e di navigazione
Le proprietà chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:
- Un reparto può avere o non avere un amministratore.
- Un amministratore è sempre un insegnante. Di conseguenza la proprietà
InstructorID
è inclusa come chiave esterna per l'entitàInstructor
.
La proprietà di navigazione è denominata Administrator
ma contiene un'entità Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Il punto interrogativo (?) nel codice precedente specifica che la proprietà è nullable.
Un reparto può avere molti corsi, pertanto è disponibile una proprietà di navigazione Courses:
public ICollection<Course> Courses { get; set; }
Nota: per convenzione, EF Core abilita l'eliminazione a catena per gli FK non nullable e per le relazioni molti-a-molti. L'eliminazione a catena può generare regole di eliminazione a catena circolari. Quando viene aggiunta una migrazione, le regole di eliminazione a catena circolari determinano un'eccezione.
Ad esempio, se la proprietà Department.InstructorID
è stata definita come nullable:
EF Core configura una regola di eliminazione a catena per eliminare il reparto quando l'insegnante viene eliminato.
L'eliminazione del reparto quando viene eliminato l'insegnante non è il comportamento previsto.
L'API Fluent seguente imposta una regola di limitazione anziché a catena.
modelBuilder.Entity<Department>() .HasOne(d => d.Administrator) .WithMany() .OnDelete(DeleteBehavior.Restrict)
Il codice precedente disabilita l'eliminazione a catena per la relazione reparto-insegnante.
Aggiornare l'entità Enrollment
Un record di iscrizione è relativo a un solo corso frequentato da un solo studente.
Aggiornare Models/Enrollment.cs
con il codice seguente:
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; }
}
}
Proprietà chiave esterna e di navigazione
Le proprietà chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:
Un record di iscrizione è relativo a un solo corso, pertanto sono presenti una proprietà chiave esterna CourseID
e una proprietà di navigazione Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Un record di iscrizione è relativo a un solo studente, pertanto sono presenti una proprietà chiave esterna StudentID
e una proprietà di navigazione Student
:
public int StudentID { get; set; }
public Student Student { get; set; }
Relazioni molti-a-molti
Esiste una relazione molti-a-molti tra le entità Student
e Course
. L'entità Enrollment
funziona come una tabella di join molti-a-molti con payload nel database. "Con payload" significa che la tabella Enrollment
contiene dati aggiuntivi oltre alle chiavi esterne delle tabelle di join (in questo caso la chiave primaria e Grade
).
La figura seguente illustra l'aspetto di queste relazioni in un diagramma di entità. Questo diagramma è stato generato tramite EF Power Tools per EF 6.x. La creazione del diagramma non fa parte dell'esercitazione.
Ogni riga della relazione inizia con un 1 e termina con un asterisco (*), per indicare una relazione uno-a-molti.
Se la tabella Enrollment
non include informazioni sul livello, è sufficiente che contenga le due chiavi esterne CourseID
e StudentID
. Una tabella di join molti-a-molti senza payload è anche detta tabella di join pura (PJT, Pure Join Table).
Le entità Instructor
e Course
hanno una relazione molti-a-molti con una tabella di join pura.
Nota: EF 6.x supporta tabelle di join implicite per relazioni molti-a-molti, ma EF Core non. Per altre informazioni, vedere Relazioni molti-a-molti nella EF Core versione 2.0.
Entità CourseAssignment
Creare Models/CourseAssignment.cs
con il codice seguente:
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; }
}
}
Instructor-to-Courses
La relazione molti-a-molti Instructor-to-Courses (Insegnante-Corsi):
- Richiede una tabella di join che deve essere rappresentata da un set di entità.
- È una tabella di join pura (tabella senza payload).
È pratica comune assegnare a un'entità di join un nome EntityName1EntityName2
. Ad esempio la tabella di join Instructor-to-Courses che usa questa convenzione sarà CourseInstructor
. È tuttavia consigliabile usare un nome che descrive la relazione.
I modelli di dati iniziano come strutture semplici, quindi le loro dimensioni aumentano. In molti casi ai join senza payload vengono assegnati payload in un secondo momento. Se si assegna inizialmente un nome di entità descrittivo, non sarà necessario modificarlo quando la tabella di join cambia. Idealmente l'entità di join dovrebbe avere il proprio nome naturale (se possibile composto da un'unica parola) nel dominio di business. Ad esempio Books (Documentazione) e Customers (Clienti) potrebbero essere collegati mediante un'entità di join Ratings (Valutazioni). Per la relazione molti-a-molti Instructor-to-Courses CourseAssignment
è preferibile a CourseInstructor
.
Chiave composta
Le chiavi esterne non sono nullable. Le due chiavi esterne in CourseAssignment
(InstructorID
e CourseID
) identificano insieme in modo univoco ogni riga della tabella CourseAssignment
. CourseAssignment
non richiede una chiave primaria dedicata. Le proprietà InstructorID
e CourseID
funzionano come una chiave primaria composta. L'unico modo per specificare i PK compositi da EF Core usare è l'API Fluent. La sezione successiva illustra come configurare la chiave primaria composta.
La chiave composta garantisce quanto segue:
- Sono consentite più righe per un corso.
- Sono consentite più righe per un insegnante.
- Non sono consentite più righe per lo stesso insegnante e lo stesso corso.
L'entità di join Enrollment
definisce la propria chiave primaria, pertanto sono possibili i duplicati di questo tipo. Per evitare tali duplicati:
- Aggiungere un indice univoco ai campi chiave esterna oppure
- Configurare
Enrollment
con una chiave primaria composta simile aCourseAssignment
. Per altre informazioni, vedere Indexes (Indici).
Aggiornare il contesto del database
Aggiungere il codice evidenziato seguente a Data/SchoolContext.cs
:
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollment { get; set; }
public DbSet<Student> Student { 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 });
}
}
}
Questo codice aggiunge le nuove entità e configura la chiave primaria composta dell'entità CourseAssignment
.
Alternativa API Fluent agli attributi
Il OnModelCreating
metodo nel codice precedente usa l'API Fluent per configurare EF Core il comportamento. L'API è denominata "API Fluent" perché viene spesso usata unendo una serie di chiamate di metodi in un'unica istruzione. Il codice seguente è un esempio di API Fluent:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
In questa esercitazione l'API Fluent viene usata solo per le operazioni di mapping del database che non possono essere eseguite con gli attributi. Tuttavia l'API Fluent può specificare la maggior parte delle regole di formattazione, convalida e mapping specificabili tramite gli attributi.
Alcuni attributi quali MinimumLength
non possono essere applicati con l'API Fluent. MinimumLength
non modifica lo schema, ma si limita ad applicare una regola di convalida per la lunghezza minima.
Alcuni sviluppatori preferiscono usare esclusivamente l'API Fluent in modo che possano mantenere "pulite" le classi di entità. Gli attributi e l'API Fluent possono essere misti. Alcune configurazioni possono essere eseguite solo con l'API Fluent (specificando una chiave primaria composta). Altre configurazioni possono essere eseguite solo con gli attributi (MinimumLength
). La procedura consigliata per l'uso dell'API Fluent o degli attributi è la seguente:
- Scegliere uno dei due approcci.
- Usare l'approccio scelto con la massima coerenza possibile.
Alcuni attributi di questa esercitazione vengono usati per:
- Solo convalida (ad esempio
MinimumLength
). - EF Core solo configurazione (ad esempio,
HasKey
). - Convalida e EF Core configurazione (ad esempio,
[StringLength(50)]
).
Per altre informazioni sul confronto tra attributi e API Fluent, vedere Metodi di configurazione.
Diagramma dell'entità che visualizza le relazioni
La figura seguente visualizza il diagramma creato da EF Power Tools per il modello School completato.
Il diagramma precedente mostra quanto segue:
- Diverse linee di relazione uno-a-molti (da 1 a *).
- La linea di relazione uno-a-zero-o-uno (da 1 a 0..1) tra le entità
Instructor
eOfficeAssignment
. - La linea di relazione zero-o-uno-a-molti (da 0..1 a *) tra le entità
Instructor
eDepartment
.
Inizializzare il database con dati di test
Aggiornare il codice in Data/DbInitializer.cs
:
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.Student.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.Student.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.Enrollment.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollment.Add(e);
}
}
context.SaveChanges();
}
}
}
Il codice precedente offre i dati di inizializzazione per le nuove entità. La maggior parte di questo codice crea nuovi oggetti entità e carica dati di esempio. I dati di esempio vengono usati per i test. Visualizzare Enrollments
e CourseAssignments
per alcuni esempi del modo in cui può essere impostato il valore di inizializzazione per le tabelle join molti-a-molti.
Aggiungere una migrazione
Compilare il progetto.
Add-Migration ComplexDataModel
Il comando precedente visualizza un avviso sulla possibile perdita di dati.
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'
Se viene eseguito il comando database update
, viene generato l'errore seguente:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
Applicare la migrazione
Ora che è disponibile un database esistente, è necessario preoccuparsi di come applicare eventuali modifiche future. Questa esercitazione illustra due approcci:
- Eliminare e ricreare il database
- Applicare la migrazione al database esistente. Anche se questo metodo è più complesso e richiede più tempo, è l'approccio consigliato per gli ambienti di produzione reali. Nota: questa è una sezione facoltativa dell'esercitazione. È possibile eseguire i passaggi di eliminazione e ricreazione e ignorare questa sezione. Se si vuole seguire la procedura descritta in questa sezione, non eseguire i passaggi di eliminazione e ricreazione.
Eliminare e ricreare il database
Il codice aggiornato in DbInitializer
aggiunge dati di inizializzazione per le nuove entità. Per forzare EF Core la creazione di un nuovo database, eliminare e aggiornare il database:
Nella console di Gestione pacchetti eseguire il comando seguente:
Drop-Database
Update-Database
Eseguire Get-Help about_EntityFrameworkCore
dalla console di Gestione pacchetti per ottenere informazioni.
Eseguire l'app. Quando si esegue l'app viene eseguito il metodo DbInitializer.Initialize
. DbInitializer.Initialize
popola il nuovo database.
Aprire il database in SSOX:
- Se SSOX è stato aperto in precedenza, fare clic sul pulsante Aggiorna.
- Espandere il nodo Tabelle. Vengono visualizzate le tabelle create.
Esaminare la tabella CourseAssignment:
- Fare clic con il pulsante destro del mouse sulla tabella CourseAssignment e selezionare Visualizza dati.
- Verificare che la tabella CourseAssignment contenga dati.
Applicare la migrazione al database esistente
Questa sezione è facoltativa. Questa procedura funziona solo se è stata ignorata la sezione Eliminare e ricreare il database precedente.
Quando le migrazioni vengono eseguite con dati esistenti, possono essere presenti vincoli di chiave esterna che non vengono soddisfatti con i dati esistenti. Con i dati di produzione, è necessario eseguire passaggi per la migrazione dei dati esistenti. Questa sezione visualizza un esempio di correzione delle violazioni dei vincoli di chiave esterna. Non apportare queste modifiche al codice senza un backup. Non apportare queste modifiche al codice se è stata completata la sezione precedente e il database è stato aggiornato.
Il {timestamp}_ComplexDataModel.cs
file contiene il codice seguente:
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
Il codice precedente aggiunge una chiave esterna non nullable DepartmentID
alla tabella Course
. Il database dell'esercitazione precedente contiene righe in Course
, pertanto la tabella non può essere aggiornata mediante le migrazioni.
Per far sì che la migrazione ComplexDataModel
funzioni con i dati esistenti:
- Modificare il codice per assegnare un valore predefinito alla nuova colonna (
DepartmentID
). - Creare un reparto fittizio denominato "Temp" che assume il ruolo di reparto predefinito.
Risolvere i vincoli della chiave esterna
Aggiornare il metodo Up
delle classi ComplexDataModel
:
- Apri il file
{timestamp}_ComplexDataModel.cs
. - Impostare come commento la riga di codice che aggiunge la colonna
DepartmentID
alla tabellaCourse
.
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);
Aggiungere il codice evidenziato seguente. Il nuovo codice viene inserito dopo il blocco .CreateTable( name: "Department"
:
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);
Con le modifiche precedenti, le righe esistenti Course
saranno correlate al reparto "Temp" dopo l'esecuzione del ComplexDataModel
Up
metodo.
Un'app di produzione:
- Includerà codice o script per l'aggiunta di righe
Department
e righeCourse
correlate alle nuove righeDepartment
. - Non userà il reparto "Temp" o il valore predefinito per
Course.DepartmentID
.
L'esercitazione successiva illustra i dati correlati.