Udostępnij za pośrednictwem


Niestandardowe konwencje Code First

Uwaga

Tylko rozwiązanie EF6 i nowsze wersje — Funkcje, interfejsy API itp. omówione na tej stronie zostały wprowadzone w rozwiązaniu Entity Framework 6. Jeśli korzystasz ze starszej wersji, niektóre lub wszystkie podane informacje nie mają zastosowania.

W przypadku korzystania z funkcji Code First model jest obliczany na podstawie klas przy użyciu zestawu konwencji. Domyślne konwencje Code First określają takie elementy, jak właściwość, która staje się kluczem podstawowym jednostki, nazwą tabeli, do której jest mapowany jednostka, oraz jaką precyzję i skalowanie kolumny dziesiętnej ma domyślnie.

Czasami te konwencje domyślne nie są idealne dla modelu i trzeba je obejść, konfigurując wiele pojedynczych jednostek przy użyciu adnotacji danych lub interfejsu API Fluent. Niestandardowe konwencje Code First umożliwiają definiowanie własnych konwencji, które zapewniają domyślne ustawienia konfiguracji dla modelu. W tym przewodniku zapoznamy się z różnymi typami konwencji niestandardowych i sposobem ich tworzenia.

Konwencje oparte na modelu

Na tej stronie omówiono interfejs API DbModelBuilder dla konwencji niestandardowych. Ten interfejs API powinien być wystarczający do tworzenia większości konwencji niestandardowych. Istnieje jednak również możliwość tworzenia konwencji opartych na modelu — konwencji, które manipulują ostatnim modelem po jego utworzeniu — do obsługi zaawansowanych scenariuszy. Aby uzyskać więcej informacji, zobacz Konwencje oparte na modelu.

 

Nasz model

Zacznijmy od zdefiniowania prostego modelu, którego możemy użyć z naszymi konwencjami. Dodaj następujące klasy do projektu.

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }
    }

    public class Product
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public decimal? Price { get; set; }
        public DateTime? ReleaseDate { get; set; }
        public ProductCategory Category { get; set; }
    }

    public class ProductCategory
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public List<Product> Products { get; set; }
    }

 

Wprowadzenie do konwencji niestandardowych

Napiszmy konwencję, która konfiguruje dowolną właściwość o nazwie Key jako klucz podstawowy dla jego typu jednostki.

Konwencje są włączone w konstruktorze modelu, do którego można uzyskać dostęp, przesłaniając element OnModelCreating w kontekście. Zaktualizuj klasę ProductContext w następujący sposób:

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Properties()
                        .Where(p => p.Name == "Key")
                        .Configure(p => p.IsKey());
        }
    }

Teraz każda właściwość w naszym modelu o nazwie Key zostanie skonfigurowana jako klucz podstawowy dowolnej jednostki.

Możemy również uczynić nasze konwencje bardziej szczegółowymi, filtrując typ właściwości, którą skonfigurujemy:

    modelBuilder.Properties<int>()
                .Where(p => p.Name == "Key")
                .Configure(p => p.IsKey());

Spowoduje to skonfigurowanie wszystkich właściwości o nazwie Klucz jako klucza podstawowego jednostki, ale tylko wtedy, gdy są liczbą całkowitą.

Interesującą funkcją metody IsKey jest to, że jest to dodatek. Oznacza to, że w przypadku wywołania funkcji IsKey dla wielu właściwości wszystkie staną się częścią klucza złożonego. Jednym z zastrzeżeń jest to, że po określeniu wielu właściwości klucza należy również określić kolejność dla tych właściwości. Możesz to zrobić, wywołując metodę HasColumnOrder, jak pokazano poniżej:

    modelBuilder.Properties<int>()
                .Where(x => x.Name == "Key")
                .Configure(x => x.IsKey().HasColumnOrder(1));

    modelBuilder.Properties()
                .Where(x => x.Name == "Name")
                .Configure(x => x.IsKey().HasColumnOrder(2));

Ten kod skonfiguruje typy w naszym modelu tak, aby miały klucz złożony składający się z kolumny Int Key i kolumny Nazwa ciągu. Jeśli wyświetlimy model w projektancie, będzie on wyglądać następująco:

composite Key

Innym przykładem konwencji właściwości jest skonfigurowanie wszystkich właściwości DateTime w moim modelu tak, aby mapować na typ datetime2 w programie SQL Server zamiast daty/godziny. Można to osiągnąć, wykonując następujące czynności:

    modelBuilder.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));

 

Klasy konwencji

Innym sposobem definiowania konwencji jest użycie klasy konwencji do hermetyzacji konwencji. W przypadku używania klasy Konwencji należy utworzyć typ dziedziczony z klasy Convention w przestrzeni nazw System.Data.Entity.ModelConfiguration.Conventions.

Możemy utworzyć klasę konwencji z konwencją datetime2, którą pokazaliśmy wcześniej, wykonując następujące czynności:

    public class DateTime2Convention : Convention
    {
        public DateTime2Convention()
        {
            this.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));        
        }
    }

Aby poinformować platformę EF o użyciu tej konwencji, należy dodać ją do kolekcji Conventions (Konwencje) w temacie OnModelCreating (Tworzenie modelu OnModel), które w przypadku korzystania z przewodnika będą wyglądać następująco:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Properties<int>()
                    .Where(p => p.Name.EndsWith("Key"))
                    .Configure(p => p.IsKey());

        modelBuilder.Conventions.Add(new DateTime2Convention());
    }

Jak widać, dodamy wystąpienie naszej konwencji do kolekcji konwencji. Dziedziczenie z konwencji zapewnia wygodny sposób grupowania i udostępniania konwencji między zespołami lub projektami. Możesz na przykład mieć bibliotekę klas z typowym zestawem konwencji używanych przez wszystkie projekty organizacji.

 

Atrybuty niestandardowe

Innym doskonałym zastosowaniem konwencji jest umożliwienie używania nowych atrybutów podczas konfigurowania modelu. Aby to zilustrować, utwórzmy atrybut, którego możemy użyć do oznaczania właściwości ciągów jako innych niż Unicode.

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class NonUnicode : Attribute
    {
    }

Teraz utwórzmy konwencję, aby zastosować ten atrybut do naszego modelu:

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
                .Configure(c => c.IsUnicode(false));

Za pomocą tej konwencji możemy dodać atrybut NonUnicode do dowolnej z naszych właściwości ciągu, co oznacza, że kolumna w bazie danych będzie przechowywana jako varchar zamiast nvarchar.

Jedną z rzeczy, które należy zwrócić uwagę na tę konwencję, jest to, że jeśli umieścisz atrybut NonUnicode na niczym innym niż właściwość ciągu, zgłosi wyjątek. Dzieje się tak, ponieważ nie można skonfigurować kodu IsUnicode w dowolnym typie innym niż ciąg. Jeśli tak się stanie, możesz zwiększyć szczegółową konwencję, aby odfiltrować wszystko, co nie jest ciągiem.

Chociaż powyższa konwencja działa do definiowania atrybutów niestandardowych, istnieje inny interfejs API, który może być znacznie łatwiejszy w użyciu, zwłaszcza gdy chcesz używać właściwości z klasy atrybutów.

W tym przykładzie zaktualizujemy nasz atrybut i zmienimy go na atrybut IsUnicode, więc wygląda następująco:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    internal class IsUnicode : Attribute
    {
        public bool Unicode { get; set; }

        public IsUnicode(bool isUnicode)
        {
            Unicode = isUnicode;
        }
    }

Gdy to zrobimy, możemy ustawić wartość logiczną na naszym atrybucie, aby określić konwencję, czy właściwość powinna mieć wartość Unicode. Możemy to zrobić w konwencji, która już istnieje, korzystając z właściwości ClrProperty klasy konfiguracji w następujący sposób:

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
                .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));

Jest to wystarczająco łatwe, ale istnieje bardziej zwięzły sposób osiągnięcia tego przy użyciu metody Having interfejsu API konwencji. Metoda Having ma parametr typu Func<PropertyInfo, T> , który akceptuje właściwośćInfo tak samo jak metoda Where, ale oczekuje się, że zwróci obiekt. Jeśli zwrócony obiekt ma wartość null, właściwość nie zostanie skonfigurowana, co oznacza, że można odfiltrować właściwości tak jak gdzie, ale różni się w tym, że przechwyci również zwrócony obiekt i przekaże go do metody Configure. Działa to podobnie do następujących:

    modelBuilder.Properties()
                .Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
                .Configure((config, att) => config.IsUnicode(att.Unicode));

Atrybuty niestandardowe nie są jedyną przyczyną używania metody Having. Jest to przydatne w dowolnym miejscu, w którym należy wnioskować o czymś, co filtrujesz podczas konfigurowania typów lub właściwości.

 

Konfigurowanie typów

Do tej pory wszystkie nasze konwencje były przeznaczone dla właściwości, ale istnieje inny obszar interfejsu API konwencji do konfigurowania typów w modelu. Środowisko jest podobne do konwencji, które widzieliśmy do tej pory, ale opcje wewnątrz konfiguracji będą na poziomie jednostki zamiast właściwości.

Jedną z rzeczy, które konwencje na poziomie typu mogą być naprawdę przydatne, jest zmiana konwencji nazewnictwa tabel, aby zamapować na istniejący schemat, który różni się od domyślnego programu EF lub utworzyć nową bazę danych z inną konwencją nazewnictwa. Aby to zrobić, najpierw potrzebujemy metody, która może akceptować typeInfo dla typu w naszym modelu i zwracać nazwę tabeli dla tego typu:

    private string GetTableName(Type type)
    {
        var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Ta metoda przyjmuje typ i zwraca ciąg, który używa małych liter ze znakami podkreślenia zamiast CamelCase. W naszym modelu oznacza to, że klasa ProductCategory zostanie zamapowana na tabelę o nazwie product_category zamiast ProductCategories.

Gdy mamy tę metodę, możemy ją wywołać w konwencji podobnej do następującej:

    modelBuilder.Types()
                .Configure(c => c.ToTable(GetTableName(c.ClrType)));

Ta konwencja konfiguruje każdy typ w naszym modelu do mapowania na nazwę tabeli zwracaną z naszej metody GetTableName. Ta konwencja jest odpowiednikiem wywoływania metody ToTable dla każdej jednostki w modelu przy użyciu interfejsu API Fluent.

Jedną z rzeczy, które należy zauważyć, jest to, że po wywołaniu funkcji ToTable EF zostanie wyświetlony ciąg, który podajesz jako dokładną nazwę tabeli, bez żadnej z liczby mnogiej, którą zwykle można wykonać podczas określania nazw tabel. Dlatego nazwa tabeli z naszej konwencji jest product_category zamiast product_categories. Możemy rozwiązać ten problem w naszej konwencji, wywołując usługę mnogiej.

W poniższym kodzie użyjemy funkcji Rozpoznawanie zależności dodanej w programie EF6, aby pobrać usługę pluralizacji, której użyto w programie EF i w liczbie mnogiej nazwy tabeli.

    private string GetTableName(Type type)
    {
        var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();

        var result = pluralizationService.Pluralize(type.Name);

        result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Uwaga

Ogólna wersja usługi GetService jest metodą rozszerzenia w przestrzeni nazw System.Data.Entity.Infrastructure.DependencyResolution, należy dodać instrukcję using do kontekstu, aby go użyć.

ToTable i dziedziczenie

Innym ważnym aspektem tabeli ToTable jest to, że jeśli jawnie zamapujesz typ na daną tabelę, możesz zmienić strategię mapowania, która będzie używana przez program EF. Jeśli wywołasz metodę ToTable dla każdego typu w hierarchii dziedziczenia, przekazując nazwę typu jako nazwę tabeli, tak jak powyżej, zmienisz domyślną strategię mapowania Tabela na hierarchię (TPH) na typ tabeli (TPT). Najlepszym sposobem na opisanie tego jest konkretny przykład:

    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Manager : Employee
    {
        public string SectionManaged { get; set; }
    }

Domyślnie zarówno pracownik, jak i menedżer są mapowane na tę samą tabelę (Employees) w bazie danych. Tabela będzie zawierać zarówno pracowników, jak i menedżerów z dyskryminującą kolumną, która informuje o typie wystąpienia przechowywanego w każdym wierszu. Jest to mapowanie TPH, ponieważ istnieje jedna tabela dla hierarchii. Jeśli jednak wywołasz metodę ToTable w obu klasach, każdy typ zostanie zamapowany na własną tabelę, znaną również jako TPT, ponieważ każdy typ ma własną tabelę.

    modelBuilder.Types()
                .Configure(c=>c.ToTable(c.ClrType.Name));

Powyższy kod będzie mapować na strukturę tabeli, która wygląda następująco:

tpt Example

Możesz tego uniknąć i zachować domyślne mapowanie TPH na kilka sposobów:

  1. Wywołaj metodę ToTable o tej samej nazwie tabeli dla każdego typu w hierarchii.
  2. Wywołaj metodę ToTable tylko w klasie bazowej hierarchii, w naszym przykładzie, który byłby pracownikiem.

 

Kolejność wykonywania

Konwencje działają w sposób ostatnio wygrywany, tak samo jak interfejs API Fluent. Oznacza to, że jeśli napiszesz dwie konwencje, które konfigurują tę samą opcję tej samej właściwości, to ostatni do wykonania wygrywa. Na przykład w kodzie poniżej maksymalnej długości wszystkich ciągów jest ustawiona wartość 500, ale następnie konfigurujemy wszystkie właściwości o nazwie Name w modelu tak, aby miały maksymalną długość 250.

    modelBuilder.Properties<string>()
                .Configure(c => c.HasMaxLength(500));

    modelBuilder.Properties<string>()
                .Where(x => x.Name == "Name")
                .Configure(c => c.HasMaxLength(250));

Ponieważ konwencja ustawiania maksymalnej długości na 250 jest po tym, który ustawia wszystkie ciągi na 500, wszystkie właściwości o nazwie Name w naszym modelu będą miały wartość MaxLength 250, podczas gdy wszystkie inne ciągi, takie jak opisy, będą miały wartość 500. Użycie konwencji w ten sposób oznacza, że można podać ogólną konwencję typów lub właściwości w modelu, a następnie przejąć je dla podzbiorów, które są różne.

W określonych przypadkach można również zastąpić konwencję za pomocą interfejsu API Fluent i adnotacji danych. W naszym przykładzie powyżej, jeśli użyliśmy interfejsu API Fluent do ustawienia maksymalnej długości właściwości, moglibyśmy umieścić ją przed lub po konwencji, ponieważ bardziej szczegółowy interfejs API Fluent wygra bardziej ogólną konwencję konfiguracji.

 

Konwencje wbudowane

Ponieważ konwencje niestandardowe mogą mieć wpływ na domyślne konwencje Code First, warto dodać konwencje do uruchamiania przed inną konwencją lub po niej. W tym celu można użyć metod AddBefore i AddAfter kolekcji Conventions w pochodnym obiekcie DbContext. Poniższy kod doda utworzoną wcześniej klasę konwencji, aby była uruchamiana przed wbudowaną konwencją odnajdywania kluczy.

    modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());

Będzie to najbardziej używane podczas dodawania konwencji, które muszą być uruchamiane przed lub po wbudowanych konwencjach, listę wbudowanych konwencji można znaleźć tutaj: System.Data.Entity.ModelConfiguration.Conventions Przestrzeni nazw.

Możesz również usunąć konwencje, których nie chcesz stosować do modelu. Aby usunąć konwencję, użyj metody Remove. Oto przykład usuwania nazwy PluralizingTableNameConvention.

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