Část 5, Razor Stránky s EF Core ASP.NET Jádrem – datový model

Tom Dykstra, Jeremy Likness a Jon P Smith

Webová aplikace Contoso University ukazuje, jak vytvářet Razor webové aplikace Pages pomocí EF Core sady Visual Studio. Informace o sérii kurzů najdete v prvním kurzu.

Pokud narazíte na problémy, které nemůžete vyřešit, stáhněte si dokončenou aplikaci a porovnejte tento kód s tím, co jste vytvořili podle kurzu.

Předchozí kurzy pracovaly se základním datovým modelem složeným ze tří entit. V tomto kurzu:

  • Přidají se další entity a relace.
  • Datový model je přizpůsobený zadáním pravidel formátování, ověřování a mapování databáze.

Dokončený datový model je znázorněn na následujícím obrázku:

Entity diagram

Následující diagram databáze byl proveden pomocí dataedo:

Dataedo diagram

Vytvoření databázového diagramu pomocí Dataedo:

V předchozím diagramu CourseInstructor Dataedo je tabulka spojení vytvořená rozhraním Entity Framework. Další informace najdete v tématu M:N

Entita Student

Nahraďte kód Models/Student.cs 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 Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

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

Předchozí kód přidá FullName vlastnost a přidá do existujících vlastností následující atributy:

Počítaná vlastnost FullName

FullName je počítaná vlastnost, která vrací hodnotu vytvořenou zřetězením dvou dalších vlastností. FullName nejde nastavit, takže má jenom přístupový objekt get. V databázi se nevytvořil žádný FullName sloupec.

Atribut DataType

[DataType(DataType.Date)]

U dat zápisu studentů se na všech stránkách aktuálně zobrazuje čas dne spolu s datem, i když je relevantní jenom datum. Pomocíatributůch

Atribut DataType určuje datový typ, který je konkrétnější než vnitřní typ databáze. V tomto případě by se mělo zobrazit jenom datum, nikoli datum a čas. Výčet DataType poskytuje mnoho datových typů, jako je datum, čas, Telefon Číslo, Měna, EmailAddress atd. Atribut DataType může také aplikaci povolit, aby automaticky poskytovala funkce specifické pro typ. Příklad:

  • Odkaz mailto: se automaticky vytvoří pro DataType.EmailAddress.
  • Ve většině prohlížečů je k dispozici DataType.Date selektor data.

Atribut DataType generuje atributy HTML 5 data- (vyslovuje se pomlčka dat). Atributy DataType neposkytují ověření.

Atribut DisplayFormat

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

DataType.Date nezadá formát zobrazeného data. Ve výchozím nastavení se pole data 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. Nastavení ApplyFormatInEditMode určuje, že formátování se má použít také v uživatelském rozhraní pro úpravy. Některá pole by neměla používat ApplyFormatInEditMode. Symbol měny by se například neměl zobrazovat v textovém poli pro úpravy.

Atribut DisplayFormat lze použít sám. Obecně je vhodné použít DataType atribut s atributem DisplayFormat . Atribut DataType vyjadřuje sémantiku dat na rozdíl od toho, jak je vykreslit na obrazovce. Atribut DataType poskytuje následující výhody, které nejsou k dispozici v DisplayFormat:

  • Prohlížeč může povolit funkce HTML5. Můžete například zobrazit ovládací prvek kalendáře, symbol měny odpovídající národnímu prostředí, e-mailové odkazy a ověření vstupu na straně klienta.
  • Ve výchozím nastavení prohlížeč vykreslí data pomocí správného formátu na základě národního prostředí.

Další informace najdete v dokumentaci ke vstupním> pomocným <rutině značek.

Atribut StringLength

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

Pomocí atributů je možné zadat pravidla ověření dat a chybové zprávy ověřování. Atribut StringLength určuje minimální a maximální délku znaků, které jsou povoleny v datovém poli. Zobrazený kód omezuje názvy maximálně na 50 znaků. Příklad, který nastaví minimální délku řetězce, se zobrazí později.

Atribut StringLength také poskytuje ověřování na straně klienta a na straně serveru. Minimální hodnota nemá žádný vliv na schéma databáze.

Atribut StringLength nezabrání uživateli v zadávání prázdných znaků pro jméno. Atribut RegularExpression lze použít 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]*$")]

V SQL Serveru Průzkumník objektů (SSOX) otevřete návrháře tabulek Student poklikáním na tabulku Student.

Students table in SSOX before migrations

Předchozí obrázek znázorňuje schéma tabulky Student . Pole názvů mají typ nvarchar(MAX). Při vytvoření a použití migrace později v tomto kurzu se pole názvů stanou nvarchar(50) výsledkem atributů délky řetězce.

Atribut Column

[Column("FirstName")]
public string FirstMidName { get; set; }

Atributy mohou řídit, jak se třídy a vlastnosti mapují na databázi. Student V modelu Column se atribut používá k mapování názvu FirstMidName vlastnosti na "FirstName" v databázi.

Při vytváření databáze se názvy vlastností v modelu používají pro názvy sloupců (s výjimkou použití atributu Column ). Model Student používá FirstMidName pro pole s křestní jméno, protože pole může obsahovat také prostřední název.

S atributem [Column]Student.FirstMidName se v datovém modelu mapuje na FirstName sloupec Student tabulky. Přidání atributu Column změní model backing the SchoolContext. Model, který SchoolContext už databázi nepodporuje, neodpovídá. Tento nesrovnalosti vyřešíte přidáním migrace později v tomto kurzu.

Požadovaný atribut

[Required]

Atribut Required vytvoří požadované vlastnosti názvu. Atribut Required není potřeba pro typy bez hodnoty null, jako jsou například typy hodnot (například DateTime, inta double). 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; }

MinimumLength a Required povolte, aby ověření splňovalo prázdné znaky. RegularExpression Atribut použijte pro úplnou kontrolu nad řetězcem.

Atribut Display

[Display(Name = "Last Name")]

Atribut Display určuje, že popis pro textová pole by měla být "Jméno", "Příjmení", "Celé jméno" a "Datum registrace". Výchozí popis neměly žádné mezery, které by dělily slova, například "Příjmení".

Vytvoření migrace

Spusťte aplikaci a přejděte na stránku Studenti. Vyvolá se výjimka. Atribut [Column] způsobí, že EF očekává nalezení sloupce s názvem FirstName, ale název sloupce v databázi je stále FirstMidName.

Chybová zpráva je podobná následujícímu příkladu:

SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:

SchoolContext
  • V PMC zadejte následující příkazy pro vytvoření nové migrace a aktualizaci databáze:

    Add-Migration ColumnFirstName
    Update-Database
    
    

    První z těchto příkazů vygeneruje následující zprávu upozornění:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    Upozornění se vygeneruje, protože pole názvů jsou teď omezená na 50 znaků. Pokud název v databázi obsahoval více než 50 znaků, ztratí se 51 až poslední znak.

  • Otevřete tabulku Student v SSOX:

    Students table in SSOX after migrations

    Před použitím migrace byly sloupce názvů typu nvarchar(MAX). Sloupce názvů jsou nyní nvarchar(50). Název sloupce se změnil z FirstMidName na FirstName.

  • Spusťte aplikaci a přejděte na stránku Studenti.
  • Všimněte si, že časy nejsou vstupní ani zobrazené spolu s kalendářními daty.
  • Vyberte Vytvořit nový a zkuste zadat název delší než 50 znaků.

Poznámka

V následujících částech sestavení aplikace v některých fázích generuje chyby kompilátoru. Pokyny určují, kdy se má aplikace sestavit.

Entita instruktora

Vytvořte Models/Instructor.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 Instructor
    {
        public int ID { get; set; }

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

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

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

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

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

Více atributů může být na jednom řádku. Atributy HireDate lze zapsat takto:

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

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

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

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

Instruktor může mít maximálně jednu kancelář, takže OfficeAssignment vlastnost obsahuje jednu OfficeAssignment entitu. OfficeAssignment má hodnotu null, pokud není přiřazena žádná kancelář.

public OfficeAssignment OfficeAssignment { get; set; }

Entita 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íč

Atribut [Key] slouží k identifikaci vlastnosti jako primárního klíče (PK), pokud je název vlastnosti něco jiného než classnameID nebo ID.

Mezi entitami InstructorOfficeAssignment existuje vztah 1:1 nebo 1. Přiřazení kanceláře existuje pouze ve vztahu k instruktorovi, ke kterému je přiřazený. Pk OfficeAssignment je také jeho cizí klíč (FK) pro entitu Instructor . Relace 1:0 nebo 1 nastane v případě, že pk v jedné tabulce je PK i FK v jiné tabulce.

EF Core nemůže automaticky rozpoznat InstructorID jako PK, OfficeAssignment protože InstructorID neodpovídá konvenci pojmenování ID nebo classnameID. Proto se Key atribut používá k identifikaci InstructorID jako PK:

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

Ve výchozím nastavení považuje klíč za negenerovaný databází, EF Core protože sloupec slouží k identifikaci relace. Další informace najdete v tématu Klíče EF.

Vlastnost navigace instruktora

Vlastnost Instructor.OfficeAssignment navigace může mít hodnotu null, protože pro daného instruktora nemusí být OfficeAssignment řádek. Instruktor nemusí mít zadání kanceláře.

Vlastnost OfficeAssignment.Instructor navigace bude mít vždy entitu instruktora, protože typ cizího klíče InstructorID je int, typ hodnoty, který není null. Zadání kanceláře nemůže existovat bez instruktora.

Pokud má entita Instructor související OfficeAssignment entitu, každá entita má ve své navigační vlastnosti odkaz na druhou entitu.

Entita kurzu

Aktualizujte Models/Course.cs následujícím kódem:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<Instructor> Instructors { get; set; }
    }
}

Entita Course má vlastnost DepartmentIDcizího klíče (FK). DepartmentID odkazuje na související Department entitu. Entita CourseDepartment navigační vlastnost.

EF Core nevyžaduje vlastnost cizího klíče pro datový model, pokud má model navigační vlastnost pro související entitu. EF Core v databázi automaticky vytváří sady FK, kdekoli jsou potřeba. EF Core vytvoří stínové vlastnosti pro automaticky vytvořené sady FK. Explicitní zahrnutí FK do datového modelu však může zjednodušit a zefektivnit aktualizace. Představte si například model, ve kterém není zahrnuta vlastnost DepartmentID FK. Když se entita kurzu načte pro úpravy:

  • Vlastnost Department je null , pokud není explicitně načtena.
  • Chcete-li aktualizovat entitu kurzu, musí být entita Department nejprve načtena.

Pokud je vlastnost DepartmentID FK součástí datového modelu, není nutné před aktualizací načíst entitu Department .

Atribut DatabaseGenerated

Atribut [DatabaseGenerated(DatabaseGeneratedOption.None)] určuje, že PK je poskytována aplikací místo generování databáze.

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

Ve výchozím nastavení se předpokládá, EF Core že databáze generuje hodnoty PK. Vygenerovaná databáze je obecně nejlepší přístup. Pro Course entity určuje uživatel PK. Například číslo kurzu, například 1000 řad pro matematické oddělení, 2000 série pro anglické oddělení.

Atribut DatabaseGenerated lze také použít k vygenerování výchozích hodnot. Databáze může například automaticky vygenerovat pole kalendářního data pro 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 (FK) a vlastnosti navigace v entitě Course odrážejí následující relace:

Kurz je přiřazen k jednomu oddělení, takže je k DepartmentID dispozici FK a Department navigační vlastnost.

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 Instructors navigační vlastnost je kolekce:

public ICollection<Instructor> Instructors { get; set; }

Entita Oddělení

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 byl Column atribut použit 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. Sloupec Budget se definuje pomocí typu peněz SQL Serveru v databázi:

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

Mapování sloupců se obecně nevyžaduje. EF Core zvolí odpovídající datový typ SQL Serveru na základě typu CLR pro vlastnost. Typ CLR decimal se mapuje na typ SQL Serveru decimal . Budget je pro měnu a datový typ peněz je vhodnější pro měnu.

Vlastnosti cizího klíče a navigace

Vlastnosti FK a navigace odrážejí následující relace:

  • Oddělení může nebo nemusí mít správce.
  • Správce je vždy instruktor. InstructorID Proto je vlastnost zahrnuta jako FK entityInstructor.

Navigační vlastnost je pojmenovaná Administrator , ale obsahuje entitu Instructor :

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

V ? předchozím kódu určuje vlastnost nullable.

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

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

Podle konvence EF Core umožňuje kaskádové odstranění pro nenulové FK a pro relace M:N. Toto výchozí chování může vést k cyklický kaskádový odstranění pravidel. Cyklické kaskádové odstranění pravidel způsobí výjimku při přidání migrace.

Pokud Department.InstructorID byla například vlastnost definována jako nenulová, EF Core nakonfigurovala by kaskádové pravidlo odstranění. V takovém případě by se oddělení odstranilo, když se odstraní instruktor přiřazený jako správce. V tomto scénáři by pravidlo omezení dávalo větší smysl. Následující fluentové rozhraní API by nastavilo pravidlo omezení a zakázalo kaskádové odstranění.

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

Vlastnosti cizího klíče zápisu a navigace

Záznam o registraci je určen pro jeden kurz pořízený jedním studentem.

Enrollment entity

Aktualizujte Models/Enrollment.cs následujícím kódem:

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

Vlastnosti FK a navigační vlastnosti odrážejí následující relace:

Záznam registrace je pro jeden kurz, takže vlastnost CourseID FK 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 StudentID FK a Student navigační vlastnost:

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

Relace M:N

Mezi entitami StudentCourse a entitami existuje vztah M:N. Entita Enrollment funguje jako tabulka spojení M:N s datovou částí v databázi. Datová část znamená, že Enrollment tabulka obsahuje další data kromě FK pro spojené tabulky. V entitě Enrollment jsou další data kromě FK PK a Grade.

Následující obrázek znázorňuje, jak tyto relace vypadají v diagramu entit. (Tento diagram byl generován pomocí EF Power Tools pro EF 6.x. Vytvoření diagramu není součástí kurzu.)

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, musí obsahovat pouze dvě sady FK CourseID a StudentID. Tabulka spojení M:N bez datové části se někdy označuje jako čistá join table (PJT).

Course Entity Instructor mají relaci M:N pomocí PJT.

Aktualizace kontextu databáze

Aktualizujte Data/SchoolContext.cs následujícím kódem:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable(nameof(Course))
                .HasMany(c => c.Instructors)
                .WithMany(i => i.Courses);
            modelBuilder.Entity<Student>().ToTable(nameof(Student));
            modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
        }
    }
}

Předchozí kód přidá nové entity a nakonfiguruje vztah M:N mezi entitami Instructor a Course entitami.

Alternativa rozhraní Fluent API k atributům

Metoda OnModelCreating v předchozím kódu používá rozhraní API fluent ke konfiguraci EF Core chování. Rozhraní API se nazývá "fluent", protože se často používá řetězcem řady volání metod do jednoho příkazu. Následující kód je příkladem rozhraní FLUENT API:

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

V tomto kurzu se rozhraní API fluent používá pouze pro mapování databáze, které se nedají provádět s atributy. Rozhraní API fluent však může určovat většinu pravidel formátování, ověřování a mapování, která se dají provádět s atributy.

Některé atributy, jako MinimumLength je například nelze použít s rozhraním FLUENT API. MinimumLength nezmění schéma, použije pouze ověřovací pravidlo minimální délky.

Někteří vývojáři raději používají rozhraní API fluent výhradně, aby mohli třídy entit udržovat čisté. Atributy a fluentské rozhraní API je možné kombinovat. Existují některé konfigurace, které je možné provádět pouze s rozhraním FLUENT API, například zadáním složené infrastruktury veřejných klíčů. Existují některé konfigurace, které je možné provádět pouze s atributy (MinimumLength). Doporučený postup pro používání rozhraní API nebo atributů fluent:

  • Zvolte jeden z těchto dvou přístupů.
  • Zvolený přístup používejte co nejvíce konzistentně.

Některé atributy použité v tomto kurzu se používají pro:

  • Pouze ověřování (například MinimumLength).
  • EF Core pouze konfigurace (například HasKey).
  • Ověřování a EF Core konfigurace (například [StringLength(50)]).

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

Počáteční hodnota databáze

Aktualizujte kód v Data/DbInitializer.cs:

using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var alexander = new Student
            {
                FirstMidName = "Carson",
                LastName = "Alexander",
                EnrollmentDate = DateTime.Parse("2016-09-01")
            };

            var alonso = new Student
            {
                FirstMidName = "Meredith",
                LastName = "Alonso",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var anand = new Student
            {
                FirstMidName = "Arturo",
                LastName = "Anand",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var barzdukas = new Student
            {
                FirstMidName = "Gytis",
                LastName = "Barzdukas",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var li = new Student
            {
                FirstMidName = "Yan",
                LastName = "Li",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var justice = new Student
            {
                FirstMidName = "Peggy",
                LastName = "Justice",
                EnrollmentDate = DateTime.Parse("2017-09-01")
            };

            var norman = new Student
            {
                FirstMidName = "Laura",
                LastName = "Norman",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var olivetto = new Student
            {
                FirstMidName = "Nino",
                LastName = "Olivetto",
                EnrollmentDate = DateTime.Parse("2011-09-01")
            };

            var students = new Student[]
            {
                alexander,
                alonso,
                anand,
                barzdukas,
                li,
                justice,
                norman,
                olivetto
            };

            context.AddRange(students);

            var abercrombie = new Instructor
            {
                FirstMidName = "Kim",
                LastName = "Abercrombie",
                HireDate = DateTime.Parse("1995-03-11")
            };

            var fakhouri = new Instructor
            {
                FirstMidName = "Fadi",
                LastName = "Fakhouri",
                HireDate = DateTime.Parse("2002-07-06")
            };

            var harui = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Harui",
                HireDate = DateTime.Parse("1998-07-01")
            };

            var kapoor = new Instructor
            {
                FirstMidName = "Candace",
                LastName = "Kapoor",
                HireDate = DateTime.Parse("2001-01-15")
            };

            var zheng = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Zheng",
                HireDate = DateTime.Parse("2004-02-12")
            };

            var instructors = new Instructor[]
            {
                abercrombie,
                fakhouri,
                harui,
                kapoor,
                zheng
            };

            context.AddRange(instructors);

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    Instructor = fakhouri,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    Instructor = harui,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    Instructor = kapoor,
                    Location = "Thompson 304" }
            };

            context.AddRange(officeAssignments);

            var english = new Department
            {
                Name = "English",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = abercrombie
            };

            var mathematics = new Department
            {
                Name = "Mathematics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = fakhouri
            };

            var engineering = new Department
            {
                Name = "Engineering",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = harui
            };

            var economics = new Department
            {
                Name = "Economics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = kapoor
            };

            var departments = new Department[]
            {
                english,
                mathematics,
                engineering,
                economics
            };

            context.AddRange(departments);

            var chemistry = new Course
            {
                CourseID = 1050,
                Title = "Chemistry",
                Credits = 3,
                Department = engineering,
                Instructors = new List<Instructor> { kapoor, harui }
            };

            var microeconomics = new Course
            {
                CourseID = 4022,
                Title = "Microeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var macroeconmics = new Course
            {
                CourseID = 4041,
                Title = "Macroeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var calculus = new Course
            {
                CourseID = 1045,
                Title = "Calculus",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { fakhouri }
            };

            var trigonometry = new Course
            {
                CourseID = 3141,
                Title = "Trigonometry",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { harui }
            };

            var composition = new Course
            {
                CourseID = 2021,
                Title = "Composition",
                Credits = 3,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var literature = new Course
            {
                CourseID = 2042,
                Title = "Literature",
                Credits = 4,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var courses = new Course[]
            {
                chemistry,
                microeconomics,
                macroeconmics,
                calculus,
                trigonometry,
                composition,
                literature
            };

            context.AddRange(courses);

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    Student = alexander,
                    Course = chemistry,
                    Grade = Grade.A
                },
                new Enrollment {
                    Student = alexander,
                    Course = microeconomics,
                    Grade = Grade.C
                },
                new Enrollment {
                    Student = alexander,
                    Course = macroeconmics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = calculus,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = trigonometry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = anand,
                    Course = chemistry
                },
                new Enrollment {
                    Student = anand,
                    Course = microeconomics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = barzdukas,
                    Course = chemistry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = li,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = justice,
                    Course = literature,
                    Grade = Grade.B
                }
            };

            context.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}

Předchozí kód poskytuje počáteční data pro nové entity. Většina tohoto kódu vytvoří nové objekty entity a načte ukázková data. Ukázková data se používají k testování.

Použití migrace nebo vyřazení a opětovné vytvoření

U existující databáze existují dva přístupy ke změně databáze:

Obě možnosti fungují pro SQL Server. I když je metoda apply-migration složitější a časově náročná, je to upřednostňovaný přístup pro skutečná produkční prostředí.

Odstranění a opětovné vytvoření databáze

Pokud chcete vynutit EF Core vytvoření nové databáze, odstraňte a aktualizujte databázi:

  • Odstraňte složku Migrace.
  • V konzole Správce balíčků (PMC) spusťte následující příkazy:
Drop-Database
Add-Migration InitialCreate
Update-Database

Spustit aplikaci. Spuštění aplikace spustí metodu DbInitializer.Initialize . Naplní DbInitializer.Initialize novou databázi.

Otevřete databázi v SSOX:

  • Pokud jste SSOX otevřeli dříve, klikněte na tlačítko Aktualizovat .
  • Rozbalte uzel Tabulky. Zobrazí se vytvořené tabulky.

Další kroky

V dalších dvou kurzech se dozvíte, jak číst a aktualizovat související data.

Předchozí kurzy pracovaly se základním datovým modelem složeným ze tří entit. V tomto kurzu:

  • Přidají se další entity a relace.
  • Datový model je přizpůsobený zadáním pravidel formátování, ověřování a mapování databáze.

Dokončený datový model je znázorněn na následujícím obrázku:

Entity diagram

Entita Student

Student entity

Nahraďte kód Models/Student.cs 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 Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

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

Předchozí kód přidá FullName vlastnost a přidá do existujících vlastností následující atributy:

  • [DataType]
  • [DisplayFormat]
  • [StringLength]
  • [Column]
  • [Required]
  • [Display]

Počítaná vlastnost FullName

FullName je počítaná vlastnost, která vrací hodnotu vytvořenou zřetězením dvou dalších vlastností. FullName nejde nastavit, takže má jenom přístupový objekt get. V databázi se nevytvořil žádný FullName sloupec.

Atribut DataType

[DataType(DataType.Date)]

U dat zápisu studentů se na všech stránkách aktuálně zobrazuje čas dne spolu s datem, i když je relevantní jenom datum. Pomocíatributůch

Atribut DataType určuje datový typ, který je konkrétnější než vnitřní typ databáze. V tomto případě by se mělo zobrazit jenom datum, nikoli datum a čas. Výčet DataType poskytuje mnoho datových typů, jako je datum, čas, Telefon Číslo, Měna, EmailAddress atd. Atribut DataType může také aplikaci povolit, aby automaticky poskytovala funkce specifické pro typ. Příklad:

  • Odkaz mailto: se automaticky vytvoří pro DataType.EmailAddress.
  • Ve většině prohlížečů je k dispozici DataType.Date selektor data.

Atribut DataType generuje atributy HTML 5 data- (vyslovuje se pomlčka dat). Atributy DataType neposkytují ověření.

Atribut DisplayFormat

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

DataType.Date nezadá formát zobrazeného data. Ve výchozím nastavení se pole data 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. Nastavení ApplyFormatInEditMode určuje, že formátování se má použít také v uživatelském rozhraní pro úpravy. Některá pole by neměla používat ApplyFormatInEditMode. Symbol měny by se například neměl zobrazovat v textovém poli pro úpravy.

Atribut DisplayFormat lze použít sám. Obecně je vhodné použít DataType atribut s atributem DisplayFormat . Atribut DataType vyjadřuje sémantiku dat na rozdíl od toho, jak je vykreslit na obrazovce. Atribut DataType poskytuje následující výhody, které nejsou k dispozici v DisplayFormat:

  • Prohlížeč může povolit funkce HTML5. Můžete například zobrazit ovládací prvek kalendáře, symbol měny odpovídající národnímu prostředí, e-mailové odkazy a ověření vstupu na straně klienta.
  • Ve výchozím nastavení prohlížeč vykreslí data pomocí správného formátu na základě národního prostředí.

Další informace najdete v dokumentaci ke vstupním> pomocným <rutině značek.

Atribut StringLength

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

Pomocí atributů je možné zadat pravidla ověření dat a chybové zprávy ověřování. Atribut StringLength určuje minimální a maximální délku znaků, které jsou povoleny v datovém poli. Zobrazený kód omezuje názvy maximálně na 50 znaků. Příklad, který nastaví minimální délku řetězce, se zobrazí později.

Atribut StringLength také poskytuje ověřování na straně klienta a na straně serveru. Minimální hodnota nemá žádný vliv na schéma databáze.

Atribut StringLength nezabrání uživateli v zadávání prázdných znaků pro jméno. Atribut RegularExpression lze použít 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]*$")]

V SQL Serveru Průzkumník objektů (SSOX) otevřete návrháře tabulek Student poklikáním na tabulku Student.

Students table in SSOX before migrations

Předchozí obrázek znázorňuje schéma tabulky Student . Pole názvů mají typ nvarchar(MAX). Při vytvoření a použití migrace později v tomto kurzu se pole názvů stanou nvarchar(50) výsledkem atributů délky řetězce.

Atribut Column

[Column("FirstName")]
public string FirstMidName { get; set; }

Atributy mohou řídit, jak se třídy a vlastnosti mapují na databázi. Student V modelu Column se atribut používá k mapování názvu FirstMidName vlastnosti na "FirstName" v databázi.

Při vytváření databáze se názvy vlastností v modelu používají pro názvy sloupců (s výjimkou použití atributu Column ). Model Student používá FirstMidName pro pole s křestní jméno, protože pole může obsahovat také prostřední název.

S atributem [Column]Student.FirstMidName se v datovém modelu mapuje na FirstName sloupec Student tabulky. Přidání atributu Column změní model backing the SchoolContext. Model, který SchoolContext už databázi nepodporuje, neodpovídá. Tento nesrovnalosti vyřešíte přidáním migrace později v tomto kurzu.

Požadovaný atribut

[Required]

Atribut Required vytvoří požadované vlastnosti názvu. Atribut Required není potřeba pro typy bez hodnoty null, jako jsou například typy hodnot (například DateTime, inta double). 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; }

MinimumLength a Required povolte, aby ověření splňovalo prázdné znaky. RegularExpression Atribut použijte pro úplnou kontrolu nad řetězcem.

Atribut Display

[Display(Name = "Last Name")]

Atribut Display určuje, že popis pro textová pole by měla být "Jméno", "Příjmení", "Celé jméno" a "Datum registrace". Výchozí popis neměly žádné mezery, které by dělily slova, například "Příjmení".

Vytvoření migrace

Spusťte aplikaci a přejděte na stránku Studenti. Vyvolá se výjimka. Atribut [Column] způsobí, že EF očekává nalezení sloupce s názvem FirstName, ale název sloupce v databázi je stále FirstMidName.

Chybová zpráva je podobná následujícímu příkladu:

SqlException: Invalid column name 'FirstName'.
  • V PMC zadejte následující příkazy pro vytvoření nové migrace a aktualizaci databáze:

    Add-Migration ColumnFirstName
    Update-Database
    

    První z těchto příkazů vygeneruje následující zprávu upozornění:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    Upozornění se vygeneruje, protože pole názvů jsou teď omezená na 50 znaků. Pokud název v databázi obsahoval více než 50 znaků, ztratí se 51 až poslední znak.

  • Otevřete tabulku Student v SSOX:

    Students table in SSOX after migrations

    Před použitím migrace byly sloupce názvů typu nvarchar(MAX). Sloupce názvů jsou nyní nvarchar(50). Název sloupce se změnil z FirstMidName na FirstName.

  • Spusťte aplikaci a přejděte na stránku Studenti.
  • Všimněte si, že časy nejsou vstupní ani zobrazené spolu s kalendářními daty.
  • Vyberte Vytvořit nový a zkuste zadat název delší než 50 znaků.

Poznámka

V následujících částech sestavení aplikace v některých fázích generuje chyby kompilátoru. Pokyny určují, kdy se má aplikace sestavit.

Entita instruktora

Instructor entity

Vytvořte Models/Instructor.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 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íce atributů může být na jednom řádku. Atributy HireDate lze zapsat takto:

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

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

Instruktor může mít maximálně jednu kancelář, takže OfficeAssignment vlastnost obsahuje jednu OfficeAssignment entitu. OfficeAssignment má hodnotu null, pokud není přiřazena žádná kancelář.

public OfficeAssignment OfficeAssignment { get; set; }

Entita 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íč

Atribut [Key] slouží k identifikaci vlastnosti jako primárního klíče (PK), pokud název vlastnosti je něco jiného než classnameID nebo ID.

Mezi entitami InstructorOfficeAssignment existuje vztah 1:1 nebo 1. Přiřazení kanceláře existuje pouze ve vztahu k instruktorovi, ke kterému je přiřazený. Pk OfficeAssignment je také jeho cizí klíč (FK) pro entitu Instructor .

EF Core nemůže automaticky rozpoznat InstructorID jako PK, OfficeAssignment protože InstructorID neodpovídá konvenci pojmenování ID nebo classnameID. Proto se Key atribut používá k identifikaci InstructorID jako PK:

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

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

Vlastnost navigace instruktora

Vlastnost Instructor.OfficeAssignment navigace může mít hodnotu null, protože pro daného instruktora nemusí být OfficeAssignment řádek. Instruktor nemusí mít zadání kanceláře.

Vlastnost OfficeAssignment.Instructor navigace bude mít vždy entitu instruktora, protože typ cizího klíče InstructorID je int, typ hodnoty, který není null. Zadání kanceláře nemůže existovat bez instruktora.

Pokud má entita Instructor související OfficeAssignment entitu, každá entita má ve své navigační vlastnosti odkaz na druhou entitu.

Entita kurzu

Course entity

Aktualizujte Models/Course.cs následujícím kódem:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

Entita Course má vlastnost DepartmentIDcizího klíče (FK). DepartmentID odkazuje na související Department entitu. Entita CourseDepartment navigační vlastnost.

EF Core nevyžaduje vlastnost cizího klíče pro datový model, pokud má model navigační vlastnost pro související entitu. EF Core v databázi automaticky vytváří sady FK, kdekoli jsou potřeba. EF Core vytvoří stínové vlastnosti pro automaticky vytvořené sady FK. Explicitní zahrnutí FK do datového modelu však může zjednodušit a zefektivnit aktualizace. Představte si například model, ve kterém není zahrnuta vlastnost DepartmentID FK. Když se entita kurzu načte pro úpravy:

  • Vlastnost Department má hodnotu null, pokud není explicitně načtena.
  • Chcete-li aktualizovat entitu kurzu, musí být entita Department nejprve načtena.

Pokud je vlastnost DepartmentID FK součástí datového modelu, není nutné před aktualizací načíst entitu Department .

Atribut DatabaseGenerated

Atribut [DatabaseGenerated(DatabaseGeneratedOption.None)] určuje, že PK je poskytována aplikací místo generování databáze.

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

Ve výchozím nastavení se předpokládá, EF Core že databáze generuje hodnoty PK. Vygenerovaná databáze je obecně nejlepší přístup. Pro Course entity určuje uživatel PK. Například číslo kurzu, například 1000 řad pro matematické oddělení, 2000 série pro anglické oddělení.

Atribut DatabaseGenerated lze také použít k vygenerování výchozích hodnot. Databáze může například automaticky vygenerovat pole kalendářního data pro 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 (FK) a vlastnosti navigace v entitě Course odrážejí následující relace:

Kurz je přiřazen k jednomu oddělení, takže je k DepartmentID dispozici FK a Department navigační vlastnost.

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:

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

CourseAssignment je vysvětleno později.

Entita 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 byl Column atribut použit 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. Sloupec Budget se definuje pomocí typu peněz SQL Serveru v databázi:

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

Mapování sloupců se obecně nevyžaduje. EF Core zvolí odpovídající datový typ SQL Serveru na základě typu CLR pro vlastnost. Typ CLR decimal se mapuje na typ SQL Serveru decimal . Budget je pro měnu a datový typ peněz je vhodnější pro měnu.

Vlastnosti cizího klíče a navigace

Vlastnosti FK a navigace odrážejí následující relace:

  • Oddělení může nebo nemusí mít správce.
  • Správce je vždy instruktor. InstructorID Proto je vlastnost zahrnuta jako FK entityInstructor.

Navigační vlastnost je pojmenovaná Administrator , ale obsahuje entitu Instructor :

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

Otazník (?) v předchozím kódu určuje vlastnost null.

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

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

Podle konvence EF Core umožňuje kaskádové odstranění pro nenulové FK a pro relace M:N. Toto výchozí chování může vést k cyklický kaskádový odstranění pravidel. Cyklické kaskádové odstranění pravidel způsobí výjimku při přidání migrace.

Pokud Department.InstructorID byla například vlastnost definována jako nenulová, EF Core nakonfigurovala by kaskádové pravidlo odstranění. V takovém případě by se oddělení odstranilo, když se odstraní instruktor přiřazený jako správce. V tomto scénáři by pravidlo omezení dávalo větší smysl. Následující fluentové rozhraní API by nastavilo pravidlo omezení a zakázalo kaskádové odstranění.

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

Entita Registrace

Záznam o registraci je určen pro jeden kurz pořízený jedním studentem.

Enrollment entity

Aktualizujte Models/Enrollment.cs 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 FK a navigační vlastnosti odrážejí následující relace:

Záznam registrace je pro jeden kurz, takže vlastnost CourseID FK 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 StudentID FK a Student navigační vlastnost:

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

Relace M:N

Mezi entitami StudentCourse a entitami existuje vztah M:N. Entita Enrollment funguje jako tabulka spojení M:N s datovou částí v databázi. "S datovou částí" znamená, že Enrollment tabulka obsahuje další data kromě FK pro spojené tabulky (v tomto případě PK a Grade).

Následující obrázek znázorňuje, jak tyto relace vypadají v diagramu entit. (Tento diagram byl generován pomocí EF Power Tools pro EF 6.x. Vytvoření diagramu není součástí kurzu.)

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, musí obsahovat pouze dvě sady FK (CourseIDaStudentID). Tabulka spojení M:N bez datové části se někdy označuje jako čistá join table (PJT).

Course Entity Instructor mají relaci M:N pomocí tabulky s čistým spojením.

Poznámka: EF 6.x podporuje implicitní spojení tabulek pro relace M:N, ale EF Core ne. Další informace naleznete v tématu Relace M:N ve EF Core verzi 2.0.

Entita CourseAssignment

CourseAssignment entity

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

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

Relace M:N pro instruktora vyžaduje tabulku spojení a entita pro tuto tabulku spojení je CourseAssignment.

Instructor-to-Courses m:M

Je běžné pojmenovat entitu EntityName1EntityName2spojení . Například tabulka spojení Instruktor-to-Courses pomocí tohoto vzoru by byla CourseInstructor. Doporučujeme ale použít název, který popisuje relaci.

Datové modely začínají jednoduchým a růstem. Spojení tabulek bez datové části (PJT) se často vyvíjí tak, aby zahrnovaly datovou část. Když začnete s popisným názvem entity, název se při změně tabulky spojení nemusí mě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 s entitou spojení s názvem Hodnocení. Pro vztah CourseAssignment typu Instruktor-to-Courses M:N je preferován před CourseInstructor.

Složený klíč

Oba sady FK v CourseAssignment tabulce (InstructorID a CourseID) společně jednoznačně identifikují každý řádek CourseAssignment tabulky. CourseAssignment nevyžaduje vyhrazenou pk. Funkce InstructorID a CourseID vlastnosti slouží jako složená infrastruktura veřejných klíčů. Jediným způsobem, jak zadat složené sady PK, je EF Corerozhraní FLUENT API. V další části se dozvíte, jak nakonfigurovat složenou pk.

Složený klíč zajistí, že:

  • Pro jeden kurz je povoleno více řádků.
  • Pro jednoho instruktora je povoleno více řádků.
  • Pro stejného instruktora a kurz není povoleno více řádků.

Entita Enrollment join definuje vlastní PK, takže duplicitní položky tohoto typu jsou možné. Chcete-li těmto duplicitám zabránit:

  • Přidání jedinečného indexu do polí FK nebo
  • Nakonfigurujte Enrollment primární složený klíč podobný CourseAssignment. Další informace najdete v tématu Indexy.

Aktualizace kontextu databáze

Aktualizujte Data/SchoolContext.cs následujícím kódem:

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

Předchozí kód přidá nové entity a nakonfiguruje CourseAssignment složenou PK entity.

Alternativa rozhraní Fluent API k atributům

Metoda OnModelCreating v předchozím kódu používá rozhraní API fluent ke konfiguraci EF Core chování. Rozhraní API se nazývá "fluent", protože se často používá řetězcem řady volání metod do jednoho příkazu. Následující kód je příkladem rozhraní FLUENT API:

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

V tomto kurzu se rozhraní API fluent používá pouze pro mapování databáze, které se nedají provádět s atributy. Rozhraní API fluent však může určovat většinu pravidel formátování, ověřování a mapování, která se dají provádět s atributy.

Některé atributy, jako MinimumLength je například nelze použít s rozhraním FLUENT API. MinimumLength nezmění schéma, použije pouze ověřovací pravidlo minimální délky.

Někteří vývojáři raději používají výhradně rozhraní API fluent, aby mohli třídy entit udržovat "čisté". Atributy a fluentské rozhraní API je možné kombinovat. Existují některé konfigurace, které je možné provádět pouze s rozhraním FLUENT API (určením složené pk). Existují některé konfigurace, které je možné provádět pouze s atributy (MinimumLength). Doporučený postup pro používání rozhraní API nebo atributů fluent:

  • Zvolte jeden z těchto dvou přístupů.
  • Zvolený přístup používejte co nejvíce konzistentně.

Některé atributy použité v tomto kurzu se používají pro:

  • Pouze ověřování (například MinimumLength).
  • EF Core pouze konfigurace (například HasKey).
  • Ověřování a EF Core konfigurace (například [StringLength(50)]).

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

Diagram entit

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

Entity diagram

Předchozí diagram znázorňuje:

  • Několik čar relací 1:N (1 až *).
  • Čára relace 1:nula nebo 1 (1 až 0.1) mezi entitami Instructor a OfficeAssignment entitami.
  • Čára relace nula nebo 1:N (0..1 až *) mezi entitami Instructor a Department entitami.

Počáteční hodnota databáze

Aktualizujte kód v Data/DbInitializer.cs:

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

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

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

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

            context.Students.AddRange(students);
            context.SaveChanges();

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

            context.Instructors.AddRange(instructors);
            context.SaveChanges();

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

            context.Departments.AddRange(departments);
            context.SaveChanges();

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

            context.Courses.AddRange(courses);
            context.SaveChanges();

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

            context.OfficeAssignments.AddRange(officeAssignments);
            context.SaveChanges();

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

            context.CourseAssignments.AddRange(courseInstructors);
            context.SaveChanges();

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

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

Předchozí kód poskytuje počáteční data pro nové entity. Většina tohoto kódu vytvoří nové objekty entity a načte ukázková data. Ukázková data se používají k testování. Podívejte Enrollments se na CourseAssignments příklady toho, jak se dají spojit tabulky M:N.

Přidání migrace

Sestavte projekt.

V PMC spusťte následující příkaz.

Add-Migration ComplexDataModel

Předchozí příkaz zobrazí 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.
To undo this action, use 'ef migrations remove'

database update Pokud je příkaz spuštěn, vytvoří se následující chyba:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

V další části se dozvíte, co dělat s touto chybou.

Použití migrace nebo vyřazení a opětovné vytvoření

Teď, když máte existující databázi, je potřeba se zamyslet nad tím, jak na ni použít změny. Tento kurz ukazuje dvě alternativy:

Obě možnosti fungují pro SQL Server. I když je metoda apply-migration složitější a časově náročná, je to upřednostňovaný přístup pro skutečná produkční prostředí.

Odstranění a opětovné vytvoření databáze

Tuto část přeskočte, pokud používáte SQL Server a chcete použít migraci v následující části.

Pokud chcete vynutit EF Core vytvoření nové databáze, odstraňte a aktualizujte databázi:

  • V konzole Správce balíčků (PMC) spusťte následující příkaz:

    Drop-Database
    
  • Odstraňte složku Migrations a spusťte následující příkaz:

    Add-Migration InitialCreate
    Update-Database
    

Spustit aplikaci. Spuštění aplikace spustí metodu DbInitializer.Initialize . Naplní DbInitializer.Initialize novou databázi.

Otevřete databázi v SSOX:

  • Pokud jste SSOX otevřeli dříve, klikněte na tlačítko Aktualizovat .

  • Rozbalte uzel Tabulky. Zobrazí se vytvořené tabulky.

    Tables in SSOX

  • Prozkoumejte tabulku CourseAssignment:

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

    CourseAssignment data in SSOX

Použití migrace

Tato část je nepovinná. Tyto kroky fungují jenom pro SQL Server LocalDB a pouze v případě, že jste přeskočili předchozí drop a znovu vytvořili oddíl databáze .

Při spuštění migrace s existujícími daty můžou existovat omezení FK, která nejsou s existujícími daty spokojená. U produkčních dat je potřeba provést kroky pro migraci existujících dat. Tato část obsahuje příklad opravy porušení omezení FK. Tyto změny kódu neprodávejte bez zálohy. Tyto změny kódu neprovádějte, pokud jste dokončili předchozí drop a znovu vytvořili oddíl databáze .

Soubor {timestamp}_ComplexDataModel.cs obsahuje následující kód:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

Předchozí kód přidá do Course tabulky nenulový DepartmentID FK. Databáze z předchozího kurzu obsahuje řádky v Coursetabulce, aby je migrace neaktualizovala.

Aby migrace fungovala ComplexDataModel s existujícími daty:

  • Změňte kód tak, aby nový sloupec (DepartmentID) dal výchozí hodnotu.
  • Vytvořte falešné oddělení s názvem "Temp", které bude fungovat jako výchozí oddělení.

Oprava omezení cizího klíče

ComplexDataModel Ve třídě migrace aktualizujte metoduUp:

  • Otevřete soubor {timestamp}_ComplexDataModel.cs.
  • Zakomentujte řádek kódu, který přidá DepartmentID sloupec do Course tabulky.
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);

Přidejte následující zvýrazněný kód. Nový kód následuje za blokem .CreateTable( name: "Department" :

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

Při předchozích změnách budou existující Course řádky po spuštění metody souviset s oddělením ComplexDataModel.Up Temp.

Způsob zpracování zde uvedené situace je pro účely tohoto kurzu zjednodušený. Produkční aplikace:

  • Přidejte kód nebo skripty pro přidání Department řádků a souvisejících Course řádků do nových Department řádků.
  • Nepoužívejte oddělení Temp nebo výchozí hodnotu pro Course.DepartmentID.
  • V konzole Správce balíčků (PMC) spusťte následující příkaz:

    Update-Database
    

Vzhledem k tomu, že DbInitializer.Initialize metoda je navržená tak, aby fungovala pouze s prázdnou databází, odstraňte všechny řádky v tabulkách Student a Course pomocí SSOX. (Kaskádové odstranění se postará o tabulku Registrace.)

Spustit aplikaci. Spuštění aplikace spustí metodu DbInitializer.Initialize . Naplní DbInitializer.Initialize novou databázi.

Další kroky

V dalších dvou kurzech se dozvíte, jak číst a aktualizovat související data.

Předchozí kurzy pracovaly se základním datovým modelem složeným ze tří entit. V tomto kurzu:

  • Přidají se další entity a relace.
  • Datový model je přizpůsobený zadáním pravidel formátování, ověřování a mapování databáze.

Třídy entit dokončeného datového modelu jsou znázorněny na následujícím obrázku:

Entity diagram

Pokud narazíte na problémy, které nemůžete vyřešit, stáhněte si dokončenou aplikaci.

Přizpůsobení datového modelu pomocí atributů

V této části je datový model přizpůsobený pomocí atributů.

Atribut DataType

Stránky studentů aktuálně zobrazují čas data registrace. Pole kalendářních dat obvykle zobrazují jenom datum, nikoli čas.

Aktualizujte Models/Student.cs následujícím zvýrazněným kódem:

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 určuje datový typ, který je konkrétnější než vnitřní typ databáze. V tomto případě by se mělo zobrazit jenom datum, nikoli datum a čas. Výčet DataType poskytuje mnoho datových typů, jako je datum, čas, Telefon Číslo, Měna, EmailAddress atd. Atribut DataType může také aplikaci povolit, aby automaticky poskytovala funkce specifické pro typ. Příklad:

  • Odkaz mailto: se automaticky vytvoří pro DataType.EmailAddress.
  • Ve většině prohlížečů je k dispozici DataType.Date selektor data.

Atribut DataType generuje atributy HTML 5 data- (vyslovující datový spojovník), které prohlížeče HTML 5 spotřebovávají. Atributy DataType neposkytují ověření.

DataType.Date nezadá formát zobrazeného data. Ve výchozím nastavení se pole data 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é v uživatelském rozhraní pro úpravy. Některá pole by neměla používat ApplyFormatInEditMode. Symbol měny by se například neměl zobrazovat v textovém poli pro úpravy.

Atribut DisplayFormat lze použít sám. Obecně je vhodné použít DataType atribut s atributem DisplayFormat . Atribut DataType vyjadřuje sémantiku dat na rozdíl od toho, jak je vykreslit na obrazovce. Atribut DataType poskytuje následující výhody, které nejsou k dispozici v DisplayFormat:

  • Prohlížeč může povolit funkce HTML5. Můžete například zobrazit ovládací prvek 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č vykreslí data pomocí správného formátu na základě národního prostředí.

Další informace najdete v dokumentaci ke vstupním> pomocným <rutině značek.

Spustit aplikaci. Přejděte na stránku Index studentů. Časy se už nezobrazují. Každé zobrazení, které používá Student model, zobrazuje datum bez času.

Students index page showing dates without times

Atribut StringLength

Pomocí atributů je možné zadat pravidla ověření dat a chybové zprávy ověřování. Atribut StringLength určuje minimální a maximální délku znaků, které jsou povoleny v datovém poli. Atribut StringLength také poskytuje ověřování na straně klienta a na straně serveru. Minimální hodnota nemá žádný vliv na schéma databáze.

Student Aktualizujte model následujícím kódem:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

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

Předchozí kód omezuje názvy maximálně na 50 znaků. Atribut StringLength nezabrání uživateli v zadávání prázdných znaků pro jméno. Atribut RegularExpression se používá 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]*$")]

Spusťte aplikaci:

  • Přejděte na stránku Studenti.
  • Vyberte Vytvořit nový a zadejte název delší než 50 znaků.
  • Vyberte Vytvořit, ověření na straně klienta zobrazí chybovou zprávu.

Students index page showing string length errors

V SQL Serveru Průzkumník objektů (SSOX) otevřete návrháře tabulek Student poklikáním na tabulku Student.

Students table in SSOX before migrations

Předchozí obrázek znázorňuje schéma tabulky Student . Pole názvů mají typ nvarchar(MAX) , protože migrace nebyla v databázi spuštěna. Když se migrace spustí později v tomto kurzu, stanou se nvarchar(50)pole názvů .

Atribut Column

Atributy mohou řídit, jak se třídy a vlastnosti mapují na databázi. V této části Column se atribut používá k mapování názvu FirstMidName vlastnosti na "FirstName" v databázi.

Při vytváření databáze se názvy vlastností v modelu používají pro názvy sloupců (s výjimkou použití atributu Column ).

Model Student používá FirstMidName pro pole s křestní jméno, protože pole může obsahovat také prostřední název.

Student.cs Aktualizujte soubor následujícím zvýrazněným kódem:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

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

Při předchozí změně Student.FirstMidName se v aplikaci mapuje na FirstName sloupec Student tabulky.

Přidání atributu Column změní model backing the SchoolContext. Model, který SchoolContext už databázi nepodporuje, neodpovídá. Pokud se aplikace spustí před použitím migrací, vygeneruje se následující výjimka:

SqlException: Invalid column name 'FirstName'.

Aktualizace databáze:

  • Sestavte projekt.
  • Otevřete příkazové okno ve složce projektu. Zadáním následujících příkazů vytvořte novou migraci a aktualizujte databázi:
Add-Migration ColumnFirstName
Update-Database

Příkaz migrations add ColumnFirstName vygeneruje následující zprávu upozornění:

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.

Upozornění se vygeneruje, protože pole názvů jsou teď omezená na 50 znaků. Pokud název v databázi obsahoval více než 50 znaků, ztratí se 51 až poslední znak.

  • Otestujete aplikaci.

Otevřete tabulku Student v SSOX:

Students table in SSOX after migrations

Před použitím migrace byly sloupce názvů typu nvarchar(MAX). Sloupce názvů jsou nyní nvarchar(50). Název sloupce se změnil z FirstMidName na FirstName.

Poznámka

V následující části sestavení aplikace v některých fázích generuje chyby kompilátoru. Pokyny určují, kdy se má aplikace sestavit.

Aktualizace studentských entit

Student entity

Aktualizujte Models/Student.cs 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 Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

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

Požadovaný atribut

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

Atribut Required může být nahrazen parametrem minimální délky v atributu StringLength :

[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
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í", "Celé jméno" a "Datum registrace". Výchozí popis neměly žádné mezery, které by dělily slova, například "Příjmení".

Počítaná vlastnost FullName

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

Vytvoření entity instruktora

Instructor entity

Vytvořte Models/Instructor.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 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íce atributů může být na jednom řádku. Atributy HireDate lze zapsat 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 obsahuje více entit:

  • Musí se jednat o typ seznamu, do kterého lze položky přidat, odstranit a aktualizovat.

Mezi typy navigačních vlastností patří:

  • ICollection<T>
  • List<T>
  • HashSet<T>

Pokud ICollection<T> je zadáno, EF Core vytvoří ve výchozím nastavení kolekci HashSet<T> .

Entita CourseAssignment je vysvětlená 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ář. Vlastnost OfficeAssignment obsahuje jednu OfficeAssignment entitu. OfficeAssignment má 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íč

Atribut [Key] slouží k identifikaci vlastnosti jako primárního klíče (PK), pokud název vlastnosti je něco jiného než classnameID nebo ID.

Mezi entitami InstructorOfficeAssignment existuje vztah 1:1 nebo 1. Přiřazení kanceláře existuje pouze ve vztahu k instruktorovi, ke kterému je přiřazený. Pk OfficeAssignment je také jeho cizí klíč (FK) pro entitu Instructor . EF Core nemůže automaticky rozpoznat InstructorID jako PK OfficeAssignment z následujících důvodů:

  • InstructorID neodpovídá konvenci pojmenování ID nebo classnameID.

Proto se Key atribut používá k identifikaci InstructorID jako PK:

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

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

Vlastnost navigace instruktora

Vlastnost OfficeAssignment navigace pro entitu Instructor je nullable, protože:

  • Odkazové typy (například třídy mají možnou hodnotu null).
  • Instruktor nemusí mít zadání kanceláře.

Entita OfficeAssignment má nenulovou Instructor navigační vlastnost, protože:

  • InstructorID je nenulová.
  • Zadání kanceláře nemůže existovat bez instruktora.

Pokud má entita Instructor související OfficeAssignment entitu, každá entita má ve své navigační vlastnosti odkaz na druhou entitu.

Atribut [Required] lze použít pro Instructor navigační vlastnost:

[Required]
public Instructor Instructor { get; set; }

Předchozí kód určuje, že musí existovat související instruktor. Předchozí kód není nutný, protože InstructorID cizí klíč (což je také PK) není nullable.

Úprava entity kurzu

Course entity

Aktualizujte Models/Course.cs následujícím kódem:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

Entita Course má vlastnost DepartmentIDcizího klíče (FK). DepartmentID odkazuje na související Department entitu. Entita CourseDepartment navigační vlastnost.

EF Core nevyžaduje vlastnost FK pro datový model, pokud má model navigační vlastnost pro související entitu.

EF Core v databázi automaticky vytváří sady FK, kdekoli jsou potřeba. EF Core vytvoří stínové vlastnosti pro automaticky vytvořené sady FK. Použití FK v datovém modelu může zjednodušit a zefektivnit aktualizace. Představte si například model, ve kterém není zahrnuta vlastnost DepartmentID FK. Když se entita kurzu načte pro úpravy:

  • Entita Department má hodnotu null, pokud není explicitně načtena.
  • Chcete-li aktualizovat entitu kurzu, musí být entita Department nejprve načtena.

Pokud je vlastnost DepartmentID FK součástí datového modelu, není nutné před aktualizací načíst entitu Department .

Atribut DatabaseGenerated

Atribut [DatabaseGenerated(DatabaseGeneratedOption.None)] určuje, že PK je poskytována aplikací místo generování databáze.

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

Ve výchozím nastavení se předpokládá, EF Core že databáze generuje hodnoty PK. Nejlepší přístup je obecně přístup generovaný databází PK. Pro Course entity určuje uživatel PK. Například číslo kurzu, například 1000 řad pro matematické oddělení, 2000 série pro anglické oddělení.

Atribut DatabaseGenerated lze také použít k vygenerování výchozích hodnot. Databáze může například automaticky vygenerovat pole kalendářního data pro 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 (FK) a vlastnosti navigace v entitě Course odrážejí následující relace:

Kurz je přiřazen k jednomu oddělení, takže je k DepartmentID dispozici FK a Department navigační vlastnost.

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:

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

CourseAssignment je vysvětleno později.

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 byl Column atribut použit 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. Sloupec Budget se definuje pomocí typu peněz SQL Serveru v databázi:

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

Mapování sloupců se obecně nevyžaduje. EF Core obecně zvolí příslušný datový typ SQL Serveru na základě typu CLR pro vlastnost. Typ CLR decimal se mapuje na typ SQL Serveru decimal . Budget je pro měnu a datový typ peněz je vhodnější pro měnu.

Vlastnosti cizího klíče a navigace

Vlastnosti FK a navigace odrážejí následující relace:

  • Oddělení může nebo nemusí mít správce.
  • Správce je vždy instruktor. InstructorID Proto je vlastnost zahrnuta jako FK entityInstructor.

Navigační vlastnost je pojmenovaná Administrator , ale obsahuje entitu Instructor :

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

Otazník (?) v předchozím kódu určuje vlastnost null.

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 EF Core umožňuje kaskádové odstranění pro nenulové FK a pro relace M:N. Kaskádové odstranění může vést k cyklický kaskádový odstranění pravidel. Pravidla cyklických kaskádových odstranění způsobí výjimku při přidání migrace.

Pokud Department.InstructorID byla například vlastnost definována jako nenulová:

  • EF Core nakonfiguruje kaskádové pravidlo odstranění, které odstraní oddělení při odstranění instruktora.

  • Odstranění oddělení při odstranění instruktora není zamýšlené chování.

  • Následující fluentové rozhraní API by nastavilo pravidlo omezení místo kaskádové.

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

Předchozí kód zakáže kaskádové odstranění vztahu instruktora oddělení.

Aktualizace entity registrace

Záznam o registraci je určen pro jeden kurz pořízený jedním studentem.

Enrollment entity

Aktualizujte Models/Enrollment.cs 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 FK a navigační vlastnosti odrážejí následující relace:

Záznam registrace je pro jeden kurz, takže vlastnost CourseID FK 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 StudentID FK a Student navigační vlastnost:

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

Relace M:N

Mezi entitami StudentCourse a entitami existuje vztah M:N. Entita Enrollment funguje jako tabulka spojení M:N s datovou částí v databázi. "S datovou částí" znamená, že Enrollment tabulka obsahuje další data kromě FK pro spojené tabulky (v tomto případě PK a Grade).

Následující obrázek znázorňuje, jak tyto relace vypadají v diagramu entit. (Tento diagram byl generován pomocí EF Power Tools pro EF 6.x. Vytvoření diagramu není součástí kurzu.)

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, musí obsahovat pouze dvě sady FK (CourseIDaStudentID). Tabulka spojení M:N bez datové části se někdy označuje jako čistá join table (PJT).

Course Entity Instructor mají relaci M:N pomocí tabulky s čistým spojením.

Poznámka: EF 6.x podporuje implicitní spojení tabulek pro relace M:N, ale EF Core ne. Další informace naleznete v tématu Relace M:N ve EF Core verzi 2.0.

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

Kurzy instruktora

Instructor-to-Courses m:M

Vztah M:N instruktora:

  • Vyžaduje tabulku spojení, která musí být reprezentována sadou entit.
  • Je tabulka s čistým spojením (tabulka bez datové části).

Je běžné pojmenovat entitu EntityName1EntityName2spojení . Například tabulka spojení Instruktor-to-Courses pomocí tohoto vzoru je CourseInstructor. Doporučujeme ale použít název, který popisuje relaci.

Datové modely začínají jednoduchým a růstem. Spojení bez datové části (PJT) se často vyvíjejí, aby zahrnovala datovou část. Když začnete s popisným názvem entity, název se při změně tabulky spojení nemusí mě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 s entitou spojení s názvem Hodnocení. Pro vztah CourseAssignment typu Instruktor-to-Courses M:N je preferován před CourseInstructor.

Složený klíč

Sady FK nejsou s možnou hodnotou null. Oba sady FK v CourseAssignment tabulce (InstructorID a CourseID) společně jednoznačně identifikují každý řádek CourseAssignment tabulky. CourseAssignment nevyžaduje vyhrazenou pk. Funkce InstructorID a CourseID vlastnosti slouží jako složená infrastruktura veřejných klíčů. Jediným způsobem, jak zadat složené sady PK, je EF Corerozhraní FLUENT API. V další části se dozvíte, jak nakonfigurovat složenou pk.

Složený klíč zajišťuje:

  • Pro jeden kurz je povoleno více řádků.
  • Pro jednoho instruktora je povoleno více řádků.
  • Více řádků pro stejného instruktora a kurz není povoleno.

Entita Enrollment join definuje vlastní PK, takže duplicitní položky tohoto typu jsou možné. Chcete-li těmto duplicitám zabránit:

  • Přidání jedinečného indexu do polí FK nebo
  • Nakonfigurujte Enrollment primární složený klíč podobný CourseAssignment. Další informace najdete v tématu Indexy.

Aktualizace kontextu databáze

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

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Models
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollment { get; set; }
        public DbSet<Student> Student { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

Předchozí kód přidá nové entity a nakonfiguruje CourseAssignment složenou PK entity.

Alternativa rozhraní Fluent API k atributům

Metoda OnModelCreating v předchozím kódu používá rozhraní API fluent ke konfiguraci EF Core chování. Rozhraní API se nazývá "fluent", protože se často používá řetězcem řady volání metod do jednoho příkazu. Následující kód je příkladem rozhraní FLUENT API:

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

V tomto kurzu se rozhraní API fluent používá pouze pro mapování databáze, které nelze provádět s atributy. Rozhraní API fluent však může určovat většinu pravidel formátování, ověřování a mapování, která se dají provádět s atributy.

Některé atributy, jako MinimumLength je například nelze použít s rozhraním FLUENT API. MinimumLength nezmění schéma, použije pouze ověřovací pravidlo minimální délky.

Někteří vývojáři raději používají výhradně rozhraní API fluent, aby mohli třídy entit udržovat "čisté". Atributy a fluentské rozhraní API je možné kombinovat. Existují některé konfigurace, které je možné provádět pouze s rozhraním FLUENT API (určením složené pk). Existují některé konfigurace, které je možné provádět pouze s atributy (MinimumLength). Doporučený postup pro používání rozhraní API nebo atributů fluent:

  • Zvolte jeden z těchto dvou přístupů.
  • Zvolený přístup používejte co nejvíce konzistentně.

Některé atributy použité v tomto kurzu se používají pro:

  • Pouze ověřování (například MinimumLength).
  • EF Core pouze konfigurace (například HasKey).
  • Ověřování a EF Core konfigurace (například [StringLength(50)]).

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 EF Power Tools vytvoří pro dokončený školní model.

Entity diagram

Předchozí diagram znázorňuje:

  • Několik čar relací 1:N (1 až *).
  • Čára relace 1:nula nebo 1 (1 až 0.1) mezi entitami Instructor a OfficeAssignment entitami.
  • Čára relace nula nebo 1:N (0..1 až *) mezi entitami Instructor a Department entitami.

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

Aktualizujte kód v Data/DbInitializer.cs:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Předchozí kód poskytuje počáteční data pro nové entity. Většina tohoto kódu vytvoří nové objekty entity a načte ukázková data. Ukázková data se používají k testování. Podívejte Enrollments se na CourseAssignments příklady toho, jak se dají spojit tabulky M:N.

Přidání migrace

Sestavte projekt.

Add-Migration ComplexDataModel

Předchozí příkaz zobrazí 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'

database update Pokud je příkaz spuštěn, vytvoří se následující chyba:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

Použití migrace

Teď, když máte existující databázi, musíte přemýšlet o tom, jak na ni použít budoucí změny. Tento kurz ukazuje dva přístupy:

  • Odstranění a opětovné vytvoření databáze
  • Použijte migraci na existující databázi. I když je tato metoda složitější a časově náročná, je to upřednostňovaný přístup pro skutečná produkční prostředí. Poznámka: Toto je volitelná část kurzu. Můžete provést přetažení a znovu vytvořit kroky a přeskočit tuto část. Pokud chcete postupovat podle kroků v této části, neprovádějte kroky přetažení a znovu vytvořte kroky.

Odstranění a opětovné vytvoření databáze

Kód v aktualizovaném DbInitializer kódu přidá počáteční data pro nové entity. Pokud chcete vynutit EF Core vytvoření nové databáze, odstraňte a aktualizujte databázi:

V konzole Správce balíčků (PMC) spusťte následující příkaz:

Drop-Database
Update-Database

Spuštěním Get-Help about_EntityFrameworkCore z PMC získejte informace o nápovědě.

Spustit aplikaci. Spuštění aplikace spustí metodu DbInitializer.Initialize . Naplní DbInitializer.Initialize novou databázi.

Otevřete databázi v SSOX:

  • Pokud jste SSOX otevřeli dříve, klikněte na tlačítko Aktualizovat .
  • Rozbalte uzel Tabulky. Zobrazí se vytvořené tabulky.

Tables in SSOX

Prozkoumejte tabulku CourseAssignment:

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

CourseAssignment data in SSOX

Použití migrace na existující databázi

Tato část je nepovinná. Tyto kroky fungují jenom v případě, že jste přeskočili předchozí drop a znovu vytvořili oddíl databáze .

Při spuštění migrace s existujícími daty můžou existovat omezení FK, která nejsou s existujícími daty spokojená. U produkčních dat je potřeba provést kroky pro migraci existujících dat. Tato část obsahuje příklad opravy porušení omezení FK. Tyto změny kódu neprodávejte bez zálohy. Pokud jste dokončili předchozí část a aktualizovali databázi, tyto změny kódu neprodávejte.

Soubor {timestamp}_ComplexDataModel.cs obsahuje následující kód:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

Předchozí kód přidá do Course tabulky nenulový DepartmentID FK. Databáze z předchozího kurzu obsahuje řádky, Courseaby je migrace nemohla aktualizovat.

Aby migrace fungovala ComplexDataModel s existujícími daty:

  • Změňte kód tak, aby nový sloupec (DepartmentID) dal výchozí hodnotu.
  • Vytvořte falešné oddělení s názvem "Temp", které bude fungovat jako výchozí oddělení.

Oprava omezení cizího klíče

Aktualizujte metodu ComplexDataModel tříd Up :

  • Otevřete soubor {timestamp}_ComplexDataModel.cs.
  • Zakomentujte řádek kódu, který přidá DepartmentID sloupec do Course tabulky.
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);

Přidejte následující zvýrazněný kód. Nový kód následuje za blokem .CreateTable( name: "Department" :

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

Při předchozích změnách budou existující Course řádky po spuštění metody souviset s oddělením ComplexDataModelUp Temp.

Produkční aplikace:

  • Přidejte kód nebo skripty pro přidání Department řádků a souvisejících Course řádků do nových Department řádků.
  • Nepoužívejte oddělení Temp nebo výchozí hodnotu pro Course.DepartmentID.

Další kurz se zabývá souvisejícími daty.

Další prostředky