Vytvoření složitějšího datového modelu pro aplikaci ASP.NET MVC (4 z 10)
Ukázková webová aplikace Contoso University ukazuje, jak vytvářet aplikace ASP.NET MVC 4 pomocí entity Framework 5 Code First a sady Visual Studio 2012. Informace o sérii kurzů najdete v prvním kurzu v této sérii.
Poznámka
Pokud narazíte na problém, který nemůžete vyřešit, stáhněte si dokončenou kapitolu a zkuste problém reprodukovat. Obecně můžete najít řešení problému porovnáním kódu s dokončeným kódem. Informace o některých běžných chybách a jejich řešení najdete v tématu Chyby a alternativní řešení.
V předchozích kurzech jste pracovali s jednoduchým datovým modelem, který se skládal ze tří entit. V tomto kurzu přidáte další entity a relace a přizpůsobíte datový model zadáním pravidel formátování, ověřování a mapování databází. Uvidíte dva způsoby přizpůsobení datového modelu: přidáním atributů do tříd entit a přidáním kódu do třídy kontextu databáze.
Jakmile budete hotovi, třídy entit vytvoří dokončený datový model, který je znázorněný na následujícím obrázku:
Přizpůsobení datového modelu pomocí atributů
V této části se dozvíte, jak přizpůsobit datový model pomocí atributů, které určují pravidla formátování, ověřování a mapování databáze. Potom v několika následujících částech vytvoříte úplný School
datový model přidáním atributů do již vytvořených tříd a vytvořením nových tříd pro zbývající typy entit v modelu.
Atribut Datového typu
U dat registrace studentů se na všech webových stránkách aktuálně zobrazuje čas spolu s datem, ale jediné, co vás zajímá, je datum. Pomocí atributů datových poznámek můžete provést jednu změnu kódu, která opraví formát zobrazení v každém zobrazení, které zobrazuje data. Pokud chcete vidět příklad, jak to udělat, přidáte atribut do EnrollmentDate
vlastnosti ve Student
třídě .
V souboru Models\Student.cs přidejte using
příkaz pro System.ComponentModel.DataAnnotations
obor názvů a do EnrollmentDate
vlastnosti přidejte DataType
atributy a DisplayFormat
, jak je znázorněno v následujícím příkladu:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int StudentID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Atribut DataType slouží k určení datového typu, který je konkrétnější než vnitřní typ databáze. V tomto případě chceme sledovat pouze datum, ne datum a čas. Výčet DataType poskytuje mnoho datových typů, například Datum, Čas, Telefonní číslo, Měna, EmailAddress a další. Atribut DataType
může také aplikaci umožnit, aby automaticky poskytovala funkce specifické pro typ. mailto:
Například odkaz může být vytvořen pro DataType.EmailAddress a v prohlížečích, které podporují HTML5, lze pro DataType.Date zadat výběr data. Atributy DataType vygenerují atributy HTML 5 ( vyslovuje se datová pomlčka), kterým prohlížeče HTML 5 rozumí. Atributy DataType neposkytují žádné ověření.
DataType.Date
neurčoval formát data, které je zobrazeno. Ve výchozím nastavení je datové pole zobrazeno podle výchozích formátů založených na CultureInfo serveru.
Atribut DisplayFormat
slouží k explicitní specifikaci formátu data:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
Nastavení ApplyFormatInEditMode
určuje, že se má zadané formátování použít také při zobrazení hodnoty v textovém poli pro úpravy. (U některých polí to možná nebudete chtít – například pro hodnoty měny nebudete chtít symbol měny v textovém poli pro úpravy.)
Atribut DisplayFormat můžete použít samostatně, ale obecně je vhodné použít také atribut DataType . Atribut DataType
vyjadřuje sémantiku dat na rozdíl od toho, jak je vykreslit na obrazovce, a poskytuje následující výhody, které nemáte s DisplayFormat
:
- Prohlížeč může povolit funkce HTML5 (například k zobrazení ovládacího prvku kalendáře, symbolu měny odpovídajícího národnímu prostředí, e-mailových odkazů atd.).
- Ve výchozím nastavení prohlížeč vykresluje data pomocí správného formátu na základě vašeho národního prostředí.
- Atribut DataType umožňuje MVC zvolit správnou šablonu pole pro vykreslení dat ( DisplayFormat , pokud ho použije sám, používá šablonu řetězce). Další informace najdete v tématu Šablony ASP.NET MVC 2 od Brada Wilsona. (I když je tento článek napsaný pro MVC 2, stále platí pro aktuální verzi ASP.NET MVC.)
Pokud použijete DataType
atribut s polem data, musíte atribut zadat DisplayFormat
také, aby se zajistilo, že se pole v prohlížečích Chrome správně vykresluje. Další informace najdete v tomto vlákně StackOverflow.
Znovu spusťte stránku Index studenta a všimněte si, že se pro data registrace už nezobrazují časy. Totéž bude platit pro všechna zobrazení, která model používají Student
.
Atribut StringLengthAttribute
Pomocí atributů můžete také zadat pravidla ověření dat a zprávy. Předpokládejme, že chcete zajistit, aby uživatelé nezadáli do názvu více než 50 znaků. Chcete-li přidat toto omezení, přidejte do LastName
vlastností a FirstMidName
atributy StringLength, jak je znázorněno v následujícím příkladu:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int StudentID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Atribut StringLength nezabrání uživateli v zadání prázdných znaků pro jméno. Pomocí atributu RegularExpression můžete použít omezení vstupu. Například následující kód vyžaduje, aby první znak byl velkými písmeny a zbývající znaky abecedně:
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
Atribut MaxLength poskytuje podobné funkce jako atribut StringLength , ale neposkytuje ověření na straně klienta.
Spusťte aplikaci a klikněte na kartu Studenti . Zobrazí se následující chyba:
Model, který zálohuje kontext SchoolContext, se od vytvoření databáze změnil. Zvažte použití Migrace Code First k aktualizaci databáze (https://go.microsoft.com/fwlink/?LinkId=238269).
Model databáze se změnil způsobem, který vyžaduje změnu schématu databáze, a Entity Framework to zjistila. Pomocí migrací aktualizujete schéma bez ztráty dat, která jste přidali do databáze pomocí uživatelského rozhraní. Pokud jste změnili data vytvořená metodou Seed
, změní se zpět do původního stavu kvůli metodě AddOrUpdate , kterou v Seed
metodě používáte. (AddOrUpdate je ekvivalentem operace upsert z terminologie databáze.)
V konzole Správce balíčků (PMC) zadejte následující příkazy:
add-migration MaxLengthOnNames
update-database
Příkaz add-migration MaxLengthOnNames
vytvoří soubor s názvem <timeStamp>_MaxLengthOnNames.cs. Tento soubor obsahuje kód, který aktualizuje databázi tak, aby odpovídala aktuálnímu datovému modelu. Časové razítko před název souboru migrace se používá v Entity Frameworku k řazení migrací. Pokud po vytvoření více migrací databázi odstraníte nebo pokud nasadíte projekt pomocí migrace, použijí se všechny migrace v pořadí, ve kterém byly vytvořeny.
Spusťte stránku Vytvořit a zadejte název delší než 50 znaků. Jakmile překročíte 50 znaků, ověření na straně klienta okamžitě zobrazí chybovou zprávu.
Atribut column
Atributy můžete také použít k řízení způsobu mapování tříd a vlastností na databázi. Předpokládejme, že jste použili název FirstMidName
pro pole se jménem, protože pole může obsahovat také druhé jméno. Chcete ale, aby se sloupec databáze jmenoval FirstName
, protože uživatelé, kteří budou psát dotazy ad hoc na databázi, jsou na tento název zvyklí. K tomuto mapování můžete použít atribut .Column
Atribut Column
určuje, že při vytvoření databáze bude mít sloupec Student
tabulky mapovaná na FirstMidName
vlastnost název FirstName
. Jinými slovy, když váš kód odkazuje na Student.FirstMidName
, data budou pocházet ze sloupce tabulky nebo se aktualizují ve FirstName
sloupci Student
tabulky. Pokud nezadáte názvy sloupců, dostanou stejný název jako název vlastnosti.
Přidejte do vlastnosti příkaz using pro System.ComponentModel.DataAnnotations.Schema a atribut FirstMidName
názvu sloupce, jak je znázorněno v následujícím zvýrazněném kódu:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int StudentID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Přidání atributu Column změní model backing schoolContext, takže nebude odpovídat databázi. Zadáním následujících příkazů v PMC vytvořte další migraci:
add-migration ColumnFirstName
update-database
V Průzkumníku serveru (Průzkumník databáze , pokud používáte Express pro web) poklikejte na tabulku Student .
Následující obrázek ukazuje původní název sloupce, který byl před prvními dvěma migracemi. Kromě toho, že se název sloupce změnil z FirstMidName
na FirstName
, se změnily dva sloupce názvů z MAX
délky na 50 znaků.
Změny mapování databází můžete provést také pomocí rozhraní Fluent API, jak uvidíte později v tomto kurzu.
Poznámka
Pokud se pokusíte zkompilovat před vytvořením všech těchto tříd entit, může dojít k chybám kompilátoru.
Vytvoření entity instruktora
Vytvořte soubor Models\Instructor.cs a nahraďte kód šablony následujícím kódem:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int InstructorID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
}
Všimněte si, že několik vlastností v entitách a Instructor
je stejnýchStudent
. V kurzu Implementace dědičnosti dále v této sérii budete refaktorovat dědičnost, abyste tuto redundanci vyloučili.
Požadované a zobrazované atributy
Atributy LastName
vlastnosti určují, že se jedná o povinné pole, že popis pro textové pole by mělo být "Příjmení" (místo názvu vlastnosti, který by byl "Příjmení" bez mezer) a že hodnota nesmí být delší než 50 znaků.
[Required]
[Display(Name="Last Name")]
[StringLength(50)]
public string LastName { get; set; }
Atribut StringLength nastavuje maximální délku v databázi a poskytuje ověření na straně klienta a serveru pro ASP.NET MVC. V tomto atributu můžete také zadat minimální délku řetězce, ale minimální hodnota nemá žádný vliv na schéma databáze. Atribut Required není potřeba pro typy hodnot, jako jsou DateTime, int, double a float. Hodnotovým typům nelze přiřadit hodnotu null, takže jsou ze své podstaty povinné. Atribut Required můžete odebrat a nahradit ho parametrem minimální délky atributu StringLength
:
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
Na jeden řádek můžete umístit více atributů, takže můžete také napsat třídu instruktora následujícím způsobem:
public class Instructor
{
public int InstructorID { get; set; }
[Display(Name = "Last Name"),StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
[Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
public string FirstMidName { get; set; }
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime HireDate { get; set; }
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
Vypočtená vlastnost FullName
FullName
je počítaná vlastnost, která vrací hodnotu vytvořenou zřetězením dvou dalších vlastností. Proto má pouze get
příslušenství a v databázi nebude vygenerován žádný FullName
sloupec.
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
Navigační vlastnosti Kurzů a OfficeAssignment
Vlastnosti Courses
a OfficeAssignment
jsou vlastnosti navigace. Jak bylo vysvětleno dříve, jsou obvykle definovány jako virtuální , aby mohly využívat výhod funkce Entity Framework označované jako opožděné načítání. Kromě toho, pokud navigační vlastnost může obsahovat více entit, musí její typ implementovat ICollection<T> Rozhraní. (Například IList<T> kvalifikuje, ale ne IEnumerable<T> , protože IEnumerable<T>
neimplementuje Add.
Instruktor může vyučovat libovolný počet kurzů, takže Courses
je definován jako kolekce Course
entit. Naše obchodní pravidla uvádějí, že instruktor může mít maximálně jednu kancelář, takže OfficeAssignment
je definována jako jedna OfficeAssignment
entita (což může být null
v případě, že není přiřazena žádná kancelář).
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
Vytvoření entity OfficeAssignment
Vytvořte Models\OfficeAssignment.cs s následujícím kódem:
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; }
}
}
Sestavte projekt, který uloží změny a ověří, že jste neprovedli žádné chyby kopírování a vkládání, které kompilátor dokáže zachytit.
Atribut klíče
Mezi entitami a OfficeAssignment
entitami je vztah Instructor
1:nula nebo 1. Přiřazení kanceláře existuje pouze ve vztahu k instruktorovi, kterému je přiřazeno, a proto je jeho primárním klíčem také cizí klíč entity Instructor
. Entity Framework ale nemůže automaticky rozpoznat InstructorID
jako primární klíč této entity, protože její název neodpovídá ID
konvenci vytváření názvů názvů nebo classnameID
. Atribut se Key
proto používá k jeho identifikaci jako klíče:
[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }
Atribut můžete použít také v Key
případě, že entita má vlastní primární klíč, ale chcete vlastnost pojmenovat jinak než classnameID
nebo ID
. Ef ve výchozím nastavení považuje klíč za negenerovaný databází, protože sloupec je určený pro identifikující relaci.
Atribut ForeignKey
Pokud mezi dvěma entitami (například mezi OfficeAssignment
a Instructor
) existuje relace 1:nula nebo 1 nebo 1:1, ef nemůže zjistit, který konec relace je objektem zabezpečení a který konec je závislý. Relace 1:1 mají v každé třídě odkaz na navigační vlastnost na druhou třídu. Atribut ForeignKey lze použít na závislé třídy k vytvoření relace. Pokud vynecháte atribut ForeignKey, při pokusu o vytvoření migrace se zobrazí následující chyba:
Nelze určit hlavní konec přidružení mezi typy ContosoUniversity.Models.OfficeAssignment a ContosoUniversity.Models.Instructor. Hlavní konec tohoto přidružení musí být explicitně nakonfigurovaný pomocí rozhraní API fluent relace nebo datových poznámek.
Později v tomto kurzu si ukážeme, jak nakonfigurovat tuto relaci pomocí rozhraní FLUENT API.
Vlastnost Navigace instruktora
Entita Instructor
má navigační vlastnost s možnou OfficeAssignment
hodnotou null (protože instruktor nemusí mít přiřazení kanceláře) a OfficeAssignment
entita má navigační vlastnost nenulovatelnou Instructor
(protože přiřazení kanceláře nemůže existovat bez instruktora – InstructorID
není možné null). Pokud má entita Instructor
související OfficeAssignment
entitu, bude mít každá entita odkaz na druhou entitu ve své navigační vlastnosti.
Do vlastnosti Navigace instruktora můžete zadat [Required]
atribut, který určí, že musí existovat související instruktor, ale nemusíte to dělat, protože cizí klíč InstructorID (který je také klíčem k této tabulce) nemá hodnotu null.
Úprava entity kurzu
V souboru Models\Course.cs nahraďte dříve přidaný kód následujícím kódem:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Title { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
[Display(Name = "Department")]
public int DepartmentID { get; set; }
public virtual Department Department { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
public virtual ICollection<Instructor> Instructors { get; set; }
}
}
Entita kurzu má vlastnost DepartmentID
cizího klíče, která odkazuje na související Department
entitu Department
a má vlastnost navigace. Entity Framework nevyžaduje přidání vlastnosti cizího klíče do datového modelu, pokud máte vlastnost navigace pro související entitu. EF automaticky vytvoří cizí klíče v databázi všude, kde jsou potřeba. Ale cizí klíč v datovém modelu může zjednodušit a zefektivnit aktualizace. Když například načtete entitu kurzu, kterou chcete upravit, bude mít entita hodnotu null, Department
pokud ji nenačtete, takže při aktualizaci entity kurzu budete muset nejprve načíst entitu Department
. Pokud je vlastnost DepartmentID
cizího klíče součástí datového modelu, nemusíte entitu Department
před aktualizací načítat.
Atribut DatabaseGenerated
Atribut DatabaseGenerated s parametrem None ve CourseID
vlastnosti určuje, že hodnoty primárního klíče jsou poskytovány uživatelem, nikoli generovány databází.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Ve výchozím nastavení Entity Framework předpokládá, že hodnoty primárního klíče jsou generovány databází. To je to, co chcete ve většině scénářů. Pro Course
entity ale použijete uživatelem zadané číslo kurzu, například řadu 1000 pro jedno oddělení, řadu 2000 pro jiné oddělení atd.
Vlastnosti cizího klíče a navigace
Vlastnosti cizího klíče a navigační vlastnosti v entitě Course
odrážejí následující relace:
Kurz je přiřazen k jednomu oddělení, takže z výše uvedených důvodů je
DepartmentID
k dispozici cizí klíč aDepartment
navigační vlastnost.public int DepartmentID { get; set; } public virtual Department Department { get; set; }
Kurz může mít zapsaný libovolný počet studentů, takže
Enrollments
navigační vlastnost je kolekce:public virtual ICollection<Enrollment> Enrollments { get; set; }
Kurz může vyučovat více instruktorů, takže
Instructors
navigační vlastnost je kolekce:public virtual ICollection<Instructor> Instructors { get; set; }
Vytvoření entity oddělení
Vytvořte Models\Department.cs s následujícím kódem:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength=3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
public DateTime StartDate { get; set; }
[Display(Name = "Administrator")]
public int? InstructorID { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
}
Atribut column
Dříve jste ke změně mapování názvů sloupců použili atribut Column . V kódu entity Department
se atribut používá ke změně mapování datových typů SQL tak, Column
aby se sloupec definoval pomocí typu SQL Server peněz v databázi:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Mapování sloupců se obecně nevyžaduje, protože Entity Framework obvykle zvolí odpovídající datový typ SQL Server na základě typu CLR, který definujete pro vlastnost. Typ CLR decimal
se mapuje na typ SQL Serverdecimal
. V tomto případě ale víte, že sloupec bude obsahovat částky v měně a datový typ peníze je pro to vhodnější.
Vlastnosti cizího klíče a navigace
Vlastnosti cizího klíče a navigace odrážejí následující relace:
Oddělení může nebo nemusí mít správce a správce je vždy instruktor.
InstructorID
Vlastnost je proto zahrnuta jako cizí klíč entityInstructor
a zaint
označení typu se přidá otazník, který vlastnost označí jako s možnou hodnotou null. Vlastnost navigace má název,Administrator
ale obsahuje entituInstructor
:public int? InstructorID { get; set; } public virtual Instructor Administrator { get; set; }
Oddělení může mít mnoho kurzů, takže je
Courses
k dispozici vlastnost navigace:public virtual ICollection<Course> Courses { get; set; }
Poznámka
Podle konvence Entity Framework umožňuje kaskádové odstranění cizích klíčů, které nemají hodnotu null, a pro relace M:N. To může vést k cyklické kaskádové odstranění pravidel, která způsobí výjimku při spuštění kódu inicializátoru. Pokud byste například nedefinovali vlastnost s možnou
Department.InstructorID
hodnotou null, při spuštění inicializátoru by se zobrazila následující zpráva o výjimce: "Referenční relace bude mít za následek cyklický odkaz, který není povolený." Pokud vaše obchodní pravidla vyžadujíInstructorID
vlastnost s možnou hodnotou null, museli byste k zakázání kaskádového odstranění relace použít následující rozhraní FLUENT API:
modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);
Úprava entity Student
V části Models\Student.cs nahraďte dříve přidaný kód následujícím kódem. Změny jsou zvýrazněné.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int StudentID { get; set; }
[StringLength(50, MinimumLength = 1)]
public string LastName { get; set; }
[StringLength(50, MinimumLength = 1, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Entita registrace
V části Models\Enrollment.cs nahraďte dříve přidaný kód následujícím kódem.
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; }
}
}
Vlastnosti cizího klíče a navigace
Vlastnosti cizího klíče a navigační vlastnosti odrážejí následující relace:
Záznam registrace se používá pro jeden kurz, takže existuje vlastnost cizího
CourseID
klíče aCourse
vlastnost navigace:public int CourseID { get; set; } public virtual Course Course { get; set; }
Záznam registrace je pro jednoho studenta, takže je k dispozici vlastnost cizího
StudentID
klíče aStudent
vlastnost navigace:public int StudentID { get; set; } public virtual Student Student { get; set; }
Relace M:N
Mezi entitami a Course
existuje relace Student
M:N a Enrollment
entita funguje jako tabulka spojení M:N s datovou částí v databázi. To znamená, že Enrollment
tabulka obsahuje další data kromě cizích klíčů pro spojené tabulky (v tomto případě primární klíč a Grade
vlastnost).
Následující obrázek znázorňuje, jak tyto relace vypadají v diagramu entit. (Tento diagram se vygeneroval pomocí power tools entity frameworku; vytvoření diagramu není součástí kurzu, ale používá se tady jenom jako ilustrace.)
Každá čára relace má na jednom konci 1 a hvězdičku (*) na druhém, což označuje relaci 1:N.
Enrollment
Pokud tabulka neobsahuje informace o známce, bude muset obsahovat pouze dva cizí klíče CourseID
a StudentID
. V takovém případě by odpovídala tabulce M:N v databázi bez datové části (nebo tabulky čistého spojení) a vůbec byste pro ni nemuseli vytvářet třídu modelu. Entity Instructor
a Course
mají takový druh vztahu M:N, a jak vidíte, mezi nimi není žádná třída entit:
V databázi je však vyžadována tabulka spojení, jak je znázorněno v následujícím diagramu databáze:
Entity Framework automaticky vytvoří CourseInstructor
tabulku a vy ji čtete a aktualizujete nepřímo čtením a aktualizací Instructor.Courses
vlastností a Course.Instructors
navigace.
Diagram entit znázorňující relace
Následující obrázek znázorňuje diagram, který entity Framework Power Tools vytvoří pro dokončený školní model.
Kromě čar relace M:N (* až *) a čar relace 1:N (1:*) můžete vidět mezi entitami a entitami Instruktor a Department přímku relace 1:N (1 až 0...1) mezi Instructor
entitami a OfficeAssignment
nulou nebo 1:N (0...1 až *).
Přizpůsobení datového modelu přidáním kódu do kontextu databáze
Dále přidáte nové entity do SchoolContext
třídy a přizpůsobíte některé mapování pomocí fluent api volání. (Rozhraní API je "fluent", protože se často používá při řetězení řady volání metod do jednoho příkazu.)
V tomto kurzu použijete rozhraní FLUENT API jenom pro mapování databáze, které nemůžete dělat s atributy. Můžete ale také použít rozhraní FLUENT API k určení většiny pravidel formátování, ověřování a mapování, která můžete provést pomocí atributů. Některé atributy, například MinimumLength
, se nedají použít s rozhraním FLUENT API. Jak je uvedeno výše, MinimumLength
nemění schéma, použije se pouze ověřovací pravidlo na straně klienta a serveru.
Někteří vývojáři raději používají rozhraní FLUENT API výhradně, aby mohli udržovat třídy entit "čisté". Pokud chcete, můžete kombinovat atributy a rozhraní API fluent a existuje několik přizpůsobení, která se dají provést pouze pomocí rozhraní FLUENT API, ale obecně se doporučuje zvolit jeden z těchto dvou přístupů a používat ho co nejvíce konzistentně.
Pokud chcete do datového modelu přidat nové entity a provést mapování databáze, které jste neudělali pomocí atributů, nahraďte kód v souboru DAL\SchoolContext.cs následujícím kódem:
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"));
}
}
}
Nový příkaz v metodě OnModelCreating konfiguruje tabulku spojení M:N:
Pro relaci M:N mezi
Instructor
entitami aCourse
určuje kód názvy tabulek a sloupců pro tabulku spojení. Code First za vás může nakonfigurovat relaci M:N bez tohoto kódu, ale pokud ho nezavoláte, zobrazí se výchozí názvyInstructorInstructorID
jako proInstructorID
sloupec.modelBuilder.Entity<Course>() .HasMany(c => c.Instructors).WithMany(i => i.Courses) .Map(t => t.MapLeftKey("CourseID") .MapRightKey("InstructorID") .ToTable("CourseInstructor"));
Následující kód poskytuje příklad, jak byste mohli k určení vztahu mezi Instructor
entitami a OfficeAssignment
použít fluent API místo atributů:
modelBuilder.Entity<Instructor>()
.HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);
Informace o tom, co příkazy fluent API dělají na pozadí, najdete v blogovém příspěvku rozhraní Fluent API .
Nasypat databázi testovacími daty
Nahraďte kód v souboru Migrations\Configuration.cs následujícím kódem, aby bylo možné poskytnout počáteční data pro nové entity, které jste vytvořili.
namespace ContosoUniversity.Migrations
{
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SchoolContext context)
{
var students = new List<Student>
{
new Student { FirstMidName = "Carson", LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};
students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var instructors = new List<Instructor>
{
new Instructor { FirstMidName = "Kim", LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};
instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var departments = new List<Department>
{
new Department { Name = "English", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Abercrombie").InstructorID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").InstructorID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").InstructorID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Kapoor").InstructorID }
};
departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
context.SaveChanges();
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
var officeAssignments = new List<OfficeAssignment>
{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").InstructorID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Harui").InstructorID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Kapoor").InstructorID,
Location = "Thompson 304" },
};
officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.Location, s));
context.SaveChanges();
AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
AddOrUpdateInstructor(context, "Chemistry", "Harui");
AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");
AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
AddOrUpdateInstructor(context, "Trigonometry", "Harui");
AddOrUpdateInstructor(context, "Composition", "Abercrombie");
AddOrUpdateInstructor(context, "Literature", "Abercrombie");
context.SaveChanges();
var enrollments = new List<Enrollment>
{
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").StudentID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").StudentID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").StudentID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").StudentID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").StudentID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};
foreach (Enrollment e in enrollments)
{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.StudentID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
{
var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
if (inst == null)
crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
}
}
}
Jak jste viděli v prvním kurzu, většina tohoto kódu jednoduše aktualizuje nebo vytvoří nové objekty entit a načte ukázková data do vlastností podle potřeby pro testování. Všimněte si ale, jak Course
se zpracovává entita, která má s entitou Instructor
vztah M:N:
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
Department = departments.Single( s => s.Name == "Engineering"),
Instructors = new List<Instructor>()
},
...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
Při vytváření objektu Course
inicializujete Instructors
vlastnost navigace jako prázdnou kolekci pomocí kódu Instructors = new List<Instructor>()
. To umožňuje přidat Instructor
entity, které s tím Course
souvisejí, pomocí Instructors.Add
metody . Pokud byste nevytvořili prázdný seznam, nemohli byste tyto relace přidat, protože Instructors
vlastnost by byla null a neměla by metodu Add
. Inicializaci seznamu můžete také přidat do konstruktoru.
Přidání migrace a aktualizace databáze
V PMC zadejte add-migration
příkaz:
PM> add-Migration Chap4
Pokud se v tomto okamžiku pokusíte databázi aktualizovat, zobrazí se následující chyba:
Příkaz ALTER TABLE byl v konfliktu s omezením CIZÍHO KLÍČE "FK_dbo. Course_dbo. Department_DepartmentID". Ke konfliktu došlo v databázi ContosoUniversity, tabulce dbo. Department", sloupec 'DepartmentID'.
< Upravte soubor časového razítka>_Chap4.cs a proveďte následující změny kódu (přidáte příkaz SQL a upravíte AddColumn
příkaz):
CreateTable(
"dbo.CourseInstructor",
c => new
{
CourseID = c.Int(nullable: false),
InstructorID = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.CourseID, t.InstructorID })
.ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
.ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
.Index(t => t.CourseID)
.Index(t => t.InstructorID);
// Create a department for course to point to.
Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// default value for FK points to department created above.
AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1));
//AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));
AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));
AddForeignKey("dbo.Course", "DepartmentID", "dbo.Department", "DepartmentID", cascadeDelete: true);
CreateIndex("dbo.Course", "DepartmentID");
}
public override void Down()
{
(Při přidávání nového řádku nezapomeňte stávající řádek okomentovat nebo odstranit AddColumn
, jinak se při zadávání update-database
příkazu zobrazí chyba.)
Někdy při provádění migrací s existujícími daty potřebujete do databáze vložit zasunutá data, aby se splnila omezení cizího klíče, a to je to, co teď děláte. Vygenerovaný kód přidá do tabulky cizí klíč s možnou DepartmentID
Course
hodnotou null. Pokud už při spuštění kódu v Course
tabulce existují řádky, operace by selhala, AddColumn
protože SQL Server neví, jakou hodnotu vložit do sloupce, která nemůže mít hodnotu null. Proto jste změnili kód tak, aby dal novému sloupci výchozí hodnotu, a vytvořili jste oddělení s zástupnými kódy s názvem Temp, které bude fungovat jako výchozí oddělení. V důsledku toho, pokud při spuštění tohoto kódu existují Course
řádky, budou všechny souviset s dočasným oddělením.
Když se Seed
metoda spustí, vloží do tabulky řádky Department
a bude s těmito novými Department
řádky souviset existující Course
řádky. Pokud jste do uživatelského rozhraní nepřidali žádné kurzy, už nebudete potřebovat dočasné oddělení ani výchozí hodnotu ve sloupci Course.DepartmentID
. Abyste umožnili možnost, že někdo přidal kurzy pomocí aplikace, měli byste také aktualizovat Seed
kód metody, aby se zajistilo, že všechny Course
řádky (nejen řádky vložené dřívějšími spuštěními Seed
metody) budou mít platné DepartmentID
hodnoty před odebráním výchozí hodnoty ze sloupce a odstraněním oddělení Temp.
Po dokončení úprav < souboru časového razítka>_Chap4.cs zadejte update-database
v PMC příkaz pro spuštění migrace.
Poznámka
Při migraci dat a provádění změn schématu se můžou zobrazit další chyby. Pokud dojde k chybám migrace, které nemůžete vyřešit, můžete buď změnit připojovací řetězec v souboruWeb.config , nebo odstranit databázi. Nejjednodušším způsobem je přejmenovat databázi v souboruWeb.config . Změňte například název databáze na CU_test, jak je znázorněno na následujícím obrázku:
<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;
Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\CU_Test.mdf"
providerName="System.Data.SqlClient" />
U nové databáze nejsou k dispozici žádná data, která by bylo možné migrovat, a update-database
je mnohem pravděpodobnější, že se příkaz dokončí bez chyb. Pokyny k odstranění databáze najdete v tématu Jak odstranit databázi ze sady Visual Studio 2012.
Otevřete databázi v Průzkumníku serveru stejně jako dříve a rozbalte uzel Tabulky , abyste viděli, že byly vytvořeny všechny tabulky. (Pokud máte Průzkumník serveru stále otevřený z dřívějšího času, klikněte na tlačítko Aktualizovat .)
Nevytvořili jste pro CourseInstructor
tabulku třídu modelu. Jak bylo vysvětleno dříve, jedná se o tabulku spojení pro relaci M:N mezi Instructor
entitami a Course
.
Klikněte pravým tlačítkem na CourseInstructor
tabulku a vyberte Zobrazit data tabulky a ověřte, že obsahuje data v důsledku Instructor
entit, které jste přidali do Course.Instructors
vlastnosti navigace.
Souhrn
Teď máte složitější datový model a odpovídající databázi. V následujícím kurzu se dozvíte více o různých způsobech přístupu k souvisejícím datům.
Odkazy na další prostředky Entity Framework najdete v mapě obsahu ASP.NET Data Access.
Váš názor
https://aka.ms/ContentUserFeedback.
Připravujeme: V průběhu roku 2024 budeme postupně vyřazovat problémy z GitHub coby mechanismus zpětné vazby pro obsah a nahrazovat ho novým systémem zpětné vazby. Další informace naleznete v tématu:Odeslat a zobrazit názory pro