Sdílet prostřednictvím


Vlastní konvence Code First

Poznámka

Pouze EF6 a novější – Funkce, rozhraní API atd. popsané na této stránce byly představeny v Entity Framework 6. Pokud používáte starší verzi, některé nebo všechny informace nemusí být platné.

Když použijete Code First, model se vypočítá z tříd pomocí sady konvencí. Výchozí konvence Code First určují, jak se vlastnost stane primárním klíčem entity, název tabulky, na kterou se entita mapuje, a na jakou přesnost a měřítko má ve výchozím nastavení desetinný sloupec.

Někdy tyto výchozí konvence nejsou ideální pro váš model a musíte je obejít konfigurací mnoha jednotlivých entit pomocí datových poznámek nebo rozhraní Fluent API. Vlastní konvence Code First umožňují definovat vlastní konvence, které poskytují výchozí hodnoty konfigurace pro váš model. V tomto názorném postupu prozkoumáme různé typy vlastních konvencí a způsob jejich vytvoření.

Modelové konvence

Tato stránka popisuje rozhraní DBModelBuilder API pro vlastní konvence. Toto rozhraní API by mělo stačit pro vytváření většiny vlastních konvencí. Existuje však také možnost vytvářet konvence založené na modelu – konvence, které manipulují s posledním modelem po vytvoření – pro zpracování pokročilých scénářů. Další informace najdete v tématu Konvence založené na modelu.

 

Náš model

Začněme definováním jednoduchého modelu, který můžeme použít s našimi konvencemi. Přidejte do projektu následující třídy.

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

 

Představujeme vlastní konvence

Pojďme napsat konvenci, která nakonfiguruje libovolnou vlastnost s názvem Klíč jako primární klíč pro daný typ entity.

Konvence jsou povolené v tvůrci modelů, ke kterým je možné přistupovat přepsáním OnModelCreating v kontextu. Aktualizujte třídu ProductContext následujícím způsobem:

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

Teď se jakákoli vlastnost v našem modelu s názvem Key nakonfiguruje jako primární klíč libovolné entity, ve které je její součástí.

Naše konvence můžeme také upřesnit filtrováním typu vlastnosti, kterou nakonfigurujeme:

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

Tím se nakonfigurují všechny vlastnosti s názvem Klíč jako primární klíč jejich entity, ale pouze pokud jsou celé číslo.

Zajímavou funkcí metody IsKey je, že se jedná o sčítání. To znamená, že pokud zavoláte IsKey na více vlastnostech a všechny se stanou součástí složeného klíče. Jedním z důvodů je, že když pro klíč zadáte více vlastností, musíte také zadat pořadí těchto vlastností. Můžete to provést voláním metody HasColumnOrder, například níže:

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

Tento kód nakonfiguruje typy v našem modelu tak, aby obsahoval složený klíč skládající se ze sloupce int Key a sloupce Název řetězce. Pokud model zobrazíme v návrháři, bude vypadat takto:

composite Key

Dalším příkladem konvencí vlastností je konfigurace všech vlastností DateTime v mém modelu tak, aby se mapoval na typ datetime2 v SQL Serveru místo datetime. Můžete toho dosáhnout následujícím způsobem:

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

 

Třídy konvence

Dalším způsobem definování konvencí je použití třídy Convention k zapouzdření konvence. Při použití třídy konvence pak vytvoříte typ, který dědí z třídy Convention v system.Data.Entity.ModelConfiguration.Conventions oboru názvů.

Třídu konvence můžeme vytvořit s konvencí datetime2, kterou jsme si ukázali dříve:

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

Pokud chcete efu říct, aby používal tuto konvenci, přidáte ji do kolekce Conventions v OnModelCreating, která pokud jste postupovali společně s návodem, bude vypadat takto:

    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 vidíte, do kolekce konvencí přidáme instanci naší konvence. Dědění z konvence poskytuje pohodlný způsob seskupení a sdílení konvencí napříč týmy nebo projekty. Můžete mít například knihovnu tříd se společnou sadou konvencí, které používají všechny vaše organizace.

 

Vlastní atributy

Dalším skvělým využitím konvencí je umožnit použití nových atributů při konfiguraci modelu. Abychom to mohli ilustrovat, vytvoříme atribut, který můžeme použít k označení vlastností řetězce jako ne unicode.

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

Teď vytvoříme konvenci pro použití tohoto atributu na náš model:

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

S touto konvencí můžeme přidat atribut NonUnicode do libovolné vlastnosti řetězce, což znamená, že sloupec v databázi bude uložen jako varchar místo nvarchar.

Jednou z věcí, kterou si o této konvenci poznamenejte, je, že pokud jste atribut NonUnicode umístili na cokoli jiného než řetězcovou vlastnost, pak vyvolá výjimku. Dělá to proto, že nemůžete nakonfigurovat IsUnicode pro žádný jiný typ než řetězec. Pokud k tomu dojde, můžete konvenci nastavit konkrétněji, aby vyfiltruje cokoli, co není řetězec.

I když výše uvedená konvence funguje pro definování vlastních atributů, existuje další rozhraní API, které může být mnohem jednodušší používat, zejména pokud chcete použít vlastnosti z třídy atributů.

V tomto příkladu aktualizujeme atribut a změníme ho na atribut IsUnicode, takže vypadá takto:

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

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

Jakmile to máme, můžeme nastavit logickou hodnotu atributu, abychom konvenci řekli, zda má být vlastnost Unicode nebo ne. Mohli bychom to udělat v konvenci, kterou jsme už vytvořili tak, že přistupujeme k ClrProperty třídy konfigurace takto:

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

To je dost snadné, ale existuje stručnější způsob, jak toho dosáhnout pomocí metody Rozhraní API konvencí. Having metoda má parametr typu Func<PropertyInfo, T> , který přijímá PropertyInfo stejné jako Where metoda, ale má vrátit objekt. Pokud vrácený objekt má hodnotu null, vlastnost nebude nakonfigurována, což znamená, že můžete vyfiltrovat vlastnosti s ním stejně jako Where, ale liší se v tom, že zachytí vrácený objekt a předá ho metodě Configure. Funguje takto:

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

Vlastní atributy nejsou jediným důvodem použití metody Having, je užitečné kdekoli, kde potřebujete důvod k něčemu, co filtrujete při konfiguraci typů nebo vlastností.

 

Konfigurace typů

Zatím byly všechny naše konvence určené pro vlastnosti, ale pro konfiguraci typů ve vašem modelu existuje další oblast rozhraní API konvencí. Prostředí je podobné konvencím, které jsme zatím viděli, ale možnosti uvnitř konfigurace budou na úrovni entity místo na úrovni vlastností.

Jednou z věcí, které konvence na úrovni typů můžou být opravdu užitečné při změně konvence vytváření názvů tabulek, a to buď tak, aby se mapovat na existující schéma, které se liší od výchozího nastavení EF, nebo vytvořit novou databázi s jinou konvencí pojmenování. K tomu nejprve potřebujeme metodu, která může přijmout TypeInfo pro typ v našem modelu a vrátit název tabulky pro tento typ:

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

        return result.ToLower();
    }

Tato metoda vezme typ a vrátí řetězec, který používá malá písmena s podtržítky místo CamelCase. V našem modelu to znamená, že třída ProductCategory bude mapována na tabulku s názvem product_category místo ProductCategories.

Jakmile budeme mít tuto metodu, můžeme ji volat v konvenci takto:

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

Tato konvence konvence konfiguruje každý typ v našem modelu tak, aby mapovat na název tabulky, který se vrátí z metody GetTableName. Tato konvence je ekvivalentem volání metody ToTable pro každou entitu v modelu pomocí rozhraní Fluent API.

Je třeba si uvědomit, že když zavoláte ToTable EF, vezme řetězec, který zadáte jako přesný název tabulky, bez jakékoli pluralizace, kterou by normálně dělal při určování názvů tabulek. Proto je název tabulky z naší konvence product_category místo product_categories. To můžeme vyřešit v naší konvenci tím, že zavoláme službu pluralizace sami.

V následujícím kódu použijeme funkci řešení závislostí přidanou v EF6 k načtení služby pluralizace, kterou EF použil a v pluralizaci názvu naší tabulky.

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

Poznámka

Obecná verze GetService je rozšiřující metoda v oboru názvů System.Data.Entity.Infrastructure.DependencyResolution, budete muset přidat příkaz using do kontextu, aby ho bylo možné použít.

ToTable and Inheritance

Dalším důležitým aspektem toTable je, že pokud explicitně mapujete typ na danou tabulku, můžete změnit strategii mapování, kterou ef bude používat. Pokud zavoláte ToTable pro každý typ v hierarchii dědičnosti, předáte název typu jako název tabulky, jako jsme to udělali výše, pak změníte výchozí strategii mapování tabulek na hierarchii (TPH) na typ tabulky (TPT). Nejlepším způsobem, jak to popsat, je betonový příklad:

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

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

Ve výchozím nastavení se zaměstnanci i nadřízený mapují na stejnou tabulku (Zaměstnanci) v databázi. Tabulka bude obsahovat zaměstnance i manažery s nediskriminačním sloupcem, který vám řekne, jaký typ instance je uložen v každém řádku. Toto je mapování TPH, protože pro hierarchii existuje jedna tabulka. Pokud však zavoláte ToTable v obou třídách, bude každý typ namapován na vlastní tabulku, označovanou také jako TPT, protože každý typ má svou vlastní tabulku.

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

Výše uvedený kód se mapuje na strukturu tabulky, která vypadá takto:

tpt Example

Můžete se tomu vyhnout a zachovat výchozí mapování TPH několika způsoby:

  1. Volání ToTable se stejným názvem tabulky pro každý typ v hierarchii.
  2. Volání ToTable pouze na základní třídě hierarchie v našem příkladu, který by byl zaměstnancem.

 

Pořadí provádění

Konvence fungují posledním způsobem výhry, stejně jako rozhraní Fluent API. To znamená, že pokud napíšete dvě konvence, které konfigurují stejnou možnost stejné vlastnosti, pak poslední pro spuštění wins. Například v kódu pod maximální délkou všech řetězců je nastavená hodnota 500, ale pak nakonfigurujeme všechny vlastnosti nazvané Název v modelu tak, aby měly maximální délku 250.

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

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

Vzhledem k tomu, že konvence nastavit maximální délku na 250 je za tím, který nastaví všechny řetězce na 500, budou všechny vlastnosti nazvané Název v našem modelu mít MaxLength 250, zatímco všechny ostatní řetězce, jako jsou popisy, budou 500. Použití konvencí tímto způsobem znamená, že můžete poskytnout obecnou konvenci pro typy nebo vlastnosti v modelu a pak je převést pro podmnožina, které se liší.

Rozhraní Fluent API a datové poznámky lze také použít k přepsání konvence v konkrétních případech. V našem příkladu výše jsme použili rozhraní Fluent API k nastavení maximální délky vlastnosti, mohli bychom ji dát před konvenci nebo po ní, protože konkrétnější rozhraní Fluent API získá obecnější konvenci konfigurace.

 

Předdefinované konvence

Vzhledem k tomu, že výchozí konvence Code First můžou mít vliv na vlastní konvence, může být užitečné přidat konvence ke spuštění před nebo po jiné konvenci. K tomu můžete použít Metody AddBefore a AddAfter kolekce Conventions na odvozené DbContext. Následující kód by přidal třídu konvence, kterou jsme vytvořili dříve, aby se spustila před předdefinované konvence zjišťování klíčů.

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

To bude nejvhodnější při přidávání konvencí, které je potřeba spustit před nebo po předdefinovaných konvencích, najdete seznam předdefinovaných konvencí tady: System.Data.Entity.ModelConfiguration.Conventions – obor názvů.

Můžete také odebrat konvence, které nechcete u modelu použít. Pokud chcete odebrat konvenci, použijte metodu Remove. Tady je příklad odebrání PluralizingTableNameConvention.

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