Egyéni kód – első konvenciók

Megjegyzés:

CSAK EF6 – Az ezen az oldalon tárgyalt funkciók, API-k stb. az Entity Framework 6-ban jelentek meg. Ha korábbi verziót használ, az információk egy része vagy egésze nem érvényes.

A Code First használatakor a modell az osztályokból konvenciók halmazával lesz kiszámítva. Az alapértelmezett első kódegyezmények határozzák meg, hogy melyik tulajdonság lesz egy entitás elsődleges kulcsa, az entitás által leképzett tábla neve, valamint a tizedesoszlopok pontossága és skálázása alapértelmezés szerint.

Néha ezek az alapértelmezett konvenciók nem ideálisak a modellhez, és úgy kell kerülni ezeket, hogy sok egyéni entitást konfigurál adatmegjegyzésekkel vagy a Fluent API használatával. Az egyéni kód első konvenciók segítségével saját konvenciók definiálhatók, amelyek a modell konfigurációs alapértelmezett beállításait biztosítják. Ebben az útmutatóban megismerjük az egyéni konvenciók különböző típusait és azok létrehozását.

Modellezésalapú elgondolások

Ez az oldal az egyéni konvenciókhoz készült DbModelBuilder API-t ismerteti. Ennek az API-nak elegendőnek kell lennie a legtöbb egyéni konvenció elkészítéséhez. A speciális forgatókönyvek kezelésére azonban lehetőség van olyan modellalapú konvenciók létrehozására is, amelyek a végső modellt módosítják. További információért lásd: Model-alapú konvenciók.

 

Modellünk

Kezdjük azzal, hogy meghatározunk egy egyszerű modellt, amelyet a konvenciók alapján használhatunk. Adja hozzá a következő osztályokat a projekthez.

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

 

Egyéni konvenciók bemutatása

Írjunk egy konvenciót, amely konfigurálja, hogy a Kulcs nevű tulajdonság legyen az entitástípus elsődleges kulcsa.

A modellkészítőn engedélyezve vannak a konvenciók, amelyek a környezet OnModelCreating felülírásával érhetők el. Frissítse a ProductContext osztályt az alábbiak szerint:

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

Most a modellünkben minden Kulcs nevű tulajdonság annak az entitásnak az elsődleges kulcsaként lesz beállítva, amelynek a része.

A konvenciókat a konfigurálni kívánt tulajdonság típusának szűrésével is pontosabbá tehetjük:

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

Ez konfigurálja a Kulcs nevű összes tulajdonságot az entitás elsődleges kulcsának, de csak akkor, ha egész szám.

Az IsKey metódus érdekessége, hogy additív. Ez azt jelenti, hogy ha több tulajdonságon hívja meg az IsKey-et, és mindegyik egy összetett kulcs részévé válik. Ennek egyik kifogása az, hogy ha egy kulcshoz több tulajdonságot is megad, meg kell adnia a tulajdonságok sorrendjét is. Ehhez hívja meg a HasColumnOrder metódust az alábbi módon:

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

Ez a kód úgy konfigurálja a modellben lévő típusokat, hogy egy összetett kulcs legyen, amely az int Key oszlopból és a sztringnév oszlopból áll. Ha a modellt a tervezőben tekintjük meg, az a következőképpen nézne ki:

összetett kulcs

Egy másik példa a tulajdonságkonvenciákra, ha a modellem összes DateTime tulajdonságát úgy konfigurálja, hogy a datetime2 típusra legyen megfeleltetve az SQL Serveren a datetime helyett. Ezt a következőkkel érheti el:

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

 

Szabvány osztályok

A konvenciók meghatározásának másik módja, ha konvenciós osztályt használ a konvenciók beágyazásához. Konvenciós osztály használatakor létrehoz egy típust, amely a System.Data.Entity.ModelConfiguration.Conventions névtér Konvenció osztályától örököl.

A korábban bemutatott datetime2 konvencióval létrehozhatunk egy konvenciós osztályt az alábbi módon:

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

Ha meg szeretné mondani az EF-nek, hogy használja ezt a konvenciót, vegye fel az OnModelCreating Konvenciók gyűjteményéhez, amely ha követte az útmutatót, a következőképpen fog kinézni:

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

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

Mint látható, konvenciónk egy példányát hozzáadjuk a konvenciók gyűjteményéhez. A konvenciótól való öröklés kényelmes módot kínál a konvenciók csoportok vagy projektek közötti csoportosítására és megosztására. Létrehozhat például egy osztálykönyvtárat, amely az összes szervezet projektje által használt konvenciók közös készletével rendelkezik.

 

Egyéni attribútumok

A konvenciók másik nagyszerű felhasználása az új attribútumok használatának engedélyezése a modell konfigurálásakor. Ennek szemléltetéséhez hozzunk létre egy attribútumot, amellyel a sztringtulajdonságokat nem Unicode-ként jelölhetjük meg.

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

Most hozzunk létre egy konvenciót, amely ezt az attribútumot alkalmazza a modellre:

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

Ezzel a konvencióval bármelyik sztringtulajdonságunkhoz hozzáadhatjuk a NonUnicode attribútumot, ami azt jelenti, hogy az adatbázis oszlopa az nvarchar helyett varcharként lesz tárolva.

Egy dolog, amit meg kell jegyezni ezzel a konvencióval kapcsolatban, hogy ha a NonUnicode attribútumot nem karakterlánc-tulajdonságra helyezi, akkor kivételt fog tenni. Ezt azért teszi, mert az IsUnicode nem konfigurálható sztringen kívül más típusra. Ha ez történik, akkor pontosabbá teheti a szabályrendszert, hogy kiszűrje azokat, amik nem karakterláncok.

Bár a fenti konvenció egyéni attribútumok meghatározására is használható, van egy másik API, amely sokkal egyszerűbben használható, különösen akkor, ha az attribútumosztály tulajdonságait szeretné használni.

Ebben a példában frissíteni fogjuk az attribútumot, és egy IsUnicode attribútumra módosítjuk, így a következőképpen néz ki:

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

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

Ha ez megvan, beállíthatunk egy bool-t az attribútumunkra, hogy meg tudjuk állapítani a konvenciót, hogy egy tulajdonságnak Unicode-nak kell-e lennie. Ezt a már meglévő konvencióban is megtehetjük a konfigurációs osztály ClrProperty-éhez való hozzáféréssel, például a következő módon:

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

Ez elég egyszerű, de a konvenciók API Having metódusával még tömörebb módon is el lehet érni ezt. A Having metódus rendelkezik egy Func típusú paraméterrel, amely a PropertyInfo-t ugyanúgy fogadja, mint a Where metódus, de várhatóan egy objektumot ad vissza. Ha a visszaadott objektum null értékű, akkor a tulajdonság nem lesz konfigurálva, ami azt jelenti, hogy ugyanúgy szűrheti ki a tulajdonságokat, mint a Hol, de abban különbözik, hogy a visszaadott objektumot is rögzíti, és átadja a Konfigurálás metódusnak. Ez a következőképpen működik:

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

Nem az egyéni attribútumok az egyetlen ok a Having metódus használatára, bárhol hasznos lehet, ha meg kell indokolnia valamit, amire a típusok vagy tulajdonságok konfigurálásakor szűr.

 

Típusok konfigurálása

Eddig az összes konvenció a tulajdonságokra volt, de a konvenciós API egy másik területe a modell típusainak konfigurálásához. A felhasználói élmény hasonló az eddig látott konvenciókhoz, de a konfiguráláson belül a beállítások a tulajdonságszint helyett az entitáson lesznek.

A típusszintű konvenciók egyik hasznos alkalmazása az lehet, hogy megváltoztatják a tábla elnevezési konvencióját, akár egy meglévő sémára való áthasonlításához, amely eltér az EF alapértelmezésétől, akár egy új adatbázis létrehozása eltérő elnevezési konvencióval. Ehhez először olyan metódusra van szükségünk, amely elfogadhatja a TypeInfo típust a modellben, és visszaadhatja az adott típus táblanevét:

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

        return result.ToLower();
    }

Ez a metódus egy típust használ, és egy olyan sztringet ad vissza, amely a CamelCase helyett kisbetűs aláhúzásokat használ. A modellben ez azt jelenti, hogy a ProductCategory osztály a ProductCategories helyett egy product_category nevű táblára lesz leképezve.

Amint megvan ez a módszer, meghívhatjuk egy szabvány szerint:

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

Ez a konvenció a modell minden típusát úgy konfigurálja, hogy a GetTableName metódusból visszaadott táblanévre legyen megfeleltetve. Ez a konvenció egyenértékű azzal, hogy a Modell minden entitásához meghívja a ToTable metódust a Fluent API használatával.

Egy dolog, amit meg kell jegyezni, hogy amikor meghívja a ToTable EF-t, a megadott sztringet fogja pontosan a táblanévként megadni, anélkül, hogy bármilyen pluralizálást kellene végrehajtania a táblanevek meghatározásakor. Ezért a "product_categories" helyett a "product_category" a konvenciónk szerinti táblanevünk. A konvenciónkban ezt úgy oldhatjuk meg, hogy magunk hívjuk meg a pluralizálási szolgáltatást.

Az alábbi kódban az EF6-ban hozzáadott Függőségfeloldási funkcióval lekérjük azt a pluralizálási szolgáltatást, amelyet az EF használt volna, és pluralizáljuk a táblanevünket.

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

Megjegyzés:

A GetService általános verziója egy kiterjesztési metódus a System.Data.Entity.Infrastructure.DependencyResolution névtérben, használatához hozzá kell adnia egy 'using' utasítást a környezethez.

ToTable és öröklés

A ToTable másik fontos eleme, hogy ha egy típust kifejezetten egy adott táblára képez le, akkor módosíthatja az EF által használt leképezési stratégiát. Ha minden típushoz meghívja az öröklési hierarchiában a ToTable függvényt, a típusnevet adva meg a tábla neveként, hasonlóan a fenti példához, akkor módosítja az alapértelmezett Table-Per-Hierarchy (TPH) leképezési stratégiát Table-Per-Type (TPT) stratégiára. Ennek leírására a legjobb megoldás egy konkrét példa:

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

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

Alapértelmezés szerint mind az alkalmazott, mind a felettes az adatbázis ugyanazon táblájához kerülnek hozzárendelésre, a(z) 'Employees' táblához. A táblázat az alkalmazottakat és a vezetőket is tartalmazza egy diszkriminatív oszlopmal, amelyből megtudhatja, hogy az egyes sorokban milyen típusú példány van tárolva. Ez a TPH-leképezés, mivel a hierarchia egyetlen táblája van. Ha azonban mindkét osztályon meghívja a ToTablet, akkor a rendszer ehelyett az egyes típusokat a saját táblájára, más néven TPT-re rendeli, mivel mindegyik típusnak saját táblája van.

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

A fenti kód egy táblázatszerkezetre lesz megfeleltetve, amely a következőhöz hasonlóan néz ki:

tpt példa

Ezt elkerülheti, és néhány módon fenntarthatja az alapértelmezett TPH-leképezést:

  1. Az összes hierarchiában lévő típushoz hívja meg a ToTable-t ugyanazzal a táblanévvel.
  2. A ToTable metódust csak a hierarchia alaposztályán kell meghívni, a példánkban ez az alkalmazott osztály.

 

Végrehajtási rendelés

A konvenciók "last wins" módon működnek, ugyanúgy, mint a Fluent API. Ez azt jelenti, hogy ha két konvenciót ír, amelyek ugyanazon tulajdonság ugyanazon opcióját konfigurálják, akkor az utolsóként végrehajtott érvényesül. Például az alábbi kódban az összes sztring maximális hossza 500-ra van állítva, de a modell összes Név nevű tulajdonságát úgy konfiguráljuk, hogy a maximális hossza 250 legyen.

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

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

Mivel az a konvenció, amely a maximális hosszúságot 250-re állítja, csak azután következik, hogy minden sztringet 500-ra állítottunk, a modellben a Név nevű összes tulajdonság maximális hossza 250 lesz, míg a többi sztring, például a leírások, 500 marad. Az ilyen konvenciók használata azt jelenti, hogy általános konvenciót adhat meg a modell típusaihoz vagy tulajdonságaihoz, majd felülírhatja őket a különböző részhalmazok esetében.

A Fluent API és az adatjegyzetek adott esetekben egy konvenció felülbírálásához is használhatók. A fenti példában, ha a Fluent API-val állítottuk volna be egy tulajdonság maximális hosszát, akkor a konvenció előtt vagy után is elhelyezhettük volna, mert a konkrétabb Fluent API az általánosabb konfigurálási konvenciót fogja átvenni.

 

Beépített konvenciók

Mivel az egyéni konvenciókra hatással lehetnek az alapértelmezett Code First konvenciók, hasznos lehet olyan konvenciók hozzáadása, amelyek egy másik konvenció előtt vagy után futnak. Ehhez használhatja a konvenciók gyűjteményének AddBefore és AddAfter metódusait a származtatott DbContextben. Az alábbi kód hozzáadná a korábban létrehozott konvencióosztályt, hogy az a beépített kulcsfelderítési konvenció előtt fusson.

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

Ez lesz a leginkább hasznos a beépített konvenciók előtt vagy után futtatandó konvenciók hozzáadásakor, a beépített konvenciók listája itt található: System.Data.Entity.ModelConfiguration.Conventions Namespace.

Eltávolíthatja azokat a konvenciókat is, amelyeket nem szeretne alkalmazni a modellre. Konvenciók eltávolításához használja az Eltávolítás metódust. Íme egy példa a PluralizingTableNameConvention eltávolítására.

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