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

V předchozích kurzech jste pracovali s jednoduchým datovým modelem složeným ze tří entit. V tomto kurzu přidáte další entity a relace a datový model přizpůsobíte zadáním formátování, ověření a pravidel mapování databáze.

Po dokončení tvoří třídy entit dokončený datový model, který je zobrazený na následujícím obrázku:

Entity diagram

V tomto kurzu se naučíte:

  • Přizpůsobení datového modelu
  • Provádění změn v entitě Student
  • Vytvoření entity Instruktor
  • Vytvoření entity OfficeAssignment
  • Úprava entity kurzu
  • Vytvoření entity oddělení
  • Úprava entity registrace
  • Aktualizace kontextu databáze
  • Počáteční databáze s testovacími daty
  • Přidání migrace
  • Změna připojovací řetězec
  • Aktualizace databáze

Požadavky

Přizpůsobení datového modelu

V této části se dozvíte, jak přizpůsobit datový model pomocí atributů, které určují formátování, ověřování a pravidla mapování databáze. Potom v několika následujících částech vytvoříte kompletní datový model Školy 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

U dat zápisu studentů se na všech webových stránkách aktuálně zobrazuje čas spolu s datem, i když všechno, co vás zajímá o toto pole, 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 toho, jak to udělat, přidáte do vlastnosti ve Student třídě atributEnrollmentDate.

V Models/Student.cs, přidat using příkaz pro System.ComponentModel.DataAnnotations obor názvů a přidat DataType a DisplayFormat atributy do EnrollmentDate vlastnosti, 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; }
        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 se používá 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ů, jako je datum, čas, Telefon Číslo, Měna, EmailAddress a další. Atribut DataType může také aplikaci povolit, aby automaticky poskytovala funkce specifické pro typ. Například mailto: lze vytvořit odkaz pro DataType.EmailAddressa selektor data lze zadat v DataType.Date prohlížečích, které podporují HTML5. Atribut DataType generuje atributy HTML 5 data- (vyslovující datový spojovník), které můžou prohlížeče HTML 5 pochopit. Atributy DataType neposkytují žádné ověření.

DataType.Date nezadá formát zobrazeného data. Ve výchozím nastavení se datové pole zobrazí podle výchozích formátů založených na souboru CultureInfo serveru.

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

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

Nastavení ApplyFormatInEditMode určuje, že formátování se má použít také při zobrazení hodnoty v textovém poli pro úpravy. (U některých polí to možná nechcete – například pro hodnoty měny, nemusíte chtít symbol měny v textovém poli pro úpravy.)

Atribut můžete použít DisplayFormat samostatně, ale obecně je vhodné atribut použít DataType také. Atribut DataType vyjadřuje sémantiku dat na rozdíl od toho, jak je vykreslit na obrazovce, a poskytuje následující výhody, se DisplayFormatkterými se nedostanete:

  • Prohlížeč může povolit funkce HTML5 (například zobrazení ovládacího prvku kalendáře, symbol měny odpovídající národnímu prostředí, e-mailové odkazy, ověření vstupu na straně klienta 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í.

Další informace najdete v dokumentaci pomocné rutiny <vstupních> značek.

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

Students index page showing dates without times

Atribut StringLength

Pomocí atributů můžete také zadat pravidla ověření dat a chybové zprávy ověření. Atribut StringLength nastaví maximální délku databáze a poskytuje ověřování na straně klienta a na straně serveru pro ASP.NET Core MVC. Můžete také zadat minimální délku řetězce v tomto atributu, ale minimální hodnota nemá žádný vliv na schéma databáze.

Předpokládejme, že chcete zajistit, aby uživatelé nezadáli více než 50 znaků pro jméno. Chcete-li přidat toto omezení, přidejte StringLength atributy do LastName a FirstMidName vlastnosti, 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ávání prázdných znaků pro jméno. Atribut můžete použít RegularExpression k použití omezení pro vstup. Například následující kód vyžaduje, aby první znak byl velkým písmenem a zbývající znaky byly abecední:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

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

Model databáze se teď změnil způsobem, který vyžaduje změnu ve schématu databáze. Migrace použijete k aktualizaci schématu bez ztráty dat, která jste mohli přidat 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 migrations add varuje, že může dojít ke ztrátě dat, protože změna zkracuje maximální délku pro dva sloupce. Migrace vytvoří soubor s názvem <timeStamp>_MaxLengthOnNames.cs. Tento soubor obsahuje kód v Up metodě, která aktualizuje databázi tak, aby odpovídala aktuálnímu datovému modelu. Příkaz database update spustil tento kód.

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

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

Atribut Column

Pomocí atributů můžete také řídit, jak se třídy a vlastnosti mapují na databázi. Předpokládejme, že jste použili název FirstMidName pole pro jméno, protože pole může obsahovat také prostřední název. Ale chcete, aby byl databázový sloupec pojmenován FirstName, protože uživatelé, kteří budou psát ad hoc dotazy na databázi, jsou zvyklí na tento název. K vytvoření tohoto mapování můžete použít Column atribut.

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

Student.cs V souboru přidejte using příkaz pro System.ComponentModel.DataAnnotations.Schema a přidejte atribut názvu sloupce do FirstMidName vlastnosti, 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 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 Column změní model, který zálohuje SchoolContext, takže nebude odpovídat databázi.

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 Serveru Průzkumník objektů otevřete návrháře tabulek Student poklikáním na tabulku Student.

Students table in SSOX after migrations

Před použitím prvních dvou migrací byly sloupce názvů typu nvarchar(MAX). Jsou teď 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 v entitě Student

Student entity

Nahraďte Models/Student.csdří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 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; }
    }
}

Požadovaný atribut

Atribut Required vytvoří požadované vlastnosti názvu. Atribut Required není potřeba pro typy bez hodnoty null, jako jsou například typy hodnot (DateTime, int, double, float atd.). Typy, které nemohou být null, se automaticky považují za povinná pole.

Atribut Required musí být použit s vynuceným vynuceným atributem MinimumLengthMinimumLength .

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

Atribut Display

Atribut Display určuje, že popis pro textová pole by měla být "Jméno", "Příjmení", "Příjmení", "Celé jméno" a "Datum registrace" místo názvu vlastnosti v každé instanci (která neobsahuje mezeru rozdělující slova).

Počítaná vlastnost FullName

FullName je počítaná vlastnost, která vrací hodnotu vytvořenou zřetězením dvou dalších vlastností. Proto má pouze přístupové objekty get a v databázi se nevygeneruje žádný FullName sloupec.

Vytvoření entity Instruktor

Instructor entity

Vytvořte a nahraďte Models/Instructor.cskó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 v entitách studenta a instruktora je několik vlastností stejných. V kurzu Implementace dědičnosti dále v této sérii refaktorujete tento kód, abyste eliminovali redundanci.

Na jeden řádek můžete vložit více atributů, takže můžete atributy také napsat HireDate takto:

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

Vlastnosti navigace CourseAssignments a OfficeAssignment

Vlastnosti CourseAssignments a OfficeAssignment vlastnosti jsou navigační vlastnosti.

Instruktor může uč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 jeho typ seznamem, ve kterém lze přidávat, odstraňovat a aktualizovat položky. Můžete zadat nebo zadat ICollection<T> typ, například List<T> nebo HashSet<T>. Pokud zadáte ICollection<T>, EF ve výchozím nastavení vytvoří kolekci HashSet<T> .

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

Obchodní pravidla společnosti Contoso pro vysokoškoláky uvádějí, že instruktor může mít maximálně jednu kancelář, takže OfficeAssignment vlastnost obsahuje jednu entitu OfficeAssignment (která může mít hodnotu null, pokud není přiřazena žádná kancelář).

public OfficeAssignment OfficeAssignment { get; set; }

Vytvoření entity OfficeAssignment

OfficeAssignment entity

Vytvořte Models/OfficeAssignment.cs pomocí následujícího kódu:

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

Atribut Klíč

Mezi entitami OfficeAssignment existuje relace Instructor 1:0 nebo 1. Přiřazení kanceláře existuje pouze ve vztahu k instruktorovi, kterému je přiřazen, a proto je jeho primárním klíčem i jeho cizí klíč k entitě Instructor . Entity Framework ale nedokáže automaticky rozpoznat InstructorID jako primární klíč této entity, protože jeho název nedodržuje ID zásady vytváření názvů ani classnameID jejich názvů. Key Atribut se proto používá k identifikaci jako klíče:

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

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

Ef ve výchozím nastavení považuje klíč za negenerovaný databází, protože sloupec slouží k identifikaci relace.

Vlastnost navigace instruktora

Entita Instruktor má vlastnost navigace s možnou OfficeAssignment hodnotou null (protože instruktor nemusí mít přiřazení kanceláře) a entita OfficeAssignment má nenulovou navigační vlastnost (protože přiřazení kanceláře nemůže existovat bez instruktora – InstructorID je nenulovatelnéInstructor). Pokud má entita Instruktor související entitu OfficeAssignment, každá entita bude mít odkaz na druhou entitu ve své navigační vlastnosti.

Do vlastnosti navigace instruktora můžete zadat [Required] atribut, který určuje, že musí existovat související instruktor, ale nemusíte to udělat, protože InstructorID cizí klíč (což je také klíč k této tabulce) není nullable.

Úprava entity kurzu

Course entity

Nahraďte Models/Course.csdříve přidaný kód následujícím kódem. Změny jsou zvýrazněné.

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 DepartmentID cizího klíče, která odkazuje na související entitu Oddělení a má Department navigační vlastnost.

Entity Framework nevyžaduje, abyste do datového modelu přidali vlastnost cizího klíče, pokud máte navigační vlastnost pro související entitu. EF automaticky vytvoří cizí klíče v databázi, kdykoli jsou potřeba, a vytvoří pro ně stínové vlastnosti . Cizí klíč v datovém modelu ale může zjednodušit a zefektivnit aktualizace. Pokud například načtete entitu Course , která se má upravit, má entita hodnotu null, Department pokud ji nenačtete, takže když entitu Course aktualizujete, budete ji muset nejprve načíst Department . Pokud je vlastnost DepartmentID cizího klíče součástí datového modelu, nemusíte před aktualizací načíst entitu Department .

Atribut DatabaseGenerated

Atribut DatabaseGenerated s parametrem None vlastnosti CourseID určuje, že hodnoty primárního klíče jsou poskytovány uživatelem místo generování databáze.

[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 Course entit ale použijete uživatelem zadané číslo kurzu, například 1000 řad pro jedno oddělení, 2000 řad pro jiné oddělení atd.

Atribut DatabaseGenerated lze také použít k vygenerování výchozích hodnot, jako v případě databázových sloupců sloužících k zaznamenání data vytvoření nebo aktualizace řádku. Další informace naleznete v tématu Vygenerované vlastnosti.

Vlastnosti cizího klíče a navigace

Vlastnosti cizího klíče a vlastnosti navigace 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ů existuje DepartmentID cizí klíč a Department vlastnost navigace.

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

Kurz může mít v něm zaregistrovaný libovolný počet studentů, takže Enrollments navigační vlastnost je kolekce:

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

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

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

Vytvoření entity oddělení

Department entity

Vytvořte Models/Department.cs pomocí následujícího kódu:

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 ke změně mapování názvů sloupců. V kódu entity Column se atribut používá ke změně mapování datového Department typu SQL tak, aby byl sloupec definován pomocí typu SQL Serveru money v databázi:

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

Mapování sloupců se obecně nevyžaduje, protože Rozhraní Entity Framework zvolí příslušný datový typ SQL Serveru na základě typu CLR, který definujete pro vlastnost. Typ CLR decimal se mapuje na typ SQL Serveru decimal . V tomto případě ale víte, že sloupec bude obsahovat částky měny a datový typ peněz je vhodnější pro tento typ.

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 instruktorem. InstructorID Proto je vlastnost zahrnuta jako cizí klíč pro entitu Instruktor a za označení typu se přidá int otazník, který označuje vlastnost jako null. Navigační vlastnost má název Administrator , ale obsahuje entitu Instruktor:

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

Oddělení může mít mnoho kurzů, takže je zde vlastnost Navigace v kurzech:

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

Poznámka

Podle konvence umožňuje Entity Framework kaskádové odstranění pro cizí klíče bez hodnoty null a pro relace M:N. Výsledkem může být cyklický kaskádový odstranění pravidel, která při pokusu o přidání migrace způsobí výjimku. Pokud jste například vlastnost nedefinovali Department.InstructorID jako hodnotu null, ef nakonfiguruje kaskádové pravidlo odstranění, které odstraní oddělení, když odstraníte instruktora, což není to, co chcete udělat. Pokud vaše obchodní pravidla vyžadovala InstructorID , aby vlastnost neměla hodnotu null, museli byste k zakázání kaskádového odstranění relace použít následující příkaz rozhraní API fluent:

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

Úprava entity registrace

Enrollment entity

Nahraďte Models/Enrollment.csdříve přidaný kód následujícím kódem:

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ího klíče a navigace

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

Záznam registrace je určený pro jeden kurz, takže vlastnost cizího CourseID klíče a Course navigační vlastnost:

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

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

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

Relace M:N

Mezi entitami a entitami existuje relace StudentCourse M:N a Enrollment entita funguje jako tabulka spojení 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í obrázek znázorňuje, jak tyto relace vypadají v diagramu entit. (Tento diagram se vygeneroval pomocí nástrojů Entity Framework Power Tools pro EF 6.x. Vytvoření diagramu není součástí kurzu, ale právě se tady používá jako obrázek.)

Student-Course many to many relationship

Každá čára relace má na jednom konci 1 a hvězdičku (*) na druhé, která 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 to byla tabulka spojení M:N bez datové části (nebo tabulky čistého spojení) v databázi. Course Entity Instructor mají takový druh relace M:N a dalším krokem je vytvoření třídy entity, která bude fungovat jako tabulka spojení bez datové části.

EF Core podporuje implicitní spojení tabulek pro relace M:N, ale tato tuto funkci nebyla aktualizována tak, aby používala implicitní tabulku spojení. Podívejte se na Razor relace M:N, verze stránek tohoto kurzu, která byla aktualizována.

Entita CourseAssignment

CourseAssignment entity

Vytvořte Models/CourseAssignment.cs pomocí následujícího kódu:

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

Spojit názvy entit

Tabulka spojení je vyžadována v databázi pro relaci M:N instruktora k kurzům a musí být reprezentována sadou entit. Je běžné pojmenovat entitu EntityName1EntityName2spojení , která by v tomto případě byla CourseInstructor. Doporučujeme ale zvolit název, který popisuje relaci. Datové modely začínají jednoduše a zvětšují se, protože připojení bez datové části často získávají datové části později. Pokud začnete s popisným názvem entity, nebudete muset název později změnit. V ideálním případě by entita spojení měla v obchodní doméně svůj vlastní přirozený (případně jednoslovný) název. Knihy a zákazníci můžou být například propojeny prostřednictvím hodnocení. Pro tento vztah CourseAssignment je lepší volbou než CourseInstructor.

Složený klíč

Vzhledem k tomu, že cizí klíče nejsou nullable a společně jednoznačně identifikují každý řádek tabulky, není potřeba samostatný primární klíč. Vlastnosti InstructorID by CourseID měly fungovat jako složený primární klíč. Jediný způsob, jak identifikovat složené primární klíče k EF, je použití rozhraní API fluent (nejde to provést 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 více řádků pro stejného instruktora a kurz. Entita Enrollment join definuje svůj vlastní primární klíč, takže duplicitní položky tohoto typu jsou možné. Chcete-li těmto duplicitám zabránit, můžete přidat jedinečný index do polí cizího klíče nebo nakonfigurovat Enrollment primární složený klíč podobný CourseAssignment. Další informace najdete v tématu Indexy.

Aktualizace kontextu databáze

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

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.

Informace o alternativním rozhraní API fluent

Kód v OnModelCreating metodě DbContext třídy používá fluent API ke konfiguraci chování EF. Rozhraní API se nazývá "fluent", protože se často používá řetězcem řady volání metod do jednoho příkazu, jak je znázorněno v tomto příkladu EF Core z dokumentace:

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

V tomto kurzu používáte rozhraní API fluent pouze pro mapování databáze, které nemůžete provádět s atributy. K určení většiny formátování, ověřování a mapování pravidel, která můžete provádět pomocí atributů, ale můžete také použít rozhraní API fluent. Některé atributy, jako MinimumLength je například nelze použít s rozhraním FLUENT API. Jak už bylo zmíněno dříve, MinimumLength nezmění schéma, použije pouze ověřovací pravidlo na straně klienta a serveru.

Někteří vývojáři raději používají výhradně rozhraní API fluent, aby mohli třídy entit udržovat "č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žít ho co nejvíce konzistentně. Pokud použijete obojí, mějte na paměti, že pokud dojde ke konfliktu, rozhraní Fluent API přepíše atributy.

Další informace o atributech a rozhraní API fluent naleznete v tématu Metody konfigurace.

Diagram entit znázorňující relace

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

Entity diagram

Kromě čar relací 1:N (1 až *) vidíte mezi entitami instruktora a oddělením relace 1:N (1 až 0,.1) InstructorOfficeAssignment mezi čarou relace 1:N (0,.1 až *).

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

Nahraďte kód v Data/DbInitializer.cs souboru následujícím kódem, abyste mohli poskytnout počáteční data pro nové entity, které jste vytvořili.

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 ukázková data do vlastností podle potřeby pro testování. Všimněte si, jak se zpracovávají relace M:N: kód vytváří relace vytvořením entit v Enrollments sadách entit a CourseAssignment spojením entit.

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 nedělejte 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 v tomto okamžiku database update pokusili spustit příkaz (zatím ho neudělávejte), zobrazí se následující chyba:

Příkaz ALTER TABLE byl v konfliktu s omezením CIZÍ KLÍČ "FK_dbo. Course_dbo. Department_DepartmentID". Ke konfliktu došlo v databázi ContosoUniversity, tabulce dbo. Department", column 'DepartmentID'.

Někdy při provádění migrací s existujícími daty je potřeba vložit do databáze data zástupných procedur, aby vyhovovala omezením cizího klíče. Vygenerovaný kód v Up metodě přidá do Course tabulky nenulový DepartmentID cizí klíč. Pokud už jsou v tabulce Course při spuštění kódu řádky, operace selže, AddColumn protože SQL Server neví, jakou hodnotu má vložit do sloupce, který nemůže mít hodnotu null. V tomto kurzu spustíte migraci na novou databázi, ale v produkční aplikaci budete muset nastavit, aby migrace zpracovávala stávající data, takže následující pokyny ukazují příklad toho, jak to udělat.

Pokud chcete, aby tato migrace fungovala s existujícími daty, musíte změnit kód tak, aby nový sloupec měl 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ýsledkem je, že existující řádky kurzu budou po spuštění metody souviset s oddělením Up Temp.

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

  • Zakomentujte řá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);
    
  • Za kód, 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 napsali kód nebo skripty pro přidání řádků oddělení a propojíte řádky kurzu s novými řádky oddělení. Pak už nebudete potřebovat oddělení Temp nebo výchozí hodnotu ve sloupci Course.DepartmentID .

Uložte změny a sestavte projekt.

Změna připojovací řetězec

Nyní máte v DbInitializer třídě nový kód, který přidává počáteční data pro nové entity do prázdné databáze. Pokud chcete, aby ef vytvořil novou prázdnou databázi, změňte název databáze v připojovací řetězec na appsettings.json ContosoUniversity3 nebo jiný název, který jste nepoužívali na 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.jsonsouboru .

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 příkaz rozhraní příkazového database drop řádku:

dotnet ef database drop

Aktualizace databáze

Po změně názvu databáze nebo odstranění databáze spusťte migraci spuštěním database update příkazu v příkazovém okně.

dotnet ef database update

Spusťte aplikaci, která způsobí DbInitializer.Initialize , že metoda spustí a naplní novou databázi.

Otevřete databázi v SSOX, jak jste to udělali dříve, a rozbalte uzel Tabulky , abyste viděli, že všechny tabulky byly vytvořeny. (Pokud máte stále otevřený SSOX od dřívějšího času, klikněte na tlačítko Tlačítko Aktualizovat .)

Tables in SSOX

Spuštěním aplikace aktivujte inicializační kód, který databázi zasadí.

Klikněte pravým tlačítkem myši na tabulku CourseAssignment a vyberte Zobrazit data a ověřte, že obsahuje data.

CourseAssignment data in SSOX

Získání kódu

Stáhněte nebo zobrazte dokončenou aplikaci.

Další kroky

V tomto kurzu se naučíte:

  • Přizpůsobení datového modelu
  • Provedené změny v entitě Student
  • Vytvořená entita Instruktor
  • Vytvoření entity OfficeAssignment
  • Upravená entita kurzu
  • Vytvořená entita oddělení
  • Upravená entita registrace
  • Aktualizace kontextu databáze
  • Počáteční databáze s testovacími daty
  • Přidání migrace
  • Změna připojovací řetězec
  • Aktualizace databáze

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