Spojování tabulek v NotORM

Školení, která pořádám

V NotORM se poměrně krkolomně provádí operace, které jdou přes více tabulek. Obvykle jsou sice možné, ale dá to trochu práce a nezvyklého přemýšlení. Vezměme si třeba, že bychom chtěli vypsat všechny aplikace setříděné podle jména autora. Já bych to udělal takhle:

<?php
foreach ($db->author()->order("name") as $author) {
    foreach ($author->application() as $application) {
        echo "$author[name]: $application[title]\n";
    }
}
?>

Položené dotazy (se zapnutou cache):

SELECT id, name FROM author ORDER BY name;
SELECT id, author_id, title FROM application WHERE (application.author_id IN ('12', '11'));

Pokud bychom ale do dotazu chtěli přidat nějakou podmínku z tabulky application, tak se dotaz dost zkomplikuje (pokud zbytečně nechceme procházet i autory vyřazených aplikací).

<?php
$authors = $db->application("slogan LIKE ?", "Database%")->select("author_id");
foreach ($db->author("id", $authors)->order("name") as $author) {
    foreach ($author->application("slogan LIKE ?", "Database%") as $application) {
        echo "$author[name]: $application[title]\n";
    }
}
?>

Položené dotazy:

-- v MySQL
SELECT author_id FROM application WHERE (slogan LIKE ?);
SELECT id, name FROM author WHERE (id IN ('11', '12')) ORDER BY name;
SELECT id, author_id, title FROM application WHERE (application.author_id IN ('12', '11')) AND (slogan LIKE ?);

-- v jiných databázích
SELECT id, name FROM author WHERE (id IN (SELECT author_id FROM application WHERE (slogan LIKE ?))) ORDER BY name;
SELECT id, author_id, title FROM application WHERE (application.author_id IN ('12', '11')) AND (slogan LIKE ?);

Omezit počet vrácených záznamů by tímto způsobem nešlo už vůbec.

Pro tyto úlohy se prostě nejlíp hodí spojování tabulek. Ladislav Ševcůj mi poslal patch, který tuto možnost doplňuje, já jsem ale NotORM napsal proto, abych už nikdy žádný JOIN napsat nemusel, i když bych k tomu měl příjemnější API.

Zamyslel jsem se proto nad tím, jaké API bych si pro řešení této úlohy představoval, a dospěl jsem k tomuto:

<?php
foreach ($db->application("slogan LIKE ?", "Database%")->order("author.name") as $application) {
    echo $application->author["name"] . ": $application[title]\n";
}
?>

A to je vše, pohodlnější to být už nemůže. Cizí tabulky se rozpoznávají kdekoliv v dotazu. Položené dotazy:

SELECT application.id, application.author_id, application.title FROM application LEFT JOIN author ON application.author_id = author.id WHERE (slogan LIKE ?) ORDER BY author.name;
SELECT id, name FROM author WHERE (author.id IN ('12', '11'));

Pokud by nám nevadilo, že se jméno autora bude přenášet opakovaně (to v tomto případě asi skutečně nevadí), tak můžeme jeden dotaz ušetřit:

<?php
foreach ($db->application("slogan LIKE ?", "Database%")->select("author.name, application.title")->order("author.name") as $application) {
    echo "$application[name]: $application[title]\n";
}
?>

Položený dotaz:

SELECT author.name, application.title
FROM application
LEFT JOIN author ON application.author_id = author.id
WHERE (slogan LIKE ?)
ORDER BY author.name;
Jakub Vrána, Seznámení s oblastí, 8.11.2010, diskuse: 32 (nové: 0)

Diskuse

ikona Pekelník:

Mohl bys, prosím, doplnit SQL dotaz(y), které tohle provede?

ikona Jakub Vrána OpenID:

Dobrý nápad, doplnil jsem to.

Franta:

Ten první výpis SQL (dva dotazy) patří k tomu prvnímu výpisu PHP? Nějak to nechápu – vidím tam dva cykly, kolik prvků má $author? Dva? Jak může program při prvním průchodu vnějším cyklem vědět, že budou ještě další autoři a že se má nakonec v SQL objevit IN ('12', '11')? Přijde mi, že v tom článku něco chybí. Co tedy znamená „zapnout cache“?

ikona Jakub Vrána OpenID:

Přečti si nejprve http://php.vrana.cz/notorm.php. Pak ti snad všechno bude jasné.

Franta:

Tak nějak jsem to čekal – při prvním průchodu cyklem se získají data ke všem dalším prvkům (pro další průchody cyklem). Tenhle styl mi zrovna nesedne (zavolání metody, resp. zjištění vlastnosti, jednoho objektu způsobí načtení dat úplně jiných objektů). Ale už nebudu rýpat, nepoužívám to, ptal jsem se jen tak ze zvědavosti :-)

ikona Jakub Vrána OpenID:

Tento styl se využije v naprosté většině případů, u webových aplikací prakticky vždy – když chci zjistit detail jednoho objektu v cyklu, budu nejspíš chtít zjistit i detail všech ostatních. Pokud bych to ve výjimečném případě nepotřeboval (třeba bych chtěl seznam všech aplikací, ale jen u první bych chtěl jejího autora), dá se to celkem snadno obejít:

<?php
foreach ($db->application() as $application) {
    if (
$first) {
       
$author = $db->author[$application["author_id"]];
    }
}
?>

Ale ten případ, kdy budu potřebovat všechno, je o tolik běžnější, že pohodlnější syntaxe patří jednoznačně jemu.

Miroslav Hruška:

Mohu se zeptat, zda lze NotORM využít při spojení dvou databází? Díky.

ikona Jakub Vrána OpenID:

Asi by to nějak šlo, ale s takovou možností nepočítám. Na co by to mělo být potřeba?

Miroslav Hruška:

Potřebuji spravovat 3 projekty s jedním administračním rozhraním a pro rozdělení výkonu bych to rád postavil na různých databázích.

Miroslav Hruška:

Tak nakonec to asi vyřeším replikací, ale mám ještě jeden dotaz. Lze nějak pracovat s prefixy tabulek? Díky.

ikona Jakub Vrána OpenID:

Já prefixy tabulek nesnáším, ale mělo by to jít dvěma způsoby:

1. Definicí vlastní struktury (třída implementující NotORM_Structure) – to považuji za vhodný způsob. http://www.notorm.com/#api

2. Psaním kódu v podobě $notORM->{$prefix . "tab"}().

Tharos:

Jakube, je možné nějak rozumně pracovat s více než dvěmi tabulkami? Mám následující strukturu:

• user
id
username

• companies
id
users_id

• projects
id
companies_id

• log
id
projects_id
date

A potřebuji následující: vypsat všechny položky z tabulky log, které patří k určitému uživateli (restrikce podle users.username) a které jsou dále limitovány určitým datem (log.date) - podle něj také mají být seřazené. Opravdu jsem nenašel hezký způsob, jak tohle v NotORMu napsat. Předem díky za pomoc.

ikona Jakub Vrána OpenID:

Jde to takhle:

<?php
$users
= $db->user("username", $username);
$companies = $db->companies("users_id", $users);
$projects = $db->projects("companies_id", $companies);
$db->log("projects_id", $projects)->where("date", $date)->order("date");
?>

Mám připravený patch, který dovolí psát log.projects.companies.users.username, ale nejsem si jist, že ho zveřejním – jde o poměrně neobvyklý zápis.

Tharos:

Funguje to výborně, děkuji vřele za reakci. Co se patche týče, minimálně já bych měl o jeho zveřejnění zájem. Osobně se totiž se strukturou databáze, kde se pro některá data zkrátka musí přes více než jednu spojovací tabulku, setkávám ani ne až tak vzácně... A pak už je přece jen NotORM zápis poměrně složitý a dlouhý.

ikona Jakub Vrána OpenID:

Je to v Gitu: https://github.com/vrana/notorm/. Teď by tedy mělo stačit napsat <?php $db->log("projects.companies.users.username", $username); ?>.

Tharos:

Díky za uveřejnění patche, dnes jsem se konečně dostal k jeho otestování a funguje dobře. Nekoukal jsi ještě na nekorektně vygenerovaný obsah klauzuje IN, o kterém píši v komentáří vedle?

Tharos:

Tak, malý zádrhel. Řekněme, že v šabloně při vypisování budeme nakonec chtít i nějaké další sloupce například z tabulky companies (řekněme jméno společnosti, ke kterému se záznam v logu vztahuje). Když upravím kód na následující podobu, vygenerovaný dotaz získávající projekty je nesmyslný (je nesmyslná část v IN(...)):

<?php
$users
= $db->user("username", $username);
$companies = $db->companies("users_id", $users)->select('*');
$projects = $db->projects("companies_id", $companies);
$db->log("projects_id", $projects)->where("date", $date)->order("date");
?>

ikona Jakub Vrána OpenID:

K těm stačí přistoupit normálně: $log->projects->companies[""].

Miroslav Hruška:

Nešlo by do NotROM doplnit něco takového?
<?php
foreach ($notORM->application as $application) {
  echo
$application['author.name'] . ' ' . $application['author.web'];
}
?>

Zatím jsem si to upravil sám ve vlastní \NotORM_Row ale něco takového by bylo přínosné myslím pro všechny. Jde o to, že NotORM nepracuje s plošnými daty (jak je většina rozšíření například v Nette zvyklá [DataGrid]). Ve třídě \NotORM_Row stačí upravit metodu offsetGet:
<?php
function offsetGet($key)
{
  if (
strstr($key, '.') !== FALSE) {
   
$exp = array_filter(explode('.', $key));
    if (
count($exp) != 2) {
      return
parent::offsetGet($key);
    }
    else {
     
$row = $this->__get($exp[0]);
      return
$row[$exp[1]];
    }
  }
  else {
    return
parent::offsetGet($key);
  }
}
?>

Dalo by se to asi upravit i lépe (další zanoření), například:
<?php
foreach ($notORM->application as $application) {
  echo
$application['author.name'] . ' ' . $application['author.company.web'] . ' ' . $application['author.company.region.capital'];
}
?>

Díky za odpověď.

Miroslav Hruška:

Beru zpět, v NotORM to již takto funguje, předchozí nefunkčnost způsobil špatné pojmenovaný vazební sloupec.

ikona Jakub Vrána OpenID:

Co je špatného na syntaxi $application->author['name']?

Miroslav Hruška:

Nic, já s tím mám problém pouze v mém datagridu, který přijímá data jako pole a následně generuje tabulku například podle jednoduchého zápisu:
<?php
$table
->data = $notORM->user;
$table->addColumn('name', 'Jméno');
$table->addColumn('sname', 'Příjmení');
$table->addColumn('company.name', 'Firma');
?>

Jinak bych musel psát něco takového:
<?php
  $table
->data = $notORM->user;
 
$table->addColumn('name', 'Jméno');
 
$table->addColumn('sname', 'Příjmení');
 
$table->addColumn('companyName', 'Firma', function($row) {
    return
$row->company['name'];
  });
?>

ikona Jakub Vrána OpenID:

Já jsem přemýšlel o tom, že by syntaxe $row['company.name'] přistupovala k datům pomocí JOINu, ale bez zapnuté keše by to bylo strašně pomalé.

Miroslav Hruška:

Mno to by bylo naprosto super. Možná je to trochu moc magické a nevím, do jaké míry realizovatelné, ale nešlo by tam jen přidat něco jako:
<?php
 
...

  if (
$this->cache instanceof NotORM_Cache) {
   
//join
 
}
  else {
   
//2 selecty
 
}

  ...
?>

Je to jen nástřel ale myslím si, že rozdílné chování (pokud bude naprosto transparentní) při zapnuté / vypnuté cache by nevadilo, pokud se to napíše do dokumentace. Dalo by se jít ještě dál a založit něco jako "procesor", což by nebylo nic jiného než třída s jednoduchým interface, která by se zaváděla na základě nějakých enviromentálních stavů (cache ano/ne, driver, etc...).

ikona Jakub Vrána OpenID:

Problém je v tom, že pomocí PDO se nedá univerzálně napsat company.name AS `company.name` (neexistuje funkce pro ošetřování identifikátorů). Do NotORM tuto vrstvu dávat nechci, ale v Nette je, takže tam by to přidat šlo.

ikona Schmutzka:

Pokud s notorm začínáte, nebo potřebujete vědět, jak co napsat (OR, IN s více poli, JOIN, COUNT("*")), vytvořil jsem přehlednou tabulku příkazů, tedy helpku, jak to psát: http://bit.ly/SQLcQ

Snad vám bude nápomocna a ušetří spousty hodin a nervů :)

Roman Svoboda:

Ahoj,
potřeboval bych pomocí NotORM napsat dotaz:
SELECT c.* FROM content c JOIN content ci ON (c.id = ci.content_id AND ci.template_item_id = $promena)
ORDER BY ci.value DESC, ale netuším, jak na to. :(

Zkoušel jsem :
$this->db->content()->order('content_item.value DESC')->where('content_item.template_item_id = ?',$promenna);

NotORM mi však vygeneruje:
SELECT content.* FROM content
LEFT JOIN content_item ON content.content_item_id = content_item.id
WHERE (content_item.template_item_id = ?)
ORDER BY content_item.value DESC

Nešlo by to udělat nějak jinak prosím?

Předem děkuji za odpověď.

Roman Svoboda:

Pardon, právě jsem objevil  https://github.com/vrana/notorm/pull/32/files ,
který vyřešil můj problém :)
Pokud však existuje i jiný způsob v NotOrm, jak si to zapsat, určitě by mne zajímal.

ikona Jakub Vrána OpenID:

Já bych to napsal takhle:

<?php
foreach ($db->content_item("template_item_id", $promenna)->order("value DESC") as $content_item) {
   
$content = $content_item->content;
}
?>

Martin Kopeček:

Zdravím, koukám, že už je to tu trošku starší, ale zkusím to:

mám podobný problém s hledáním rozdílu více "stejných" tabulek (šlo by to i v jedné s dalším sloupcem definujícím kategorii:

zpevniky_akordy > zpevniky_id, akordy_id
zpevniky_akordy_koncept > zpevniky_id, akordy_id

a potřeboval bych najít co je v konceptu navíc a co na míň.

např: to, co je navíc:

SELECT zpevniky_akordy_koncept.akordy_id FROM zpevniky_akordy_koncept LEFT JOIN zpevniky_akordy USING(zpevniky_id,akordy_id) WHERE zpevniky_akordy.akordy_id IS NULL

Jde to výše popsaným způsobem, ale přijde mi zbytečné tahat z DB stovky ID, abych je vložil do další query, když je možné vytáhnout jen ty tři podstatné.

Mimochodem, proč je pro mysql vypnuté IN (SELECT...) ? Tím by se to snad dalo také vyřešit.

Behe:

Ahoj, předem bych rád poděkoval za NotOrm, ale mám trošku problém s hledáním v několika na sobě závislých tabulkách:

team:
- id
- number
- name

costcenter
- id
- number

team_costcenter
- id
- team_id
- costcenter_segment_id

team_costcenter
- id
- team_id
- costcenter_segment_id

costcenter_segment
- id
- name
- segment_id
- costcenter_id

<?php
foreach ($db->team_costcenter()
            ->
where("team.name LIKE '%" . $search . "%' OR
                     team.number LIKE '%"
. $search . "%' OR
                     costcenter_segment.name LIKE '%"
. $search . "%'")
as
$q)
{
   
$this->query[] = array('id' => $q->team['id'],
                           
'number' => $q->team['number'],
                           
'name' => $q->team['name'],
                           
'costcenter' => $q->costcenter_segment->costcenter['number'] . ' - ' . $q->costcenter_segment['name']);
}
?>

toto funguje bezproblémů, ale chtěl bych hledat i podle čísla nákl. střediska - tedy přidat do where('costcenter.number LIKE '%" . $search . "%') a to už nefunguje

jde to vůbec tímto způsobem?

Za případnou odpověď předem velice děkuji

Behe:

jej, tabulka team_costcenter je tam samozřejmě jen jednou.

Vložit příspěvek

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

© 2005-2012 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.