Kurz: Vytvoření komplexního datového modelu – ASP.NET MVC s EF Core

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í.

Jakmile budete hotovi, třídy entit tvoří dokončený datový model, který je znázorněn na následujícím obrázku:

Diagram entit

V tomto kurzu jste:

  • Přizpůsobení datového modelu
  • Provedení změn entity Student
  • Vytvoření entity instruktora
  • Vytvoření entity OfficeAssignment
  • Úprava entity Course
  • Vytvoření entity oddělení
  • Úprava entity Registrace
  • Aktualizace kontextu databáze
  • Dosycená databáze s testovacími daty
  • Přidání migrace
  • Změna připojovacího řetězce
  • Aktualizace databáze

Požadavky

Přizpůsobení datového modelu

V této části uvidíte, jak přizpůsobit datový model pomocí atributů, které určují pravidla formátování, ověřování a mapování databází. Potom v několika následujících částech vytvoříte kompletní datový model School přidáním atributů do tříd, které jste už vytvořili, a vytvořením nových tříd pro zbývající typy entit v modelu.

Atribut DataType

Pro data registrace studentů se na všech webových stránkách aktuálně zobrazuje čas spolu s datem, i když vám na tomto poli záleží jen na datu. 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 zobrazit příklad, jak to provést, přidáte atribut do EnrollmentDate vlastnosti ve Student třídě .

V souboru Models/Student.cs přidejte příkaz pro obor názvů a přidejte atributy a do vlastnosti , jak je using System.ComponentModel.DataAnnotations DataType DisplayFormat EnrollmentDate 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 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; }
    }
}

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, nikoli datum a čas. Výčet DataType poskytuje mnoho datových typů, například Datum, Čas, PhoneNumber, Měna, EmailAddress a další. Atribut DataType může také aplikaci umožnit automatické poskytování funkcí specifických pro konkrétní typ. Můžete například vytvořit odkaz pro a v prohlížečích, které podporují HTML5, selektor mailto: DataType.EmailAddress DataType.Date data. Atribut DataType vysílá atributy HTML 5 (vyslovuje se datový pomlčka), které rozumí prohlížeč data- HTML 5. Atributy DataType neposkytují žádné ověření.

DataType.Date nezadá formát data, které se zobrazí. Ve výchozím nastavení se datové pole zobrazuje podle výchozích formátů na základě informací o jazykové verzi serveru.

Atribut DisplayFormat se používá k explicitním zadání formátu data:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

Nastavení určuje, že formátování se má použít také v případě, že se hodnota zobrazí v ApplyFormatInEditMode textovém poli pro úpravy. (U některých polí to pravděpodobně nechcete – například u hodnot měny možná nechcete symbol měny v textovém poli pro úpravy.)

Atribut můžete použít sám o sobě, ale obecně je vhodné použít DisplayFormat DataType také atribut . Atribut DataType vyjadřuje sémantiku dat místo toho, jak je vykreslit na obrazovce, a poskytuje následující výhody, které s ním DisplayFormat nemáte:

  • Prohlížeč může povolit funkce HTML5 (například zobrazit ovládací prvek kalendáře, symbol měny odpovídající národní prostředí, e-mailové odkazy, některé ověřování vstupu na straně klienta atd.).

  • Ve výchozím nastavení prohlížeč vykreslí data ve správném formátu na základě vašeho národního prostředí.

Další informace najdete v dokumentaci <input> k pomocná značek.

Spusťte aplikaci, přejděte na stránku Students Index a všimněte si, že časy se už pro data registrace nezobrazí. Totéž platí pro každé zobrazení, které používá model Student.

Stránka indexu Students zobrazující data bez časů

Atribut StringLength

Pomocí atributů můžete také zadat pravidla ověřování dat a chybové zprávy ověřování. Atribut nastaví maximální délku databáze a poskytuje ověřování na straně klienta a serveru StringLength pro ASP.NET Core 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.

Předpokládejme, že chcete zajistit, aby uživatelé nezadá název delší než 50 znaků. Pokud chcete toto omezení přidat, StringLength přidejte atributy do LastName vlastností FirstMidName a , 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 ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50)]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Atribut StringLength nezabrání uživateli v zadání prázdného místa pro jméno. Pomocí atributu RegularExpression můžete u vstupu použít omezení. 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]*$")]

Atribut MaxLength poskytuje funkce podobné StringLength atributu, ale neposkytuje ověřování na straně klienta.

Databázový model se teď změnil způsobem, který vyžaduje změnu ve schématu databáze. Pomocí migrací aktualizujete schéma bez ztráty dat, která jste přidali do databáze pomocí uživatelského rozhraní aplikace.

Uložte změny a sestavte projekt. Pak otevřete příkazové okno ve složce projektu a zadejte následující příkazy:

dotnet ef migrations add MaxLengthOnNames
dotnet ef database update

Příkaz varuje, že ke ztrátě dat může dojít, protože změna zkrátí maximální délku pro migrations add dva sloupce. Migrace vytvoří soubor s názvem <timeStamp> _MaxLengthOnNames.cs. Tento soubor obsahuje kód v metodě , který aktualizuje databázi tak, aby Up odpovídala aktuálnímu datovému modelu. Příkaz database update tento kód spustili.

Časové razítko s předponou názvu souboru migrace používá Entity Framework pořadí migrací. Před spuštěním příkazu update-database můžete vytvořit několik migrací a pak se všechny migrace použijí v pořadí, ve kterém byly vytvořeny.

Spusťte aplikaci, vyberte kartu Students (Studenti), klikněte na Create New (Vytvořit nový) a zkuste zadat název delší než 50 znaků. Aplikace by vám v tom měla zabránit.

Atribut Column

Atributy můžete použít také k řízení mapování tříd a vlastností na databázi. Předpokládejme, že jste použili název pole pro jméno, protože pole může také obsahovat FirstMidName prostřední jméno. Ale chcete, aby sloupec databáze měl název , protože uživatelé, kteří budou psát ad hoc dotazy na databázi, jsou na FirstName tento název zvyklí. Pokud chcete toto mapování provést, můžete použít Column atribut .

Atribut určuje, že při vytvoření databáze bude mít sloupec tabulky, která se mapuje Column na vlastnost , název Student FirstMidName FirstName . Jinými slovy, když váš kód odkazuje na , data budou pochádněná nebo aktualizovaná Student.FirstMidName ve FirstName sloupci Student tabulky. Pokud nezadáte názvy sloupců, budou mít stejný název jako název vlastnosti.

V souboru Student.cs přidejte příkaz pro a přidejte atribut názvu sloupce do vlastnosti , jak je znázorněno v using následujícím System.ComponentModel.DataAnnotations.Schema FirstMidName 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 ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50)]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Přidání atributu změní model za , aby se neshoduje Column SchoolContext s databází.

Uložte změny a sestavte projekt. Pak otevřete příkazové okno ve složce projektu a zadáním následujících příkazů vytvořte další migraci:

dotnet ef migrations add ColumnFirstName
dotnet ef database update

V SQL Server Průzkumník objektů otevřete návrháře tabulky Student poklikáním na tabulku Student.

Tabulka Students v SSOX po migraci

Před použitím prvních dvou migrací byly sloupce s názvem typu nvarchar(MAX). Jsou to nyní nvarchar(50) a název sloupce se změnil z FirstMidName na FirstName.

Poznámka

Pokud se pokusíte zkompilovat před dokončením vytváření všech tříd entit v následujících částech, může dojít k chybám kompilátoru.

Změny entity Student

Entita Student

V souboru Models/Student.cs nahraďte kód, který jste přidali dříve, 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 ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50)]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Atribut Required

Atribut Required vytvoří požadovaná pole vlastností názvu. Atribut není potřeba pro typy s možnou hodnotou null, jako jsou typy hodnot Required (DateTime, int, double, float atd.). Typy, které nemůže mít hodnotu null, jsou automaticky považovány za povinná pole.

Atribut Required musí být použit s , aby se MinimumLength MinimumLength vynucoval .

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

Atribut Display

Atribut určuje, že popisek textových polí by měl být Display "Jméno", "Příjmení", "Celé jméno" a "Datum registrace" místo názvu vlastnosti v každé instanci (který nemá mezeru, která vyděluje slova).

Vypočítaná vlastnost FullName

FullName je vypočtená vlastnost, která vrací hodnotu, která je vytvořena zřetězením dvou dalších vlastností. Proto má pouze přistupující objekt get a FullName v databázi nebude vygenerován žádný sloupec.

Vytvořit entitu instruktora

Entita instruktora

Vytvořte modely/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 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; }
    }
}

Všimněte si, že několik vlastností je stejné v entitách student a instruktor. V kurzu Implementace dědičnosti níže v této sérii můžete tento kód Refaktorovat, aby se vyloučila redundance.

Na jeden řádek můžete umístit více atributů, takže můžete také napsat HireDate atributy následujícím způsobem:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

Navigační vlastnosti CourseAssignments a OfficeAssignment

CourseAssignmentsVlastnosti a OfficeAssignment jsou navigační vlastnosti.

Instruktor může naučit libovolný počet kurzů, takže CourseAssignments je definován jako kolekce.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Pokud navigační vlastnost může obsahovat více entit, musí být její typ seznam, ve kterém je možné přidávat, odstraňovat a aktualizovat položky. Můžete zadat ICollection<T> nebo typ jako List<T> nebo HashSet<T> . Pokud zadáte ICollection<T> , EF vytvoří HashSet<T> ve výchozím nastavení kolekci.

Důvod, proč se jedná CourseAssignment o entity, jsou vysvětleny níže v části o relacích n:n.

Obchodní pravidla společnosti Contoso University mají stav, že instruktor může mít jenom jednu kancelář, takže OfficeAssignment vlastnost obsahuje jednu entitu OfficeAssignment (která může být null, pokud není přiřazená žádná sada Office).

public OfficeAssignment OfficeAssignment { get; set; }

Vytvořit entitu OfficeAssignment

OfficeAssignment – entita

Vytvořte modely/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]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Klíčový atribut

Mezi Instructor entitami a entitami je vztah 1:1 nebo jedna hodnota OfficeAssignment . Přiřazení kanceláře existuje jenom ve vztahu k instruktorovi, ke kterému je přiřazená, a proto jeho primární klíč je také cizímu klíči k Instructor entitě. Entity Framework ale nemůžou automaticky rozpoznat InstructorID jako primární klíč této entity, protože její název nedodržuje ID classnameID konvence pojmenování nebo. Proto Key atribut slouží k identifikaci jako klíč:

[Key]
public int InstructorID { get; set; }

Atribut můžete použít také v případě, že má Key entita svůj vlastní primární klíč, ale chcete pojmenovat vlastnost jinou než classnameID nebo ID.

Ve výchozím nastavení EF považuje klíč za generovaný nedatabází, protože sloupec je určen pro identifikaci vztahu.

Navigační vlastnost instruktora

Entita Instructor má vlastnost navigace s možnou hodnotou null OfficeAssignment (protože instruktor nemusí mít přiřazený Office) a entita OfficeAssignment má vlastnost navigace, která nemůže mít hodnotu null Instructor (protože přiřazení kanceláře nemůže existovat bez instruktora-- InstructorID nepovoluje hodnotu null). Pokud má entita instruktora související entitu OfficeAssignment, bude mít Každá entita odkaz na jinou entitu v její navigační vlastnosti.

Můžete umístit atribut do [Required] navigační vlastnosti instruktora a určit tak, že se musí jednat o související instruktor, ale nemusíte to dělat, protože InstructorID cizí klíč (což je také klíč k této tabulce) nemůže mít hodnotu null.

Upravit entitu kurzu

Entita kurzu

V modelu/Course. cs nahraďte kód, který jste přidali dříve, následujícím kódem. Změny jsou zvýrazněny.

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; }
    }
}

Entita kurzu má vlastnost cizího klíče, DepartmentID která odkazuje na související entitu oddělení a má Department vlastnost navigace.

Entity Framework nevyžaduje, abyste do datového modelu přidali vlastnost cizího klíče, když máte vlastnost navigace pro související entitu. EF v databázi automaticky vytvoří cizí klíče bez ohledu na to, kde jsou potřeba, a vytvoří pro ně vlastnosti stínu . Ale při používání cizího klíče v datovém modelu může být aktualizace jednodušší a efektivnější. Když například načtete Course entitu, která se má upravit, má Department entita hodnotu null, pokud ji nenačtete, takže když Course entitu aktualizujete, budete muset nejdřív načíst Department entitu. Pokud DepartmentID je v datovém modelu zahrnutá vlastnost cizího klíče, nemusíte Department před aktualizací entitu načítat.

Atribut DatabaseGenerated

DatabaseGeneratedAtribut s None parametrem CourseID vlastnosti určuje, že uživatel zadá hodnoty primárního klíče místo vygenerovaného databází.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

Ve výchozím nastavení Entity Framework předpokládá, že databáze generuje hodnoty primárního klíče. To je to, co chcete ve většině scénářů. U entit ale Course použijete uživatelem zadané číslo kurzu, jako je například řada 1000, pro jedno oddělení, 2000 Series pro jiné oddělení a tak dále.

DatabaseGeneratedAtribut lze také použít ke generování výchozích hodnot, jako v případě databázových sloupců použitých k záznamu data vytvoření nebo aktualizace řádku. Další informace najdete v tématu vygenerované vlastnosti.

Vlastnosti cizích klíčů a navigace

Vlastnosti cizího klíče a navigační vlastnosti v Course entitě odráží následující vztahy:

Kurz se přiřadí jednomu oddělení, takže existuje DepartmentID cizí klíč a Department vlastnost navigace z výše uvedených důvodů.

public int DepartmentID { get; set; }
public Department Department { get; set; }

V rámci kurzu může být zaregistrované několik studentů, takže Enrollments navigační vlastnost je kolekce:

public ICollection<Enrollment> Enrollments { get; set; }

Kurz může být výukou více instruktorů, takže CourseAssignments navigační vlastnost je kolekce (Tento typ CourseAssignment je vysvětlen později):

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Vytvořit entitu oddělení

Entita oddělení

Vytvořte modely/oddělení. 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)]
        [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; }
    }
}

Atribut Column

Dříve jste použili Column atribut pro změnu mapování názvu sloupce. v kódu pro Department entitu se Column atribut používá ke změně SQL mapování datových typů, takže sloupec bude definován pomocí SQL Server money typu v databázi:

[Column(TypeName="money")]
public decimal Budget { get; set; }

mapování sloupce není obecně vyžadováno, protože Entity Framework zvolí vhodný SQL Server datový typ založený na typu CLR, který definujete pro vlastnost. typ CLR se decimal mapuje na typ SQL Server decimal . Ale v tomto případě víte, že se ve sloupci budou držet peněžní částky, a datový typ Money je pro to vhodnější.

Vlastnosti cizích klíčů a navigace

Vlastnosti cizího klíče a navigace odrážejí následující vztahy:

Oddělení může nebo nemusí mít správce a správce je vždy instruktorem. Proto InstructorID je vlastnost zahrnutá jako cizí klíč k entitě instruktor a po int označení typu k označení vlastnosti jako Nullable se přidá otazník. Navigační vlastnost má název, Administrator ale obsahuje entitu instruktora:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

Oddělení může mít spoustu kurzů, takže máme navigační vlastnost kurzů:

public ICollection<Course> Courses { get; set; }

Poznámka

Podle konvence Entity Framework umožňuje odstranit Kaskádové odstraňování cizích klíčů, které neumožňují hodnotu null, a pro relace m:n. Výsledkem může být cyklická kaskádová odstranění pravidel, která způsobí výjimku při pokusu o přidání migrace. Například pokud jste vlastnost nedefinovali Department.InstructorID jako Nullable, EF by nakonfigurovali pravidlo kaskádového odstranění, které odstraní oddělení, když odstraníte instruktora, což nevede k tomu, co chcete mít. Pokud vaše obchodní pravidla vyžadují InstructorID vlastnost, která není null, budete muset použít následující příkaz rozhraní Fluent API, který v relaci zakáže kaskádové odstranění:

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

Upravit entitu registrace

Entita registrace

V modelů/zápisu. cs nahraďte kód, který jste přidali dříve, pomocí následujícího kódu:

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; }
    }
}

Vlastnosti cizích klíčů a navigace

Vlastnosti cizího klíče a vlastnosti navigace odrážejí následující vztahy:

Záznam zápisu je pro jeden kurz, takže existuje CourseID vlastnost cizího klíče a Course navigační vlastnost:

public int CourseID { get; set; }
public Course Course { get; set; }

Záznam zápisu je určen pro jednoho studenta, takže existuje StudentID vlastnost cizího klíče a Student navigační vlastnost:

public int StudentID { get; set; }
public Student Student { get; set; }

Relace m:n

Mezi Student entitami a je relace m:n Course a Enrollment entita funguje jako tabulka JOIN typu m:n s datovou částí v databázi. "S datovou částí" 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í ilustrace znázorňuje, co tyto vztahy vypadají jako v diagramu entit. (Tento diagram byl vygenerován pomocí Entity Framework nástrojů Power Tools pro EF 6. x; vytvoření diagramu není součástí kurzu, stačí ho použít jako ilustraci.)

Vztah Student-Course mnoha k mnoha

Každá čára relace má 1 na jednom konci a hvězdičku (*) na druhé straně, která indikuje relaci 1: n.

Pokud Enrollment tabulka neobsahuje informace o třídě, musí obsahovat pouze dva cizí klíče CourseID a StudentID . V takovém případě by to byla tabulka JOIN typu m:n (celá řada) bez datové části (nebo čistá spojovací tabulka) v databázi. InstructorEntity a Course mají tento druh vztahu n:n a vaším dalším krokem je vytvoření třídy entity, která bude fungovat jako spojovací tabulka bez datové části.

EF Core podporuje implicitní spojení tabulek pro relace m:n, ale tato Tutoral nebyla aktualizována, aby používala implicitní spojovací tabulku. Podívejte se na téma relace m:n, Razor verze tohoto kurzu, který se aktualizoval.

Entita CourseAssignment

CourseAssignment – entita

Vytvořte modely/CourseAssignment. 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 CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

Spojování názvů entit

V databázi je vyžadována tabulka JOIN pro relaci n:n typu n:1 a musí být reprezentovaná sadou entit... Je běžné pojmenovat entitu Join EntityName1EntityName2 , která by v tomto případě byla CourseInstructor . Doporučujeme však zvolit název, který popisuje vztah. Modely dat začínají jednoduchým a roste a často se nepoužívají spojení bez datové části, které později načítá datovou část. Pokud začnete s popisným názvem entity, nebudete muset později změnit název. V ideálním případě by entita JOIN měla vlastní přirozený název (případně jeden Word) v obchodní doméně. Například knihy a zákazníci mohou být propojeni pomocí hodnocení. Pro tento vztah CourseAssignment je lepší volbou než CourseInstructor .

Složený klíč

Vzhledem k tomu, že cizí klíče neumožňují hodnotu null a společně jednoznačně identifikují každý řádek tabulky, není nutné používat samostatný primární klíč. InstructorIDVlastnosti a CourseID by měly fungovat jako složený primární klíč. Jediným způsobem, jak identifikovat složené primární klíče k EF, je použití rozhraní Fluent API (nemůže být provedeno pomocí atributů). V další části se dozvíte, jak nakonfigurovat složený primární klíč.

Složený klíč zajišťuje, že i když můžete mít více řádků pro jeden kurz a více řádků pro jednoho instruktora, nemůžete mít pro stejný instruktor a kurz více řádků. EnrollmentEntita JOIN definuje svůj vlastní primární klíč, takže je možné duplicity tohoto řazení provést. Aby tyto duplicity nedocházelo, mohli byste do polí cizího klíče přidat jedinečný index nebo nakonfigurovat Enrollment primární složený klíč podobný tomuto CourseAssignment :. Další informace najdete v tématu indexy.

Aktualizace kontextu databáze

Do souboru data/SchoolContext. cs přidejte následující zvýrazněný kód:

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 });
        }
    }
}

Tento kód přidá nové entity a nakonfiguruje složený primární klíč entity CourseAssignment.

O alternativní rozhraní API Fluent

Kód v OnModelCreating metodě DbContext třídy používá rozhraní Fluent API ke konfiguraci chování EF. Rozhraní API se nazývá "Fluent", protože se často používá k zřetězení řady volání metody do jednoho příkazu, jako v tomto příkladu v dokumentaci EF Core:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

V tomto kurzu používáte rozhraní Fluent API jenom pro mapování databáze, které nemůžete s atributy dělat. Rozhraní Fluent API ale můžete použít i k určení většiny pravidel formátování, ověřování a mapování, která můžete provádět pomocí atributů. Některé atributy, jako například, MinimumLength se nedají použít s rozhraním API Fluent. Jak bylo zmíněno dříve, MinimumLength nemění schéma, používá pouze pravidlo ověřování na straně klienta a serveru.

Někteří vývojáři dávají přednost použití rozhraní Fluent API, aby mohli zachovat třídy entit "vyčistit". V případě potřeby můžete kombinovat atributy a rozhraní API Fluent a existuje několik úprav, které je možné provést jenom pomocí rozhraní Fluent API, ale obecně doporučujeme zvolit jednu z těchto dvou přístupů a použít ji konzistentně co nejvíce. pokud použijete obojí, pamatujte, že pokud dojde ke konfliktu, Fluent rozhraní API přepisuje atributy.

Další informace o atributech vs. Fluent API najdete v tématu metody konfigurace.

Diagram entit znázorňující vztahy

Následující ilustrace znázorňuje diagram, který nástroje Entity Framework Power Tools vytvoří pro dokončený školní model.

Diagram entit

Kromě čar vztahů 1:1 (1 – n * ) se tady můžete podívat na řádek relace 1:1 (1 – 0.. 1) mezi Instructor OfficeAssignment entitami a a řádkem relace 0.. 1 až n z. mezi entitami instruktora a Department (0.. 1 až *).

Počáteční databáze s testovacími daty

Nahraďte kód v souboru data/DbInitializer. cs následujícím kódem, aby bylo možné poskytnout počáteční data pro nově vytvořené entity.

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

Jak jste viděli v prvním kurzu, většina tohoto kódu jednoduše vytvoří nové objekty entity a načte vzorová data do vlastností podle potřeby pro testování. Všimněte si, jak jsou zpracovávány relace m:n: kód vytvoří relace vytvořením entit v Enrollments CourseAssignment sadách entit a.

Přidání migrace

Uložte změny a sestavte projekt. Pak otevřete příkazové okno ve složce projektu a zadejte migrations add příkaz (zatím neprovádějte příkaz Update-Database):

dotnet ef migrations add ComplexDataModel

Zobrazí se upozornění na možnou ztrátu dat.

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'

Pokud jste se database update v tomto okamžiku pokusili spustit příkaz (ještě to neuděláte), zobrazí se následující chyba:

Příkaz ALTER TABLE je 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". Oddělení ", sloupec" DepartmentID ".

Při provádění migrace s existujícími daty je někdy potřeba vložit do databáze zástupná data, aby bylo možné splnit omezení cizího klíče. Vygenerovaný kód v Up metodě přidá DepartmentID do tabulky cizí klíč, který nemůže mít hodnotu null Course . pokud se v tabulce kurzu již nacházejí řádky, když je kód spuštěn, AddColumn operace se nezdařila, protože SQL Server neví, jaká hodnota má být vložena do sloupce, který nemůže mít hodnotu null. Pro účely tohoto kurzu spustíte migraci na nové databázi, ale v produkční aplikaci, kterou byste museli udělat, aby migrace zpracovala existující data, takže následující pokyny ukazují příklad toho, jak to udělat.

Pokud chcete, aby tato migrace fungovala se stávajícími daty, je nutné změnit kód tak, aby nový sloupec poskytoval výchozí hodnotu, a vytvořit oddělení zástupných procedur s názvem "Temp", které bude fungovat jako výchozí oddělení. V důsledku toho budou existující řádky kurzu až po spuštění metody v relaci s "dočasným" oddělením Up .

  • Otevřete soubor {timestamp} _ComplexDataModel. cs .

  • Odkomentujte řádek kódu, který přidá sloupec DepartmentID do tabulky Course.

    migrationBuilder.AlterColumn<string>(
        name: "Title",
        table: "Course",
        maxLength: 50,
        nullable: true,
        oldClrType: typeof(string),
        oldNullable: true);
                
    //migrationBuilder.AddColumn<int>(
    //    name: "DepartmentID",
    //    table: "Course",
    //    nullable: false,
    //    defaultValue: 0);
    
  • Po kódu, který vytvoří tabulku oddělení, přidejte následující zvýrazněný kód:

    migrationBuilder.CreateTable(
        name: "Department",
        columns: table => new
        {
            DepartmentID = table.Column<int>(nullable: false)
                .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
            Budget = table.Column<decimal>(type: "money", nullable: false),
            InstructorID = table.Column<int>(nullable: true),
            Name = table.Column<string>(maxLength: 50, nullable: true),
            StartDate = table.Column<DateTime>(nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Department", x => x.DepartmentID);
            table.ForeignKey(
                name: "FK_Department_Instructor_InstructorID",
                column: x => x.InstructorID,
                principalTable: "Instructor",
                principalColumn: "ID",
                onDelete: ReferentialAction.Restrict);
        });
    
    migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    // Default value for FK points to department created above, with
    // defaultValue changed to 1 in following AddColumn statement.
    
    migrationBuilder.AddColumn<int>(
        name: "DepartmentID",
        table: "Course",
        nullable: false,
        defaultValue: 1);
    

V produkční aplikaci byste mohli napsat kód nebo skripty pro přidání řádků oddělení a související řádky kurzu pro nové řádky oddělení. Nebudete už potřebovat "dočasné" oddělení, nebo výchozí hodnotu Course.DepartmentID sloupce.

Uložte změny a sestavte projekt.

Změna připojovacího řetězce

Nyní máte nový kód ve DbInitializer třídě, který přidá počáteční data pro nové entity do prázdné databáze. Chcete-li, aby EF vytvořil novou prázdnou databázi, změňte název databáze v připojovacím řetězci v appsettings.json na ContosoUniversity3 nebo jiný název, který jste nepoužili v počítači, který používáte.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

Uložte změnu do appsettings.json .

Poznámka

Jako alternativu ke změně názvu databáze můžete databázi odstranit. použijte SQL Server Průzkumník objektů (SSOX) nebo database drop příkaz CLI:

dotnet ef database drop

Aktualizace databáze

Poté, co jste změnili název databáze nebo odstranili databázi, spusťte database update příkaz v příkazovém okně a spusťte migrace.

dotnet ef database update

Spusťte aplikaci, aby se DbInitializer.Initialize Metoda spustila a naplnila nová databáze.

Otevřete databázi v SSOX jako dříve a rozbalte uzel tabulky , abyste viděli, že se vytvořily všechny tabulky. (Pokud máte stále SSOX otevřený ze staršího času, klikněte na tlačítko aktualizovat .)

Tabulky v SSOX

Spusťte aplikaci, aby aktivovala inicializační kód, který vysemena databáze.

Klikněte pravým tlačítkem na tabulku CourseAssignment a vyberte Zobrazit data a ověřte, zda obsahuje data.

Data CourseAssignment v SSOX

Získání kódu

Stažení nebo zobrazení dokončené aplikace.

Další kroky

V tomto kurzu jste:

  • Přizpůsobení datového modelu
  • Provedli jsme změny entity studenta
  • Vytvořená entita instruktora
  • Vytvořila se entita OfficeAssignment
  • Upravená entita kurzu
  • Entita vytvořeného oddělení
  • Upravená entita registrace
  • Aktualizace kontextu databáze
  • Dosazení databáze s testovacími daty
  • Přidání migrace
  • Změna připojovacího řetězce
  • Aktualizace databáze

V dalším kurzu se dozvíte víc o tom, jak přistupovat k souvisejícím datům.