Návrhový vzor - Entity Framework   zodpovězená otázka

Entity Framework

Zdravím, řeším problém jak používat Entity Framework (EF). Do nedávna jsem používal ADO komunikaci a vlastní třídy včetně parsování. Před nějakým čase jsem přešel na EF a osobně se mi moc nelíbí používání návrhového vzoru repository, nebo facade (to se moc nepoužívá, ALE). Od jednoho známého jsem dostal tip na CQRS. Moc nevím jestli je to nej., pro každý dotaz dělat 2-3 třídy. Navrhněte včetně nějakých exámplů jak k těmto entitám přistupovat a to i s možností změny entit (za ADO, nebo NHibernate, ...)

Děkuji

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Psát to tak, aby bylo možné EF nahradit ADO.NET je hovadina, protože tyto dvě věci jsou z hlediska použití zcela odlišné.

Nejsem si jist, co myslíte těmi návrhovými vzory. Bez problémů se dá pracovat pouze s tím, co VS vytvoří na základě databáze a není třeba psát žádnou vlastní infrastrukturu.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Žádný nástroj/framework není náplast na každou bolest, proto vyzkoušejte, co vyhovuje Vám. Já například často používám Dapper, protože mi vyhovuje.

Prosím, použijte google a najděte si příklady sám a zde se ptejte na konkrétní otázky a nejasnosti. Většina projektů, které chtějí být úspěšné mají nějakou dokumentaci i s příklady, tam bych začal. Také, jestli Vám známý něco doporučil, pak Vám možné také něco v rychlosti může ukázat v hospodě u piva, ne?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Děkuji za názor. Tady nejde o nutnou změnu wrapperu, ale spíš o objektovou použitelnou techniku psaní větších struktur. Ono vystačit si jen s EF je dost neohrabané a neefektivní. Ano je to jen pozlátko, ale z pohledu objektového programátora a s ohledem na výkon databázových dotazů není moc dobré napsat selekt na všemi sloupci. Ano je možné to vyřešit vlastní výstupní třídou, ale to právě řeším. Nepíši aplikace typu vypiš kolekci uživatelů z jedné tabulky, ale předávám objekty dále z mého API a to buď WS, nebo webový portál, nebo pro jinou cílovou skupinu. Takže ve finále mi jde o použití nějakých wrapovacích tříd pro následnou práci. Jen s entitou vygenerovanou EF není vždy použitelné. Ne vždy samozřejmě.

Ale děkuji.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Je třeba EF řádně poznat a vyzkoušet, než začnete dělat závěry. Většina negací na EF je dán neznalostí.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ta vygenerovaná entita jde upravit podle vlastní potřeby. Také lze použít vlastní POCO třídy místo vygenerovaných entit, ale to smrdí a nikdy jsem to nezkoušel.

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

V praxi je dobrý "pattern" oddělit objekty vlastních entit datové vrstvy a objekty modelů/DTO - objektů vyšší vrstvy.

Například v db budeme mít tabulku Novinka, v datové vrstvě k ní budeme mít třídu pro EF (Code Only) entitu (ta bude navíc pouze internal, protože o ní vyšší vrstva nemusí nic vědět):

namespace Web.Data.DataModel
{
    internal sealed class Novinka
    {
        #region Configuration class
        internal sealed class NovinkaConfiguration : EntityTypeConfiguration<Novinka>
        {
            #region constructors and destructors
            public NovinkaConfiguration()
            {
                //Primary Key
                this.HasKey(t => t.novIDNovinky);

                //Properties
                this.Property(t => t.novTitulek)
                    .IsRequired()
                    .HasMaxLength(100);
                this.Property(t => t.novText)
                    .IsRequired()
                    .HasMaxLength(4000);
                this.Property(t => t.novLinkUrl)
                    .HasMaxLength(255);

                //Table & Column Mappings
                this.ToTable("Novinka");
                this.Property(t => t.novIDNovinky).HasColumnName("novIDNovinky");
                this.Property(t => t.novZapsano).HasColumnName("novZapsano");
                this.Property(t => t.novZmeneno).HasColumnName("novZmeneno");
                this.Property(t => t.novIDUzivatele).HasColumnName("novIDUzivatele");
                this.Property(t => t.novDatum).HasColumnName("novDatum");
                this.Property(t => t.novTitulek).HasColumnName("novTitulek");
                this.Property(t => t.novText).HasColumnName("novText");
                this.Property(t => t.novLinkUrl).HasColumnName("novLinkUrl");
                this.Property(t => t.novLinkExternal).HasColumnName("novLinkExternal");
                this.Property(t => t.novZverejnit).HasColumnName("novZverejnit");

                //Relationships
                this.HasRequired(o => o.Uzivatel)
                    .WithMany()
                    .HasForeignKey(f => f.novIDUzivatele);
            }
            #endregion
        }
        #endregion

        #region member varible and default property initialization
        [Key]
        public int novIDNovinky { get; internal set; }
        public DateTime novZapsano { get; set; }
        public DateTime novZmeneno { get; set; }
        public int novIDUzivatele { get; internal set; }
        internal Uzivatel Uzivatel { get; set; }
        public DateTime novDatum { get; set; }
        public DateTime novPlatnostOd { get; set; }
        public DateTime? novPlatnostDo { get; set; }
        public string novTitulek { get; set; }
        public string novText { get; set; }
        public string novLinkUrl { get; set; }
        public bool novLinkExternal { get; set; }
        public bool novZverejnit { get; set; }
        #endregion
    }
}

A dále budeme mít třídu pro model/DTO, který bude naopak public:

namespace Web.Data.Models
{
    [System.Diagnostics.DebuggerDisplay("\\{ IDNovinky = {IDNovinky}, Datum = {Datum}, Titulek = {Titulek}, LinkUrl = {LinkUrl}, Zverejnit = {Zverejnit}, Zapsano = {Zapsano}, Zmeneno = {Zmeneno}, Uzivatel = {Uzivatel} \\}")]
    public sealed class Novinka
    {
        #region member varible and default property initialization
        [ScaffoldColumn(false)]
        public int IDNovinky { get; internal set; }

        [Required(ErrorMessage = "Datum je povinný údaj.")]
        [Display(Name = "Datum", Order = 1)]
        [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
        public DateTime Datum { get; set; }

        [Required(ErrorMessage = "Titulek je povinný údaj.")]
        [StringLength(100, ErrorMessage = "Titulek je příliš dlouhý.")]
        [Display(Name = "Titulek", Order = 2)]
        [DataType("TextLong")]
        public string Titulek { get; set; }

        [Required(ErrorMessage = "Text je povinný údaj.")]
        [StringLength(4000, ErrorMessage = "Text je příliš dlouhý.")]
        [Display(Name = "Text", Order = 3)]
        [DataType("Html")]
        public string Text { get; set; }

        [StringLength(255, ErrorMessage = "Odkaz je příliš dlouhý.")]
        [Display(Name = "Odkaz", Order = 4)]
        [DataType("Url")]
        public string LinkUrl { get; set; }

        [Display(Name = "Externí odkaz", Order = 5)]
        public bool LinkExternal { get; set; }

        [Display(Name = "Zveřejnit", Order = 6)]
        public bool Zverejnit { get; set; }

        [HideColumnIn(PageTemplate.Insert)]
        [Display(Name = "Datum vytvoření", Order = 7)]
        [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:g}")]
        public DateTime Zapsano { get; internal set; }

        [HideColumnIn(PageTemplate.Insert)]
        [Display(Name = "Poslední změna", Order = 8)]
        [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:g}")]
        public DateTime Zmeneno { get; internal set; }

        [HideColumnIn(PageTemplate.Insert)]
        [Display(Name = "Změnil", Order = 9)]
        public string Uzivatel { get; internal set; }
        #endregion

        #region constructors and destructors
        public Novinka()
        {
            this.Datum = DateTime.Today;
            this.Zverejnit = true;
        }
        #endregion
    }
}

A datová vrstva se nám bude starat o vracení a ukládání změn prováděných nad tímto modelem.

V tomto případě se model a entita co se týče datových položek zas tolik neliší (pouze do modelu bude dotahováno jméno uživatele jako string), ale obecně se může lišit hodně. Model může obecně obsahovat různá odvozená data nebo data dotažená z jiných entit.

Pokud se jedná například o ASP.NET WebForms/MVC aplikaci, může mít model jako například v tomto případě atributy pro validaci, scafolding apod.,

Pokud budou tyto objekty pouze vráceny/posílány při volání WebAPI/WCF (například pokud je klient v Knockout JS), obvykle se označují jako DTO - Data Transfer Objects. I v takovém případě by šlo validační atributy využít, například pokud by šlo "skutečný" model na klientu z těchto tříd generovat automaticky.

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Přijde mi, že to trochu motáte - fasáda nemá s EF de facto nic společného a psát si na EF vlastní repozitáře nemusíte (i když většinou to dává smysl). CQRS je úplně něco jiného, hodí se především pro distribuované aplikace.

Já to dělám nějak takto (jen stručný popis, pokud to chcete podrobněji, mrkněte na www.dotnetcollege.cz a objednejte si kurz):

1) Repozitáře, které umožňují pouze GetById, Insert, Update, Delete. Neumí se dotazovat na více entit, to dělá akorát problémy. Je to sice vrstva navíc nad EF, ale hodí se ve chvíli, kdy se objeví data, která se neukládají do databáze (např. tabulka, která se odkazuje na soubory ve filesystému, nebo nedávno jsme potřebovali zaindexovat řádky v jedné tabulce v Lucene a na to je repository ideální). Dále můžete na úrovni repository mít různé eventy atd., prostě nikdy nevíte, kdy se to bude hodit, a i když je nakrásně nepoužijete, tak ta vrstva je tak tenká, že ničemu nevadí.

2) Query objekty - každý dotaz, který vrací něco jiného než jednu konkrétní entitu podle jejího primárního klíče, je reprezentován samostatnou třídou. Je to dobré ze dvou důvodů - jednak máte jasně definované všechny dotazy, které nad databází děláte, a nestane se vám, že máte stejný dotaz v aplikaci 10x. Dále je to dobré z výkonnostních důvodů, protože občas EF prostě nějaký dotaz překládá špatně a dá se to zrychlit tak, že si napíšete storku, kterou pak zavoláte - a díky tomu, že dotaz je na jednom místě, stačí upravit jednu třídu.

3) DTO - v žádném případě není dobrý nápad probublávat entity z EF až do UI, všechny Query objekty vrací už DTO a ne samotné entity. Pro mapování entit na DTO používám většinou AutoMapper, psát to ručně je otrava.

nahlásit spamnahlásit spam 2 / 2 odpovědětodpovědět

3) Proč nepoužít rovnou entitové třídy a cpát to ještě do nějakých mezitříd?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Z mnoha důvodů, namátkou:

1) Entity obsahují i sloupce, které se týkají toho, jak jsou data reprezentována v databázi (např. cizí klíče atd.) - nemám rád, když implementační detaily týkající se relačních databází prosakují výše než do business vrstvy - nemají tam co dělat.

Správně by se při návrhu aplikace nemělo začínat návrhem schématu databáze, ale měl by se udělat doménový model. Pak by se teprve mělo přemýšlet, jak bude vypadat storage model a jak se na sebe budou mapovat. Praxe je samozřejmě jiná, nicméně horní vrstvy aplikace by neměly řešit, jak jsou data persistována, to je interní věc.

2) V UI málokdy zobrazuji přesně jen data z jedné tabulky, ale většinou v dotazech potřebuju JOIN. Ano, můžu udělat context.Products.Include(p => p.Category).Include(p => p.Supplier).Include(p => p.Orders) a do UI dát celé entity, ale v gridu z tabulky Categories a Suppliers potřebuju jen sloupec Name a ne těch 10 dalších, a na vlastnosti Orders zavolám jen .Count - je tedy zbytečné vytahovat všechno, když potřebuju jen něco. Takže si nadeklaruju třídu, která bude vypadat asi takto:

public class ProductDTO {
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime CreatedDate { get; set; }
    public string CategoryName { get; set; }
    public string SupplierName { get; set; }
    public int OrdersCount { get; set; }
}

Na mapování používám knihovnu AutoMapper, které řeknu, ať mi třídu Product namapuje na ProductDTO. Ta knihovna je inteligentní, takže když vidí, že Product nemá vlastnost CategoryName, ale že jde zavolat Category.Name, tak to použije, stejně tak když vlastnost končí Count a to předtím je kolekce, a jde volat Orders.Count(), tak to udělá. A dokonce umí vygenerovat lambda expression nad IQueryable, takže to použiju přímo v dotazu a přeloží se to celé do SQL:

context.Products.Project().To<ProductDTO>()

Dá se pochopitelně použít i pro mapování opačným směrem, takže pokud chci editovat nějaký řádek tabulky, promítnu si ho z entity na DTO, v UI jej upravím, a pak změny zase namapuju zpátky - AutoMapper umí například i synchronizovat kolekce podle primárních klíčů, dá se to různě nastavovat.

3) Další problém s entitami je lazy loading - občas je to úžasná věc, na druhou stranu když tu entitu zkusíte serializovat, tak při zapnutém lazy loadingu to rozbalí všechny property - nedávno jsme dělali jedné firmě konzultace ohledně výkonnosti aplikace a všimli jsme si, že posílají entity přes WCF - bylo to pomalé, protože se přenášelo strašně moc dat, de facto jim to vyserializovalo část databáze, která se k té entitě vázala.

Jako jedinou výjimku, kdy entity mohou do UI, bych akceptoval u nějakých číselníků nebo primitivních tabulek, které mají 2 nebo 3 sloupce - tam bych na dělání DTO asi netrval, zvlášť pokud by do DTO bylo úplně stejné jako ta entita. Nicméně v praxi se mi spíš osvědčilo všechny tyhle číselníkové tabulky namapovat na jednu DTO třídu, díky čemuž můžu s číselníky v aplikaci pracovat obecně.

Anebo bych to ještě pochopil u malých aplikací, které mají řekněme do 10 databázových tabulek - tam je možná tenhle cirkus zbytečný, pokud vím, že se ta aplikace nebude rozrůstat.

nahlásit spamnahlásit spam 2 / 2 odpovědětodpovědět

Dovolil bych si stručný dotaz, protože toto mi přijde už víc použitelné. Takže jak to vidíte s výkonem. Přijde mi to pomalejší, ale to jen v konzoli kde to testuji. Což může být zavádějící, chápu.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Nevím, které zpomalení máte na mysli.

Pokud nějakou režii AutoMapperu, tak je to určitě pomalejší, než psát si to ručně - na druhou stranu on funguje tak, že přes reflexi očuchá ty třídy a vygeneruje za běhu přímo IL, který pak spouští.

Navíc se bavíme o databázových aplikacích, kde komunikace a získání dat ze SQL Serveru typicky trvá desítky milisekund, takže nějaké drobné zpomalení je irelevantní.

Pokud jde o zpomalení způsobené používáním Entity Frameworku, tam to smysl dává - je potřeba hlídat, abychom nevytahovali více dat, než potřebujeme, a velmi složité LINQ dotazy je lepší přepsat jako uložené procedury, ale to lze řešit až časem ve chvíli, kdy to skutečně vadí. Ale vzhledem k tomu, kolik EF ušetří času, se mi dnes na žádném projektu nevyplatí psát si data access layer vlastní.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Musím přiznat, že na první pohled se mi to moc líbilo. Ale když jsem si dal pod sebe dna totožné dotazy do DB. Jednou pomocí AutoMapper a podruhé ruční dotaz a parsování, tak je tam jen víc kódu, ale ručně to je o mnoho rychlejší. Co je plus dotaz to sestaví "dobrý". Dovolím si tvrdit, že databázi zvládnu obstojně a dotaz e mi zdál dobrý (jiný než bych použil, ale OK). Ovšem to mapování je dost mínus. Ale zase jste mě navedl na jednu myšlenku a to AutoMapper provést sám. Já opravdu nefandím tomu pracovat EF objektem na úrovni aplikace UI a proto inklinuji k používání vlastních objektů. Resp. když dotazuji report nad nějakou tabulkou, tak je to vždy o speciálním objektu. Nyní koukám ještě na EmitMapper, ale bojím se, že to nebude lepší. Uvidím.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Pokud ti stačí pouze mapování na základě názvů prop, tak můžeš zkusit toto:

 public static class ExtensionMethods
    {
        public static T1 CopyFrom<T1, T2>(this T1 obj, T2 otherObject)
            where T1 : class
            where T2 : class
        {
            PropertyInfo[] srcFields = otherObject.GetType().GetProperties(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);

            PropertyInfo[] destFields = obj.GetType().GetProperties(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);

            foreach (var property in srcFields)
            {
                var dest = destFields.FirstOrDefault(x => x.Name == property.Name);
                if (dest != null)
                    dest.SetValue(obj, property.GetValue(otherObject, null), null);
            }
            return obj;
        }
    }

Není to sice inteligentní jako Automapper, ale mohlo by to být rychlejší a názvy prop musí být stejné.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ten seznam fieldů, které se mají kopírovat, by šel pro různá T1 a T2 sestavit pouze 1x a "zacacheovat" do statické proměnné, viz článek:

http://www.dotnetportal.cz/blogy/15/Null...

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Tohle určitě nebude při opakovaném spouštění rychlejší než AutoMapper, ten si totiž vygeneruje tu funkci v IL a pak ho spouští.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

A co vám konkrétně vadí na AutoMapperu? Jako můžete si mapování samozřejmě dělat sám, stačí do každého dotazu dopsat .Select(entita => new MojeDTO() { ... }), akorát mi to přijde jako dost pracné a pokud do tabulky přidáte nový sloupec, ne úplně snadno se tyhle projekce dohledávají a občas se stane, že to tam zapomenete přidat.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Jak jsem psal líbí se mi to, ale když si spustím jediný dotaz do databáze pomocí tohoto AutoMapperu nad tabulkou Northwind.dbo.Products a pomocí jen linq, tak je to obrovský nepoměr. Možná něco dělám špatně, ale i na webu jsem na toto našel kritiku z pohledu výkonu.

Nehledě na to, že třeba v současné DB struktuře mám v tabulce Northwind.dbo.Products dost rozdílné sloupce, než píšete. chápu, že to je problém můj jak si to sestavím, ale jak se to třeba zachová ke sloupci, který já v každé tabulce stavím jako ID a někdo ProductId a .NET samo mám jen Id. to bude potřeba dalšího mapování. Obdobně jako v EF. Já to opravdu nechci dramaticky hodnotit, je to jen rychlí test a hledání na webu.

Jo a pohybujeme se na webu, tedy já.

SELECT TOP 1000 [ProductID]

,[ProductName]

,[SupplierID]

,[CategoryID]

,[QuantityPerUnit]

,[UnitPrice]

,[UnitsInStock]

,[UnitsOnOrder]

,[ReorderLevel]

,[Discontinued]

FROM [Northwind].[dbo].[Products]

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Dejme tomu, že dotaz vybere 1000 záznamů. Místo toho aby se rovnou použily entity, bude se všech tisíc entit přesrávat na pomocné třídy ještě nějakým pochybným AutoMapperem. Veškeré výhody jsou v ten moment ztraceny režií na konverzi.

Jedinou výhodu vidím v možnosti označení vlastností těch pomocných tříd pomocí atributů, které si potom přechroustá UI a zobrazí jak je potřeba.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

1) AutoMapper umí vygenerovat Select nad IQueryable, takže ten SQL dotaz nevybírá 1000 entit, ale rovnou už 1000 výsledných objektů. Navíc si AutoMapper to vše kompiluje do IL při startu aplikace (resp. ve chvíli, kdy mapování zaregistrujete).

2) Jak už jsem psal, občas entity obsahují spoustu zbytečných sloupců, které v UI nepotřebujete, a typicky v UI nezobrazujete data z jedné tabulky, ale potřebujete i tabulky navázané (a z nich také jen typicky pár sloupců, ne všechny). Místo vytahování celých entit s haldou includů se vytahují jen ty sloupce, které potřebujete, protože AutoMapper vygeneruje Select a EF ten Select přeloží do SQL.

3) I kdyby to AutoMapper dělal za běhu přes reflexi, tak čekání na data z databáze jsou řádově milisekundy, kdežto zpracování entit AutoMapperem jsou v řádech nanosekund.

Odekorování vlastností atributy dělají spíše Data Annotations, ty s AutoMapperem nesouvisí, i když je to také užitečná technologie.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Dobrý den, ještě bych měl jednu otázku. Když testuji AutoMapper, tak při prvním dotazu je to opravdu dlouhé a v té samé metodě zavolání toho samého, ta je to již OK. Při použití ve webovém prostředí se po dokončení zpracování stránky vše zahodí. Takže se táži, dá se nějak, nebo jak to funguje, inicializovat dopředně. Možná se špatně vyjadřuji, ale rychlost je dost důležitá a pokud se to inicializuje stále dokola, tak je to nevýkonné.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

To je divné, nevoláte Mapper.CreateMap opakovaně při každém requestu? Případně nepadá vám worker proces vlivem nějaké jiné chyby?

Automapper při prvním použití nějakou režii má, ale pak jsou už všechny operace velmi rychlé.

Jakým kódem přesně mapování provádíte?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

No pravdou je, že jsem to testoval v konzolové aplikaci. Otestuji to v reálném prostředí a uvidím. V jakém místě používáte mapování. A má to fungovat tak, že když zavolám jednou nějaký dotaz, tak když se zeptá úplně jiný request jiného uživatele (session), tak je to už namapované, je to rychlé? No já zkrátka zkusím to použít na reálném webu a uvidím ať nefabuluji.

Moc děkuji.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Mapper.CreateMap by se měl volat v Global.asax, nebo třeba ve statickém konstruktoru nějaké třídy, ve které se to mapování používá - stačí volat jednou, nacacheuje se to pro celou appdomain.

Pak už stačí už jen volat Mapper.Map nebo .Project.To atd. a mělo by to být rychlé.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.
  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback