Jazyk XQuery
XQuery je dotazovací a funkcionální programovací jazyk pro XML, který spravuje W3C. Jeho současnou verzi 3.1 (březen 2017) plně podporuje BaseX. XQuery využívá XPath pro odkazování na uzly a atributy v XML dokumentech a přidává možnost výsledky řadit, agregovat nebo propojovat výsledky více XPath výrazů. V jazyku lze mimo jiné používat větvící konstrukty (if-then-else, case-switch) či iterační operátor (for). Výrazy jsou case-sensitive.
Doporučené čtení: W3C School materiály s příklady, Aktuální verze XQuery 3.1.
Jazyk obecně pracuje se sekvencemi, což jsou uspořádané množiny XML uzlů, či atomických hodnot. Sekvence obvykle obsahují více prvků, ale mohou být i jednoprvkové či prázdné. Základní syntaktické schema pro XQuery dotaz je označováno akronymem FLWOR skládajícím se z:
Pouze klíčové slovo return je povinné v XQuery dotazu.
Představení FLWOR klíčových slov
V dalších příkladech je pracováno se záznamy o mezinárodních organizacích, které jsou v datasetu factbook.xml vedené jako uzly s tagem organization. Pro lepší představu o struktuře těchto uzlů se lze podívat do databázového indexu, případně si jeden z nich vypsat vhodným kliknutím do vizualizace obsahu databáze, nebo XQuery dotazem.
Proměnné jsou prefixované symbolem $. Filtrace první organizace proběhla v Q1 již na úrovni XPath adresace a nebylo nutné použít klíčové slovo where.
(: Q1 výběr první organizace v datasetu :)
let $org := //organization[1]
return $org
Organizace mají ve svých atributech uložené jméno a běžně používanou zkratku. Dotazem Q2.1 lze nalézt všechny organizace a aplikovat projekci na zmíněné atributy.
Klíčové slovo for slouží k iteraci přes všechny uzly nalezené XPath adresací //organization. Zavedení proměnných $orgName a $orgAbbr není v tomto příkladu nezbytně nutné - pravou stranu přiřazení by bylo možné použít přímo v řádce return. Funkce concat slouží k transformaci řetězců do jedné výsledné hodnoty (pro každou organizaci). Výsledkem je 168 záznamů s charakterem CSV formátu (se středníkem jako oddělovacím znakem).
Pozn. Jazyky XPath 3.1, XQuery 3.1 a XSLT 3.0 sdílejí stejnou knihovnu funkcí a většinu běžných funkcí jako concat lze aplikovat i v XPath.
Přirozenější by v XML prostředí bylo vracet výsledky dotazu jako XML soubor. XQuery umožňuje za klíčovým slovem return definovat šablonu, do které budou vysázeny nalezené záznamy.
V příkladech stojí za povšimnutí chování proměnné $orgName. V Q2.1 a Q2.2 je do proměnné přiřazena key-value dvojice XML atribut s názvem name a příslušnou hodnotou. Při transformaci do výstupu dojde v Q2.1 k implicitní aplikaci funkce data(), která vrátí pouze hodnotu atributu. V Q2.2 je vložena celá key-value dvojice do výsledného XML tagu. V Q2.3 je potřeba explicitně zavolat funkci data(), jinak by se proměnná propsala jako atribut XML uzlu s tagem name.
Pozn. XQuery dotaz Q2.1 by bylo možné alternativně zapsat i jako čistě XPath výraz //organization/concat(@abbrev, ";", @name)
(: Q2.1 seznam zkratek a jmen organizací :)
for $org in //organization
let $orgName := $org/@name
let $orgAbbr := $org/@abbrev
return concat($orgAbbr, ';', $orgName)
(: EU;European Union :)
(: Q2.2 seznam zkratek a jmen organizací
vrácené v XML #1 :)
for $org in //organization
let $orgName := $org/@name
let $orgAbbr := $org/@abbrev
return <organization>
{$orgName}{$orgAbbr}
</organization>
(: <organization name="European Union"
abbrev="EU"/> :)
(: Q2.3 seznam zkratek a jmen organizací
vrácené v XML #2 :)
for $org in //organization
let $orgName := data($org/@name)
let $orgAbbr := data($org/@abbrev)
return <organization>
<name>{$orgName}</name>
<abbr>{$orgAbbr}</abbr>
</organization>
(: <organization><name>European Union</name>
<abbr>EU</abbr></organization> :)
V dalším kroku může být chtěno vyfiltrovat pouze určité organizace. Dotaz Q3 aplikuje na každý uzel organizace podmínku hledající, zda se v textu názvu organizace nachází slovo International (pomocí funkce contains()). Podobně jako v případě klíčového slova let lze slovo where opakovat a každá podmínka bude muset být vyhodnocena jako pravdivá (ve smyslu logického AND). Pro podobný účel lze použít logické spojky and a or v rámci jedné where klauzule.
Opět stojí za povšimnutí, že dojde k implicitnímu volání funkce data() nad proměnnou $orgName.
(: Q3 organizace s International v názvu :)
for $org in //organization
let $orgName := $org/@name
let $orgAbbr := $org/@abbrev
where contains($orgName, "International")
return concat($orgAbbr, ';', $orgName)
Posledním nevyužitým klíčovým slovem v akronymu FLWOR je order by sloužící ke změně řazení výsledků, což je funkcionalita, na kterou již samotný XPath nestačí. V příkladu Q4 je nejprve spočteno, kolik má každá organizace členů a následně je seznam seřazen sestupně dle počtu členů.
Ke spočtení členů je použita agregační funkce count(), která vrátí velikost příslušné sekvence.
Podobně jako v SQL je přirozeně řazeno vzestupně. K opačnému řazení je potřeba použít slovo descending umístěné za výrazem. Zároveň je možné řadit hierarchicky vyjmenováním posloupnosti výrazů k řazení.
(: Q4 seřazení organizací dle počtu členů.
V případě shody abecedně. :)
for $org in //organization
let $orgAbbr := $org/@abbrev
let $c := count($org/members)
order by $c descending, $orgAbbr
return concat($orgAbbr, ';', $c)
Pseudo JOIN operace v XQuery
U členů organizací jsou v datasetu uváděné pouze textové identifikátory bez dalších podrobností. Pokud je při dotazování chtěno zpracovat i data jednotlivých států, je potřeba provést operaci podobnou JOIN v SQL. V některých případech by toto propojení bylo řešitelné pomocí čistého XPath, nicméně XQuery řešení bývá čitelnější. Následujícím způsobem lze například vypsat názvy států, které jsou členy Evropské unie (v období, které dataset zachycuje).
Pozn. Q5.1 a Q5.2 jsou v rámci BaseX optimalizátoru převedeny na stejný dotaz a jsou vyhodnoceny rychleji než Q5.3.
(: Q5.1 seznam členů EU - většina operací v XQuery :)
for $org in //organization
where $org/@abbrev = "EU"
for $countryId in $org/members/@country
let $countryName := //country[@id=$countryId]/@name
return data($countryName)
(: Q5.2 seznam členů EU - XQuery s filtrací v XPath :)
for $countryId in //organization[@abbrev="EU"]/members/@country
let $countryName := //country[@id=$countryId]/@name
return data($countryName)
(: Q5.3 seznam členů EU - XPath :)
//country[@id=//organization[@abbrev="EU"]/members/@country]/data(name)
XQuery k vyzkoušení
X1 Vypsat názvy organizací, ve kterých je Česká republika členským státem. Nerozlišujte typ členství. Nepoužívejte ručně zadanou hodnotu @id České republiky. (46)
Group by operace
V rámci XQuery lze provádět seskupující operace téměř shodné s GROUP BY v SQL. Na ukázkovém dotazu Q6 je řešen výpočet počtu organizací, kterých jsou jednotlivé státy členy. Primární informace, tedy vazba organizace-stát, se nachází pod elementem organization. Z tohoto lze seskupením dle ID státu zjistit počet členství. Jméno státu je sekundární informace, kterou je nutné dohledat.
U tohoto případu je patrné, že u komplexnějších dotazů záleží na pořadí jednotlivých operací. V obou případech nejprve dojde k vyhledání všech uzlů members u všech organizací. Ve variantě Q6.1 je následně dohledaná informace o názvu státu pro každé párování a až následně operace agregace dle názvu. Ve variantě Q6.2 dojde nejprve k agregaci dle ID státu a informace o názvu se dohledá pouze jednou pro každý ze států. Obě query vrací stejný výsledek, ale druhá query je vykonána přibližně 30x rychleji.
Ve výsledcích dotazu Q6 nejsou vypsané státy, které nejsou členy žádné organizace. To je na první pohled odlišné chování oproti Q4, kde byly vypsány i organizace, které nemají žádné členy. Státy nejsou vypsané kvůli logice dotazu, kde jsou iterovány záznamy příslušnosti k organizaci a nikoliv jednotlivé státy.
(: Q6.1 seznam zemí s počtem organizací,
jejichž jsou členy - pomalý výpočet :)
for $members in //organization/members
let $cId := $members/@country,
$cName := //country[@id=$cId]/@name
group by $cName
let $cnt := count($members)
order by $cnt descending
return concat($cName, ";", $cnt)
(: Q6.2 seznam zemí s počtem organizací,
jejichž jsou členy - rychlejší výpočet :)
for $members in //organization/members
let $cId := $members/@country
group by $cId
let $cnt := count($members),
$cName := //country[@id=$cId]/@name
order by $cnt descending
return concat($cName, ";", $cnt)
Pokud má být dohledáno, které státy ve výsledcích Q6 nejsou, bylo by vhodné sestavit dotaz z druhého směru. V Q7 je tedy iterováno po jednotlivých country. Ve variantě Q7.1 je pro každý stát do proměnné přiřazena sekvence organizací odkazujících na daný stát v atributu members/@country. Funkce count poté pouze určí velikost sekvence a do výsledku jsou zahrnuty pouze státy s prázdnou sekvencí. V tomto případě nemá smysl používat group by.
Alternativa Q7.2 demonstruje použití negace funkce exists, která vrací pravdivostní hodnotu, zda se nějaký požadovaný uzel odpovídající podmínce v datech vyskytuje.
Pozn. obě varianty vrací stejný výsledek, ale Q7.1 je přibližně 6x rychlejší na výpočet.
(: Q7.1 země bez členství v organizaci :)
for $country in //country
let $cId := $country/@id
let $orgs :=
//organization/members[@country=$cId]
where count($orgs) = 0
return $country/@name
(: Q7.2 země bez členství v organizaci :)
for $country in //country
let $cId := $country/@id
where not(exists(
//organization/members[@country=$cId]))
return $country/@name
XQuery k vyzkoušení
V této sekci je několik námětů k praktickému vyzkoušení XQuery dotazování nad datasetem factbook.xml. Otázky jsou řazeny subjektivně dle odhadované obtížnosti. Vpravo je uveden očekávaný výsledek.
X2 Vyhledejte všechny druhy státních zřízení (či formy vlády) a seřaďte je sestupně podle počtu zemí, ve kterých jsou uplatněné.
country.
(: X2
republic;86
parliamentary democracy;17
constitutional monarchy;15
... :)
X3 Kolik států leží na jednotlivých kontinentech? Jaká je celková populace a hrubý domácí produkt kontinentů?
for na tagu country.
$foo/..
count existují další agregační funkce jako avg(), min(), max(), sum(), které ze sekvence hodnot spočtou jednu hodnotu.
(: X3
kont.;p. států;total GDP; total populace
Africa;55;1.184157E6;7.3103833E8
Europe;51;9.099316E6;7.92002189E8
Asia;49;1.315599E7;3.701832588E9
America;48;1.08393944E7;7.83861486E8
Australia/Oceania;31;496501.7;2.9729307E7
:)
X4 Kolik států leží na jednotlivých kontinentech? Státy, které jsou rozložené na více kontinentech, rozpočítejte příslušným podílem.
encompassed.
div pro reálné dělení nebo idiv pro celočíselné. Alternativou je vystačit si se symbolem * reprezentující operátor multiplikace.
(: X4
Europe;49.52
Asia;47.58
Australia/Oceania;31
America;48
Africa;54.9
:)
X5 Vypište názvy řek evidovaných v datasetu a pokud je informace k dispozici, vypište názvy kontinentů, na kterých se nachází.
string-join(sekvence, separátor) pro transformaci do jednoho řetězce.
(: X5
Ajan-Jurjach;
Amazonas;America
Amudarja;
Amur;Europe+Asia
Anuwimi;
Argun;Europe+Asia
... :)
Práce s kolekcí obsahující více XML souborů
Databáze, nad kterou byl postaven obsah tohoto cvičení, obsahovala pouze jeden XML soubor. BaseX však umožňuje pracovat s celými kolekcemi XML souborů, které mohou mít různou strukturu. Při zakládání nové databáze je možné místo konkrétního souboru zvolit adresář, jehož obsah se naimportuje do vytvářené databáze. Jedná se o jednorázovou akci, tedy následná změna ve zdrojovém souboru nevyvolá změnu v obsahu databáze. Do existující databáze je možné přidávat další soubory přes dialogové okno v Properties -> záložka Resources -> podzáložka General. Stiskem tlačítka Add... dojde k importování aktuálně zvoleného souboru nebo obsahu adresáře. Alternativně lze do databáze importovat obsah souboru příkazem ADD <cesta k souboru>.
Importování probíhá přidáním obsahu souboru do kořenového uzlu databáze. Při opakovaném nahrání stejného souboru dojde k vložení duplicitního záznamu do databáze.
S databází obsahující více importovaných souborů se pracuje stejně jako s databází nad jedním souborem. Jediným rozdílem je, že lze v případě potřeby adresovat data z určitého dokumentu dle jeho názvu. Např. db:get("facts", "factbook.xml")//country pro získání uzlů s tagem country v databázi facts a souboru factbook.xml.
Práce s více kolekcemi
Při vykonávání dotazů BaseX používá aktuálně otevřenou kolekci (databázi) jako kontext. V GUI je informace o této skutečnosti zobrazena v panelu s editorem - např. Context: db:get("factbook"). Pokud by bylo potřeba provádět XQuery příkaz bez předchozího otevření databáze, je možné rozšířit v dotazu příslušné adresace o volání db:get(<název kolekce>).
Zároveň je při použití adresace skrze db:get() možné v jednom dotazu pracovat s daty z různých kolekcí. Pokud by například obsah ukázkového souboru byl rozdělen do kolekcí factbook-org a factbook-country, bylo by možné dotaz Q5.2 upravit následujícím způsobem.
(: Q8 seznam členů EU - data z různých kolekcí :)
for $countryId in db:get("factbook-org")//organization[@abbrev="EU"]/members/@country
let $countryName := db:get("factbook-country")//country[@id=$countryId]/@name
return data($countryName)
Odkazy na literaturu obsahující další informace
Ukázky pokročilých technik XQuery 3.0 (group by, window funkce, if/switch, try-catch, ...) BaseX dokumentace
Ukázky pokročilých technik XQuery 3.1 (JSON, map, lookup operator, arrow operator, ...) BaseX dokumentace
Ukázky technik XQuery Update 1.0 (insert, delete, replace, ...) BaseX dokumentace