Część 5, Razor strony z EF Core ASP.NET Core — model danych

Przez Tom Dykstra, Jeremy Likness i Jon P Smith

Aplikacja internetowa Contoso University pokazuje, jak tworzyć Razor aplikacje internetowe stron przy użyciu programu EF Core Visual Studio. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek.

Jeśli napotkasz problemy, których nie możesz rozwiązać, pobierz ukończoną aplikację i porównaj ten kod z utworzonymi elementami, wykonując czynności opisane w samouczku.

Poprzednie samouczki współpracowały z podstawowym modelem danych składającym się z trzech jednostek. W tym samouczku:

  • Dodano więcej jednostek i relacji.
  • Model danych jest dostosowywany przez określenie reguł formatowania, walidacji i mapowania bazy danych.

Ukończony model danych przedstawiono na poniższej ilustracji:

Entity diagram

Następujący diagram bazy danych został wykonany z usługą Dataedo:

Dataedo diagram

Aby utworzyć diagram bazy danych za pomocą usługi Dataedo:

Na powyższym diagramie CourseInstructor Dataedo jest to tabela sprzężenia utworzona przez program Entity Framework. Aby uzyskać więcej informacji, zobacz Wiele do wielu

Jednostka Student

Zastąp kod w Models/Student.cs pliku następującym kodem:

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

Powyższy kod dodaje FullName właściwość i dodaje następujące atrybuty do istniejących właściwości:

Właściwość obliczeniowa FullName

FullName jest właściwością obliczeniową, która zwraca wartość utworzoną przez łączenie dwóch innych właściwości. FullName nie można ustawić, więc ma tylko metodę dostępu. W bazie danych nie FullName jest tworzona żadna kolumna.

Atrybut DataType

[DataType(DataType.Date)]

W przypadku dat rejestracji uczniów wszystkie strony aktualnie wyświetlają godzinę dnia wraz z datą, chociaż tylko data jest odpowiednia. Za pomocą atrybutów adnotacji danych można wprowadzić jedną zmianę kodu, która naprawi format wyświetlania na każdej stronie, na której są wyświetlane dane.

Atrybut DataType określa typ danych, który jest bardziej szczegółowy niż typ wewnętrzny bazy danych. W takim przypadku powinna być wyświetlana tylko data, a nie data i godzina. Wyliczenie DataType zawiera wiele typów danych, takich jak Data, Godzina, Telefon Number, Waluta, Adres e-mail itp. Atrybut DataType może również umożliwić aplikacji automatyczne udostępnianie funkcji specyficznych dla typu. Przykład:

  • Link mailto: jest tworzony automatycznie dla elementu DataType.EmailAddress.
  • Selektor dat jest udostępniany DataType.Date w większości przeglądarek.

Atrybut DataType emituje atrybuty HTML 5 data- (wymawiane kreska danych). Atrybuty DataType nie zapewniają walidacji.

Atrybut DisplayFormat

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

DataType.Date nie określa formatu wyświetlanej daty. Domyślnie pole daty jest wyświetlane zgodnie z domyślnymi formatami na podstawie informacji o kulturze serwera.

Atrybut DisplayFormat jest używany do jawnego określenia formatu daty. Ustawienie ApplyFormatInEditMode określa, że formatowanie powinno być również stosowane do interfejsu użytkownika edycji. Niektóre pola nie powinny używać elementu ApplyFormatInEditMode. Na przykład symbol waluty zazwyczaj nie powinien być wyświetlany w polu tekstowym edycji.

Atrybut DisplayFormat może być używany samodzielnie. Zazwyczaj dobrym pomysłem jest użycie atrybutu DataType z atrybutem DisplayFormat . Atrybut DataType przekazuje semantyka danych, w przeciwieństwie do sposobu renderowania ich na ekranie. Atrybut DataType zapewnia następujące korzyści, które nie są dostępne w programie DisplayFormat:

  • Przeglądarka może włączyć funkcje HTML5. Na przykład pokaż kontrolkę kalendarza, symbol waluty odpowiedniej dla ustawień regionalnych, linki poczty e-mail i walidację danych wejściowych po stronie klienta.
  • Domyślnie przeglądarka renderuje dane przy użyciu poprawnego formatu na podstawie ustawień regionalnych.

Aby uzyskać więcej informacji, zobacz dokumentację pomocnika tagów <wejściowych>.

Atrybut StringLength

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

Reguły walidacji danych i komunikaty o błędach walidacji można określić za pomocą atrybutów. Atrybut StringLength określa minimalną i maksymalną długość znaków dozwolonych w polu danych. Pokazany kod ogranicza nazwy do nie więcej niż 50 znaków. Przykład ustawiający minimalną długość ciągu jest wyświetlany później.

Atrybut StringLength zapewnia również weryfikację po stronie klienta i po stronie serwera. Minimalna wartość nie ma wpływu na schemat bazy danych.

Atrybut StringLength nie uniemożliwia użytkownikowi wprowadzania białych znaków dla nazwy. Atrybut RegularExpression może służyć do stosowania ograniczeń do danych wejściowych. Na przykład poniższy kod wymaga, aby pierwszy znak był wielkimi literami, a pozostałe znaki mają postać alfabetyczną:

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

W programie SQL Server Eksplorator obiektów (SSOX) otwórz projektanta tabeli Student, klikając dwukrotnie tabelę Student.

Students table in SSOX before migrations

Na powyższym obrazie przedstawiono schemat tabeli Student . Pola nazw mają typ nvarchar(MAX). Po utworzeniu i zastosowaniu migracji w dalszej części tego samouczka pola nazw stają się nvarchar(50) wynikiem atrybutów długości ciągu.

Atrybut Kolumna

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

Atrybuty mogą kontrolować sposób mapowania klas i właściwości na bazę danych. Student W modelu Column atrybut jest używany do mapowania nazwy FirstMidName właściwości na "FirstName" w bazie danych.

Podczas tworzenia bazy danych nazwy właściwości w modelu są używane dla nazw kolumn (z wyjątkiem przypadków użycia atrybutu Column ). Model Student używa FirstMidName pola imię, ponieważ pole może również zawierać nazwę środkową.

Za pomocą atrybutu [Column]Student.FirstMidName w modelu danych mapuje się na kolumnę FirstNameStudent tabeli. Dodanie atrybutu Column zmienia model kopii zapasowej SchoolContextelementu . Model kopii zapasowej SchoolContext bazy danych nie jest już zgodny z bazą danych. Ta rozbieżność zostanie rozwiązana przez dodanie migracji w dalszej części tego samouczka.

Wymagany atrybut

[Required]

Atrybut Required sprawia, że właściwości nazwy są wymagane pola. Atrybut Required nie jest wymagany w przypadku typów innych niż null, DateTimetakich jak typy wartości (na przykład , , inti double). Typy, które nie mogą mieć wartości null, są automatycznie traktowane jako wymagane pola.

Atrybut Required musi być używany z elementem MinimumLengthMinimumLength , aby można było wymusić.

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

MinimumLength i Required zezwalaj na sprawdzanie poprawności przez biały znak. Użyj atrybutu , RegularExpression aby uzyskać pełną kontrolę nad ciągiem.

Atrybut Wyświetlania

[Display(Name = "Last Name")]

Atrybut Display określa, że podpis pól tekstowych powinny być "Imię", "Nazwisko", "Imię", "Imię", "Imię" i "Data rejestracji". Domyślne podpis nie miały spacji dzielącej wyrazy, na przykład "Lastname".

Tworzenie migracji

Uruchom aplikację i przejdź do strony Uczniowie. Zgłaszany jest wyjątek. Atrybut [Column] powoduje, że program EF oczekuje znalezienia kolumny o nazwie FirstName, ale nazwa kolumny w bazie danych nadal FirstMidNamema wartość .

Komunikat o błędzie jest podobny do następującego przykładu:

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

SchoolContext
  • W usłudze PMC wprowadź następujące polecenia, aby utworzyć nową migrację i zaktualizować bazę danych:

    Add-Migration ColumnFirstName
    Update-Database
    
    

    Pierwsze z tych poleceń generuje następujący komunikat ostrzegawczy:

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

    Ostrzeżenie jest generowane, ponieważ pola nazw są teraz ograniczone do 50 znaków. Jeśli nazwa w bazie danych miała więcej niż 50 znaków, utracono znak od 51 do ostatniego.

  • Otwórz tabelę Student w programie SSOX:

    Students table in SSOX after migrations

    Przed zastosowaniem migracji kolumny nazw były typu nvarchar(MAX). Kolumny nazw to teraz nvarchar(50). Nazwa kolumny zmieniła się z FirstMidName na FirstName.

  • Uruchom aplikację i przejdź do strony Uczniowie.
  • Zwróć uwagę, że czasy nie są wejściowe lub wyświetlane wraz z datami.
  • Wybierz pozycję Utwórz nowy i spróbuj wprowadzić nazwę dłuższą niż 50 znaków.

Uwaga

W poniższych sekcjach kompilowanie aplikacji na niektórych etapach generuje błędy kompilatora. Instrukcje określają, kiedy należy skompilować aplikację.

Jednostka instruktora

Utwórz Models/Instructor.cs za pomocą następującego kodu:

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

Wiele atrybutów może znajdować się w jednym wierszu. Atrybuty HireDate można napisać w następujący sposób:

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

Właściwości Courses i OfficeAssignment to właściwości nawigacji.

Instruktor może uczyć dowolną liczbę kursów, dlatego Courses jest definiowany jako kolekcja.

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

Instruktor może mieć co najwyżej jedno biuro, więc OfficeAssignment nieruchomość posiada jedną OfficeAssignment jednostkę. OfficeAssignment parametr ma wartość null, jeśli nie przypisano żadnego pakietu Office.

public OfficeAssignment OfficeAssignment { get; set; }

Jednostka OfficeAssignment

OfficeAssignment entity

Utwórz Models/OfficeAssignment.cs za pomocą następującego kodu:

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

Atrybut Klucz

Atrybut [Key] służy do identyfikowania właściwości jako klucza podstawowego (PK), gdy nazwa właściwości jest inna niż classnameID lub ID.

Istnieje relacja jeden do zera lub jednego między jednostkami Instructor i OfficeAssignment . Przypisanie biura istnieje tylko w odniesieniu do instruktora, do niego przypisano. Klucz OfficeAssignment PK jest również jego kluczem obcym (FK) do Instructor jednostki. Relacja jeden do zera lub jednego występuje, gdy klucz PK w jednej tabeli jest zarówno kluczem PK, jak i kluczem FK w innej tabeli.

EF Core Program nie może automatycznie rozpoznać InstructorID klucza PK, OfficeAssignment ponieważ InstructorID nie jest zgodna z konwencją nazewnictwa identyfikatora ani identyfikatora klasy. W związku z tym Key atrybut jest używany do identyfikowania InstructorID jako PK:

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

Domyślnie klucz jest traktowany jako niegenerowany przez bazę danych, EF Core ponieważ kolumna służy do identyfikowania relacji. Aby uzyskać więcej informacji, zobacz Ef Keys (Klucze EF).

Właściwość nawigacji instruktora

Właściwość Instructor.OfficeAssignment nawigacji może mieć wartość null, ponieważ może nie być OfficeAssignment wiersz dla danego instruktora. Instruktor może nie mieć przypisania biura.

Właściwość OfficeAssignment.Instructor nawigacji zawsze będzie mieć jednostkę instruktora, ponieważ typ klucza InstructorID obcego to int, typ wartości innej niż null. Przypisanie biura nie może istnieć bez instruktora.

Instructor Gdy jednostka ma powiązaną OfficeAssignment jednostkę, każda jednostka ma odwołanie do drugiej jednostki we właściwości nawigacji.

Jednostka kursu

Zaktualizuj Models/Course.cs za pomocą następującego kodu:

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

Jednostka Course ma właściwość DepartmentIDklucza obcego (FK). DepartmentID wskazuje powiązaną Department jednostkę. Jednostka Course ma Department właściwość nawigacji.

EF Core nie wymaga właściwości klucza obcego dla modelu danych, gdy model ma właściwość nawigacji dla powiązanej jednostki. EF Core automatycznie tworzy zestawy FKs w bazie danych wszędzie tam, gdzie są potrzebne. EF Core Tworzy właściwości w tle dla automatycznie utworzonych zestawów FKs. Jednak jawne uwzględnienie klucza FK w modelu danych może sprawić, że aktualizacje będą prostsze i bardziej wydajne. Rozważmy na przykład model, w którym właściwość DepartmentID FK nie jest uwzględniona. Po pobraniu jednostki kursu do edycji:

  • Właściwość Department jest null , jeśli nie jest jawnie załadowana.
  • Aby zaktualizować jednostkę kursu, Department należy najpierw pobrać jednostkę.

Jeśli właściwość DepartmentID FK jest uwzględniona w modelu danych, nie ma potrzeby pobierania Department jednostki przed aktualizacją.

Atrybut DatabaseGenerated

Atrybut [DatabaseGenerated(DatabaseGeneratedOption.None)] określa, że klucz PK jest dostarczany przez aplikację, a nie generowany przez bazę danych.

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

Domyślnie zakłada się, EF Core że wartości PK są generowane przez bazę danych. Generowanie bazy danych jest zazwyczaj najlepszym rozwiązaniem. W przypadku Course jednostek użytkownik określa klucz PK. Na przykład numer kursu, taki jak seria 1000 dla działu matematyki, seria 2000 dla działu angielskiego.

Atrybut może być również używany do generowania DatabaseGenerated wartości domyślnych. Na przykład baza danych może automatycznie wygenerować pole daty, aby zarejestrować datę utworzenia lub zaktualizowania wiersza. Aby uzyskać więcej informacji, zobacz Wygenerowane właściwości.

Właściwości klucza obcego i nawigacji

Właściwości klucza obcego i właściwości nawigacji w jednostce Course odzwierciedlają następujące relacje:

Kurs jest przypisywany do jednego działu, więc istnieje DepartmentID klucz FK i Department właściwość nawigacji.

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

Kurs może mieć dowolną liczbę zarejestrowanych w nim uczniów, więc Enrollments właściwość nawigacji jest kolekcją:

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

Kurs może być nauczany przez wielu instruktorów, więc Instructors właściwość nawigacji jest kolekcją:

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

Jednostka Dział

Utwórz Models/Department.cs za pomocą następującego kodu:

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

Atrybut Kolumna

Column Wcześniej atrybut był używany do zmiany mapowania nazw kolumn. W kodzie Department jednostki Column atrybut jest używany do zmiany mapowania typu danych SQL. Kolumna jest definiowana Budget przy użyciu typu pieniędzy programu SQL Server w bazie danych:

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

Mapowanie kolumn zwykle nie jest wymagane. EF Core wybiera odpowiedni typ danych programu SQL Server na podstawie typu CLR dla właściwości . Typ CLR decimal jest mapowy na typ programu SQL Server decimal . Budget jest dla waluty, a typ danych pieniężnych jest bardziej odpowiedni dla waluty.

Właściwości klucza obcego i nawigacji

Właściwości FK i nawigacji odzwierciedlają następujące relacje:

  • Dział może lub nie ma administratora.
  • Administrator jest zawsze instruktorem. W związku z InstructorID tym właściwość jest dołączana jako klucz FK do Instructor jednostki.

Właściwość nawigacji ma nazwę Administrator , ale zawiera Instructor jednostkę:

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

W ? poprzednim kodzie określa, że właściwość jest dopuszczana do wartości null.

Dział może mieć wiele kursów, więc istnieje właściwość nawigacji Kursy:

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

Zgodnie z konwencją umożliwia EF Core usuwanie kaskadowe dla niepustych zestawów FKs i relacji wiele-do-wielu. To domyślne zachowanie może spowodować cykliczne reguły usuwania kaskadowego. Reguły usuwania kaskadowego cykliczne powodują wyjątek podczas dodawania migracji.

Jeśli na przykład Department.InstructorID właściwość została zdefiniowana jako niepusta, EF Core skonfiguruje regułę usuwania kaskadowego. W takim przypadku dział zostanie usunięty po usunięciu instruktora przypisanego jako jego administrator. W tym scenariuszu reguła ograniczeń będzie bardziej zrozumiała. Następujący płynny interfejs API ustawi regułę ograniczania i wyłączy kaskadowe usuwanie.

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

Właściwości klucza obcego i nawigacji Rejestracji

Rekord rejestracji dotyczy jednego kursu podjętego przez jednego ucznia.

Enrollment entity

Zaktualizuj Models/Enrollment.cs za pomocą następującego kodu:

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

Właściwości klucza FK i właściwości nawigacji odzwierciedlają następujące relacje:

Rekord rejestracji dotyczy jednego kursu, więc istnieje CourseID właściwość FK i Course właściwość nawigacji:

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

Rekord rejestracji jest przeznaczony dla jednego ucznia, więc istnieje StudentID właściwość FK i Student właściwość nawigacji:

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

Relacje wiele-do-wielu

Istnieje relacja wiele-do-wielu między jednostkami Student i Course . Jednostka Enrollment pełni funkcję tabeli sprzężenia wiele-do-wielu z ładunkiem w bazie danych. Z ładunkiem oznacza, że Enrollment tabela zawiera dodatkowe dane oprócz zestawów FKs dla tabel sprzężonych. W jednostce Enrollment dodatkowe dane oprócz zestawów FK to PK i Grade.

Poniższa ilustracja przedstawia wygląd tych relacji na diagramie jednostki. (Ten diagram został wygenerowany przy użyciu polecenia Narzędzia EF Power Tools for EF 6.x. Tworzenie diagramu nie jest częścią samouczka).

Student-Course many to many relationship

Każda linia relacji ma wartość 1 na jednym końcu i gwiazdkę (*) z drugiej strony wskazującą relację jeden do wielu.

Enrollment Jeśli tabela nie zawierała informacji o klasie, będzie ona musiała zawierać tylko dwa zestawy FKs i CourseIDStudentID. Tabela sprzężenia wiele-do-wielu bez ładunku jest czasami nazywana czystą tabelą sprzężenia (PJT).

Jednostki Instructor i Course mają relację wiele do wielu przy użyciu protokołu PJT.

Aktualizowanie kontekstu bazy danych

Zaktualizuj Data/SchoolContext.cs za pomocą następującego kodu:

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

Powyższy kod dodaje nowe jednostki i konfiguruje relację wiele-do-wielu między jednostkami Instructor i Course .

Fluent API alternatywa dla atrybutów

Metoda OnModelCreating w poprzednim kodzie używa płynnego interfejsu API do konfigurowania EF Core zachowania. Interfejs API jest nazywany "płynnym", ponieważ jest często używany przez ciągowanie serii wywołań metod w jedną instrukcję. Poniższy kod jest przykładem płynnego interfejsu API:

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

W tym samouczku płynny interfejs API jest używany tylko do mapowania bazy danych, których nie można wykonać za pomocą atrybutów. Jednak płynny interfejs API może określać większość reguł formatowania, walidacji i mapowania, które można wykonać za pomocą atrybutów.

Niektóre atrybuty, takie jak MinimumLength nie można zastosować za pomocą płynnego interfejsu API. MinimumLength nie zmienia schematu, ale stosuje tylko regułę weryfikacji minimalnej długości.

Niektórzy deweloperzy wolą używać płynnego interfejsu API wyłącznie tak, aby mogli zachować czyste klasy jednostek. Atrybuty i płynny interfejs API można mieszać. Istnieją pewne konfiguracje, które można wykonać tylko za pomocą płynnego interfejsu API, na przykład określając złożony klucz PK. Istnieją pewne konfiguracje, które można wykonać tylko za pomocą atrybutów (MinimumLength). Zalecana praktyka korzystania z płynnego interfejsu API lub atrybutów:

  • Wybierz jedno z tych dwóch podejść.
  • Użyj wybranego podejścia spójnie, jak najwięcej.

Niektóre atrybuty używane w tym samouczku są używane w następujących celach:

  • Tylko walidacja (na przykład MinimumLength).
  • EF Core tylko konfiguracja (na przykład HasKey).
  • Walidacja i EF Core konfiguracja (na przykład [StringLength(50)]).

Aby uzyskać więcej informacji na temat atrybutów i płynnego interfejsu API, zobacz Metody konfiguracji.

Inicjowanie bazy danych

Zaktualizuj kod w pliku 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();
        }
    }
}

Powyższy kod zawiera dane inicjujące dla nowych jednostek. Większość tego kodu tworzy nowe obiekty jednostek i ładuje przykładowe dane. Przykładowe dane są używane do testowania.

Zastosuj migrację lub upuść i utwórz ponownie

W przypadku istniejącej bazy danych istnieją dwa podejścia do zmiany bazy danych:

Wybór działa w przypadku programu SQL Server. Chociaż metoda apply-migration jest bardziej złożona i czasochłonna, jest to preferowane podejście dla rzeczywistych środowisk produkcyjnych.

Usuwanie i ponowne tworzenie bazy danych

Aby wymusić EF Core utworzenie nowej bazy danych, upuść i zaktualizować bazę danych:

  • Usuń folder Migracje.
  • W konsoli Menedżer pakietów (PMC) uruchom następujące polecenia:
Drop-Database
Add-Migration InitialCreate
Update-Database

Uruchom aplikację. Uruchomienie aplikacji powoduje uruchomienie DbInitializer.Initialize metody . Obiekt DbInitializer.Initialize wypełnia nową bazę danych.

Otwórz bazę danych w programie SSOX:

  • Jeśli system SSOX został wcześniej otwarty, kliknij przycisk Odśwież .
  • Rozwiń węzeł Tabele. Zostaną wyświetlone utworzone tabele.

Następne kroki

W kolejnych dwóch samouczkach pokazano, jak odczytywać i aktualizować powiązane dane.

Poprzednie samouczki współpracowały z podstawowym modelem danych składającym się z trzech jednostek. W tym samouczku:

  • Dodano więcej jednostek i relacji.
  • Model danych jest dostosowywany przez określenie reguł formatowania, walidacji i mapowania bazy danych.

Ukończony model danych przedstawiono na poniższej ilustracji:

Entity diagram

Jednostka Student

Student entity

Zastąp kod w Models/Student.cs pliku następującym kodem:

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

Powyższy kod dodaje FullName właściwość i dodaje następujące atrybuty do istniejących właściwości:

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

Właściwość obliczeniowa FullName

FullName jest właściwością obliczeniową, która zwraca wartość utworzoną przez łączenie dwóch innych właściwości. FullName nie można ustawić, więc ma tylko metodę dostępu. W bazie danych nie FullName jest tworzona żadna kolumna.

Atrybut DataType

[DataType(DataType.Date)]

W przypadku dat rejestracji uczniów wszystkie strony aktualnie wyświetlają godzinę dnia wraz z datą, chociaż tylko data jest odpowiednia. Za pomocą atrybutów adnotacji danych można wprowadzić jedną zmianę kodu, która naprawi format wyświetlania na każdej stronie, na której są wyświetlane dane.

Atrybut DataType określa typ danych, który jest bardziej szczegółowy niż typ wewnętrzny bazy danych. W takim przypadku powinna być wyświetlana tylko data, a nie data i godzina. Wyliczenie DataType zawiera wiele typów danych, takich jak Data, Godzina, Telefon Number, Waluta, Adres e-mail itp. Atrybut DataType może również umożliwić aplikacji automatyczne udostępnianie funkcji specyficznych dla typu. Przykład:

  • Link mailto: jest tworzony automatycznie dla elementu DataType.EmailAddress.
  • Selektor dat jest udostępniany DataType.Date w większości przeglądarek.

Atrybut DataType emituje atrybuty HTML 5 data- (wymawiane kreska danych). Atrybuty DataType nie zapewniają walidacji.

Atrybut DisplayFormat

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

DataType.Date nie określa formatu wyświetlanej daty. Domyślnie pole daty jest wyświetlane zgodnie z domyślnymi formatami na podstawie informacji o kulturze serwera.

Atrybut DisplayFormat jest używany do jawnego określenia formatu daty. Ustawienie ApplyFormatInEditMode określa, że formatowanie powinno być również stosowane do interfejsu użytkownika edycji. Niektóre pola nie powinny używać elementu ApplyFormatInEditMode. Na przykład symbol waluty zazwyczaj nie powinien być wyświetlany w polu tekstowym edycji.

Atrybut DisplayFormat może być używany samodzielnie. Zazwyczaj dobrym pomysłem jest użycie atrybutu DataType z atrybutem DisplayFormat . Atrybut DataType przekazuje semantyka danych, w przeciwieństwie do sposobu renderowania ich na ekranie. Atrybut DataType zapewnia następujące korzyści, które nie są dostępne w programie DisplayFormat:

  • Przeglądarka może włączyć funkcje HTML5. Na przykład pokaż kontrolkę kalendarza, symbol waluty odpowiedniej dla ustawień regionalnych, linki poczty e-mail i walidację danych wejściowych po stronie klienta.
  • Domyślnie przeglądarka renderuje dane przy użyciu poprawnego formatu na podstawie ustawień regionalnych.

Aby uzyskać więcej informacji, zobacz dokumentację pomocnika tagów <wejściowych>.

Atrybut StringLength

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

Reguły walidacji danych i komunikaty o błędach walidacji można określić za pomocą atrybutów. Atrybut StringLength określa minimalną i maksymalną długość znaków dozwolonych w polu danych. Pokazany kod ogranicza nazwy do nie więcej niż 50 znaków. Przykład ustawiający minimalną długość ciągu jest wyświetlany później.

Atrybut StringLength zapewnia również weryfikację po stronie klienta i po stronie serwera. Minimalna wartość nie ma wpływu na schemat bazy danych.

Atrybut StringLength nie uniemożliwia użytkownikowi wprowadzania białych znaków dla nazwy. Atrybut RegularExpression może służyć do stosowania ograniczeń do danych wejściowych. Na przykład poniższy kod wymaga, aby pierwszy znak był wielkimi literami, a pozostałe znaki mają postać alfabetyczną:

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

W programie SQL Server Eksplorator obiektów (SSOX) otwórz projektanta tabeli Student, klikając dwukrotnie tabelę Student.

Students table in SSOX before migrations

Na powyższym obrazie przedstawiono schemat tabeli Student . Pola nazw mają typ nvarchar(MAX). Po utworzeniu i zastosowaniu migracji w dalszej części tego samouczka pola nazw stają się nvarchar(50) wynikiem atrybutów długości ciągu.

Atrybut Kolumna

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

Atrybuty mogą kontrolować sposób mapowania klas i właściwości na bazę danych. Student W modelu Column atrybut jest używany do mapowania nazwy FirstMidName właściwości na "FirstName" w bazie danych.

Podczas tworzenia bazy danych nazwy właściwości w modelu są używane dla nazw kolumn (z wyjątkiem przypadków użycia atrybutu Column ). Model Student używa FirstMidName pola imię, ponieważ pole może również zawierać nazwę środkową.

Za pomocą atrybutu [Column]Student.FirstMidName w modelu danych mapuje się na kolumnę FirstNameStudent tabeli. Dodanie atrybutu Column zmienia model kopii zapasowej SchoolContextelementu . Model kopii zapasowej SchoolContext bazy danych nie jest już zgodny z bazą danych. Ta rozbieżność zostanie rozwiązana przez dodanie migracji w dalszej części tego samouczka.

Wymagany atrybut

[Required]

Atrybut Required sprawia, że właściwości nazwy są wymagane pola. Atrybut Required nie jest wymagany w przypadku typów innych niż null, DateTimetakich jak typy wartości (na przykład , , inti double). Typy, które nie mogą mieć wartości null, są automatycznie traktowane jako wymagane pola.

Atrybut Required musi być używany z elementem MinimumLengthMinimumLength , aby można było wymusić.

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

MinimumLength i Required zezwalaj na sprawdzanie poprawności przez biały znak. Użyj atrybutu , RegularExpression aby uzyskać pełną kontrolę nad ciągiem.

Atrybut Wyświetlania

[Display(Name = "Last Name")]

Atrybut Display określa, że podpis pól tekstowych powinny być "Imię", "Nazwisko", "Imię", "Imię", "Imię" i "Data rejestracji". Domyślne podpis nie miały spacji dzielącej wyrazy, na przykład "Lastname".

Tworzenie migracji

Uruchom aplikację i przejdź do strony Uczniowie. Zgłaszany jest wyjątek. Atrybut [Column] powoduje, że program EF oczekuje znalezienia kolumny o nazwie FirstName, ale nazwa kolumny w bazie danych nadal FirstMidNamema wartość .

Komunikat o błędzie jest podobny do następującego przykładu:

SqlException: Invalid column name 'FirstName'.
  • W usłudze PMC wprowadź następujące polecenia, aby utworzyć nową migrację i zaktualizować bazę danych:

    Add-Migration ColumnFirstName
    Update-Database
    

    Pierwsze z tych poleceń generuje następujący komunikat ostrzegawczy:

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

    Ostrzeżenie jest generowane, ponieważ pola nazw są teraz ograniczone do 50 znaków. Jeśli nazwa w bazie danych miała więcej niż 50 znaków, utracono znak od 51 do ostatniego.

  • Otwórz tabelę Student w programie SSOX:

    Students table in SSOX after migrations

    Przed zastosowaniem migracji kolumny nazw były typu nvarchar(MAX). Kolumny nazw to teraz nvarchar(50). Nazwa kolumny zmieniła się z FirstMidName na FirstName.

  • Uruchom aplikację i przejdź do strony Uczniowie.
  • Zwróć uwagę, że czasy nie są wejściowe lub wyświetlane wraz z datami.
  • Wybierz pozycję Utwórz nowy i spróbuj wprowadzić nazwę dłuższą niż 50 znaków.

Uwaga

W poniższych sekcjach kompilowanie aplikacji na niektórych etapach generuje błędy kompilatora. Instrukcje określają, kiedy należy skompilować aplikację.

Jednostka instruktora

Instructor entity

Utwórz Models/Instructor.cs za pomocą następującego kodu:

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

Wiele atrybutów może znajdować się w jednym wierszu. Atrybuty HireDate można napisać w następujący sposób:

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

Właściwości CourseAssignments i OfficeAssignment to właściwości nawigacji.

Instruktor może uczyć dowolną liczbę kursów, dlatego CourseAssignments jest definiowany jako kolekcja.

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

Instruktor może mieć co najwyżej jedno biuro, więc OfficeAssignment nieruchomość posiada jedną OfficeAssignment jednostkę. OfficeAssignment parametr ma wartość null, jeśli nie przypisano żadnego pakietu Office.

public OfficeAssignment OfficeAssignment { get; set; }

Jednostka OfficeAssignment

OfficeAssignment entity

Utwórz Models/OfficeAssignment.cs za pomocą następującego kodu:

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

Atrybut Klucz

Atrybut [Key] służy do identyfikowania właściwości jako klucza podstawowego (PK), gdy nazwa właściwości jest czymś innym niż classnameID lub ID.

Istnieje relacja jeden do zera lub jednego między jednostkami Instructor i OfficeAssignment . Przypisanie biura istnieje tylko w odniesieniu do instruktora, do niego przypisano. Klucz OfficeAssignment PK jest również jego kluczem obcym (FK) do Instructor jednostki.

EF Core Program nie może automatycznie rozpoznać InstructorID klucza PK, OfficeAssignment ponieważ InstructorID nie jest zgodna z konwencją nazewnictwa identyfikatora ani identyfikatora klasy. W związku z tym Key atrybut jest używany do identyfikowania InstructorID jako PK:

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

Domyślnie klucz jest traktowany jako niegenerowany przez bazę danych, EF Core ponieważ kolumna służy do identyfikowania relacji.

Właściwość nawigacji instruktora

Właściwość Instructor.OfficeAssignment nawigacji może mieć wartość null, ponieważ może nie być OfficeAssignment wiersz dla danego instruktora. Instruktor może nie mieć przypisania biura.

Właściwość OfficeAssignment.Instructor nawigacji zawsze będzie mieć jednostkę instruktora, ponieważ typ klucza InstructorID obcego to int, typ wartości innej niż null. Przypisanie biura nie może istnieć bez instruktora.

Instructor Gdy jednostka ma powiązaną OfficeAssignment jednostkę, każda jednostka ma odwołanie do drugiej jednostki we właściwości nawigacji.

Jednostka kursu

Course entity

Zaktualizuj Models/Course.cs za pomocą następującego kodu:

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

Jednostka Course ma właściwość DepartmentIDklucza obcego (FK). DepartmentID wskazuje powiązaną Department jednostkę. Jednostka Course ma Department właściwość nawigacji.

EF Core nie wymaga właściwości klucza obcego dla modelu danych, gdy model ma właściwość nawigacji dla powiązanej jednostki. EF Core automatycznie tworzy zestawy FKs w bazie danych wszędzie tam, gdzie są potrzebne. EF Core Tworzy właściwości w tle dla automatycznie utworzonych zestawów FKs. Jednak jawne uwzględnienie klucza FK w modelu danych może sprawić, że aktualizacje będą prostsze i bardziej wydajne. Rozważmy na przykład model, w którym właściwość DepartmentID FK nie jest uwzględniona. Po pobraniu jednostki kursu do edycji:

  • Właściwość Department ma wartość null, jeśli nie jest jawnie załadowana.
  • Aby zaktualizować jednostkę kursu, Department należy najpierw pobrać jednostkę.

Jeśli właściwość DepartmentID FK jest uwzględniona w modelu danych, nie ma potrzeby pobierania Department jednostki przed aktualizacją.

Atrybut DatabaseGenerated

Atrybut [DatabaseGenerated(DatabaseGeneratedOption.None)] określa, że klucz PK jest dostarczany przez aplikację, a nie generowany przez bazę danych.

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

Domyślnie zakłada się, EF Core że wartości PK są generowane przez bazę danych. Generowanie bazy danych jest zazwyczaj najlepszym rozwiązaniem. W przypadku Course jednostek użytkownik określa klucz PK. Na przykład numer kursu, taki jak seria 1000 dla działu matematyki, seria 2000 dla działu angielskiego.

Atrybut może być również używany do generowania DatabaseGenerated wartości domyślnych. Na przykład baza danych może automatycznie wygenerować pole daty, aby zarejestrować datę utworzenia lub zaktualizowania wiersza. Aby uzyskać więcej informacji, zobacz Wygenerowane właściwości.

Właściwości klucza obcego i nawigacji

Właściwości klucza obcego i właściwości nawigacji w jednostce Course odzwierciedlają następujące relacje:

Kurs jest przypisywany do jednego działu, więc istnieje DepartmentID klucz FK i Department właściwość nawigacji.

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

Kurs może mieć dowolną liczbę zarejestrowanych w nim uczniów, więc Enrollments właściwość nawigacji jest kolekcją:

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

Kurs może być nauczany przez wielu instruktorów, więc CourseAssignments właściwość nawigacji jest kolekcją:

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

CourseAssignment jest wyjaśniony później.

Jednostka Dział

Department entity

Utwórz Models/Department.cs za pomocą następującego kodu:

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

Atrybut Kolumna

Column Wcześniej atrybut był używany do zmiany mapowania nazw kolumn. W kodzie Department jednostki Column atrybut jest używany do zmiany mapowania typu danych SQL. Kolumna jest definiowana Budget przy użyciu typu pieniędzy programu SQL Server w bazie danych:

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

Mapowanie kolumn zwykle nie jest wymagane. EF Core wybiera odpowiedni typ danych programu SQL Server na podstawie typu CLR dla właściwości . Typ CLR decimal jest mapowy na typ programu SQL Server decimal . Budget jest dla waluty, a typ danych pieniężnych jest bardziej odpowiedni dla waluty.

Właściwości klucza obcego i nawigacji

Właściwości FK i nawigacji odzwierciedlają następujące relacje:

  • Dział może lub nie ma administratora.
  • Administrator jest zawsze instruktorem. W związku z InstructorID tym właściwość jest dołączana jako klucz FK do Instructor jednostki.

Właściwość nawigacji ma nazwę Administrator , ale zawiera Instructor jednostkę:

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

Znak zapytania (?) w poprzednim kodzie określa właściwość ma wartość null.

Dział może mieć wiele kursów, więc istnieje właściwość nawigacji Kursy:

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

Zgodnie z konwencją umożliwia EF Core usuwanie kaskadowe dla niepustych zestawów FKs i relacji wiele-do-wielu. To domyślne zachowanie może spowodować cykliczne reguły usuwania kaskadowego. Reguły usuwania kaskadowego cykliczne powodują wyjątek podczas dodawania migracji.

Jeśli na przykład Department.InstructorID właściwość została zdefiniowana jako niepusta, EF Core skonfiguruje regułę usuwania kaskadowego. W takim przypadku dział zostanie usunięty po usunięciu instruktora przypisanego jako jego administrator. W tym scenariuszu reguła ograniczeń będzie bardziej zrozumiała. Następujący płynny interfejs API ustawi regułę ograniczania i wyłączy kaskadowe usuwanie.

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

Jednostka Rejestracja

Rekord rejestracji dotyczy jednego kursu podjętego przez jednego ucznia.

Enrollment entity

Zaktualizuj Models/Enrollment.cs za pomocą następującego kodu:

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

Właściwości klucza obcego i nawigacji

Właściwości klucza FK i właściwości nawigacji odzwierciedlają następujące relacje:

Rekord rejestracji dotyczy jednego kursu, więc istnieje CourseID właściwość FK i Course właściwość nawigacji:

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

Rekord rejestracji jest przeznaczony dla jednego ucznia, więc istnieje StudentID właściwość FK i Student właściwość nawigacji:

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

Relacje wiele-do-wielu

Istnieje relacja wiele-do-wielu między jednostkami Student i Course . Jednostka Enrollment pełni funkcję tabeli sprzężenia wiele-do-wielu z ładunkiem w bazie danych. "Z ładunkiem" oznacza, że Enrollment tabela zawiera dodatkowe dane oprócz zestawów FKs dla tabel sprzężonych (w tym przypadku PK i Grade).

Poniższa ilustracja przedstawia wygląd tych relacji na diagramie jednostki. (Ten diagram został wygenerowany przy użyciu polecenia Narzędzia EF Power Tools for EF 6.x. Tworzenie diagramu nie jest częścią samouczka).

Student-Course many to many relationship

Każda linia relacji ma wartość 1 na jednym końcu i gwiazdkę (*) z drugiej strony wskazującą relację jeden do wielu.

Enrollment Jeśli tabela nie zawierała informacji o klasie, musi zawierać tylko dwa elementy FKs (CourseID i StudentID). Tabela sprzężenia wiele-do-wielu bez ładunku jest czasami nazywana czystą tabelą sprzężenia (PJT).

Jednostki Instructor i Course mają relację wiele do wielu przy użyciu czystej tabeli sprzężenia.

Uwaga: program EF 6.x obsługuje niejawne tabele sprzężenia dla relacji wiele-do-wielu, ale EF Core nie. Aby uzyskać więcej informacji, zobacz Relacje wiele-do-wielu w EF Core wersji 2.0.

Jednostka CourseAssignment

CourseAssignment entity

Utwórz Models/CourseAssignment.cs za pomocą następującego kodu:

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

Relacja Instruktor-to-Courses wiele do wielu wymaga tabeli sprzężenia, a jednostka dla tej tabeli sprzężenia to CourseAssignment.

Instructor-to-Courses m:M

Nazwa jednostki EntityName1EntityName2sprzężenia jest pospolita. Na przykład tabela dołączania instruktora do kursów używająca tego wzorca to CourseInstructor. Zalecamy jednak użycie nazwy, która opisuje relację.

Modele danych zaczynają się proste i rosną. Tabele sprzężenia bez ładunku (PJT) często ewoluują w celu uwzględnienia ładunku. Zaczynając od opisowej nazwy jednostki, nazwa nie musi zmieniać się po zmianie tabeli sprzężenia. Najlepiej, aby jednostka sprzężenia miała własną naturalną (prawdopodobnie pojedynczą nazwę) w domenie biznesowej. Na przykład książki i klienci mogą być połączone z jednostką sprzężenia o nazwie Ratings. W przypadku relacji CourseAssignment Instruktor-to-Courses wiele do wielu preferowane jest ponad CourseInstructor.

Klucz złożony

Dwa zestawy FKs w CourseAssignment elemecie (InstructorID i CourseID) są unikatowo identyfikowane w każdym wierszu CourseAssignment tabeli. CourseAssignment nie wymaga dedykowanego klucza PK. Właściwości InstructorID i CourseID działają jako złożony klucz PK. Jedynym sposobem określenia złożonych zestawów PKs EF Core do jest użycie płynnego interfejsu API. W następnej sekcji pokazano, jak skonfigurować złożony klucz PK.

Klucz złożony gwarantuje, że:

  • Wiele wierszy jest dozwolonych dla jednego kursu.
  • Wiele wierszy jest dozwolonych dla jednego instruktora.
  • Wiele wierszy nie jest dozwolonych dla tego samego instruktora i kursu.

Jednostka Enrollment sprzężenia definiuje własny klucz PK, więc możliwe są duplikaty tego rodzaju. Aby zapobiec takim duplikatom:

  • Dodawanie unikatowego indeksu w polach FK lub
  • Skonfiguruj Enrollment przy użyciu podstawowego klucza złożonego podobnego do CourseAssignment. Aby uzyskać więcej informacji, zobacz Indeksy.

Aktualizowanie kontekstu bazy danych

Zaktualizuj Data/SchoolContext.cs za pomocą następującego kodu:

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

Powyższy kod dodaje nowe jednostki i konfiguruje CourseAssignment złożony klucz PK jednostki.

Fluent API alternatywa dla atrybutów

Metoda OnModelCreating w poprzednim kodzie używa płynnego interfejsu API do konfigurowania EF Core zachowania. Interfejs API jest nazywany "płynnym", ponieważ jest często używany przez ciągowanie serii wywołań metod w jedną instrukcję. Poniższy kod jest przykładem płynnego interfejsu API:

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

W tym samouczku płynny interfejs API jest używany tylko do mapowania bazy danych, których nie można wykonać za pomocą atrybutów. Jednak płynny interfejs API może określać większość reguł formatowania, walidacji i mapowania, które można wykonać za pomocą atrybutów.

Niektóre atrybuty, takie jak MinimumLength nie można zastosować za pomocą płynnego interfejsu API. MinimumLength nie zmienia schematu, ale stosuje tylko regułę weryfikacji minimalnej długości.

Niektórzy deweloperzy wolą używać płynnego interfejsu API wyłącznie tak, aby mogli zachować klasy jednostek "czyste". Atrybuty i płynny interfejs API można mieszać. Istnieją pewne konfiguracje, które można wykonać tylko za pomocą płynnego interfejsu API (określając złożony klucz PK). Istnieją pewne konfiguracje, które można wykonać tylko za pomocą atrybutów (MinimumLength). Zalecana praktyka korzystania z płynnego interfejsu API lub atrybutów:

  • Wybierz jedno z tych dwóch podejść.
  • Użyj wybranego podejścia spójnie, jak najwięcej.

Niektóre atrybuty używane w tym samouczku są używane w następujących celach:

  • Tylko walidacja (na przykład MinimumLength).
  • EF Core tylko konfiguracja (na przykład HasKey).
  • Walidacja i EF Core konfiguracja (na przykład [StringLength(50)]).

Aby uzyskać więcej informacji na temat atrybutów i płynnego interfejsu API, zobacz Metody konfiguracji.

Diagram jednostki

Na poniższej ilustracji przedstawiono diagram utworzony przez narzędzia EF Power Tools dla ukończonego modelu School.

Entity diagram

Na powyższym diagramie przedstawiono:

  • Kilka wierszy relacji jeden do wielu (od 1 do *).
  • Wiersz relacji jeden do zera lub jednego (od 1 do 0,1) między jednostkami Instructor i OfficeAssignment .
  • Wiersz relacji zero lub jeden do wielu (od 0..1 do *) między jednostkami Instructor i Department .

Inicjowanie bazy danych

Zaktualizuj kod w pliku 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();
        }
    }
}

Powyższy kod zawiera dane inicjujące dla nowych jednostek. Większość tego kodu tworzy nowe obiekty jednostek i ładuje przykładowe dane. Przykładowe dane są używane do testowania. Zobacz Enrollments i CourseAssignments , aby zapoznać się z przykładami liczby do wielu tabel sprzężenia, które można zainicjować.

Dodawanie migracji

Skompiluj projekt.

W usłudze PMC uruchom następujące polecenie.

Add-Migration ComplexDataModel

Poprzednie polecenie wyświetla ostrzeżenie o możliwej utracie danych.

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 Jeśli polecenie zostanie uruchomione, zostanie wygenerowany następujący błąd:

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

W następnej sekcji zobaczysz, co zrobić z tym błędem.

Zastosuj migrację lub upuść i utwórz ponownie

Teraz, gdy masz istniejącą bazę danych, musisz zastanowić się, jak zastosować do niej zmiany. W tym samouczku przedstawiono dwie alternatywy:

Wybór działa w przypadku programu SQL Server. Chociaż metoda apply-migration jest bardziej złożona i czasochłonna, jest to preferowane podejście dla rzeczywistych środowisk produkcyjnych.

Usuwanie i ponowne tworzenie bazy danych

Pomiń tę sekcję , jeśli używasz programu SQL Server i chcesz wykonać podejście apply-migration w poniższej sekcji.

Aby wymusić EF Core utworzenie nowej bazy danych, upuść i zaktualizować bazę danych:

  • W konsoli Menedżer pakietów (PMC) uruchom następujące polecenie:

    Drop-Database
    
  • Usuń folder Migrations, a następnie uruchom następujące polecenie:

    Add-Migration InitialCreate
    Update-Database
    

Uruchom aplikację. Uruchomienie aplikacji powoduje uruchomienie DbInitializer.Initialize metody . Obiekt DbInitializer.Initialize wypełnia nową bazę danych.

Otwórz bazę danych w programie SSOX:

  • Jeśli system SSOX został wcześniej otwarty, kliknij przycisk Odśwież .

  • Rozwiń węzeł Tabele. Zostaną wyświetlone utworzone tabele.

    Tables in SSOX

  • Zapoznaj się z tabelą CourseAssignment :

    • Kliknij prawym przyciskiem myszy tabelę CourseAssignment i wybierz pozycję Wyświetl dane.
    • Sprawdź, czy tabela CourseAssignment zawiera dane.

    CourseAssignment data in SSOX

Stosowanie migracji

Ta sekcja jest opcjonalna. Te kroki działają tylko dla programu SQL Server LocalDB i tylko wtedy, gdy pominięto poprzednią sekcję Drop i ponownie utwórz bazę danych .

Gdy migracje są uruchamiane z istniejącymi danymi, mogą istnieć ograniczenia szyfrowania FK, które nie są zadowalające z istniejących danych. W przypadku danych produkcyjnych należy wykonać kroki migracji istniejących danych. Ta sekcja zawiera przykład naprawiania naruszeń ograniczeń FK. Nie wprowadzaj tych zmian w kodzie bez tworzenia kopii zapasowej. Nie wprowadzaj tych zmian w kodzie, jeśli ukończono poprzednią sekcję Upuść i ponownie utwórz bazę danych .

Plik {timestamp}_ComplexDataModel.cs zawiera następujący kod:

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

Powyższy kod dodaje do Course tabeli klucz FK bez DepartmentID wartości null. Baza danych z poprzedniego samouczka zawiera wiersze w Coursepliku , dzięki czemu nie można zaktualizować tabeli przez migracje.

Aby migracja ComplexDataModel działała z istniejącymi danymi:

  • Zmień kod, aby nadać nowej kolumnie (DepartmentID) wartość domyślną.
  • Utwórz fałszywy dział o nazwie "Temp", aby działać jako domyślny dział.

Naprawianie ograniczeń klucza obcego

ComplexDataModel W klasie migracji zaktualizuj metodę Up :

  • Otwórz plik {timestamp}_ComplexDataModel.cs.
  • Oznacz jako komentarz wiersz kodu, który dodaje kolumnę DepartmentIDCourse do tabeli.
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);

Dodaj następujący wyróżniony kod. Nowy kod przechodzi po .CreateTable( name: "Department" bloku:

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

Po wykonaniu powyższych zmian istniejące Course wiersze będą powiązane z działem "Temp" po uruchomieniu ComplexDataModel.Up metody.

Sposób obsługi sytuacji pokazanej tutaj jest uproszczony dla tego samouczka. Aplikacja produkcyjna byłaby:

  • Dołącz kod lub skrypty, aby dodać Department wiersze i powiązane Course wiersze do nowych Department wierszy.
  • Nie należy używać działu "Temp" ani wartości domyślnej dla .Course.DepartmentID
  • W konsoli Menedżer pakietów (PMC) uruchom następujące polecenie:

    Update-Database
    

DbInitializer.Initialize Ponieważ metoda jest przeznaczona do pracy tylko z pustą bazą danych, użyj programu SSOX, aby usunąć wszystkie wiersze w tabelach Student i Course. (Usunięcie kaskadowe zajmie się tabelą Enrollment).

Uruchom aplikację. Uruchomienie aplikacji powoduje uruchomienie DbInitializer.Initialize metody . Obiekt DbInitializer.Initialize wypełnia nową bazę danych.

Następne kroki

W kolejnych dwóch samouczkach pokazano, jak odczytywać i aktualizować powiązane dane.

Poprzednie samouczki współpracowały z podstawowym modelem danych składającym się z trzech jednostek. W tym samouczku:

  • Dodano więcej jednostek i relacji.
  • Model danych jest dostosowywany przez określenie reguł formatowania, walidacji i mapowania bazy danych.

Klasy jednostek dla ukończonego modelu danych przedstawiono na poniższej ilustracji:

Entity diagram

Jeśli napotkasz problemy, których nie możesz rozwiązać, pobierz ukończoną aplikację.

Dostosowywanie modelu danych za pomocą atrybutów

W tej sekcji model danych jest dostosowywany przy użyciu atrybutów.

Atrybut DataType

Strony uczniów wyświetla obecnie godzinę rejestracji. Zazwyczaj pola daty zawierają tylko datę, a nie godzinę.

Zaktualizuj Models/Student.cs za pomocą następującego wyróżnionego kodu:

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

Atrybut DataType określa typ danych, który jest bardziej szczegółowy niż typ wewnętrzny bazy danych. W takim przypadku powinna być wyświetlana tylko data, a nie data i godzina. Wyliczenie DataType zawiera wiele typów danych, takich jak Data, Godzina, Telefon Number, Waluta, Adres e-mail itp. Atrybut DataType może również umożliwić aplikacji automatyczne udostępnianie funkcji specyficznych dla typu. Przykład:

  • Link mailto: jest tworzony automatycznie dla elementu DataType.EmailAddress.
  • Selektor dat jest udostępniany DataType.Date w większości przeglądarek.

Atrybut DataType emituje atrybuty HTML 5 data- (wymawiane kreska danych), z których korzystają przeglądarki HTML 5. Atrybuty DataType nie zapewniają walidacji.

DataType.Date nie określa formatu wyświetlanej daty. Domyślnie pole daty jest wyświetlane zgodnie z domyślnymi formatami na podstawie informacji o kulturze serwera.

Atrybut DisplayFormat jest używany do jawnego określenia formatu daty:

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

Ustawienie ApplyFormatInEditMode określa, że formatowanie powinno być również stosowane do interfejsu użytkownika edycji. Niektóre pola nie powinny używać elementu ApplyFormatInEditMode. Na przykład symbol waluty zazwyczaj nie powinien być wyświetlany w polu tekstowym edycji.

Atrybut DisplayFormat może być używany samodzielnie. Zazwyczaj dobrym pomysłem jest użycie atrybutu DataType z atrybutem DisplayFormat . Atrybut DataType przekazuje semantyka danych, w przeciwieństwie do sposobu renderowania ich na ekranie. Atrybut DataType zapewnia następujące korzyści, które nie są dostępne w programie DisplayFormat:

  • Przeglądarka może włączyć funkcje HTML5. Na przykład pokaż kontrolkę kalendarza, odpowiedni symbol waluty ustawień regionalnych, linki e-mail, walidację danych wejściowych po stronie klienta itp.
  • Domyślnie przeglądarka renderuje dane przy użyciu poprawnego formatu na podstawie ustawień regionalnych.

Aby uzyskać więcej informacji, zobacz dokumentację pomocnika tagów <wejściowych>.

Uruchom aplikację. Przejdź do strony Indeks uczniów. Czasy nie są już wyświetlane. Każdy widok korzystający z Student modelu wyświetla datę bez godziny.

Students index page showing dates without times

Atrybut StringLength

Reguły walidacji danych i komunikaty o błędach walidacji można określić za pomocą atrybutów. Atrybut StringLength określa minimalną i maksymalną długość znaków dozwolonych w polu danych. Atrybut StringLength zapewnia również weryfikację po stronie klienta i po stronie serwera. Minimalna wartość nie ma wpływu na schemat bazy danych.

Student Zaktualizuj model przy użyciu następującego kodu:

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

Powyższy kod ogranicza nazwy do nie więcej niż 50 znaków. Atrybut StringLength nie uniemożliwia użytkownikowi wprowadzania białych znaków dla nazwy. Atrybut RegularExpression służy do stosowania ograniczeń do danych wejściowych. Na przykład poniższy kod wymaga, aby pierwszy znak był wielkimi literami, a pozostałe znaki mają postać alfabetyczną:

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

Uruchom aplikację:

  • Przejdź do strony Uczniowie.
  • Wybierz pozycję Utwórz nowy i wprowadź nazwę dłuższą niż 50 znaków.
  • Wybierz pozycję Utwórz, walidacja po stronie klienta wyświetla komunikat o błędzie.

Students index page showing string length errors

W programie SQL Server Eksplorator obiektów (SSOX) otwórz projektanta tabeli Student, klikając dwukrotnie tabelę Student.

Students table in SSOX before migrations

Na powyższym obrazie przedstawiono schemat tabeli Student . Pola nazw mają typ nvarchar(MAX) , ponieważ migracje nie zostały uruchomione w bazie danych. Po uruchomieniu migracji w dalszej części tego samouczka pola nazw staną się .nvarchar(50)

Atrybut Kolumna

Atrybuty mogą kontrolować sposób mapowania klas i właściwości na bazę danych. W tej sekcji atrybut Column jest używany do mapowania nazwy FirstMidName właściwości na "FirstName" w bazie danych.

Podczas tworzenia bazy danych nazwy właściwości w modelu są używane dla nazw kolumn (z wyjątkiem przypadków użycia atrybutu Column ).

Model Student używa FirstMidName pola imię, ponieważ pole może również zawierać nazwę środkową.

Student.cs Zaktualizuj plik przy użyciu następującego wyróżnionego kodu:

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

Po poprzedniej zmianie Student.FirstMidName w aplikacji jest mapowanie na kolumnę FirstNameStudent tabeli.

Dodanie atrybutu Column zmienia model kopii zapasowej SchoolContextelementu . Model kopii zapasowej SchoolContext bazy danych nie jest już zgodny z bazą danych. Jeśli aplikacja jest uruchamiana przed zastosowaniem migracji, zostanie wygenerowany następujący wyjątek:

SqlException: Invalid column name 'FirstName'.

Aby zaktualizować bazę danych:

  • Skompiluj projekt.
  • Otwórz okno polecenia w folderze projektu. Wprowadź następujące polecenia, aby utworzyć nową migrację i zaktualizować bazę danych:
Add-Migration ColumnFirstName
Update-Database

Polecenie migrations add ColumnFirstName generuje następujący komunikat ostrzegawczy:

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

Ostrzeżenie jest generowane, ponieważ pola nazw są teraz ograniczone do 50 znaków. Jeśli nazwa w bazie danych miała więcej niż 50 znaków, utracono od 51 do ostatniego znaku.

  • Testowanie aplikacji.

Otwórz tabelę Student w programie SSOX:

Students table in SSOX after migrations

Przed zastosowaniem migracji kolumny nazw były typu nvarchar(MAX). Kolumny nazw to teraz nvarchar(50). Nazwa kolumny zmieniła się z FirstMidName na FirstName.

Uwaga

W poniższej sekcji kompilowanie aplikacji na niektórych etapach generuje błędy kompilatora. Instrukcje określają, kiedy należy skompilować aplikację.

Aktualizacja jednostki ucznia

Student entity

Zaktualizuj Models/Student.cs za pomocą następującego kodu:

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

Wymagany atrybut

Atrybut Required sprawia, że właściwości nazwy są wymagane pola. Atrybut Required nie jest wymagany w przypadku typów innych niż null, takich jak typy wartości (DateTime, int, doubleitp.). Typy, które nie mogą mieć wartości null, są automatycznie traktowane jako wymagane pola.

Atrybut Required można zastąpić parametrem o minimalnej długości w atrybucie StringLength :

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

Atrybut Wyświetlania

Atrybut Display określa, że podpis pól tekstowych powinny być "Imię", "Nazwisko", "Imię", "Imię", "Imię" i "Data rejestracji". Domyślne podpis nie miały spacji dzielącej wyrazy, na przykład "Lastname".

Właściwość obliczeniowa FullName

FullName jest właściwością obliczeniową, która zwraca wartość utworzoną przez łączenie dwóch innych właściwości. FullName Nie można go ustawić, ma tylko metodę pobierania. W bazie danych nie FullName jest tworzona żadna kolumna.

Tworzenie jednostki instruktora

Instructor entity

Utwórz Models/Instructor.cs za pomocą następującego kodu:

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

Wiele atrybutów może znajdować się w jednym wierszu. Atrybuty HireDate można napisać w następujący sposób:

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

Właściwości nawigacji CourseAssignments i OfficeAssignment

Właściwości CourseAssignments i OfficeAssignment to właściwości nawigacji.

Instruktor może uczyć dowolną liczbę kursów, dlatego CourseAssignments jest definiowany jako kolekcja.

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

Jeśli właściwość nawigacji zawiera wiele jednostek:

  • Musi to być typ listy, w którym można dodawać, usuwać i aktualizować wpisy.

Typy właściwości nawigacji obejmują:

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

Jeśli ICollection<T> zostanie określony, EF Core domyślnie tworzy HashSet<T> kolekcję.

Jednostka CourseAssignment jest objaśniona w sekcji dotyczącej relacji wiele-do-wielu.

Reguły biznesowe firmy Contoso University stwierdzają, że instruktor może mieć co najwyżej jedno biuro. Właściwość OfficeAssignment zawiera jedną OfficeAssignment jednostkę. OfficeAssignment parametr ma wartość null, jeśli nie przypisano żadnego pakietu Office.

public OfficeAssignment OfficeAssignment { get; set; }

Tworzenie jednostki OfficeAssignment

OfficeAssignment entity

Utwórz Models/OfficeAssignment.cs za pomocą następującego kodu:

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

Atrybut Klucz

Atrybut [Key] służy do identyfikowania właściwości jako klucza podstawowego (PK), gdy nazwa właściwości jest czymś innym niż classnameID lub ID.

Istnieje relacja jeden do zera lub jednego między jednostkami Instructor i OfficeAssignment . Przypisanie biura istnieje tylko w odniesieniu do instruktora, do niego przypisano. Klucz OfficeAssignment PK jest również jego kluczem obcym (FK) do Instructor jednostki. EF Core Nie można automatycznie rozpoznać InstructorID klucza PK z następujących powodów OfficeAssignment :

  • InstructorID nie jest zgodna z konwencją nazewnictwa id lub classnameID.

W związku z tym Key atrybut jest używany do identyfikowania InstructorID jako PK:

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

Domyślnie klucz jest traktowany jako niegenerowany przez bazę danych, EF Core ponieważ kolumna służy do identyfikowania relacji.

Właściwość nawigacji instruktora

Właściwość OfficeAssignment nawigacji dla Instructor jednostki jest dopuszczana do wartości null, ponieważ:

  • Typy odwołań (takie jak klasy są dopuszczane do wartości null).
  • Instruktor może nie mieć przypisania biura.

Jednostka OfficeAssignment ma właściwość nawigacji bez wartości null Instructor , ponieważ:

  • InstructorID nie jest dopuszczana do wartości null.
  • Przypisanie biura nie może istnieć bez instruktora.

Instructor Gdy jednostka ma powiązaną OfficeAssignment jednostkę, każda jednostka ma odwołanie do drugiej jednostki we właściwości nawigacji.

Atrybut [Required] można zastosować do Instructor właściwości nawigacji:

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

Powyższy kod określa, że musi istnieć powiązany instruktor. Powyższy kod jest niepotrzebny, ponieważ InstructorID klucz obcy (który jest również kluczem PK) nie może zawierać wartości null.

Modyfikowanie jednostki kursu

Course entity

Zaktualizuj Models/Course.cs za pomocą następującego kodu:

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

Jednostka Course ma właściwość DepartmentIDklucza obcego (FK). DepartmentID wskazuje powiązaną Department jednostkę. Jednostka Course ma Department właściwość nawigacji.

EF Core nie wymaga właściwości FK dla modelu danych, gdy model ma właściwość nawigacji dla powiązanej jednostki.

EF Core automatycznie tworzy zestawy FKs w bazie danych wszędzie tam, gdzie są potrzebne. EF Core Tworzy właściwości w tle dla automatycznie utworzonych zestawów FKs. Posiadanie klucza FK w modelu danych może sprawić, że aktualizacje będą prostsze i bardziej wydajne. Rozważmy na przykład model, w którym właściwość DepartmentID FK nie jest uwzględniona. Po pobraniu jednostki kursu do edycji:

  • Jednostka Department ma wartość null, jeśli nie jest jawnie załadowana.
  • Aby zaktualizować jednostkę kursu, Department należy najpierw pobrać jednostkę.

Jeśli właściwość DepartmentID FK jest uwzględniona w modelu danych, nie ma potrzeby pobierania Department jednostki przed aktualizacją.

Atrybut DatabaseGenerated

Atrybut [DatabaseGenerated(DatabaseGeneratedOption.None)] określa, że klucz PK jest dostarczany przez aplikację, a nie generowany przez bazę danych.

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

Domyślnie zakłada się, EF Core że wartości PK są generowane przez bazę danych. Wartości PK wygenerowane przez bazę danych są zazwyczaj najlepszym rozwiązaniem. W przypadku Course jednostek użytkownik określa klucz PK. Na przykład numer kursu, taki jak seria 1000 dla działu matematyki, seria 2000 dla działu angielskiego.

Atrybut może być również używany do generowania DatabaseGenerated wartości domyślnych. Na przykład baza danych może automatycznie wygenerować pole daty, aby zarejestrować datę utworzenia lub zaktualizowania wiersza. Aby uzyskać więcej informacji, zobacz Wygenerowane właściwości.

Właściwości klucza obcego i nawigacji

Właściwości klucza obcego i właściwości nawigacji w jednostce Course odzwierciedlają następujące relacje:

Kurs jest przypisywany do jednego działu, więc istnieje DepartmentID klucz FK i Department właściwość nawigacji.

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

Kurs może mieć dowolną liczbę zarejestrowanych w nim uczniów, więc Enrollments właściwość nawigacji jest kolekcją:

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

Kurs może być nauczany przez wielu instruktorów, więc CourseAssignments właściwość nawigacji jest kolekcją:

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

CourseAssignment jest wyjaśniony później.

Tworzenie jednostki Dział

Department entity

Utwórz Models/Department.cs za pomocą następującego kodu:

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

Atrybut Kolumna

Column Wcześniej atrybut był używany do zmiany mapowania nazw kolumn. W kodzie Department jednostki Column atrybut jest używany do zmiany mapowania typu danych SQL. Kolumna jest definiowana Budget przy użyciu typu pieniędzy programu SQL Server w bazie danych:

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

Mapowanie kolumn zwykle nie jest wymagane. EF Core zazwyczaj wybiera odpowiedni typ danych programu SQL Server na podstawie typu CLR dla właściwości . Typ CLR decimal jest mapowy na typ programu SQL Server decimal . Budget jest dla waluty, a typ danych pieniężnych jest bardziej odpowiedni dla waluty.

Właściwości klucza obcego i nawigacji

Właściwości FK i nawigacji odzwierciedlają następujące relacje:

  • Dział może lub nie ma administratora.
  • Administrator jest zawsze instruktorem. W związku z InstructorID tym właściwość jest dołączana jako klucz FK do Instructor jednostki.

Właściwość nawigacji ma nazwę Administrator , ale zawiera Instructor jednostkę:

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

Znak zapytania (?) w poprzednim kodzie określa właściwość ma wartość null.

Dział może mieć wiele kursów, więc istnieje właściwość nawigacji Kursy:

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

Uwaga: zgodnie z konwencją EF Core umożliwia usuwanie kaskadowe dla niepustych zestawów FKs i relacji wiele-do-wielu. Kaskadowe usuwanie może spowodować cykliczne reguły usuwania kaskadowego. Reguły usuwania kaskadowego powodują wyjątek podczas dodawania migracji.

Jeśli na przykład właściwość została zdefiniowana Department.InstructorID jako niepusta:

  • EF Core Konfiguruje regułę usuwania kaskadowego w celu usunięcia działu po usunięciu instruktora.

  • Usunięcie działu, gdy instruktor zostanie usunięty, nie jest zamierzonym zachowaniem.

  • Poniższy płynny interfejs API ustawi regułę ograniczenia zamiast kaskady.

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

Powyższy kod wyłącza kaskadowe usuwanie relacji z działem instruktora.

Aktualizowanie jednostki Enrollment

Rekord rejestracji dotyczy jednego kursu podjętego przez jednego ucznia.

Enrollment entity

Zaktualizuj Models/Enrollment.cs za pomocą następującego kodu:

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

Właściwości klucza obcego i nawigacji

Właściwości klucza FK i właściwości nawigacji odzwierciedlają następujące relacje:

Rekord rejestracji dotyczy jednego kursu, więc istnieje CourseID właściwość FK i Course właściwość nawigacji:

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

Rekord rejestracji jest przeznaczony dla jednego ucznia, więc istnieje StudentID właściwość FK i Student właściwość nawigacji:

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

Relacje wiele-do-wielu

Istnieje relacja wiele-do-wielu między jednostkami Student i Course . Jednostka Enrollment pełni funkcję tabeli sprzężenia wiele-do-wielu z ładunkiem w bazie danych. "Z ładunkiem" oznacza, że Enrollment tabela zawiera dodatkowe dane oprócz zestawów FKs dla tabel sprzężonych (w tym przypadku PK i Grade).

Poniższa ilustracja przedstawia wygląd tych relacji na diagramie jednostki. (Ten diagram został wygenerowany przy użyciu polecenia Narzędzia EF Power Tools for EF 6.x. Tworzenie diagramu nie jest częścią samouczka).

Student-Course many to many relationship

Każda linia relacji ma wartość 1 na jednym końcu i gwiazdkę (*) z drugiej strony wskazującą relację jeden do wielu.

Enrollment Jeśli tabela nie zawierała informacji o klasie, musi zawierać tylko dwa elementy FKs (CourseID i StudentID). Tabela sprzężenia wiele-do-wielu bez ładunku jest czasami nazywana czystą tabelą sprzężenia (PJT).

Jednostki Instructor i Course mają relację wiele do wielu przy użyciu czystej tabeli sprzężenia.

Uwaga: program EF 6.x obsługuje niejawne tabele sprzężenia dla relacji wiele-do-wielu, ale EF Core nie. Aby uzyskać więcej informacji, zobacz Relacje wiele-do-wielu w EF Core wersji 2.0.

Jednostka CourseAssignment

CourseAssignment entity

Utwórz Models/CourseAssignment.cs za pomocą następującego kodu:

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

Kursy do instruktora

Instructor-to-Courses m:M

Relacja Instruktor-to-Courses wiele do wielu:

  • Wymaga tabeli sprzężenia, która musi być reprezentowana przez zestaw jednostek.
  • To czysta tabela sprzężenia (tabela bez ładunku).

Nazwa jednostki EntityName1EntityName2sprzężenia jest pospolita. Na przykład tabela Dołączanie instruktora do kursów przy użyciu tego wzorca to CourseInstructor. Zalecamy jednak użycie nazwy, która opisuje relację.

Modele danych zaczynają się proste i rosną. Sprzężenia no-payload (PJTs) często ewoluują w celu uwzględnienia ładunku. Zaczynając od opisowej nazwy jednostki, nazwa nie musi zmieniać się po zmianie tabeli sprzężenia. Najlepiej, aby jednostka sprzężenia miała własną naturalną (prawdopodobnie pojedynczą nazwę) w domenie biznesowej. Na przykład książki i klienci mogą być połączone z jednostką sprzężenia o nazwie Ratings. W przypadku relacji CourseAssignment Instruktor-to-Courses wiele do wielu preferowane jest ponad CourseInstructor.

Klucz złożony

FKs nie są dopuszczane do wartości null. Dwa zestawy FKs w CourseAssignment elemecie (InstructorID i CourseID) są unikatowo identyfikowane w każdym wierszu CourseAssignment tabeli. CourseAssignment nie wymaga dedykowanego klucza PK. Właściwości InstructorID i CourseID działają jako złożony klucz PK. Jedynym sposobem określenia złożonych zestawów PKs EF Core do jest użycie płynnego interfejsu API. W następnej sekcji pokazano, jak skonfigurować złożony klucz PK.

Klucz złożony zapewnia:

  • Wiele wierszy jest dozwolonych dla jednego kursu.
  • Wiele wierszy jest dozwolonych dla jednego instruktora.
  • Wiele wierszy dla tego samego instruktora i kursu nie jest dozwolone.

Jednostka Enrollment sprzężenia definiuje własny klucz PK, więc możliwe są duplikaty tego rodzaju. Aby zapobiec takim duplikatom:

  • Dodawanie unikatowego indeksu w polach FK lub
  • Skonfiguruj Enrollment przy użyciu podstawowego klucza złożonego podobnego do CourseAssignment. Aby uzyskać więcej informacji, zobacz Indeksy.

Aktualizowanie kontekstu bazy danych

Dodaj następujący wyróżniony kod 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 });
        }
    }
}

Powyższy kod dodaje nowe jednostki i konfiguruje CourseAssignment złożony klucz PK jednostki.

Fluent API alternatywa dla atrybutów

Metoda OnModelCreating w poprzednim kodzie używa płynnego interfejsu API do konfigurowania EF Core zachowania. Interfejs API jest nazywany "płynnym", ponieważ jest często używany przez ciągowanie serii wywołań metod w jedną instrukcję. Poniższy kod jest przykładem płynnego interfejsu API:

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

W tym samouczku płynny interfejs API jest używany tylko do mapowania bazy danych, których nie można wykonać za pomocą atrybutów. Jednak płynny interfejs API może określać większość reguł formatowania, walidacji i mapowania, które można wykonać za pomocą atrybutów.

Niektóre atrybuty, takie jak MinimumLength nie można zastosować za pomocą płynnego interfejsu API. MinimumLength nie zmienia schematu, ale stosuje tylko regułę weryfikacji minimalnej długości.

Niektórzy deweloperzy wolą używać płynnego interfejsu API wyłącznie tak, aby mogli zachować klasy jednostek "czyste". Atrybuty i płynny interfejs API można mieszać. Istnieją pewne konfiguracje, które można wykonać tylko za pomocą płynnego interfejsu API (określając złożony klucz PK). Istnieją pewne konfiguracje, które można wykonać tylko za pomocą atrybutów (MinimumLength). Zalecana praktyka korzystania z płynnego interfejsu API lub atrybutów:

  • Wybierz jedno z tych dwóch podejść.
  • Użyj wybranego podejścia spójnie, jak najwięcej.

Niektóre atrybuty używane w tym samouczku są używane w następujących celach:

  • Tylko walidacja (na przykład MinimumLength).
  • EF Core tylko konfiguracja (na przykład HasKey).
  • Walidacja i EF Core konfiguracja (na przykład [StringLength(50)]).

Aby uzyskać więcej informacji na temat atrybutów i płynnego interfejsu API, zobacz Metody konfiguracji.

Diagram jednostki przedstawiający relacje

Na poniższej ilustracji przedstawiono diagram utworzony przez narzędzia EF Power Tools dla ukończonego modelu School.

Entity diagram

Na powyższym diagramie przedstawiono:

  • Kilka wierszy relacji jeden do wielu (od 1 do *).
  • Wiersz relacji jeden do zera lub jednego (od 1 do 0,1) między jednostkami Instructor i OfficeAssignment .
  • Wiersz relacji zero lub jeden do wielu (od 0..1 do *) między jednostkami Instructor i Department .

Rozmieszczanie bazy danych przy użyciu danych testowych

Zaktualizuj kod w pliku 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();
        }
    }
}

Powyższy kod zawiera dane inicjujące dla nowych jednostek. Większość tego kodu tworzy nowe obiekty jednostek i ładuje przykładowe dane. Przykładowe dane są używane do testowania. Zobacz Enrollments i CourseAssignments , aby zapoznać się z przykładami liczby do wielu tabel sprzężenia, które można zainicjować.

Dodawanie migracji

Skompiluj projekt.

Add-Migration ComplexDataModel

Poprzednie polecenie wyświetla ostrzeżenie o możliwej utracie danych.

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 Jeśli polecenie zostanie uruchomione, zostanie wygenerowany następujący błąd:

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

Stosowanie migracji

Teraz, gdy masz istniejącą bazę danych, musisz zastanowić się, jak zastosować do niej przyszłe zmiany. W tym samouczku przedstawiono dwa podejścia:

  • Usuwanie i ponowne tworzenie bazy danych
  • Zastosuj migrację do istniejącej bazy danych. Chociaż ta metoda jest bardziej złożona i czasochłonna, jest to preferowane podejście dla rzeczywistych środowisk produkcyjnych. Uwaga: jest to opcjonalna sekcja samouczka. Możesz wykonać kroki upuszczania i ponownego tworzenia i pominąć tę sekcję. Jeśli chcesz wykonać kroki opisane w tej sekcji, nie wykonaj czynności upuszczania i ponownego tworzenia.

Usuwanie i ponowne tworzenie bazy danych

Kod w zaktualizowanym DbInitializer pliku dodaje dane inicjujowe dla nowych jednostek. Aby wymusić EF Core utworzenie nowej bazy danych, upuść i zaktualizować bazę danych:

W konsoli Menedżer pakietów (PMC) uruchom następujące polecenie:

Drop-Database
Update-Database

Uruchom polecenie Get-Help about_EntityFrameworkCore z pmC, aby uzyskać informacje pomocy.

Uruchom aplikację. Uruchomienie aplikacji powoduje uruchomienie DbInitializer.Initialize metody . Obiekt DbInitializer.Initialize wypełnia nową bazę danych.

Otwórz bazę danych w programie SSOX:

  • Jeśli system SSOX został wcześniej otwarty, kliknij przycisk Odśwież .
  • Rozwiń węzeł Tabele. Zostaną wyświetlone utworzone tabele.

Tables in SSOX

Zapoznaj się z tabelą CourseAssignment :

  • Kliknij prawym przyciskiem myszy tabelę CourseAssignment i wybierz pozycję Wyświetl dane.
  • Sprawdź, czy tabela CourseAssignment zawiera dane.

CourseAssignment data in SSOX

Stosowanie migracji do istniejącej bazy danych

Ta sekcja jest opcjonalna. Te kroki działają tylko wtedy, gdy pominięto poprzednią sekcję Drop i ponownie utwórz bazę danych .

Gdy migracje są uruchamiane z istniejącymi danymi, mogą istnieć ograniczenia szyfrowania FK, które nie są zadowalające z istniejących danych. W przypadku danych produkcyjnych należy wykonać kroki migracji istniejących danych. Ta sekcja zawiera przykład naprawiania naruszeń ograniczeń FK. Nie wprowadzaj tych zmian w kodzie bez tworzenia kopii zapasowej. Nie wprowadzaj tych zmian w kodzie, jeśli poprzednia sekcja została ukończona i zaktualizowana baza danych.

Plik {timestamp}_ComplexDataModel.cs zawiera następujący kod:

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

Powyższy kod dodaje do Course tabeli klucz FK bez DepartmentID wartości null. Baza danych z poprzedniego samouczka zawiera wiersze w Coursepliku , dzięki czemu nie można zaktualizować tabeli przez migracje.

Aby migracja ComplexDataModel działała z istniejącymi danymi:

  • Zmień kod, aby nadać nowej kolumnie (DepartmentID) wartość domyślną.
  • Utwórz fałszywy dział o nazwie "Temp", aby działać jako domyślny dział.

Naprawianie ograniczeń klucza obcego

Zaktualizuj metodę ComplexDataModel klas Up :

  • Otwórz plik {timestamp}_ComplexDataModel.cs.
  • Oznacz jako komentarz wiersz kodu, który dodaje kolumnę DepartmentIDCourse do tabeli.
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);

Dodaj następujący wyróżniony kod. Nowy kod przechodzi po .CreateTable( name: "Department" bloku:

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

Po wykonaniu powyższych zmian istniejące Course wiersze będą powiązane z działem "Temp" po uruchomieniu ComplexDataModelUp metody.

Aplikacja produkcyjna byłaby:

  • Dołącz kod lub skrypty, aby dodać Department wiersze i powiązane Course wiersze do nowych Department wierszy.
  • Nie należy używać działu "Temp" ani wartości domyślnej dla .Course.DepartmentID

W następnym samouczku omówiono powiązane dane.

Dodatkowe zasoby