Udostępnij za pośrednictwem


Adnotacje danych Code First

Uwaga

Tylko od wersji EF4.1 — funkcje, interfejsy API itp. omówione na tej stronie zostały wprowadzone w Entity Framework 4.1. Jeśli używasz starszej wersji, niektóre lub wszystkie te informacje nie mają zastosowania.

Zawartość na tej stronie została zaadaptowana z artykułu pierwotnie napisanego przez Julie Lerman (<http://thedatafarm.com>).

Program Entity Framework Code First umożliwia używanie własnych klas domen do reprezentowania modelu, na którym polega program EF do wykonywania zapytań, śledzenia zmian i aktualizowania funkcji. Code First wykorzystuje wzorzec programowania nazywany "konwencja ponad konfigurację". Code First zakłada, że klasy są zgodne z konwencjami programu Entity Framework, a w takim przypadku automatycznie dopracuje sposób wykonywania zadania. Jeśli jednak klasy nie są zgodne z tymi konwencjami, masz możliwość dodawania konfiguracji do klas w celu udostępnienia efowi wymaganych informacji.

Code First udostępnia dwa sposoby dodawania tych konfiguracji do klas. Jeden z nich używa prostych atrybutów o nazwie DataAnnotations, a drugi używa interfejsu API Fluent Code First, który zapewnia sposób opisywania konfiguracji imperatywnie w kodzie.

W tym artykule skoncentrujemy się na używaniu funkcji DataAnnotations (w przestrzeni nazw System.ComponentModel.DataAnnotations) w celu skonfigurowania klas — z wyróżnieniem najczęściej potrzebnych konfiguracji. Adnotacje danych są również zrozumiałe dla wielu aplikacji platformy .NET, takich jak ASP.NET MVC, co umożliwia tym aplikacjom korzystanie z tych samych adnotacji na potrzeby weryfikacji po stronie klienta.

Model

Przedstawię zastosowanie Code First DataAnnotations poprzez prostą parę klas: Blog i Post.

    public class Blog
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public DateTime DateCreated { get; set; }
        public string Content { get; set; }
        public int BlogId { get; set; }
        public ICollection<Comment> Comments { get; set; }
    }

Tak jak są, klasy Blog i Post wygodnie przestrzegają pierwszej konwencji kodu i nie wymagają żadnych poprawek, aby umożliwić zgodność z platformą EF. Można jednak również użyć adnotacji, aby dostarczyły więcej informacji o klasach i bazie danych, z którymi są powiązane.

 

Klucz

Program Entity Framework opiera się na każdej jednostce mającej wartość klucza używaną do śledzenia jednostek. Jedną z konwencji Code First są niejawne właściwości klucza; Code First będzie szukać właściwości o nazwie "Id" lub kombinacji nazwy klasy i "Id", takiej jak "BlogId". Ta właściwość zostanie odwzorowana na kolumnę z kluczem podstawowym w bazie danych.

Klasy Blog i Post są zgodne z tą konwencją. A co, jeśli nie? Co by było, gdyby zamiast tego w blogu użyto nazwy PrimaryTrackingKey lub nawet foo? Jeśli kod najpierw nie znajdzie właściwości zgodnej z tą konwencją, zgłosi wyjątek z powodu wymagania platformy Entity Framework, że musisz mieć właściwość klucza. Możesz użyć adnotacji klucza, aby określić, która właściwość ma być używana jako EntityKey.

    public class Blog
    {
        [Key]
        public int PrimaryTrackingKey { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

Jeśli używasz funkcji generowania bazy danych w trybie Code First, tabela Blog będzie mieć kolumnę klucza podstawowego o nazwie PrimaryTrackingKey, która jest domyślnie zdefiniowana jako Identity.

Tabela blogowa z kluczem podstawowym

Klucze złożone

Platforma Entity Framework obsługuje klucze złożone — klucze podstawowe składające się z więcej niż jednej właściwości. Na przykład można mieć klasę Passport, której klucz podstawowy jest kombinacją kolumn PassportNumber i IssuingCountry.

    public class Passport
    {
        [Key]
        public int PassportNumber { get; set; }
        [Key]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

Próba użycia powyższej klasy w modelu EF spowoduje InvalidOperationException:

Nie można określić złożonej kolejności klucza podstawowego dla typu "Passport". Użyj atrybutu ColumnAttribute lub metody HasKey, aby określić kolejność złożonych kluczy podstawowych.

Aby można było używać kluczy złożonych, program Entity Framework wymaga zdefiniowania kolejności dla właściwości klucza. Możesz to osiągnąć, używając adnotacji Column, aby określić kolejność.

Uwaga

Wartość zamówienia jest względna (a nie oparta na indeksie), więc można użyć dowolnych wartości. Na przykład 100 i 200 byłoby dopuszczalne zamiast wartości 1 i 2.

    public class Passport
    {
        [Key]
        [Column(Order=1)]
        public int PassportNumber { get; set; }
        [Key]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

Jeśli masz jednostki z złożonymi kluczami obcymi, musisz określić tę samą kolejność kolumn, która była używana dla odpowiednich właściwości klucza podstawowego.

Tylko względne porządkowanie we właściwościach klucza obcego musi być takie same, a dokładne wartości przypisane do zamówienia nie muszą być zgodne. Na przykład w poniższej klasie można użyć wartości 3 i 4 zamiast 1 i 2.

    public class PassportStamp
    {
        [Key]
        public int StampId { get; set; }
        public DateTime Stamped { get; set; }
        public string StampingCountry { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 1)]
        public int PassportNumber { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }

        public Passport Passport { get; set; }
    }

Wymagane

Adnotacja Required informuje ef, że wymagana jest określona właściwość.

Dodanie atrybutu Wymagane do właściwości Title wymusi na EF (i MVC) zapewnienie, że właściwość ta będzie zawierała dane.

    [Required]
    public string Title { get; set; }

Bez dodatkowych zmian kodu lub znaczników w aplikacji aplikacja MVC przeprowadzi walidację po stronie klienta, nawet dynamicznie tworząc komunikat przy użyciu właściwości i nazw adnotacji.

Wymagany atrybut będzie również mieć wpływ na wygenerowaną bazę danych przez utworzenie właściwości mapowanej bez wartości null. Zwróć uwagę, że pole Tytuł zostało zmienione na "not null".

Uwaga

W niektórych przypadkach może nie być możliwe, aby kolumna w bazie danych była nienulowalna, mimo że właściwość jest wymagana. Na przykład, gdy używa się strategii dziedziczenia TPH, dane dla wielu typów są przechowywane w jednej tabeli. Jeśli typ pochodny zawiera wymaganą właściwość, kolumna nie może być nienullowalną, ponieważ nie wszystkie typy w hierarchii tę właściwość będą miały.

 

Tabela blogów

 

MaxLength i MinLength

Atrybuty MaxLength i MinLength umożliwiają określenie dodatkowych walidacji właściwości, tak jak w przypadku Required elementu.

Oto BloggerName z wymaganiami długości. W przykładzie pokazano również, jak połączyć atrybuty.

    [MaxLength(10),MinLength(5)]
    public string BloggerName { get; set; }

Adnotacja MaxLength wpłynie na bazę danych, ustawiając długość właściwości na 10.

Tabela blogów przedstawiająca maksymalną długość kolumny BloggerName

Adnotacja po stronie klienta MVC i adnotacja po stronie serwera EF 4.1 będą honorować tę walidację, również dynamicznie tworząc komunikat o błędzie: "Pole BloggerName musi być ciągiem znaków lub typem tablicy o maksymalnej długości "10".". Ta wiadomość jest nieco długa. Wiele adnotacji umożliwia określenie komunikatu o błędzie z atrybutem ErrorMessage.

    [MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

Możesz również określić komunikat błędu „ErrorMessage” w adnotacji „Required”.

Tworzenie strony z niestandardowym komunikatem o błędzie

 

NotMapped

Konwencja Code First stanowi, że każdy atrybut, który jest obsługiwanym typem danych, ma być reprezentowany w bazie danych. Ale nie zawsze tak jest w twoich aplikacjach. Na przykład możesz mieć właściwość w klasie Blog, która tworzy kod na podstawie pól Title i BloggerName. Ta właściwość może być tworzona dynamicznie i nie musi być przechowywana. Możesz oznaczyć dowolne właściwości, które nie odpowiadają bazie danych, adnotacją NotMapped, przykładowo tę właściwość BlogCode.

    [NotMapped]
    public string BlogCode
    {
        get
        {
            return Title.Substring(0, 1) + ":" + BloggerName.Substring(0, 1);
        }
    }

 

Typ złożony

Nie jest rzadkością opisać jednostki domeny za pomocą zestawu klas, a następnie nakładać te klasy, aby stworzyć pełną jednostkę. Możesz na przykład dodać klasę o nazwie BlogDetails do modelu.

    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

Zwróć uwagę, że BlogDetails nie ma żadnej właściwości klucza. W projekcie BlogDetails opartym na domenie jest określany jako obiekt wartości. Platforma Entity Framework odwołuje się do obiektów wartości jako typów złożonych.  Nie można śledzić typów złożonych samodzielnie.

Jednak jako właściwość klasy Blog, BlogDetails będzie monitorowana jako część obiektu Blog. Aby Code First rozpoznało to, należy oznaczyć klasę BlogDetails jako ComplexType.

    [ComplexType]
    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

Teraz możesz dodać właściwość w klasie Blog, aby reprezentować BlogDetails tego bloga.

        public BlogDetails BlogDetail { get; set; }

W bazie danych tabela Blog będzie zawierać wszystkie właściwości blogu, w tym właściwości zawarte we właściwości BlogDetail. Domyślnie każdy z nich jest poprzedzony nazwą typu złożonego "BlogDetail".

Tabela blogów z typem złożonym

Sprawdzanie współbieżności

Adnotacja ConcurrencyCheck umożliwia flagowanie co najmniej jednej właściwości, która ma być używana do sprawdzania współbieżności w bazie danych, gdy użytkownik edytuje lub usuwa jednostkę. Jeśli pracujesz z projektantem EF, jest to zgodne z ustawieniem właściwości ConcurrencyMode na Fixed.

Zobaczmy, jak ConcurrencyCheck działa, dodając go do właściwości BloggerName.

    [ConcurrencyCheck, MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

Gdy SaveChanges jest wywoływana, ze względu na adnotację ConcurrencyCheck na polu BloggerName, oryginalna wartość tej właściwości zostanie użyta w aktualizacji. Polecenie podejmie próbę zlokalizowania poprawnego wiersza, filtrując nie tylko na podstawie wartości klucza, ale również na podstawie oryginalnej wartości BloggerName.  Poniżej przedstawiono kluczowe części polecenia UPDATE wysyłanego do bazy danych, gdzie można zobaczyć, że polecenie zaktualizuje wiersz, gdzie PrimaryTrackingKey to 1, a BloggerName "Julie," co było oryginalną wartością, gdy ten blog został pobrany z bazy danych.

    where (([PrimaryTrackingKey] = @4) and ([BloggerName] = @5))
    @4=1,@5=N'Julie'

Jeśli ktoś zmienił nazwę blogera dla tego blogu w międzyczasie, ta aktualizacja zakończy się niepowodzeniem i otrzymasz DbUpdateConcurrencyException, który będziesz musiał obsłużyć.

 

Znacznik czasu

Częściej używa się pól rowversion lub timestamp na potrzeby sprawdzania współbieżności. Jednak zamiast używać ConcurrencyCheck adnotacji, można użyć bardziej szczegółowej TimeStamp adnotacji, o ile typ właściwości jest tablicą bajtów. Najpierw kod będzie traktować Timestamp właściwości takie same jak ConcurrencyCheck właściwości, ale zapewni również, że pole bazy danych, które najpierw generuje kod, jest niepuste. W danej klasie można mieć tylko jedną właściwość znacznika czasu.

Dodanie następującej właściwości do klasy Blog:

    [Timestamp]
    public Byte[] TimeStamp { get; set; }

kod najpierw tworzy nienullową kolumnę sygnatury czasowej w tabeli bazy danych.

Tabela blogów z kolumną sygnatury czasowej

 

Tabela i kolumna

Jeśli zezwalasz aplikacji Code First na utworzenie bazy danych, możesz zmienić nazwę tabel i kolumn, które tworzysz. Możesz również użyć funkcji Code First z istniejącą bazą danych. Nie zawsze jednak nazwy klas i właściwości w domenie są zgodne z nazwami tabel i kolumn w bazie danych.

Moja klasa nosi nazwę Blog i zgodnie z konwencją kod najpierw zakłada, że będzie to mapowane na tabelę o nazwie Blogs. Jeśli tak nie jest, możesz określić nazwę tabeli za pomocą atrybutu Table . Na przykład adnotacja określa, że nazwa tabeli to InternalBlogs.

    [Table("InternalBlogs")]
    public class Blog

Adnotacja Column jest bardziej biegła w określaniu atrybutów zamapowanej kolumny. Można określić nazwę, typ danych, a nawet kolejność wyświetlania kolumny w tabeli. Oto przykład atrybutu Column .

    [Column("BlogDescription", TypeName="ntext")]
    public String Description {get;set;}

Nie należy wprowadzać w błąd atrybutu Kolumna TypeName z atrybutem DataType DataAnnotation. DataType to adnotacja używana dla interfejsu użytkownika i jest ignorowana przez funkcję Code First.

Poniżej przedstawiono tabelę po jej ponownej wygenerowaniu. Nazwa tabeli została zmieniona na InternalBlogs i Description kolumna z typu złożonego to teraz BlogDescription. Ponieważ nazwa została określona w adnotacji, kod nie zastosuje najpierw konwencji polegającej na rozpoczynaniu nazwy kolumny od nazwy typu złożonego.

Zmieniono nazwę tabeli i kolumny blogów

 

Generowane przez bazę danych

Ważnymi funkcjami bazy danych jest możliwość posiadania właściwości obliczeniowych. Jeśli mapujesz klasy Code First na tabele zawierające obliczone kolumny, nie chcesz, aby program Entity Framework próbował zaktualizować te kolumny. Jednak program EF ma zwracać te wartości z bazy danych po wstawieniu lub zaktualizowaniu danych. Możesz użyć adnotacji DatabaseGenerated żeby oznaczyć te właściwości w swojej klasie razem z enumeracją Computed. Inne wyliczenia to None i Identity.

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime DateCreated { get; set; }

Bazę danych wygenerowaną na kolumnach bajtów lub sygnatur czasowych można użyć, gdy kod najpierw generuje bazę danych. W przeciwnym razie należy użyć tej opcji tylko podczas wskazywania istniejących baz danych, ponieważ kod pierwszy nie będzie mógł określić formuły dla obliczonej kolumny.

Przeczytasz powyżej, że domyślnie właściwość klucza, która jest liczbą całkowitą, stanie się kluczem tożsamości w bazie danych. Byłoby to takie samo, jak ustawienie DatabaseGenerated na DatabaseGeneratedOption.Identity. Jeśli nie chcesz, aby był to klucz tożsamości, możesz ustawić wartość na DatabaseGeneratedOption.None.

 

Indeks

Uwaga

EF6.1 i nowsze tylko — Atrybut Index został wprowadzony w Entity Framework 6.1. Jeśli używasz starszej wersji, informacje przedstawione w tej sekcji nie mają zastosowania.

Indeks można utworzyć w co najmniej jednej kolumnie przy użyciu atrybutu IndexAttribute. Dodanie atrybutu do co najmniej jednej właściwości spowoduje, że EF w trakcie tworzenia bazy danych utworzy odpowiedni indeks w bazie danych lub stworzy szkielet wywołań CreateIndex, jeśli używasz migracji Code First.

Na przykład poniższy kod spowoduje utworzenie indeksu w Rating kolumnie Posts tabeli w bazie danych.

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index]
        public int Rating { get; set; }
        public int BlogId { get; set; }
    }

Domyślnie indeks będzie miał nazwę IX_<nazwa właściwości> (IX_Rating w powyższym przykładzie). Można również określić nazwę indeksu. W poniższym przykładzie określono, że indeks powinien mieć nazwę PostRatingIndex.

    [Index("PostRatingIndex")]
    public int Rating { get; set; }

Domyślnie indeksy nie są unikatowe, ale można użyć nazwanego parametru IsUnique , aby określić, że indeks powinien być unikatowy. W poniższym przykładzie wprowadzono unikatowy indeks dla nazwy logowania User.

    public class User
    {
        public int UserId { get; set; }

        [Index(IsUnique = true)]
        [StringLength(200)]
        public string Username { get; set; }

        public string DisplayName { get; set; }
    }

Indeksy wielokolumne

Indeksy obejmujące wiele kolumn są określane przy użyciu tej samej nazwy w wielu adnotacjach indeksu dla danej tabeli. Podczas tworzenia indeksów wielokolumnach należy określić kolejność kolumn w indeksie. Na przykład poniższy kod tworzy indeks wielokolumnowy dla Rating i BlogId nazywany IX_BlogIdAndRating. BlogId jest pierwszą kolumną w indeksie i Rating drugą.

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index("IX_BlogIdAndRating", 2)]
        public int Rating { get; set; }
        [Index("IX_BlogIdAndRating", 1)]
        public int BlogId { get; set; }
    }

 

Atrybuty relacji: InverseProperty i ForeignKey

Uwaga

Ta strona zawiera informacje o konfigurowaniu relacji w modelu Code First przy użyciu adnotacji danych. Aby uzyskać ogólne informacje o relacjach w programie EF oraz sposobie uzyskiwania dostępu do danych i manipulowania nimi przy użyciu relacji, zobacz Relacje i właściwości nawigacji.*

Pierwsza konwencja kodu zajmie się najbardziej typowymi relacjami w modelu, ale istnieje kilka przypadków, w których potrzebuje pomocy.

Zmiana nazwy właściwości klucza w klasie Blog spowodowała problem z relacją z Post

Podczas generowania bazy danych kod najpierw widzi BlogId właściwość w klasie Post i rozpoznaje ją zgodnie z konwencją zgodną z nazwą klasy plus identyfikatorem, jako kluczem obcym do Blog klasy. W klasie bloga nie ma właściwości BlogId. Rozwiązaniem tego problemu jest utworzenie właściwości nawigacji w Post obiekcie i użycie ForeignKey funkcji DataAnnotation, aby ułatwić kodowi najpierw zrozumienie sposobu tworzenia relacji między dwiema klasami (przy użyciu Post.BlogId właściwości), a także sposobem określania ograniczeń w bazie danych.

    public class Post
    {
            public int Id { get; set; }
            public string Title { get; set; }
            public DateTime DateCreated { get; set; }
            public string Content { get; set; }
            public int BlogId { get; set; }
            [ForeignKey("BlogId")]
            public Blog Blog { get; set; }
            public ICollection<Comment> Comments { get; set; }
    }

Ograniczenie w bazie danych pokazuje relację między InternalBlogs.PrimaryTrackingKey i Posts.BlogId

relacja między elementami InternalBlogs.PrimaryTrackingKey i Posts.BlogId

Element InverseProperty jest używany, gdy istnieje wiele relacji między klasami.

Post W klasie możesz śledzić, kto napisał wpis w blogu, a także kto go edytował. Poniżej przedstawiono dwie nowe właściwości nawigacji dla klasy Post.

    public Person CreatedBy { get; set; }
    public Person UpdatedBy { get; set; }

Należy również dodać klasę Person , do której odwołuje się te właściwości. Klasa Person ma właściwości nawigacji z powrotem do Post, jeden dla wszystkich wpisów napisanych przez osobę i jeden dla wszystkich wpisów zaktualizowanych przez daną osobę.

    public class Person
    {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<Post> PostsWritten { get; set; }
            public List<Post> PostsUpdated { get; set; }
    }

Najpierw kod nie jest w stanie dopasować właściwości w dwóch klasach samodzielnie. Tabela bazy danych dla Posts elementu powinna mieć jeden klucz obcy dla osoby i jeden dla CreatedByUpdatedBy osoby, ale kod najpierw utworzy cztery właściwości klucza obcego: Person_Id, Person_Id1, CreatedBy_Id i UpdatedBy_Id.

Tabela postów z dodatkowymi kluczami zewnętrznymi

Aby rozwiązać te problemy, możesz użyć InverseProperty adnotacji, aby określić wyrównanie właściwości.

    [InverseProperty("CreatedBy")]
    public List<Post> PostsWritten { get; set; }

    [InverseProperty("UpdatedBy")]
    public List<Post> PostsUpdated { get; set; }

Ponieważ właściwość w klasie Person wie, że to odnosi się do typu Post, zbuduje relację z Post.CreatedBy. Podobnie PostsUpdated zostanie połączony z Post.UpdatedBy. Kod najpierw nie utworzy dodatkowych kluczy obcych.

Tabela wpisów bez dodatkowych kluczy obcych

 

Podsumowanie

Atrybuty DataAnnotations nie tylko umożliwiają opisywanie walidacji po stronie klienta i serwera w klasach code first, ale również pozwalają na ulepszanie, a nawet poprawianie założeń, które code first podejmie o Twoich klasach zgodnie ze swoimi konwencjami. Za pomocą funkcji DataAnnotations można nie tylko napędzać generowanie schematu bazy danych, ale także mapować pierwsze klasy kodu na wcześniej istniejącą bazę danych.

Chociaż są one bardzo elastyczne, należy pamiętać, że funkcje DataAnnotations zapewniają tylko najczęściej potrzebne zmiany konfiguracji, które można wprowadzić w pierwszych klasach kodu. Aby skonfigurować klasy dla pewnych przypadków brzegowych, należy skorzystać z alternatywnego mechanizmu konfiguracji, interfejsu Fluent API modelu Code First.