Sdílet prostřednictvím


Vytvoření složitějšího datového modelu pro aplikaci ASP.NET MVC (4 z 10)

Tom Dykstra

Ukázková webová aplikace Contoso University ukazuje, jak vytvářet aplikace ASP.NET MVC 4 pomocí entity Framework 5 Code First a sady Visual Studio 2012. Informace o sérii kurzů najdete v prvním kurzu v této sérii.

Poznámka

Pokud narazíte na problém, který nemůžete vyřešit, stáhněte si dokončenou kapitolu a zkuste problém reprodukovat. Obecně můžete najít řešení problému porovnáním kódu s dokončeným kódem. Informace o některých běžných chybách a jejich řešení najdete v tématu Chyby a alternativní řešení.

V předchozích kurzech jste pracovali s jednoduchým datovým modelem, který se skládal ze tří entit. V tomto kurzu přidáte další entity a relace a přizpůsobíte datový model zadáním pravidel formátování, ověřování a mapování databází. Uvidíte dva způsoby přizpůsobení datového modelu: přidáním atributů do tříd entit a přidáním kódu do třídy kontextu databáze.

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

School_class_diagram

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

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

Atribut Datového typu

U dat registrace studentů se na všech webových stránkách aktuálně zobrazuje čas spolu s datem, ale jediné, co vás zajímá, je datum. Pomocí atributů datových poznámek můžete provést jednu změnu kódu, která opraví formát zobrazení v každém zobrazení, které zobrazuje data. Pokud chcete vidět příklad, jak to udělat, přidáte atribut do EnrollmentDate vlastnosti ve Student třídě .

V souboru Models\Student.cs přidejte using příkaz pro System.ComponentModel.DataAnnotations obor názvů a do EnrollmentDate vlastnosti přidejte DataType atributy a DisplayFormat , jak je znázorněno v následujícím příkladu:

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Atribut DataType slouží k určení datového typu, který je konkrétnější než vnitřní typ databáze. V tomto případě chceme sledovat pouze datum, ne datum a čas. Výčet DataType poskytuje mnoho datových typů, například Datum, Čas, Telefonní číslo, Měna, EmailAddress a další. Atribut DataType může také aplikaci umožnit, aby automaticky poskytovala funkce specifické pro typ. mailto: Například odkaz může být vytvořen pro DataType.EmailAddress a v prohlížečích, které podporují HTML5, lze pro DataType.Date zadat výběr data. Atributy DataType vygenerují atributy HTML 5 ( vyslovuje se datová pomlčka), kterým prohlížeče HTML 5 rozumí. Atributy DataType neposkytují žádné ověření.

DataType.Date neurčoval formát data, které je zobrazeno. Ve výchozím nastavení je datové pole zobrazeno podle výchozích formátů založených na CultureInfo serveru.

Atribut DisplayFormat slouží k explicitní specifikaci formátu data:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

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

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

  • Prohlížeč může povolit funkce HTML5 (například k zobrazení ovládacího prvku kalendáře, symbolu měny odpovídajícího národnímu prostředí, e-mailových odkazů atd.).
  • Ve výchozím nastavení prohlížeč vykresluje data pomocí správného formátu na základě vašeho národního prostředí.
  • Atribut DataType umožňuje MVC zvolit správnou šablonu pole pro vykreslení dat ( DisplayFormat , pokud ho použije sám, používá šablonu řetězce). Další informace najdete v tématu Šablony ASP.NET MVC 2 od Brada Wilsona. (I když je tento článek napsaný pro MVC 2, stále platí pro aktuální verzi ASP.NET MVC.)

Pokud použijete DataType atribut s polem data, musíte atribut zadat DisplayFormat také, aby se zajistilo, že se pole v prohlížečích Chrome správně vykresluje. Další informace najdete v tomto vlákně StackOverflow.

Znovu spusťte stránku Index studenta a všimněte si, že se pro data registrace už nezobrazují časy. Totéž bude platit pro všechna zobrazení, která model používají Student .

Students_index_page_with_formatted_date

Atribut StringLengthAttribute

Pomocí atributů můžete také zadat pravidla ověření dat a zprávy. Předpokládejme, že chcete zajistit, aby uživatelé nezadáli do názvu více než 50 znaků. Chcete-li přidat toto omezení, přidejte do LastName vlastností a FirstMidName atributy StringLength, jak je znázorněno v následujícím příkladu:

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

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

Atribut StringLength nezabrání uživateli v zadání prázdných znaků pro jméno. Pomocí atributu RegularExpression můžete použít omezení vstupu. Například následující kód vyžaduje, aby první znak byl velkými písmeny a zbývající znaky abecedně:

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

Atribut MaxLength poskytuje podobné funkce jako atribut StringLength , ale neposkytuje ověření na straně klienta.

Spusťte aplikaci a klikněte na kartu Studenti . Zobrazí se následující chyba:

Model, který zálohuje kontext SchoolContext, se od vytvoření databáze změnil. Zvažte použití Migrace Code First k aktualizaci databáze (https://go.microsoft.com/fwlink/?LinkId=238269).

Model databáze se změnil způsobem, který vyžaduje změnu schématu databáze, a Entity Framework to zjistila. Pomocí migrací aktualizujete schéma bez ztráty dat, která jste přidali do databáze pomocí uživatelského rozhraní. Pokud jste změnili data vytvořená metodou Seed , změní se zpět do původního stavu kvůli metodě AddOrUpdate , kterou v Seed metodě používáte. (AddOrUpdate je ekvivalentem operace upsert z terminologie databáze.)

V konzole Správce balíčků (PMC) zadejte následující příkazy:

add-migration MaxLengthOnNames
update-database

Příkaz add-migration MaxLengthOnNames vytvoří soubor s názvem <timeStamp>_MaxLengthOnNames.cs. Tento soubor obsahuje kód, který aktualizuje databázi tak, aby odpovídala aktuálnímu datovému modelu. Časové razítko před název souboru migrace se používá v Entity Frameworku k řazení migrací. Pokud po vytvoření více migrací databázi odstraníte nebo pokud nasadíte projekt pomocí migrace, použijí se všechny migrace v pořadí, ve kterém byly vytvořeny.

Spusťte stránku Vytvořit a zadejte název delší než 50 znaků. Jakmile překročíte 50 znaků, ověření na straně klienta okamžitě zobrazí chybovou zprávu.

chyba val na straně klienta

Atribut column

Atributy můžete také použít k řízení způsobu mapování tříd a vlastností na databázi. Předpokládejme, že jste použili název FirstMidName pro pole se jménem, protože pole může obsahovat také druhé jméno. Chcete ale, aby se sloupec databáze jmenoval FirstName, protože uživatelé, kteří budou psát dotazy ad hoc na databázi, jsou na tento název zvyklí. K tomuto mapování můžete použít atribut .Column

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

Přidejte do vlastnosti příkaz using pro System.ComponentModel.DataAnnotations.Schema a atribut FirstMidName názvu sloupce, jak je znázorněno v následujícím zvýrazněném kódu:

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

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

Přidání atributu Column změní model backing schoolContext, takže nebude odpovídat databázi. Zadáním následujících příkazů v PMC vytvořte další migraci:

add-migration ColumnFirstName
update-database

V Průzkumníku serveru (Průzkumník databáze , pokud používáte Express pro web) poklikejte na tabulku Student .

Snímek obrazovky s tabulkou Student v Průzkumníku serveru

Následující obrázek ukazuje původní název sloupce, který byl před prvními dvěma migracemi. Kromě toho, že se název sloupce změnil z FirstMidName na FirstName, se změnily dva sloupce názvů z MAX délky na 50 znaků.

Snímek obrazovky s tabulkou Student v Průzkumníku serveru Řádek First Name (Jméno) na předchozím snímku obrazovky se změnil tak, aby se zobrazoval jako First Mid Name (Křestní jméno).

Změny mapování databází můžete provést také pomocí rozhraní Fluent API, jak uvidíte později v tomto kurzu.

Poznámka

Pokud se pokusíte zkompilovat před vytvořením všech těchto tříd entit, může dojít k chybám kompilátoru.

Vytvoření entity instruktora

Instructor_entity

Vytvořte soubor Models\Instructor.cs a nahraďte kód šablony následujícím kódem:

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

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int InstructorID { get; set; }

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

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

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

        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

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

Všimněte si, že několik vlastností v entitách a Instructor je stejnýchStudent. V kurzu Implementace dědičnosti dále v této sérii budete refaktorovat dědičnost, abyste tuto redundanci vyloučili.

Požadované a zobrazované atributy

Atributy LastName vlastnosti určují, že se jedná o povinné pole, že popis pro textové pole by mělo být "Příjmení" (místo názvu vlastnosti, který by byl "Příjmení" bez mezer) a že hodnota nesmí být delší než 50 znaků.

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

Atribut StringLength nastavuje maximální délku v databázi a poskytuje ověření na straně klienta a serveru pro ASP.NET MVC. V tomto atributu můžete také zadat minimální délku řetězce, ale minimální hodnota nemá žádný vliv na schéma databáze. Atribut Required není potřeba pro typy hodnot, jako jsou DateTime, int, double a float. Hodnotovým typům nelze přiřadit hodnotu null, takže jsou ze své podstaty povinné. Atribut Required můžete odebrat a nahradit ho parametrem minimální délky atributu StringLength :

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

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

public class Instructor
{
   public int InstructorID { get; set; }

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

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

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

   public string FullName
   {
      get { return LastName + ", " + FirstMidName; }
   }

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

Vypočtená vlastnost FullName

FullName je počítaná vlastnost, která vrací hodnotu vytvořenou zřetězením dvou dalších vlastností. Proto má pouze get příslušenství a v databázi nebude vygenerován žádný FullName sloupec.

public string FullName
{
    get { return LastName + ", " + FirstMidName; }
}

Navigační vlastnosti Kurzů a OfficeAssignment

Vlastnosti Courses a OfficeAssignment jsou vlastnosti navigace. Jak bylo vysvětleno dříve, jsou obvykle definovány jako virtuální , aby mohly využívat výhod funkce Entity Framework označované jako opožděné načítání. Kromě toho, pokud navigační vlastnost může obsahovat více entit, musí její typ implementovat ICollection<T> Rozhraní. (Například IList<T> kvalifikuje, ale ne IEnumerable<T> , protože IEnumerable<T> neimplementuje Add.

Instruktor může vyučovat libovolný počet kurzů, takže Courses je definován jako kolekce Course entit. Naše obchodní pravidla uvádějí, že instruktor může mít maximálně jednu kancelář, takže OfficeAssignment je definována jako jedna OfficeAssignment entita (což může být null v případě, že není přiřazena žádná kancelář).

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

Vytvoření entity OfficeAssignment

OfficeAssignment_entity

Vytvořte Models\OfficeAssignment.cs s následujícím kódem:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        [ForeignKey("Instructor")]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public virtual Instructor Instructor { get; set; }
    }
}

Sestavte projekt, který uloží změny a ověří, že jste neprovedli žádné chyby kopírování a vkládání, které kompilátor dokáže zachytit.

Atribut klíče

Mezi entitami a OfficeAssignment entitami je vztah Instructor 1:nula nebo 1. Přiřazení kanceláře existuje pouze ve vztahu k instruktorovi, kterému je přiřazeno, a proto je jeho primárním klíčem také cizí klíč entity Instructor . Entity Framework ale nemůže automaticky rozpoznat InstructorID jako primární klíč této entity, protože její název neodpovídá ID konvenci vytváření názvů názvů nebo classnameID . Atribut se Key proto používá k jeho identifikaci jako klíče:

[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }

Atribut můžete použít také v Key případě, že entita má vlastní primární klíč, ale chcete vlastnost pojmenovat jinak než classnameID nebo ID. Ef ve výchozím nastavení považuje klíč za negenerovaný databází, protože sloupec je určený pro identifikující relaci.

Atribut ForeignKey

Pokud mezi dvěma entitami (například mezi OfficeAssignment a Instructor) existuje relace 1:nula nebo 1 nebo 1:1, ef nemůže zjistit, který konec relace je objektem zabezpečení a který konec je závislý. Relace 1:1 mají v každé třídě odkaz na navigační vlastnost na druhou třídu. Atribut ForeignKey lze použít na závislé třídy k vytvoření relace. Pokud vynecháte atribut ForeignKey, při pokusu o vytvoření migrace se zobrazí následující chyba:

Nelze určit hlavní konec přidružení mezi typy ContosoUniversity.Models.OfficeAssignment a ContosoUniversity.Models.Instructor. Hlavní konec tohoto přidružení musí být explicitně nakonfigurovaný pomocí rozhraní API fluent relace nebo datových poznámek.

Později v tomto kurzu si ukážeme, jak nakonfigurovat tuto relaci pomocí rozhraní FLUENT API.

Vlastnost Navigace instruktora

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

Do vlastnosti Navigace instruktora můžete zadat [Required] atribut, který určí, že musí existovat související instruktor, ale nemusíte to dělat, protože cizí klíč InstructorID (který je také klíčem k této tabulce) nemá hodnotu null.

Úprava entity kurzu

Course_entity

V souboru Models\Course.cs nahraďte dříve přidaný kód následujícím kódem:

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

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

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

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

      [Display(Name = "Department")]
      public int DepartmentID { get; set; }

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

Entita kurzu má vlastnost DepartmentID cizího klíče, která odkazuje na související Department entitu Department a má vlastnost navigace. Entity Framework nevyžaduje přidání vlastnosti cizího klíče do datového modelu, pokud máte vlastnost navigace pro související entitu. EF automaticky vytvoří cizí klíče v databázi všude, kde jsou potřeba. Ale cizí klíč v datovém modelu může zjednodušit a zefektivnit aktualizace. Když například načtete entitu kurzu, kterou chcete upravit, bude mít entita hodnotu null, Department pokud ji nenačtete, takže při aktualizaci entity kurzu budete muset nejprve načíst entitu Department . Pokud je vlastnost DepartmentID cizího klíče součástí datového modelu, nemusíte entitu Department před aktualizací načítat.

Atribut DatabaseGenerated

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

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

Ve výchozím nastavení Entity Framework předpokládá, že hodnoty primárního klíče jsou generovány databází. To je to, co chcete ve většině scénářů. Pro Course entity ale použijete uživatelem zadané číslo kurzu, například řadu 1000 pro jedno oddělení, řadu 2000 pro jiné oddělení atd.

Vlastnosti cizího klíče a navigace

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

  • Kurz je přiřazen k jednomu oddělení, takže z výše uvedených důvodů je DepartmentID k dispozici cizí klíč a Department navigační vlastnost.

    public int DepartmentID { get; set; }
    public virtual Department Department { get; set; }
    
  • Kurz může mít zapsaný libovolný počet studentů, takže Enrollments navigační vlastnost je kolekce:

    public virtual ICollection<Enrollment> Enrollments { get; set; }
    
  • Kurz může vyučovat více instruktorů, takže Instructors navigační vlastnost je kolekce:

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

Vytvoření entity oddělení

Department_entity

Vytvořte Models\Department.cs s následujícím kódem:

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

namespace ContosoUniversity.Models
{
   public class Department
   {
      public int DepartmentID { get; set; }

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

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

      [DataType(DataType.Date)]
      public DateTime StartDate { get; set; }

      [Display(Name = "Administrator")]
      public int? InstructorID { get; set; }

      public virtual Instructor Administrator { get; set; }
      public virtual ICollection<Course> Courses { get; set; }
   }
}

Atribut column

Dříve jste ke změně mapování názvů sloupců použili atribut Column . V kódu entity Department se atribut používá ke změně mapování datových typů SQL tak, Column aby se sloupec definoval pomocí typu SQL Server peněz v databázi:

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

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

Vlastnosti cizího klíče a navigace

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

  • Oddělení může nebo nemusí mít správce a správce je vždy instruktor. InstructorID Vlastnost je proto zahrnuta jako cizí klíč entity Instructor a za int označení typu se přidá otazník, který vlastnost označí jako s možnou hodnotou null. Vlastnost navigace má název, Administrator ale obsahuje entituInstructor:

    public int? InstructorID { get; set; }
    public virtual Instructor Administrator { get; set; }
    
  • Oddělení může mít mnoho kurzů, takže je Courses k dispozici vlastnost navigace:

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

    Poznámka

    Podle konvence Entity Framework umožňuje kaskádové odstranění cizích klíčů, které nemají hodnotu null, a pro relace M:N. To může vést k cyklické kaskádové odstranění pravidel, která způsobí výjimku při spuštění kódu inicializátoru. Pokud byste například nedefinovali vlastnost s možnou Department.InstructorID hodnotou null, při spuštění inicializátoru by se zobrazila následující zpráva o výjimce: "Referenční relace bude mít za následek cyklický odkaz, který není povolený." Pokud vaše obchodní pravidla vyžadují InstructorID vlastnost s možnou hodnotou null, museli byste k zakázání kaskádového odstranění relace použít následující rozhraní FLUENT API:

modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);

Úprava entity Student

Student_entity

V části Models\Student.cs nahraďte dříve přidaný kód následujícím kódem. Změny jsou zvýrazněné.

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

namespace ContosoUniversity.Models
{
   public class Student
   {
      public int StudentID { get; set; }

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

      [StringLength(50, MinimumLength = 1, ErrorMessage = "First name cannot be longer than 50 characters.")]
      [Column("FirstName")]
      public string FirstMidName { get; set; }

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

      public string FullName
      {
         get { return LastName + ", " + FirstMidName; }
      }

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

Entita registrace

V části Models\Enrollment.cs nahraďte dříve přidaný kód následujícím kódem.

using System.ComponentModel.DataAnnotations;

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

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

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

Vlastnosti cizího klíče a navigace

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

  • Záznam registrace se používá pro jeden kurz, takže existuje vlastnost cizího CourseID klíče a Course vlastnost navigace:

    public int CourseID { get; set; }
    public virtual Course Course { get; set; }
    
  • Záznam registrace je pro jednoho studenta, takže je k dispozici vlastnost cizího StudentID klíče a Student vlastnost navigace:

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

Relace M:N

Mezi entitami a Course existuje relace Student M:N a Enrollment entita funguje jako tabulka spojení M:N s datovou částí v databázi. To znamená, že Enrollment tabulka obsahuje další data kromě cizích klíčů pro spojené tabulky (v tomto případě primární klíč a Grade vlastnost).

Následující obrázek znázorňuje, jak tyto relace vypadají v diagramu entit. (Tento diagram se vygeneroval pomocí power tools entity frameworku; vytvoření diagramu není součástí kurzu, ale používá se tady jenom jako ilustrace.)

Course_many Many_relationship od studentů do many_relationship

Každá čára relace má na jednom konci 1 a hvězdičku (*) na druhém, což označuje relaci 1:N.

Enrollment Pokud tabulka neobsahuje informace o známce, bude muset obsahovat pouze dva cizí klíče CourseID a StudentID. V takovém případě by odpovídala tabulce M:N v databázi bez datové části (nebo tabulky čistého spojení) a vůbec byste pro ni nemuseli vytvářet třídu modelu. Entity Instructor a Course mají takový druh vztahu M:N, a jak vidíte, mezi nimi není žádná třída entit:

Instruktor-Course_many-to-many_relationship

V databázi je však vyžadována tabulka spojení, jak je znázorněno v následujícím diagramu databáze:

Instruktor-Course_many-many_relationship_tables

Entity Framework automaticky vytvoří CourseInstructor tabulku a vy ji čtete a aktualizujete nepřímo čtením a aktualizací Instructor.Courses vlastností a Course.Instructors navigace.

Diagram entit znázorňující relace

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

School_data_model_diagram

Kromě čar relace M:N (* až *) a čar relace 1:N (1:*) můžete vidět mezi entitami a entitami Instruktor a Department přímku relace 1:N (1 až 0...1) mezi Instructor entitami a OfficeAssignment nulou nebo 1:N (0...1 až *).

Přizpůsobení datového modelu přidáním kódu do kontextu databáze

Dále přidáte nové entity do SchoolContext třídy a přizpůsobíte některé mapování pomocí fluent api volání. (Rozhraní API je "fluent", protože se často používá při řetězení řady volání metod do jednoho příkazu.)

V tomto kurzu použijete rozhraní FLUENT API jenom pro mapování databáze, které nemůžete dělat s atributy. Můžete ale také použít rozhraní FLUENT API k určení většiny pravidel formátování, ověřování a mapování, která můžete provést pomocí atributů. Některé atributy, například MinimumLength , se nedají použít s rozhraním FLUENT API. Jak je uvedeno výše, MinimumLength nemění schéma, použije se pouze ověřovací pravidlo na straně klienta a serveru.

Někteří vývojáři raději používají rozhraní FLUENT API výhradně, aby mohli udržovat třídy entit "čisté". Pokud chcete, můžete kombinovat atributy a rozhraní API fluent a existuje několik přizpůsobení, která se dají provést pouze pomocí rozhraní FLUENT API, ale obecně se doporučuje zvolit jeden z těchto dvou přístupů a používat ho co nejvíce konzistentně.

Pokud chcete do datového modelu přidat nové entity a provést mapování databáze, které jste neudělali pomocí atributů, nahraďte kód v souboru DAL\SchoolContext.cs následujícím kódem:

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      public DbSet<Course> Courses { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("InstructorID")
                 .ToTable("CourseInstructor"));
      }
   }
}

Nový příkaz v metodě OnModelCreating konfiguruje tabulku spojení M:N:

  • Pro relaci M:N mezi Instructor entitami a Course určuje kód názvy tabulek a sloupců pro tabulku spojení. Code First za vás může nakonfigurovat relaci M:N bez tohoto kódu, ale pokud ho nezavoláte, zobrazí se výchozí názvy InstructorInstructorID jako pro InstructorID sloupec.

    modelBuilder.Entity<Course>()
        .HasMany(c => c.Instructors).WithMany(i => i.Courses)
        .Map(t => t.MapLeftKey("CourseID")
            .MapRightKey("InstructorID")
            .ToTable("CourseInstructor"));
    

Následující kód poskytuje příklad, jak byste mohli k určení vztahu mezi Instructor entitami a OfficeAssignment použít fluent API místo atributů:

modelBuilder.Entity<Instructor>()
    .HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);

Informace o tom, co příkazy fluent API dělají na pozadí, najdete v blogovém příspěvku rozhraní Fluent API .

Nasypat databázi testovacími daty

Nahraďte kód v souboru Migrations\Configuration.cs následujícím kódem, aby bylo možné poskytnout počáteční data pro nové entity, které jste vytvořili.

namespace ContosoUniversity.Migrations
{
   using System;
   using System.Collections.Generic;
   using System.Data.Entity;
   using System.Data.Entity.Migrations;
   using System.Linq;
   using ContosoUniversity.Models;
   using ContosoUniversity.DAL;

   internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
   {
      public Configuration()
      {
         AutomaticMigrationsEnabled = false;
      }

      protected override void Seed(SchoolContext context)
      {
         var students = new List<Student>
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander", 
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",    
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",     
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas", 
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",        
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",   
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",    
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",  
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

         students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
         context.SaveChanges();

         var instructors = new List<Instructor>
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie", 
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",    
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",       
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",      
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",      
                    HireDate = DateTime.Parse("2004-02-12") }
            };
         instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
         context.SaveChanges();

         var departments = new List<Department>
            {
                new Department { Name = "English",     Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").InstructorID },
                new Department { Name = "Mathematics", Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").InstructorID },
                new Department { Name = "Engineering", Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").InstructorID },
                new Department { Name = "Economics",   Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").InstructorID }
            };
         departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
         context.SaveChanges();

         var courses = new List<Course>
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
            };
         courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
         context.SaveChanges();

         var officeAssignments = new List<OfficeAssignment>
            {
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").InstructorID, 
                    Location = "Smith 17" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Harui").InstructorID, 
                    Location = "Gowan 27" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").InstructorID, 
                    Location = "Thompson 304" },
            };
         officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.Location, s));
         context.SaveChanges();

         AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
         AddOrUpdateInstructor(context, "Chemistry", "Harui");
         AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
         AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");

         AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
         AddOrUpdateInstructor(context, "Trigonometry", "Harui");
         AddOrUpdateInstructor(context, "Composition", "Abercrombie");
         AddOrUpdateInstructor(context, "Literature", "Abercrombie");

         context.SaveChanges();

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

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

      void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
      {
         var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
         var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
         if (inst == null)
            crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
      }
   }
}

Jak jste viděli v prvním kurzu, většina tohoto kódu jednoduše aktualizuje nebo vytvoří nové objekty entit a načte ukázková data do vlastností podle potřeby pro testování. Všimněte si ale, jak Course se zpracovává entita, která má s entitou Instructor vztah M:N:

var courses = new List<Course>
{
     new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
       Department = departments.Single( s => s.Name == "Engineering"),
       Instructors = new List<Instructor>() 
     },
     ...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();

Při vytváření objektu Course inicializujete Instructors vlastnost navigace jako prázdnou kolekci pomocí kódu Instructors = new List<Instructor>(). To umožňuje přidat Instructor entity, které s tím Course souvisejí, pomocí Instructors.Add metody . Pokud byste nevytvořili prázdný seznam, nemohli byste tyto relace přidat, protože Instructors vlastnost by byla null a neměla by metodu Add . Inicializaci seznamu můžete také přidat do konstruktoru.

Přidání migrace a aktualizace databáze

V PMC zadejte add-migration příkaz:

PM> add-Migration Chap4

Pokud se v tomto okamžiku pokusíte databázi aktualizovat, zobrazí se následující chyba:

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

< Upravte soubor časového razítka>_Chap4.cs a proveďte následující změny kódu (přidáte příkaz SQL a upravíte AddColumn příkaz):

CreateTable(
        "dbo.CourseInstructor",
        c => new
            {
                CourseID = c.Int(nullable: false),
                InstructorID = c.Int(nullable: false),
            })
        .PrimaryKey(t => new { t.CourseID, t.InstructorID })
        .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
        .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
        .Index(t => t.CourseID)
        .Index(t => t.InstructorID);

    // Create  a department for course to point to.
    Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    //  default value for FK points to department created above.
    AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1)); 
    //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));

    AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));
    AddForeignKey("dbo.Course", "DepartmentID", "dbo.Department", "DepartmentID", cascadeDelete: true);
    CreateIndex("dbo.Course", "DepartmentID");
}

public override void Down()
{

(Při přidávání nového řádku nezapomeňte stávající řádek okomentovat nebo odstranit AddColumn , jinak se při zadávání update-database příkazu zobrazí chyba.)

Někdy při provádění migrací s existujícími daty potřebujete do databáze vložit zasunutá data, aby se splnila omezení cizího klíče, a to je to, co teď děláte. Vygenerovaný kód přidá do tabulky cizí klíč s možnou DepartmentIDCourse hodnotou null. Pokud už při spuštění kódu v Course tabulce existují řádky, operace by selhala, AddColumn protože SQL Server neví, jakou hodnotu vložit do sloupce, která nemůže mít hodnotu null. Proto jste změnili kód tak, aby dal novému sloupci výchozí hodnotu, a vytvořili jste oddělení s zástupnými kódy s názvem Temp, které bude fungovat jako výchozí oddělení. V důsledku toho, pokud při spuštění tohoto kódu existují Course řádky, budou všechny souviset s dočasným oddělením.

Když se Seed metoda spustí, vloží do tabulky řádky Department a bude s těmito novými Department řádky souviset existující Course řádky. Pokud jste do uživatelského rozhraní nepřidali žádné kurzy, už nebudete potřebovat dočasné oddělení ani výchozí hodnotu ve sloupci Course.DepartmentID . Abyste umožnili možnost, že někdo přidal kurzy pomocí aplikace, měli byste také aktualizovat Seed kód metody, aby se zajistilo, že všechny Course řádky (nejen řádky vložené dřívějšími spuštěními Seed metody) budou mít platné DepartmentID hodnoty před odebráním výchozí hodnoty ze sloupce a odstraněním oddělení Temp.

Po dokončení úprav < souboru časového razítka>_Chap4.cs zadejte update-database v PMC příkaz pro spuštění migrace.

Poznámka

Při migraci dat a provádění změn schématu se můžou zobrazit další chyby. Pokud dojde k chybám migrace, které nemůžete vyřešit, můžete buď změnit připojovací řetězec v souboruWeb.config , nebo odstranit databázi. Nejjednodušším způsobem je přejmenovat databázi v souboruWeb.config . Změňte například název databáze na CU_test, jak je znázorněno na následujícím obrázku:

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;
      Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\CU_Test.mdf" 
      providerName="System.Data.SqlClient" />

U nové databáze nejsou k dispozici žádná data, která by bylo možné migrovat, a update-database je mnohem pravděpodobnější, že se příkaz dokončí bez chyb. Pokyny k odstranění databáze najdete v tématu Jak odstranit databázi ze sady Visual Studio 2012.

Otevřete databázi v Průzkumníku serveru stejně jako dříve a rozbalte uzel Tabulky , abyste viděli, že byly vytvořeny všechny tabulky. (Pokud máte Průzkumník serveru stále otevřený z dřívějšího času, klikněte na tlačítko Aktualizovat .)

Snímek obrazovky znázorňující databázi Průzkumníka serveru Uzel Tabulky je rozbalený.

Nevytvořili jste pro CourseInstructor tabulku třídu modelu. Jak bylo vysvětleno dříve, jedná se o tabulku spojení pro relaci M:N mezi Instructor entitami a Course .

Klikněte pravým tlačítkem na CourseInstructor tabulku a vyberte Zobrazit data tabulky a ověřte, že obsahuje data v důsledku Instructor entit, které jste přidali do Course.Instructors vlastnosti navigace.

Table_data_in_CourseInstructor_table

Souhrn

Teď máte složitější datový model a odpovídající databázi. V následujícím kurzu se dozvíte více o různých způsobech přístupu k souvisejícím datům.

Odkazy na další prostředky Entity Framework najdete v mapě obsahu ASP.NET Data Access.