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ě.

Autor: Aleš Roubíček | Přidej komentář | Delicious | Digg | FriendFeed | Facebook | Linkuj! | Jagg

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í.

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ář