Doctrine 2: Query Builder a nativní SQL

V předchozím dílu jsme si představili dotazovací jazyk DQL (Doctrine Query Language). Při práci s Doctrine 2 nejste ale omezeni pouze na DQL. Dneska si ukážeme další dvě cesty, jak si v Doctrine 2 připravit dotaz do databáze – Query Builder a nativní SQL.

Seriál: Doctrine 2 (12 dílů)

  1. Doctrine 2: úvod do systému 21.7.2010
  2. Doctrine 2: základní definice entit 4.8.2010
  3. Doctrine 2: pokročilá definice entit 11.8.2010
  4. Doctrine 2: načítání, ukládání a mazání 26.8.2010
  5. Doctrine 2:stavy entit a transakce 9.9.2010
  6. Doctrine 2: asociace 23.9.2010
  7. Doctrine 2: práce s asociacemi 14.10.2010
  8. Doctrine 2: DQL 3.11.2010
  9. Doctrine 2: Query Builder a nativní SQL 18.11.2010
  10. Doctrine 2: události 9.12.2010
  11. Doctrine 2: událostní handlery 13.1.2011
  12. Architektura aplikace nad Doctrine 2 23.2.2012

Query Builder

Query Builder je zvláštní třída, která nám umožňuje vytvořit databázový dotaz postupným voláním jejích metod. Jasnější představu si určitě hned uděláte z následujícího příkladu:

$q = $em->createQueryBuilder()
    ->select('a')
    ->from('Article', 'a')
    ->where('a.id = ?1')
    ->orderBy('a.published DESC')
    ->getQuery();

Vytvoření Query Builderu

Rozeberme si to teď postupně podrobněji. Zavoláním $em->createQueryBuilder() získáme od Entity Manageru instanci třídy  DoctrineORMQueryBuilder:

$qb = $em->createQueryBuilder();

Nad touto instancí pak voláme všechny další metody a skládáme tak postupně náš výsledný dotaz. Jednotlivé metody QueryBuilderu nabízejí fluent interface, můžeme je tedy elegantně skládat hned za sebe.

Určení typu dotazu

V první řadě musíme určit, zda chceme provádět SELECT, UPDATE nebo DELETE. V případě SELECTu pak následuje ještě určení hlavní třídy entity přes metodu from(), u zbylých dvou typů dotazu můžeme určit dotčenou entitu rovnou v příslušném volání. Pokaždé pak musíme ještě v duchu DQL ke každé třídě entity přiřadit alias, přes který se na ni dále odkazujeme. Různé možnosti a varianty jsou naznačeny v následujících příkladech:

// Mažeme články
$qb->delete('Article', 'a');
// Aktualizujeme články
$qb->update('Article', 'a');
// Načítáme články
$qb->select('a')->from('Article', 'a');

Prvním zavoláním jedné z metod select(), update() nebo delete() se QueryBuilder interně nastaví do jednoho z těchto tří režimů. Kdykoliv později si můžeme ověřit jeho aktuální režim přes metodu $qb->getType(), která vrací jednu z následujících hodnot:

  • DoctrineORMQueryBuilder::SELECT
  • DoctrineORMQueryBuilder::DELETE
  • DoctrineORMQueryBuilder::UPDATE

Omezení dotazu

V dalším kroku pak můžete dotaz rozšiřovat o další nastavení a omezení. Následuje několik příkladů. Nejprve pro SELECTy přes více entit, kde lze udělat jak INNER JOIN, tak  LEFT OUTER JOIN:

// Výběr článku s kategorií přes left join
$qb->select(array('a', 'c'))
   ->from('Article', 'a')
   ->leftJoin('a.category', 'c');
// Výběr článku s kategorií přes inner join
$qb->select(array('a', 'c'))
   ->from('Article', 'a')
   ->innerJoin('a.category', 'c');

Metody leftJoin() a innerJoin() mají ještě třetí a čtvrtý parametr, přes které lze připojení omezit dalšími dodatečnými podmínkami.

Pro aktualizaci údajů přes UPDATE přiřazujeme jednotlivé hodnoty přes metodu  set():

// Nastavíme titulek článku
$qb->set('a.title', 'Lorem ipsum');
// Nastavíme titulek článku z parametru
$qb->set('a.title', '?1');
$qb->setParameter(1, 'Lorem ipsum');
// Výpočet z dosavadní hodnoty sloupce
$qb->set('a.counter', 'a.counter + 1');

Omezující podmínky lze nejjednodušeji přidat přes metody where(), andWhere()orWhere():

// Podmínka se spojkou AND
$qb->where('a.title = ?1 AND a.published > ?2');
// Totéž s metodou andWhere
$qb->where('a.title = ?1')->andWhere('a.published > ?2');
// Podmínka se spojkou OR
$qb->where('a.title = ?1 OR a.title = ?2');
// Totéž s metodou orWhere
$qb->where('a.title = ?1')->orWhere('a.title = ?2');

Groupování se zajišťuje s pomocí metod groupBy() a addGroupBy(), související HAVING se překvapivě zajišťuje přes having(), andHaving() a orHaving().

V neposlední řadě se řazení výsledků určuje metodami orderBy()addOrderBy():

$qb->orderBy('a.published', 'DESC');
$qb->addOrderBy('a.title');

Zpracování dotazu

Po navěšení všech podmínek musíme z Query Builderu vygenerovat výsledný dotaz. Základní možnost, jak to zajistit, je zavolání metody  $qb->getQuery():

Ta vygeneruje výslednou instanci třídy DoctrineORMQuery, se kterou jsme se potkali už v předchozím dílu seriálu. Výsledkem je tedy úplně totéž, jako po sestavení DQL dotazu.

Variantně můžete místo toho zavolat metodu $qb->getDQL(), která vrátí jako string sestavený příslušný DQL dotaz.

Třída Expr

Pojďme se teď krátce podívat trochu více pod kapotu Query Builderu. Výše jsem ukazoval různé metody volané nad instancí Query Builderu, jako třeba where() nebo orderBy(). Ve skutečnosti jsou to jenom zkratky k složitějšímu, ale mnohem obecnějšímu a flexibilnějšímu řešení. Tím je třída DoctrineORMQueryExpr a navazující dílčí třídy  DoctrineORMQueryExpr*.

Nejlépe se to ilustruje na konkrétním příkladu. Mějme jednoduchý dotaz uvedený už i výše:

$qb->select('a')
   ->from('Article', 'a')
   ->where('a.id = ?1')
   ->orderBy('a.published DESC');

Ve skutečnosti jsou to všechno jen zkratky k obecnějšímu zápisu:

$qb->add('select', new DoctrineORMQueryExprSelect(array('a')))
   ->add('from', new DoctrineORMQueryExprFrom('Array', 'a'))
   ->add('where', new DoctrineORMQueryExprComparison('a.id', '=', '?1'))
   ->add('orderBy', new DoctrineORMQueryExprOrderBy('a.published', 'DESC');

Takové rozložení všech atomických operací do samostatných tříd umožňuje jednak konstruovat výrazně složitější zápisy a podmínky, jednak pak umožňuje definovat si libovolné další vlastní operátory jako prosté potomky abstraktní třídy  DoctrineORMQueryExprBase.

Navíc v Query Builderu jako takovém je definováno jen několik málo základních zkratek, pro spoustu složitějších operací musíte stejně sáhnout přímo po  DoctrineORMQueryExpr*.

Protože je ale poměrně složité a namáhavé vypisovat pokaždé úplně celé new DoctrineORMQueryExpr…, nabízí se místo toho ještě jedna možnost zápisu, a to zkratky zahrnuté ve speciální třídě DoctrineORMQueryExpr. Ta je dostupná mimo jiné z metody $qb->expr(). Poslední ekvivalentní možností zápisu výše uvedeného dotazu tedy je:

$qb->add('select', $qb->expr()->select('a'))
   ->add('from', $qb->expr()->from('Article', 'a'))
   ->add('where', $qb->expr()->eq('a.id', '?1'),
   ->add('orderBy', $qb->expr()->orderBy('a.published', 'DESC'));

Pro popis všech možných metod nabízených ve třídě DoctrineORMQueryExpr doporučuji pročíst příslušnou sekci dokumentace Doctrine 2.

Nativní SQL

Pokud vám nesedí vytváření dotazů přes DQL ani přes Query Builder, můžete se místo toho dotazovat i starým dobrým SQL. Do něj se pak skutečně zadává SQL jako takové, včetně skutečných názvů databázových tabulek, atributů či parametrů, jak jste zvyklí z PDO:

$q = $em->createNativeQuery('
    SELECT id, title, text, category_id
    FROM article WHERE id = ?', $rsm);
$q->setParameter(1, 'Lorem ipsum');
$article = $q->getResult();

Doctrine 2 ale při použití nativního SQL neví zhola nic o tom, které entity mají být na daný výsledek navázané, které jejich členské proměnné mají být naplněné jakými hodnotami apod.

Musíme jí to tedy napovědět a říct, co má na co mapovat a čím má tedy vlastně nakonec v našem případě naplnit výslednou proměnnou $article. Tato informace se předává druhým parametrem metody $em->createNativeQuery() v podobě takzvaného ResultSetMapping.

$rsm = new DoctrineORMQueryResultSetMapping;
$rsm->addEntityResult('Article', 'a');
$rsm->addFieldResult('a', 'id', 'id');
$rsm->addFieldResult('a', 'title', 'title');
$rsm->addFieldResult('a', 'text', 'text');
$rsm->addMetaResult('a', 'category_id', 'category_id');

Na výsledku dotazu výše tak dostaneme do proměnné $article instanci třídy Article naplněnou příslušnými daty.

S pomocí ResultSetMapping lze samozřejmě načítat i složitější struktury více entit najednou:

// vytvoříme mapování
$rsm = new DoctrineORMQueryResultSetMapping;
$rsm->addEntityResult('Article', 'a');
$rsm->addFieldResult('a', 'id', 'id');
$rsm->addFieldResult('a', 'title', 'title');
$rsm->addFieldResult('a', 'text', 'text');
$rsm->addJoinedEntityResult('Category' , 'c', 'a', 'category');
$rsm->addFieldResult('c', 'category_id', 'id');
$rsm->addFieldResult('c', 'category_title', 'title');
// připravíme dotaz
$q = $em->createNativeQuery('
    SELECT article.id, article.title, article.text,
    category.id category_id, category.title category_title
    FROM article
    LEFT OUER JOIN category ON category.id = article.category_id
    WHERE article.id = ?, $rsm);
// přibindujeme parametry
$q->setParameter(1, 123);
// provedeme dotaz a získáme instanci entity článku
$article = $q->getResult();
// přes její getter pak získáme instanci entity kategorie
$category = $article->getCategory();

Pokračování příště

Ať už jsme si dotaz připravili s pomocí DQL, Query Builderu nebo nativního SQL, vždy máme před sebou na konci výsledný query objekt. V příštím díle si ukážeme si všechny možnosti, jak takový dotaz limitovat, provést nad databází a získat z něj požadované výsledky v rozličných podobách.

Provozuje vývojářskou firmu Medio Interactive. Vystudoval informační a znalostní inženýrství na VŠE, kde stále příležitostně přednáší o tvorbě webů.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 6

Přehled komentářů

pribinacik Hmmm
jc Re: Hmmm
Nox Re: Hmmm
Ped Re: Hmmm
Oldis Re: Hmmm
okbob Re: Hmmm
Zdroj: https://www.zdrojak.cz/?p=3370