NOVINKA! E-learningové kurzy umělé inteligence. Nyní AI za nejlepší ceny. Zjisti více:
NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!
Avatar
Petr Nymsa
Tvůrce
Avatar
Petr Nymsa:15.9.2017 13:21

Ahoj, navazuju na předchozí dotaz zde.

Už několikátý den si hraju s různými návrhy a na internetu samozřejmě nacházím všechny-možné scénáře a doporučení.

Přepisujeme už celkem velký projekt a chceme projekt nyní navrhnout s rozumnější architekturou -> chceme, aby to bylo rozšiřitelné, ale nechceme z toho dělat over-kill.
Databáze má několik desítek tabulek a UI aplikace (WPF) má několik desítek formulářů, nastavení apod.

Přišel jsem s návrhem zhruba takto:
Peristent vrstva -> EF a nad tím Repository
Servisní vrstva -> servisy nad repository a WCF
Klient -> komunikace skrz WCF.

Cílem je oddělit a zastínit klienta od konkrétní prací s databázi apod.
Vytvořil jsem tedy Repository, které budou operovat nad jednotlivými entitami.

public class Author
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
 ......
 public interface IRepository<T> where T:class
    {
        T GetById(int id);
        IList<T> GetAll();
        void Add(T entity);
... // další operace Find, Remove,...
public class BaseRepository<T> : IRepository<T> where T : class
    {
        private Context _dbContext;
        private readonly IDbContextFactory _dbFactory;


        protected Context  DbContext => _dbContext ?? (_dbContext = _dbFactory.Init());

        public BaseRepository(IDbContextFactory contextFactory)
        {
            _dbFactory = contextFactory;
        }

        public virtual void Add(T entity)
        {
            DbContext.Set<T>().Add(entity);
        }
.... // implementace IRepository

Unit of work -> pro práci s více repozitáři, abych udržel stejný kontext

public class UnitOfWork : IUnitOfWork
  {
      private Context _dbContext;
      private readonly IDbContextFactory _dbFactory;

      protected Context DbContext => _dbContext ?? (_dbContext = _dbFactory.Init());

      public UnitOfWork(IDbContextFactory dbFactory)
      {
          this._dbFactory = dbFactory;
      }

      public void SaveChanges()
      {
          DbContext.SaveChanges();
      }
  }

Konkrétní implementace Repository nad entitou Author

public interface IAuthorRepository : IRepository<Author>
 {
     IList<Author> GetLiveAuthors();
 }

Použití

using(var dbFactory = new DbContextFactory())
{
   var uow = new UnitOfWork(dbFactory);
   var authorRepo = new AuthorRepository(dbFactory);
 // prace s repository - add, delete
 uow.SaveChanges();
}

Takto to "nějak" funguje, ale nezdá se mi to jako nejideálnější. Repository vrací entity z DB, tj ještě mezivrstva mezi přemapuje entitu na DTO.
S tím mi ale vyvstala další otázka (která podle internetu se řeší taky dost), kde přemapování provádět?
Ve vyšší vrstvě
To se mi zdá ok, až na to, že pokud budu chtít dělat složitější dotaz přes více tabulek, kde dohromady natahám 100 sloupců dat a přitom můj dotaz potřebuju jen na menší množství sloupců (např. detail o objednávce pouze datum vystavění a cenu), tak zbytečně skrz repository složím dotaz, ze které už in-memory vyfiltruju ve vyšší vrstvě data které potřebuju.
Převádět rovnou v repository
To mi dovolí skládat výše složitější dotaz skrz select. Rozhraní repozitářů tedy bude přiímat a vracet DTO. Ale to se mi opět taky nezdá. Pokud bude chtít udělat update, budu si muset natáhnout podle ID objekt, vrátí mi jej jako DTO, změním vlastnosti které pořebuju, vrátím do repozitory, v ní se přemapuje na entitu, atacch a update.
Vracet z repozitory Iqueryable
A ve vyšší vrstvě doladit dotaz a tam převod na DTO a poslat ke klientovi?

Mám v tom hokej :) Co je vhodnější? Má tenhle směr vůbec smysl? Nebo je to celé nepoužitelné?

Díky za nějaké rady, návrhy.

Odpovědět
15.9.2017 13:21
Pokrok nezastavíš, neusni a jdi s ním vpřed
Avatar
zelvicek
Člen
Avatar
zelvicek:15.9.2017 18:27

Podobné problémy jsem řešil v ASP.NET Core aplikaci. Velkou výhodou byla přítomnost IServiceCollection (pro konfiguraci) a DI (pro běh aplikace).
Ale něco podobného se dá udělat i v .NET FW pomocí UnityContainer.


Takže na každou drobnou (atomickou) functionalitu jsem udělal interface - na repositories, mappery, connectory na další služby, ...
Př.:
services.AddSco­ped<IAuthoriza­tionService,Au­thorizationSer­vice>();
services.AddSin­gleton<IUserAc­cessCache,Memo­ryUserAccessCache>();
services.AddMap­perSingleton<IS­tockItemMapper­FullOut, StockItemMapper­FullOut>();
services.AddMap­perGenerate<I­UserAccountRe­gisterMapperIn>();
services.AddSco­ped(sp => new XXXDbContext(Con­figuration["da­ta:XXXDbConnec­tionString"]));
services.AddSco­ped(sp => new YYYDbContext(Con­figuration["da­ta:YYYDbConnec­tionString"]));
services.AddSco­ped<IStockItem­Repository, SpecialStockI­temRepository>();


Všechny implementace těchto interfaců (dále jen služeb) vlastně tvoří "tělo" aplikace.
Každý, kdo potřebuje zajistit nějakou prácičku si z DI vyžádá službu, která tu prácičku zajistí.
Př.:
Controller potřebuje načíst data, z DI containeru získá repository a ten zajistí načtení dat; controller nezajímá, jak to zajistí.
Repository potřebuje DbContext, tak si o něj požádá DI.


Přechod dat mezi dvěmi vrstvami musí být zajištěny pomocí kontraktů - tříd, je mají takovou strukturu, aby pojmuly všechna data ve formě, která vyhovuje oběma vrstvám.
Př.:
Repository má poskytnout data. Pokud ta data získá přímo z DB, bod pro něj prostě to rovnou vrátí. Pokud ten repozitory získá data z DB v jiné strukruře, musí to mapovat.
Př.:
interface IUserRepository
{
IUserInfo GetUserInfo(int idUser);
}

interface IUserInfo
{
string Name {get;}
IEnumerable<string> PhoneNumbers {get;}
}

public class RepoSample1:I­UserRepository
{
public IUserInfo GetUserInfo(int idUser)
{
//celé to psát nebudu, ale přestav si jednoduchou tabulku (Id (int),Name (nvarchar),Pho­neNumbers (nvarchar))
//PhoneNumbers se samozřejmě musí splitovat - nejspíš na staně aplikace po načtení z EF
}
}

public class RepoSample2:I­UserRepository
{
public IUserInfo GetUserInfo(int idUser)
{
//celé to psát nebudu, ale přestav si jednoduché tabulky (Id (int),Name (nvarchar)) a další (Id (int),IdParent (int),PhoneNumber (nvarchar))
//PhoneNumbers se samozřejmě nebudou splitovat
}
}

Výhodou tohoto je jistá imunita vůči struktuře DB.

public class RepoSample3:I­UserRepository
{
public IUserInfo GetUserInfo(int idUser)
{
//získá data třeba z WCF
}
}


Jesliže má controller poslat data dále (na UI) nejspíš musí data mappovat. Na to si z DI containeru vyžádá mapper.


Mappery: ručo nebo auto
Přiznám se bez mučení, že nevím. Já jsem zvolil obé. Většinu jsem se snažil hodit na automat a zbytek ručně. Tím automatem mám na mysli vlastí runtime kompiler - takže mám pod kontrolou, jak pracuje.
př.:
services.AddMap­perSingleton<IS­tockItemMapper­FullOut, StockItemMapper­FullOut>(); //registrace mapperu s ručně napsanou implementací
services.AddMap­perGenerate<I­UserAccountRe­gisterMapperIn>(); //registrace mapperu se strojově vygenerovanou implementací (za běhu aplikace)

 
Nahoru Odpovědět
15.9.2017 18:27
Avatar
Odpovídá na Petr Nymsa
Marian Benčat:15.9.2017 20:45

Přemýšlel jsem, že bych to nejdříve napsal bodově.. ale asi nejdříve něco napíšu jako shrnutí.. Zvolil jste anemický model, u něj je problém ten, že by jste u něj měl postupovat neobjektově = vysr.. vykašlat se na zbytečné abstrakce, které stejně NEFUNGUJÍ ... proč nefungují?

  • už v IRepository máte několik chyb , například to, že máte primary key na tvrdku jako INT, už todle je leaky abstrakce, dále máte v 1 jediném interfacu narvané všechny metody, co když to nebude podporovat?
  • GetAll() ?? to snad ne. GetAll si muzete dovolit u domenoveho modelu, kde vite co tam prijde za data, urcite ale ne u Interfacu dalu,.. nebo opravdu chcete tahat desetitisíce řádků z databáze a poté je filtrovat in memory? :-)

Povím vám už teď příběh o naivním programátorovi, který chtěl udělat dobrou věc. Vadilo mu, že by měl mít v Core vrstvě reference na Entity Framework, tak si řekl, že "o-abstrahuje DAL" a vytvori si BaseRepository (Presto, ze DBSet je repository), vytvori si UOW (DbContext je UOW také) .. a začne nad tím vším vyvíjet software.. po krátkém čase však zjistí, že je to pro něj absolutně nepoužitelné z několika důvodů¨:

  • chybí mu v IRepository funkcionalita
  • ztrácí ohromné množství výkonu a je to pomalé
  • není schopný to paralelizovat

A je to jeho chyba? Byl pouze naivní,.. myslel si, že jak ho OOP učí, lze zcela odstínit od reality (v tomto případě, že je pod tím nějaká databáze, že má určité technické omezení atp.)


Vaše implementace UOW je také dosti omezující.. Absolutně vám zlikviduje například nějakou kompozitni akci..
Co když budete mít v Servise MetoduA a MetoduB - kazda bude mit uvnitr tento UOW, ale tyto dve metody budete chtit volat z metody MetodaC a budete chtit s temi metodami sdilet 1 UOW? :-) Jste nahraný... a proč potřebujete sdílet UOW? protože UOW u vás = dbcontext. DbContext má v sobě IdentityMap.. Zkuste si schválně vytvořit dvě instance toho DbContextu, ziskat si entitu z 1. a zkusit ji dat jako update do 2... Entity Framework vam povi, ze tu entitu nema v identity mapě.. Samozřejmě.. je možné ji ručně attachnout .. a co se může stát? :-) Že budete chtít udělat UPDATE, ale ono to udělá INSERT a spadne to na duplicate key.


::::::::::NEVER EVER ::::::::::::: nevracejte do vyšší vrstvy IQueryable. Prostě to nedělejte. Nebo se zblázníte -- pokud vám to není jasné, můžu vysvětlit proč..


Za mě tedy máte vesměs tři možnosti:

  1. Pokračovat v anemickém modelu, sledovat místní seriál o eshopu (nebo o čem to bylo) a nařezat si tam tunu průšvihů
  2. Pokračovat v anemnickém modelu a vykašlat se na abstrakci abstrakce, která stejně nebude fungovat, protože jsou tam příliš siliné závislosti na "low levlu", vykašlete se naprosto na nějaký omyly typu GenericRepository a proste si do DALu napiste normalne dotazy:
NecoRepository() {
  List<NejakyModelBlaBlaBla> GetAllcustomersOlderThan(int age) {
     return context.Customers.Where(....x.age>age).Select(x => ...).ToList();
  }
}
  1. Udělejte to skutečně OOP, coz ve sve podstate znamena.
Nahoru Odpovědět
15.9.2017 20:45
Totalitní admini..
Avatar
Odpovídá na Petr Nymsa
Marian Benčat:16.9.2017 10:12

Jinak.. IQueryable pouzit muzes.. idealnipouziti jsou extensionmetody v DALunad IQueryable.. lze sipakvelmi efektivne interfacem oanotovat jednotlive entity a usetri ti to spoustu prace... ..

EDIT: a koukam, ze mi asi odchazi mezernik.

Editováno 16.9.2017 10:15
Nahoru Odpovědět
16.9.2017 10:12
Totalitní admini..
Avatar
Petr Nymsa
Tvůrce
Avatar
Odpovídá na Marian Benčat
Petr Nymsa:16.9.2017 11:42

Díky za odpověď. Sám jsem měl pocit, že to zbytečně zesložiťuju. Scénář, kdy si nad contextem postavím "servisy", tj obalím query nad db do metod a budu z nich vracet DTO pro vyšší vrstvy (WCF -> klient -> ..) je ok?

Nahoru Odpovědět
16.9.2017 11:42
Pokrok nezastavíš, neusni a jdi s ním vpřed
Avatar
Odpovídá na Petr Nymsa
Marian Benčat:16.9.2017 13:29

TO NEDOPORUČUJI. ZA mě osobně si klidně vytvořte v DALu repository, nevymyslejte ale nad ním žádné zbytečnosti =nedejte ho genericky atp. Prostě class kde budete mít všechny metody co budou upravovat a vracet data.. S tim že je budou hned vracet jako PDO pro vyšší vrstvu. Zabalit tuto Dal logiku do něčeho je důležité právě proto, aby jste při problémech s výkonem mohl vnitřek změnit třeba z EF na SQL.

Nahoru Odpovědět
16.9.2017 13:29
Totalitní admini..
Avatar
Petr Nymsa
Tvůrce
Avatar
Odpovídá na Marian Benčat
Petr Nymsa:16.9.2017 13:40

A WCF bude volat přímo repozitory nebo ještě nějaká mezi-vrstva? Pokud nějaká mezi-vrstva, tak mi přijde, že v ní budu dělat jen to samé. Jen vnitřně zavolám akorát Repository. Nebo opět něco vynechávám a nevidím do toho :D díky moc ;)

Nahoru Odpovědět
16.9.2017 13:40
Pokrok nezastavíš, neusni a jdi s ním vpřed
Avatar
Odpovídá na Petr Nymsa
Marian Benčat:16.9.2017 13:47

Přesně jste to řekl. Pokud máte anemicky model, tak je většinou ve vrstvě service pouze to samé co v Repository + třeba v ní kontrolujete práva, řešíte modifikace atp.

Akceptované řešení
+20 Zkušeností
+2,50 Kč
Řešení problému
Nahoru Odpovědět
16.9.2017 13:47
Totalitní admini..
Avatar
Petr Nymsa
Tvůrce
Avatar
Petr Nymsa:16.9.2017 13:51

Supr díky :)

Nahoru Odpovědět
16.9.2017 13:51
Pokrok nezastavíš, neusni a jdi s ním vpřed
Avatar
Odpovídá na Petr Nymsa
Marian Benčat:16.9.2017 13:56

Zadejte si na Yt moje jméno, najdete tam přednášku z jedné googlacke akce o programovacích paradigmatech, bohužel to Muselo být hodne povrchně, ale pointu pochytite.

Nahoru Odpovědět
16.9.2017 13:56
Totalitní admini..
Avatar
Petr Nymsa
Tvůrce
Avatar
Odpovídá na Marian Benčat
Petr Nymsa:16.9.2017 13:59

Na tu už jsem koukal. A pokud nevadí, můžeme si tykat, tady na itn to je běžný.

Nahoru Odpovědět
16.9.2017 13:59
Pokrok nezastavíš, neusni a jdi s ním vpřed
Avatar
Odpovídá na Petr Nymsa
Marian Benčat:16.9.2017 14:28

Jasny, bro :))

Nahoru Odpovědět
16.9.2017 14:28
Totalitní admini..
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 12 zpráv z 12.