Esercitazione: Creare un modello di dati più complesso per un'app MVC ASP.NET
Nelle esercitazioni precedenti è stato usato un modello di dati semplice composto da tre entità. In questa esercitazione vengono aggiunte altre entità e relazioni e si personalizza il modello di dati specificando regole di formattazione, convalida e mapping del database. Questo articolo illustra due modi per personalizzare il modello di dati: aggiungendo attributi alle classi di entità e aggiungendo codice alla classe di contesto del database.
Al termine dell'operazione le classi di entità verranno incluse nel modello di dati completato, illustrato nella figura seguente:
In questa esercitazione:
- Personalizzare il modello di dati
- Aggiornare l'entità Student
- Creare l'entità Instructor
- Creare l'entità OfficeAssignment
- Modificare l'entità Course
- Creare l'entità Department
- Modificare l'entità Enrollment
- Aggiungere codice al contesto del database
- Eseguire il seeding del database con dati di test
- Aggiungere una migrazione
- Aggiornare il database
Prerequisiti
Personalizzare il modello di dati
In questa sezione si apprenderà come personalizzare il modello di dati usando attributi che specificano regole di formattazione, convalida e mapping del database. In diverse sezioni seguenti si creerà quindi il modello di dati completo School
aggiungendo attributi alle classi già create e creando nuove classi per i tipi di entità rimanenti nel modello.
Attributo DataType
Per le date di iscrizione degli studenti, tutte le pagine Web attualmente visualizzano l'ora oltre alla data, anche se l'unico elemento rilevante di questo campo è la data. Mediante gli attributi di annotazione dei dati è possibile modificare il codice per correggere il formato di visualizzazione in tutte le visualizzazioni che visualizzano i dati. Per un esempio di come eseguire questa operazione si aggiunge un attributo alla proprietà EnrollmentDate
nella classe Student
.
In Models\Student.cs aggiungere un'istruzione using
per lo spazio dei System.ComponentModel.DataAnnotations
nomi e aggiungere DataType
attributi e DisplayFormat
alla EnrollmentDate
proprietà , come illustrato nell'esempio 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 virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
L'attributo DataType viene utilizzato per specificare un tipo di dati più specifico del tipo intrinseco del database. In questo caso si vuole tenere traccia solo della data e non di data e ora. L'enumerazione DataType offre molti tipi di dati, ad esempio Data, Ora, PhoneNumber, Valuta, EmailAddress e altro ancora. L'attributo DataType
può anche consentire all'applicazione di fornire automaticamente le funzionalità specifiche del tipo. Ad esempio, è possibile creare un mailto:
collegamento per DataType.EmailAddress e un selettore data può essere fornito per DataType.Date nei browser che supportano HTML5. Gli attributi DataType generano attributi HTML 5 data- (pronunciato trattino dati) che i browser HTML 5 possono comprendere. Gli attributi DataType non forniscono alcuna convalida.
DataType.Date
non specifica il formato della data visualizzata. Per impostazione predefinita, il campo dati viene visualizzato in base ai formati predefiniti basati su 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 specificata deve essere applicata anche quando il valore viene visualizzato in una casella di testo per la modifica. È possibile che non si voglia che per alcuni campi, ad esempio per i valori di valuta, non si voglia che il simbolo di valuta nella casella di testo venga modificato.
È possibile usare l'attributo DisplayFormat da solo, ma in genere è consigliabile usare anche l'attributo DataType . L'attributo DataType
fornisce la semantica dei dati anziché come eseguirne il rendering su una schermata e offre i vantaggi seguenti che non si ottengono con DisplayFormat
:
- Il browser può abilitare le funzionalità HTML5, ad esempio per 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 eseguirà il rendering dei dati usando il formato corretto in base alle impostazioni locali.
- L'attributo DataType può consentire a MVC di scegliere il modello di campo appropriato per eseguire il rendering dei dati(DisplayFormat usa il modello stringa). Per altre informazioni, vedere Modelli ASP.NET MVC 2 di Brad Wilson. Anche se scritto per MVC 2, questo articolo si applica ancora alla versione corrente di ASP.NET MVC.
Se si usa l'attributo DataType
con un campo data, è necessario specificare anche l'attributo DisplayFormat
per assicurarsi che il rendering del campo venga eseguito correttamente nei browser Chrome. Per altre informazioni, vedere questo thread StackOverflow.
Per altre informazioni su come gestire altri formati di data in MVC, vedere Introduzione a MVC 5: Esame dei metodi di modifica e modifica visualizzazione e ricerca nella pagina per la "internazionalizzazione".
Eseguire di nuovo la pagina Student Index e notare che gli orari non vengono più visualizzati per le date di registrazione. Lo stesso vale per qualsiasi visualizzazione che usa il Student
modello.
The StringLengthAttribute
È anche possibile specificare regole di convalida dei dati e messaggi di errore di convalida mediante gli attributi. L'attributo StringLength imposta la lunghezza massima nel database e fornisce la convalida lato client e lato server per ASP.NET MVC. È anche possibile specificare la lunghezza minima della stringa in questo attributo, ma il valore minimo non ha alcun effetto sullo schema del database.
Ad esempio si supponga di voler limitare a 50 il numero massimo di caratteri che gli utenti possono immettere per un nome. Per aggiungere questa limitazione, aggiungere gli attributi StringLength alle LastName
proprietà e FirstMidName
, come illustrato nell'esempio 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 virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
L'attributo StringLength non impedisce a un utente di immettere spazi vuoti per un nome. È possibile usare l'attributo RegularExpression per applicare restrizioni all'input. Ad esempio, il codice seguente richiede che il primo carattere sia maiuscolo e i caratteri rimanenti siano alfabetici:
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
L'attributo MaxLength fornisce funzionalità simili all'attributo StringLength , ma non fornisce la convalida lato client.
Eseguire l'applicazione e fare clic sulla scheda Students (Studenti ). Viene visualizzato l'errore seguente:
Il modello che esegue il backup del contesto "SchoolContext" è stato modificato dopo la creazione del database. È consigliabile usare Migrazioni Code First per aggiornare il database (https://go.microsoft.com/fwlink/?LinkId=238269).
Il modello di database è stato modificato in modo da richiedere una modifica dello schema del database e Entity Framework ha rilevato che. Si useranno le migrazioni per aggiornare lo schema senza perdere dati aggiunti al database usando l'interfaccia utente. Se sono stati modificati i dati creati dal Seed
metodo , che verranno modificati nuovamente allo stato originale a causa del metodo AddOrUpdate usato nel Seed
metodo . AddOrUpdate equivale a un'operazione "upsert" dalla terminologia del database.
Nella console di Gestione pacchetti immettere i comandi seguenti:
add-migration MaxLengthOnNames
update-database
Il add-migration
comando crea un file denominato <timeStamp>_MaxLengthOnNames.cs. Il metodo Up
di questo file contiene codice che aggiorna il database per adattarlo al modello di dati corrente. Il comando update-database
ha eseguito tale codice.
Il timestamp anteporto al nome file delle migrazioni viene usato da Entity Framework per ordinare le migrazioni. È possibile creare più migrazioni prima di eseguire il update-database
comando e quindi tutte le migrazioni vengono applicate nell'ordine in cui sono state create.
Eseguire la pagina Crea 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: il campo LastName deve essere una stringa con una lunghezza massima di 50.
Attributo Column
È possibile usare gli attributi anche per controllare il mapping delle classi e delle proprietà nel database. Si supponga di aver usato il nome FirstMidName
per il campo first-name (Nome) perché il campo potrebbe contenere anche un secondo nome. Tuttavia si vuole che la colonna di database sia denominata FirstName
, perché gli utenti che scrivono query ad hoc per il database sono abituati a tale nome. Per eseguire questo mapping è possibile usare l'attributo Column
.
L'attributo Column
specifica che quando viene creato il database, la colonna della tabella Student
mappata sulla proprietà FirstMidName
verrà denominata FirstName
. In altri termini, quando il codice fa riferimento a Student.FirstMidName
i dati provengono dalla colonna FirstName
della tabella Student
o vengono aggiornati in tale colonna. Se non si specificano nomi di colonna, vengono assegnati lo stesso nome del nome della proprietà.
Nel file Student.cs aggiungere un'istruzione using
per System.ComponentModel.DataAnnotations.Schema e aggiungere l'attributo del nome di colonna alla FirstMidName
proprietà , come illustrato nel 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 virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
L'aggiunta dell'attributo Column modifica il modello che esegue il backup di SchoolContext, in modo che non corrisponda al database. Immettere i comandi seguenti in PMC per creare un'altra migrazione:
add-migration ColumnFirstName
update-database
In Esplora server aprire la finestra di progettazione tabelle Student facendo doppio clic sulla tabella Student .
L'immagine seguente mostra il nome della colonna originale come prima dell'applicazione delle prime due migrazioni. Oltre al nome della colonna che passa da FirstMidName
a FirstName
, le due colonne dei nomi sono cambiate da MAX
lunghezza a 50 caratteri.
È anche possibile apportare modifiche al mapping del database usando l'API Fluent, come illustrato più avanti in questa esercitazione.
Nota
Se si prova a compilare prima di aver creato tutte le classi di entità delle sezioni seguenti, possono verificarsi errori di compilazione.
Aggiornare l'entità Student
In Models\Student.cs sostituire il codice aggiunto in precedenza con il codice seguente. Le modifiche sono evidenziate.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Attributo obbligatorio
L'attributo Required rende obbligatori i campi delle proprietà del nome. Non Required attribute
è necessario per i tipi valore, ad esempio DateTime, int, double e float. I tipi valore non possono essere assegnati a un valore Null, pertanto vengono considerati intrinsecamente 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
L'attributo Display
specifica che la didascalia delle caselle di testo deve essere "First Name" (Nome), "Last Name" (Cognome), "Full Name" (Nome e cognome) ed "Enrollment Date" (Data di iscrizione) anziché il nome della proprietà (senza spazi tra le parole) in ogni istanza.
Proprietà calcolata FullName
FullName
è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. Pertanto, dispone solo di una get
funzione di accesso e non verrà generata alcuna FullName
colonna nel database.
Creare l'entità Instructor
Creare Models\Instructor.cs, sostituendo il codice del modello 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 virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
}
Si noti che molte proprietà sono uguali nelle entità Student
e Instructor
. Nell'esercitazione Implementing Inheritance (Implementazione dell'ereditarietà) più avanti in questa serie si effettuerà il refactoring di questo codice per eliminare la ridondanza.
È possibile inserire più attributi su una riga, quindi è anche possibile scrivere la classe instructor come indicato di seguito:
public class Instructor
{
public int ID { get; set; }
[Display(Name = "Last Name"),StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
[Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
public string FirstMidName { get; set; }
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
Proprietà di navigazione Courses e OfficeAssignment
Le proprietà Courses
e OfficeAssignment
sono proprietà di navigazione. Come spiegato in precedenza, vengono in genere definiti come virtuali in modo che possano sfruttare una funzionalità di Entity Framework denominata caricamento differita. Inoltre, se una proprietà di navigazione può contenere più entità, il relativo tipo deve implementare l'interfaccia T> di ICollection<. Ad esempio , IList<T> qualifica ma non IEnumerable<T> perché IEnumerable<T>
non implementa Add.
Un insegnante può insegnare un numero qualsiasi di corsi, quindi Courses
è definito come una raccolta di Course
entità.
public virtual ICollection<Course> Courses { get; set; }
Le regole business dichiarano che un insegnante può avere al massimo un ufficio, quindi OfficeAssignment
è definito come una singola OfficeAssignment
entità (che può essere null
se non viene assegnato alcun ufficio).
public virtual OfficeAssignment OfficeAssignment { get; set; }
Creare l'entità OfficeAssignment
Creare Modelli\OfficeAssignment.cs con il codice seguente:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public virtual Instructor Instructor { get; set; }
}
}
Compilare il progetto, che salva le modifiche e verifica che non siano stati eseguiti errori di copia e incolla che il compilatore può intercettare.
Attributo chiave
Esiste una relazione uno-a-zero-o-uno tra le Instructor
entità e OfficeAssignment
. Un'assegnazione di ufficio esiste solo in relazione all'insegnante a cui è assegnata e pertanto la chiave primaria è anche la chiave esterna all'entità Instructor
. Entity Framework non è tuttavia in grado di riconoscere InstructorID
automaticamente come chiave primaria di questa entità perché il nome non segue la ID
convenzione di denominazione o classnameID
. Per identificare l'entità come chiave viene usato l'attributo Key
:
[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }
È anche possibile usare l'attributo se l'entità Key
dispone di una propria chiave primaria, ma si vuole assegnare alla proprietà un nome diverso da classnameID
o ID
. Per impostazione predefinita, Entity Framework considera la chiave come non generata dal database perché la colonna è per una relazione di identificazione.
Attributo ForeignKey
Quando è presente una relazione uno-a-zero-o-uno o una relazione uno-a-uno tra due entità (ad esempio tra OfficeAssignment
e Instructor
), EF non può determinare quale fine della relazione è l'entità e quale fine dipende. Le relazioni uno-a-uno hanno una proprietà di navigazione di riferimento in ogni classe all'altra classe. L'attributo ForeignKey può essere applicato alla classe dipendente per stabilire la relazione. Se si omette l'attributo ForeignKey, viene visualizzato l'errore seguente quando si tenta di creare la migrazione:
Impossibile determinare la fine principale di un'associazione tra i tipi "ContosoUniversity.Models.OfficeAssignment" e "ContosoUniversity.Models.Instructor". La fine principale di questa associazione deve essere configurata in modo esplicito usando l'API fluent della relazione o le annotazioni dei dati.
Più avanti nell'esercitazione si vedrà come configurare questa relazione con l'API Fluent.
Proprietà di navigazione Instructor
L'entità Instructor
ha una proprietà di navigazione nullable OfficeAssignment
(perché un insegnante potrebbe non avere un'assegnazione di ufficio) e l'entità OfficeAssignment
ha una proprietà di navigazione non nullable Instructor
(perché un'assegnazione di ufficio non può esistere senza un insegnante - InstructorID
non è nullable). Quando un'entità Instructor
ha un'entità correlata OfficeAssignment
, ogni entità avrà un riferimento all'altro nella relativa proprietà di navigazione.
È possibile inserire un [Required]
attributo nella proprietà di navigazione Instructor per specificare che deve essere presente un insegnante correlato, ma non è necessario farlo perché la chiave esterna InstructorID (che è anche la chiave di questa tabella) non è nullable.
Modificare l'entità Course
In Models\Course.cs sostituire il codice aggiunto in precedenza 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 virtual Department Department { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
public virtual ICollection<Instructor> Instructors { get; set; }
}
}
L'entità course ha una proprietà DepartmentID
di chiave esterna che punta all'entità correlata Department
e ha una Department
proprietà di navigazione. In Entity Framework non è necessario aggiungere una proprietà di chiave esterna al modello di dati se è disponibile una proprietà di navigazione per un'entità correlata. Ef crea automaticamente chiavi esterne nel database ovunque siano necessarie. Tuttavia il fatto di avere la chiave esterna nel modello di dati può rendere più semplici ed efficienti gli aggiornamenti. Ad esempio, quando si recupera un'entità corso da modificare, l'entità Department
è Null se non viene caricata, quindi quando si aggiorna l'entità corso, è necessario recuperare prima l'entità Department
. Quando la proprietà DepartmentID
della chiave esterna è inclusa nel modello di dati, non è necessario recuperare l'entità prima dell'aggiornamento Department
.
Attributo DatabaseGenerated
L'attributo DatabaseGenerated con il parametro None nella CourseID
proprietà specifica che i valori di chiave primaria vengono forniti dall'utente anziché generati dal database.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Per impostazione predefinita, Entity Framework presuppone che i valori di chiave primaria vengano generati dal database. Questa è la condizione ottimale nella maggior parte degli scenari. Per le entità, tuttavia Course
, si userà un numero di corso specificato dall'utente, ad esempio una serie 1000 per un reparto, una serie 2000 per un altro reparto e così via.
Proprietà chiave esterna e navigazione
Le proprietà della chiave esterna e le proprietà di navigazione nell'entità Course
riflettono le relazioni seguenti:
Un corso viene assegnato a un solo reparto, pertanto sono presenti una chiave esterna
DepartmentID
e una proprietà di navigazioneDepartment
per i motivi indicati in precedenza.public int DepartmentID { get; set; } public virtual Department Department { get; set; }
Un corso può avere un numero qualsiasi di studenti iscritti, pertanto la proprietà di navigazione
Enrollments
è una raccolta:public virtual ICollection<Enrollment> Enrollments { get; set; }
Un corso può essere impartito da più insegnanti, pertanto la proprietà di navigazione
Instructors
è una raccolta:public virtual ICollection<Instructor> Instructors { get; set; }
Creare l'entità Department
Creare Modelli\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 virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
}
Attributo Column
In precedenza è stato usato l'attributo Column per modificare il mapping dei nomi di colonna. Nel codice per l'entità l'attributo Department
viene usato per modificare il Column
mapping dei tipi di dati SQL in modo che la colonna venga definita usando il tipo money di SQL Server nel database:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Il mapping delle colonne in genere non è obbligatorio, perché Entity Framework sceglie in genere il tipo di dati DI SQL Server appropriato in base al tipo CLR definito per la proprietà. Il tipo CLR decimal
esegue il mapping a un tipo SQL Server decimal
. In questo caso, tuttavia, si sa che la colonna conterrà gli importi in valuta e il tipo di dati money è più appropriato. Per altre informazioni sui tipi di dati CLR e su come corrispondono ai tipi di dati di SQL Server, vedere SqlClient per Entity FrameworkTypes.
Proprietà chiave esterna e navigazione
Le proprietà di chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:
Un reparto può avere o meno un amministratore e un amministratore è sempre un insegnante. Di conseguenza, la
InstructorID
proprietà viene inclusa come chiave esterna per l'entitàInstructor
e un punto interrogativo viene aggiunto dopo laint
designazione del tipo per contrassegnare la proprietà come nullable. La proprietà di navigazione è denominataAdministrator
ma contiene un'entitàInstructor
:public int? InstructorID { get; set; } public virtual Instructor Administrator { get; set; }
Un reparto può avere molti corsi, quindi c'è una
Courses
proprietà di navigazione:public virtual ICollection<Course> Courses { get; set; }
Nota
Per convenzione, Entity Framework consente l'eliminazione a catena per le chiavi esterne non nullable e per le relazioni molti-a-molti. Ciò può determinare regole di eliminazione a catena circolari, che generano un'eccezione quando si prova ad aggiungere una migrazione. Ad esempio, se la
Department.InstructorID
proprietà non è stata definita come nullable, si otterrà il messaggio di eccezione seguente: "La relazione referenziale comporterà un riferimento ciclico non consentito". Se le regole business richiedonoInstructorID
la proprietà non nullable, è necessario usare l'istruzione API Fluent seguente per disabilitare l'eliminazione a catena nella relazione:
modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);
Modificare l'entità Enrollment
In Models\Enrollment.cs sostituire il codice aggiunto in precedenza 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 virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
}
Proprietà chiave esterna e navigazione
Le proprietà di chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:
Un record di iscrizione è relativo a un singolo corso, pertanto sono presenti una proprietà di chiave esterna
CourseID
e una proprietà di navigazioneCourse
:public int CourseID { get; set; } public virtual Course Course { get; set; }
Un record di iscrizione è relativo a un singolo studente, pertanto sono presenti una proprietà di chiave esterna
StudentID
e una proprietà di navigazioneStudent
:public int StudentID { get; set; } public virtual Student Student { get; set; }
Relazioni molti-a-molti
Esiste una relazione molti-a-molti tra le Student
entità e Course
e l'entità Enrollment
funziona come tabella di join molti-a-molti con payload nel database. Ciò significa che la Enrollment
tabella contiene dati aggiuntivi oltre alle chiavi esterne per le tabelle unite (in questo caso, una chiave primaria e una Grade
proprietà).
La figura seguente illustra l'aspetto di queste relazioni in un diagramma di entità. Questo diagramma è stato generato usando Entity Framework Power Tools. La creazione del diagramma non fa parte dell'esercitazione, ma viene usata qui come illustrazione.
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 di grado, sarebbe necessario contenere solo le due chiavi CourseID
esterne e StudentID
. In tal caso, corrisponderebbe a una tabella join molti-a-molti senza payload (o una tabella di join pura) nel database e non sarebbe necessario crearne affatto una classe modello. Le Instructor
entità e Course
hanno quel tipo di relazione molti-a-molti e, come si può notare, non esiste una classe di entità tra di esse:
Nel database è tuttavia necessaria una tabella join, come illustrato nel diagramma di database seguente:
Entity Framework crea automaticamente la CourseInstructor
tabella e la si legge e la si aggiorna indirettamente leggendo e aggiornando le Instructor.Courses
proprietà di navigazione e Course.Instructors
.
Diagramma della relazione di entità
La figura seguente visualizza il diagramma creato da Entity Framework Power Tools per il modello School completato.
Oltre alle linee di relazione molti-a-molti (* a *) e alle linee di relazione uno-a-molti (da 1 a *), è possibile vedere qui la linea di relazione uno-a-zero-o-uno (da 1 a 0,.1) tra le Instructor
entità e OfficeAssignment
e la riga di relazione zero-o-uno-a-molti (da 0..1 a *) tra le entità Instructor e Department.
Aggiungere codice al contesto del database
Successivamente si aggiungeranno le nuove entità alla SchoolContext
classe e si personalizzano alcuni dei mapping usando chiamate API Fluent. L'API è "fluent" perché viene spesso usata tramite la stringa di una serie di chiamate di metodo in un'unica istruzione, come nell'esempio seguente:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
In questa esercitazione si userà l'API Fluent solo per il mapping di database che non è possibile eseguire con gli attributi. È tuttavia possibile usare l'API Fluent anche per 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. Come accennato in precedenza, MinimumLength
non modifica lo schema, ma applica solo una regola di convalida lato client e server
Alcuni sviluppatori preferiscono usare esclusivamente l'API Fluent in modo che possano mantenere "pulite" le classi di entità. È possibile combinare attributi e API Fluent se si vuole e sono disponibili alcune personalizzazioni che possono essere eseguite solo usando l'API Fluent, ma in generale la procedura consigliata consiste nel scegliere uno di questi due approcci e usarli in modo coerente il più possibile.
Per aggiungere le nuove entità al modello di dati ed eseguire il mapping del database che non è stato eseguito usando gli attributi, sostituire il codice in DAL\SchoolContext.cs con il codice seguente:
using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace ContosoUniversity.DAL
{
public class SchoolContext : DbContext
{
public DbSet<Course> Courses { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
}
}
}
La nuova istruzione nel metodo OnModelCreating configura la tabella join molti-a-molti:
Per la relazione molti-a-molti tra le
Instructor
entità eCourse
, il codice specifica i nomi di tabella e colonna per la tabella di join. Code First può configurare la relazione molti-a-molti senza questo codice, ma se non viene chiamata, si otterranno nomi predefiniti, adInstructorInstructorID
esempio per laInstructorID
colonna.modelBuilder.Entity<Course>() .HasMany(c => c.Instructors).WithMany(i => i.Courses) .Map(t => t.MapLeftKey("CourseID") .MapRightKey("InstructorID") .ToTable("CourseInstructor"));
Il codice seguente fornisce un esempio di come è possibile usare l'API Fluent anziché gli attributi per specificare la relazione tra le Instructor
entità e OfficeAssignment
:
modelBuilder.Entity<Instructor>()
.HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);
Per informazioni sulle istruzioni "Fluent API" in background, vedere il post di blog sull'API Fluent.
Eseguire il seeding del database con dati di test
Sostituire il codice nel file Migrations\Configuration.cs con il codice seguente per fornire i dati di inizializzazione per le nuove entità create.
namespace ContosoUniversity.Migrations
{
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SchoolContext context)
{
var students = new List<Student>
{
new Student { FirstMidName = "Carson", LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};
students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var instructors = new List<Instructor>
{
new Instructor { FirstMidName = "Kim", LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};
instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var departments = new List<Department>
{
new Department { Name = "English", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").ID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID }
};
departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
context.SaveChanges();
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
var officeAssignments = new List<OfficeAssignment>
{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
Location = "Thompson 304" },
};
officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
context.SaveChanges();
AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
AddOrUpdateInstructor(context, "Chemistry", "Harui");
AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");
AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
AddOrUpdateInstructor(context, "Trigonometry", "Harui");
AddOrUpdateInstructor(context, "Composition", "Abercrombie");
AddOrUpdateInstructor(context, "Literature", "Abercrombie");
context.SaveChanges();
var enrollments = new List<Enrollment>
{
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").ID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};
foreach (Enrollment e in enrollments)
{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
{
var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
if (inst == null)
crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
}
}
}
Come si è visto nella prima esercitazione, la maggior parte di questo codice semplicemente aggiorna o crea nuovi oggetti entità e carica i dati di esempio in proprietà come richiesto per il test. Si noti tuttavia che l'entità Course
, che ha una relazione molti-a-molti con l'entità Instructor
, viene gestita:
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
Quando si crea un Course
oggetto , si inizializza la Instructors
proprietà di navigazione come raccolta vuota usando il codice Instructors = new List<Instructor>()
. In questo modo è possibile aggiungere Instructor
entità correlate a questa Course
operazione usando il Instructors.Add
metodo . Se non è stato creato un elenco vuoto, non sarà possibile aggiungere queste relazioni, perché la Instructors
proprietà sarebbe null e non avrebbe un Add
metodo. È anche possibile aggiungere l'inizializzazione dell'elenco al costruttore.
Aggiungere una migrazione
Dal PMC immettere il add-migration
comando (non eseguire ancora il update-database
comando):
add-Migration ComplexDataModel
Se si prova a eseguire il comando update-database
in questa fase (evitare di farlo), si ottiene il seguente errore:
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' (L'istruzione ALTER TABLE è in conflitto con il vincolo FOREIGN KEY "FK_dbo.Course_dbo.Department_DepartmentID". Il conflitto si è verificato nella colonna 'DepartmentID' della tabella "dbo.Department"del database "ContosoUniversity").
In alcuni casi, quando si eseguono migrazioni con dati esistenti, è necessario inserire dati stub nel database per soddisfare i vincoli di chiave esterna ed è ciò che è necessario fare ora. Il codice generato nel metodo ComplexDataModel Up
aggiunge una chiave esterna non nullable DepartmentID
alla Course
tabella. Poiché sono già presenti righe nella Course
tabella durante l'esecuzione del codice, l'operazione AddColumn
avrà esito negativo perché SQL Server non conosce il valore da inserire nella colonna che non può essere Null. È quindi necessario modificare il codice per assegnare alla nuova colonna un valore predefinito e creare un reparto stub denominato "Temp" per fungere da reparto predefinito. Di conseguenza, tutte le righe esistenti Course
saranno correlate al reparto "Temp" dopo l'esecuzione del Up
metodo. È possibile correlarli ai reparti corretti nel Seed
metodo .
Modificare il <timestamp>_ComplexDataModel.cs file, impostare come commento la riga di codice che aggiunge la colonna DepartmentID alla tabella Course e aggiungere il codice evidenziato seguente (anche la riga commentata è evidenziata):
CreateTable(
"dbo.CourseInstructor",
c => new
{
CourseID = c.Int(nullable: false),
InstructorID = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.CourseID, t.InstructorID })
.ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
.ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
.Index(t => t.CourseID)
.Index(t => t.InstructorID);
// Create a department for course to point to.
Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// default value for FK points to department created above.
AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1));
//AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));
AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));
Quando viene eseguito, il Seed
metodo inserisce righe nella Department
tabella e correla le righe esistenti Course
a quelle nuove Department
righe. Se non sono stati aggiunti corsi nell'interfaccia utente, non è più necessario il reparto "Temp" o il valore predefinito nella Course.DepartmentID
colonna. Per consentire la possibilità che un utente abbia aggiunto corsi usando l'applicazione, è anche necessario aggiornare il codice del Seed
metodo per assicurarsi che tutte le Course
righe (non solo quelle inserite dalle esecuzioni precedenti del Seed
metodo) abbiano valori validi DepartmentID
prima di rimuovere il valore predefinito dalla colonna ed eliminare il reparto "Temp".
Aggiornare il database
Dopo aver completato la modifica del <timestamp>_ComplexDataModel.cs file, immettere il update-database
comando in PMC per eseguire la migrazione.
update-database
Nota
È possibile ottenere altri errori durante la migrazione dei dati e apportare modifiche allo schema. Se si verificano errori di migrazione che non si riesce a risolvere, è possibile modificare il nome del database nella stringa di connessione o eliminare il database. L'approccio più semplice consiste nel rinominare il database nel file Web.config . L'esempio seguente mostra il nome modificato in CU_Test:
<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;"
providerName="System.Data.SqlClient" />
Con un nuovo database, non sono presenti dati di cui eseguire la migrazione e il update-database
comando è molto più probabile che venga completato senza errori. Per istruzioni su come eliminare il database, vedere Come eliminare un database da Visual Studio 2012.
In caso di errore, è possibile provare a inizializzare nuovamente il database immettendo il comando seguente in PMC:
update-database -TargetMigration:0
Aprire il database in Esplora server come in precedenza ed espandere il nodo Tabelle per verificare che tutte le tabelle siano state create. (Se hai ancora Esplora server aperto dall'ora precedente, fare clic sul pulsante Aggiorna.
Non è stata creata una classe modello per la CourseInstructor
tabella. Come spiegato in precedenza, si tratta di una tabella join per la relazione molti-a-molti tra le Instructor
entità e Course
.
Fare clic con il pulsante destro del mouse sulla CourseInstructor
tabella e selezionare Mostra dati tabella per verificare che siano presenti dati in esso contenuti in seguito Instructor
alle entità aggiunte alla Course.Instructors
proprietà di navigazione.
Ottenere il codice
Scaricare il progetto completato
Risorse aggiuntive
I collegamenti ad altre risorse di Entity Framework sono disponibili in ASP.NET Accesso ai dati - Risorse consigliate.
Passaggi successivi
In questa esercitazione:
- Personalizzato il modello di dati
- Entità Student aggiornata
- Creazione dell'entità Instructor
- Creazione dell'entità OfficeAssignment
- Modifica dell'entità Course
- Creazione dell'entità Department
- Modifica dell'entità Enrollment
- Aggiunta del codice al contesto del database
- Seeding del database con dati di test
- Aggiunta di una migrazione
- Aggiornamento del database
Passare all'articolo successivo per informazioni su come leggere e visualizzare i dati correlati caricati da Entity Framework nelle proprietà di navigazione.