Klávesové zkratky na tomto webu - rozšířené Na obsah stránky

Doménové dotazy

13.53 - 30. ledna 2010 | ASP.NET 2.0

V každé aplikaci, kde se pracuje s daty, se dostaneme do situace, kdy se nad daty potřebujeme dotazovat. Pokud přijmeme za svůj vzor Repository, můžeme se dostat do situace, podobné té v následující ukázce:

public interface IArticlesRepository {
  Article FindById(long id);
  IEnumerable<Article> FindAll(PagingInfo paging);
  IEnumerable<Article> FindByCategory(Category category, PagingInfo paging);
  IEnumerable<Article> FindByAuthor(User author, PagingInfo paging);
  IEnumerable<Article> FindByTag(string tag, PagingInfo paging);
}

Jak vidno s každým typem dotazu, rozšiřujeme rozhraní naší repository. Což porušuje Open/closed principle – jeden z pilířů Solidní architektury. Ten nám říká, že objekt by měl být otevřený k rozšíření, ale uzavřený k modifikaci. Přidáváním dalších metod ho očividně rozšiřujeme. Jak na to?

Jedním z možných řešení je zavedení dotazovacích objektů, kde využijeme kompozice k docílení lepšího návrhu. A protože jde o obecný mechanismus, můžeme ho pěkně generalizovat. Dále budu předpokládat, že pracujeme vůči datovému zdroji podporujícímu LINQ dotazování, jako je NHibernate.Linq, LINQ2SQL nebo EntityFramework.

public interface IQueryObject<T> {
  int Count(IQueryable<T> table);
  IEnumerable<T> Fetch(IQueryable<T> table);
  T FetchOne(IQueryable<T> table);
}

Dotazovací objekt má tři metody. První spočítá kolik objektů splňuje podmínky dotazu. Druhá vrátí kolekci vybraných objektů a poslední vrací jedinečný záznam. K takovému rozhraní si můžeme vytvořit bázovou třídu, která nám pomůže s rychlejším vytvářením konkrétních dotazů.

public abstract class QueryObjectBase<T> : IQueryObject<T> {

  protected QueryObjectBase<T>(PagingInfo paging) {
    Paging = paging;
  }

  public PagingInfo Paging { get; private set; }

  protected abstract IQueryable<T> CreateQuery(IQueryable<T> table);

  public virtual int Count(IQueryable<T> table) {
    return CreateQuery(table).Count();
  }

  public virtual IEnumerable<T> Fetch(IQueryable<T> table) {
    return CreateQuery(table).
      Skip(Paging.Skip).
      Take(Paging.PageSize).
      ToArray();
  }

  public virtual T FetchOne(IQueryable<T> table) {
    return CreateQuery(table).FirstOrDefault();
  }
}

Konkrétní dotazovací objekt pak ve většině případů implementuje jedinou metodu, která vrací LINQ dotaz. V případě potřeby si však konkrétní implementace může připravené metody změnit k obrazu svému. Máme zde zavedenou i konvenci stránkování, která předchází generování neomezených dotazů do databáze. A teď už nám zbývá jen zabalit to pěkně do nějakého DAO objektu.

public interface IQueryExecutor<T> {
  int Count(IQueryObject<T> query);
  IEnumerable<T> Fetch(IQueryObject<T> query);
  T FetchOne(IQueryObject<T> query);
}

DAO objekt jediný zná konkrétní implementaci datového uložiště, kterou předává do dotazovacího objektu jako IQueryable<T>. Výsledné použití v praxi může pak vypadat následovně:

var articles = _articlesDao.Fetch(new CategoryArticlesQuery(category, pagingInfo));

Kde konkrétní implementace dotazovacího objektu vypadá takto:

public class CategoryArticlesQuery : QueryObjectBase<Article> {

  public CategoryArticlesQuery(Category category, PagingInfo paging)
    : base(paging) {
    Category = category;
  }

  public Category Category { get; private set; }

  protected override IQueryble<Article> CreateQuery(IQueryable<Article> table) {
    return from article in table
           where article.Category == Category
              && article.IsPublished
           orderby article.PublishDate descending
           select article;
  }
}

Výhodou i je, že pokud potřebujeme upravit dotaz, děláme to vždy jen jednou na jednom místě.

Komentáře RSS

  1.  

    Steve

    12.00 - 31. ledna 2010 | #

    Vypadá to hezky, díky práci nad IQueryable<> není implementace Query objektu závislá na použitém ORM a mohli bychom teoreticky Query objekty implementovat i v rámci aplikační vrstvy/fasády pro ní.

    S čím bych měl možná u takového řešení problém je, že IQueryable<> jako zdroj pro query object podle mě nemusí stačit (nebo ano?), někdy by člověk chtěl udělat takový dotaz, který NHibernate.Linq (resp. Criteria API) rozvine v nějaké strašné SQL, ručně v raw SQL, někdy by člověk chtěl využít specifik dané databáze jako třeba hierarchy id…

    Možností by bylo jako parametr pro Fetch nabídnou ISession, ale to zase ztrácíme nezávislost na ORM/databázi…

    mimochodem bitva repository/DA­O/Query objects je trefně popsaná na nhforge

  2.  

    Aleš Roubíček

    13.16 - 31. ledna 2010 | #

    [1] Steve: Použití IQueryable je specifické pro tento příklad, klidně na vstupu může být ISession nebo DbConnection to už je implementační detail. :)

    Strašnému SQL generovanému ORM nástrojem se dá předejít správným mapováním. K tomu nám může pomoci např. NHProf nebo podobný nástroj.

    Nezávislost na ORM je ve většině případů zcela zbytečná. Jen málo aplikací bude potřebovat nezávislost na ORM, či konkrétním DB stroji.

  3.  

    Augi

    17.36 - 31. ledna 2010 | #

    A ten IQueryExecutor používáš jako generickou náhradu Repository?

  4.  

    Steve

    18.00 - 31. ledna 2010 | #

    [2] Aleš Roubíček: Když ale použiji ISession (DbConnection), tak bych měl mít, podle mého názoru, Query objekty v datové vrstvě, potom ale vznikne přímá závistost aplikační vrstvy na té datové (narozdíl od DAO nebo repository objektů-k těm přistupuji přes interface).

    Myšlenka Query objektů se mi líbí, ale s tahle závislost mi trochu vadí a zajímá mne, jestli je nějak řešitelná… Možná je problém ve snaze o „čistotu“ návrhu za každou cenu a přitom by mohlo být efektivní udělat občas takový malý ústupek…

  5.  

    Aleš Roubíček

    21.30 - 31. ledna 2010 | #

    [3] Augi: U mne je IRepository<T> zdrojem dodávajícím IQueryable<T> pro IQueryExecutor<T>. Což neni úplně čisté řešení, ale je dané historicky evolucí projektu. :)

    [4] Steve: Otázka je, co považuješ za datovou vrstvu. Pro někoho to je databáze, pro jiného ADO.NET, pro dalšího ORM Framework… Osobně jsem na úrovni abstrakce s IQueryable. Vzhledem k tomu, že používám Castle ActiveRecord, je mapování součástí mého doménového modelu, který prosakuje i do views. Žádný problém s tím nemám. Ani problémy s čistotou. Spíš bych pak měl problém s překompikova­ností.

  6.  

    Besnik

    14.56 - 13. září 2010 | #

    Zdar, tento problem sa snazim riesit cez generic repository a specification pattern, nieco o tom som popisal tu http://besnikgeek.blogspot.com/…s-query.html.

    V zmysle OCP data z repozitara tahas takto (priklad z http://code.google.com/…crepository/):

    ICustomerRepository
    customerRepository =
    this.IoC.Resol­ve<ICustomerRe­pository>(uni­tOfWork, specification­Locator);

    IList<Customer> = customerReposi­tory.Specify<I­CustomerSpeci­fication>()
    .NameStartsWit­h("Ales")
    .OlderThan(18)
    .ToResult()
    .Take(3)
    .ToList();
  7.  

    Augi

    16.56 - 2. února 2011 | #

    [4]Aleš to má jednoduchý, protože používá IQueryable, které si může dovolit pronést skrze všechny vrstvy.

    Pokud ale chceme pracovat jinak (ne s IQueryable), tak je IMHO potřeba důsledně oddělit „popis dotazu“ (ten je v doménové/apli­kační vrstvě) a „vykonání dotazu“ – to je úkol infrastrukturního kódu (tedy repository), který musí umět dotaz správně interpretovat (tj. převést ho do správného LINQ dotazu, SQL dotazu apod.). Za takový obecný popis dotazu lze považovat expression trees, čímž se dostaneme jen o jednu úroveň níž než je IQueryable.

    Taková repository by měla interface, jehož zásadní metoda by vypadala takto:

    IEnumerable<TRe­sult> Fetch<TResult>(Ex­pression<Func<I­Queryable<T>, IQueryable<TRe­sult>>> query);

  8.  

    Tim

    09.41 - 14. prosince 2011 | #

    Ahoj, doménové dotazy se mi moc líbí. Nemůžu se ale nějak vypořádat s tím, že na rozdíl od použití repository, tak jak je nahoře, musím do Query vytáhnout všechny „tabuky“ (IQueryable objekty), nad kterými stavím dotaz. Primárně by mi to nevadilo a těžko bych asi psal dotaz, když by nebylo nad čím ho psát. Otázkou je, jak je mám do toho Query dostat?

    Pokud například potřebuji QueryObjekt, který by z knihovny vyndal seznam knih od anglického autora, mohlo by to vypadat následovně?

    class BooksByAuthorNationalityQuery {
    public virtual IEnumerable<Book> Fetch(IQueryable<Book> bookTable, IQueryable<AuthorOfBook> authorOfBookTable, IQueryable<Author> AuthorTable) { // return fetch z CreateQuery: // join tabulek, where podle nationality, select Book. } }

    Pro tenhle QueryObjekt, už ale nepoužiji IQueryExecutor a musím vytvořit extra IBookAndAutor­QueryExecutor. Ve finále se mi pak repository, tak jak je nahoře rozpadne na Query objekty a ne o mnoho méně „specializovaných“ QueryExecutor oběktů. To ale můžu rovnou udělat „specializované“ repository IBookByAuthor­NationalityRe­pository s metodou Fetch().

    Mám takový pocit, že tohle už někdo musel řešit a že mi něco uniká. Děkuji za nakopnutí správným směrem:)

  9.  

    Aleš Roubíček

    12.08 - 17. prosince 2011 | #

    Přemýšlíš příliš relačně. Ve většině případů, bude mít objekt Book vazbu na Autora a z něj můžeš skládat dotaz bez problémů. Vždy záleží na tom, jak máš namodelovaný objektový model.

  10.  

    Ben

    09.25 - 22. prosince 2011 | #

    Mohl bys zde dat i implementaci IQueryExecutor pro výše popsány Article, prosím?

  11.  

    Aleš Roubíček

    09.45 - 22. prosince 2011 | #

    [10] Implementace je závislá na konkrétní použité technologii. Tady ukázka z hlavy pro EF:

    public class ArticlesQueryExecutor : IQueryExecutor<Article> {
      readonly DbContext dbContext;
    
      public ArticlesQueryExecutor(DbContext dbContext) {
        this.dbContext = dbContext;
      }
    
      public int Count(IQueryObject<Article> query) {
        return query.Count(dbContext.Set<Article>());
      }
    
      public IEnumerable<Article> Fetch(IQueryObject<Article> query) {
        return query.Fetch(dbContext.Set<Article>());
      }
    
      public Article FetchOne(IQueryObject<Article> query) {
        return query.FetchOne(dbContext.Set<Article>());
      }

    Jak vidíš, je to opravdu jednoduchý kód.

  12.  

    Ben

    12.36 - 22. prosince 2011 | #

    [11] diky moc! Jen mi tu schází jedna věc – pokud se nejedna o web aplikaci, kde životnost dbcontextu je dana nejakym injektorem – kde uzavírám připojeni na db?

  13.  

    Aleš Roubíček

    13.20 - 22. prosince 2011 | #

    To záleží v jakém scope připojení otevíráš. Injektovat můžeš i v newebových aplikacích.

  14.  

    Ben

    16.01 - 22. prosince 2011 | #

    [13] Ad scope) U web aplikací je to snadné, scope je dán requestem. U windows services +/- taky, ale u standardní Windows aplikace je to složitější. Taky se mi moc nelíbí, že aplikační vrstva je „zaprasena“ znalostí DbContextu (v tomto případě). Sám jsem to řešil tak, že jsem pracoval s repository vždy v rámci sekce using: using (var repository = new ArticleRepository) {…}, kde v metodě Dispose() bylo uzavření spojení s DB.

    Jinak ještě jeden dotaz – kde jsou metody pro vytvoření, update a smazání objektu?

  15.  

    Aleš Roubíček

    08.31 - 23. prosince 2011 | #

    Vytváření, update a mazání nemaj s dotazováním nic společnýho. To je zodpovědnost Unit of Work. Tj. v případě EF DbContextu.

Místo pro tvůj názor

Povinné je jméno a komentář, z e-mailu se rozpoznají Gravatary.
Komentář je formátován pomocí Texy! syntaxu.
Například: **tučný text**, *kurzíva*, "text odkazu":adresa.
Internetové adresy jsou převáděny na odkazy.
Na komentáře se můžete odkazovat pomocí [číslo komentáře].

Nový komentář