Přejít k navigační liště

Zdroják » Databáze » Doctrine 2: Query Builder a nativní SQL

Doctrine 2: Query Builder a nativní SQL

Články Databáze, PHP

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.

Nálepky:

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.

Komentáře

Subscribe
Upozornit na
guest
6 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
pribinacik

…nie je nic lepsie ako pouzivat taketo somariny a absolutne odignorovat nieco ako optimalizacie query.

jc

tak myslim, ze se predpoklada, ze kdyz uz to umi nativni sql tak tam bude clovek davat optimalizovany dotazy pokud ty generovany nebudou k tomu (coz pro me zatim vetsinou byly). A mimochodem v dokumentaci doctrine je jedna kapitola o optimalizaci rychlosti a dotazu sama o sobe.

Nox

Když se do ní podíváš, zjistíš že tam je jen rada používat cache, o optimalizaci dotazů tam nic není, stejně tak v Best practices.

Doctrine2 používám (zatim ne v ničem velkým), ale výkon je něco čím si nejsem jistý…

Ped

Pochopil by som tieto nareky napr. pri pouzivani nejakeho ORM v Jave, ale vzhladom na to ze Doctrine je PHP, tak nechapem preco vas zaujima vykon.

Ak to chcete robit optimalne, pouzite C++, alebo Javu (bude to vo vacsine pripadov tak 10-200x rychlejsie, o spotrebe RAM a moznosti udrzovat data perzistentne v RAM a pod asi nema ani vyznam debatovat).
Existuju sice akesi nastroje pre PHP ktore umoznuju niektore veci ciastocne obist a priblizit sa kompilovanym jazykom, ale co som videl, vacsinou sa mensie PHP aplikacie nasadzuju tak ako su, t.j. tak 100x pomalsie nez by bol dedikovany fastCGI modul napisany v C++. A funguje to aj tak.

Oldis

a stejne se da php kod napsat tak neefektivne, ze je 100x pomalejsi nez kod napsany efektivne, stejne tak jako se necha debilne napsat kod v c++ nebo jave. Proto ty reci o optimalizaci smysl maji. A preci jen bavime se tu o php, takze reci o tom ze je vzhledem k c++ neefektivni sou v tomhle pripade flame.

okbob

V případě aplikací postavených nad databází nehraje roli (minimálně ne tak zásadní) v čem je napsaný klient – zda-li je to C, PHP, Java nebo Python. Z hlediska provozu (zvlášť při trochu větší zátěži) je zásadní jaké SQL dotazy jdou do db, a co (jaký objem) jde z db. Rozhodně není pravda, že by se reálné aplikace zrychlily např. přepisem z PHP do C 200x. Všechny kritické funkce jsou dávno implementované v C. Vámi uváděné zrychlení je možné pouze v extrémně špatně navržené, napsané aplikaci. V reálných dobře navržených aplikacích je výkonostním hrdlem čtení z disku (zápis na disk), takže zásadní není jestli je aplikace napsaná v C nebo PHP, ale zda-li se čte minimum dat z disku nebo zda-li se potřebná data dohledají na v cache.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.