Iteráció: FLWOR

Ez a kifejezés a „for” ciklus egy formája. Emlékezzünk vissza, hogy az XPath implicit módon biztosítja ez a '.' környezeti elem aktualizálásával egy cs/e kifejezésben. Az XQuery ezt programozott formában, a for ciklus segítségével biztosítja egy kollekció minden elemére, más szóval engedélyezi ugyanazt a folyamatot lefuttatni minden elemre egy kollekcióból. A FLWOR szó a „For, Let, Where, Order by, Return” szavak kezdőbetűiből tevődik össze.

Nézzük meg mire is használhatóak ezek a záradékok:

Ez azt jelenti hogy a „for” ciklus teszi lehetővé egy egész kollekció elemenkénti feldolgozását. A „where” feltétel szűrési lehetőségeket ad, hogy csak a szükséges elemeket dolgozzuk fel. Az „order” feltétel rendezettséget biztosít az eredményhalmazban, amelyet a „return” ad vissza. A „return” irányítja a speciális elemeket, amelyek a „for” ciklusból lettek visszaadva.

A FLWOR összetettsége révén lehetővé teszi:

Explicit iteráció használatának szintaktikája:

for $v [at $p] in e1
return e2

A szintaktika egy plusz lehetőség is ad, opcionálisan elérhetjük az aktuálisan feldolgozott elem pozícióját is (a szintaktikában p-vel jelölve). Azaz ha az e1 szekvencia egy (x1, ..., xn) elemsorozatot alkot, akkor az e2 n-szer kerül kiértékelésre, ahol $v kötődik az aktuális xi-hez (és p pedig i-hez). Az eredmények pedig egy szekvenciába lesznek összefogva.

// elem kettesek visszaadása
for $x in (3,2,1)
return ($x,"*")    => (3,"*",2,"*",1,"*")

// eredményhez egy plusz elem hozzáfűzése
for $x in (3,2,1)
return $x,"*"      => (3,2,1,"*")

// Descartes-szorzat
for $x in (3,2,1)
return for $y in ("a","b")
        return ($x,$y) => (3,"a",3,"b",
                           2,"a",2,"b",
                           1,"a",1,"b")

Lehet ezeket rövidíteni is:

for $v1 in e1          for $v1 in e1          for $v1 in e1
return               ≡  for $v2 in e2    ≡     $v2 in e2
    for $v2 in e2          return e3                return e3
        return e3

Ezeknél azért összetettebb problémák is megoldhatóak a függvények használatával:

// Fa mélységének meghatározása
// (!) Figyeljük meg a levélelemek összeszedésének módját (!)
max( for $i in cs/descendant-or-self::*[not(*)]
     return count($i/ancestor::*) )

// minden második elem kinyerése
// ( emlékezzünk vissza a szekvenciák egymásba ágyazhatóságára és a 0 igazságértékére )
for $i at $p in e            vagy szebben írva:     for $i at $p in e
return if ($p mod 2)                                where ($p mod 2)
        then e[$p]                                  return e[$p]
        else ()

Visszatérve még egyszer az explicit és implicit iteráció közötti különbségre, fontos megjegyezni a kontextus elem körülötti eltéréseket is:

XPath:
a[@b = "foo"]/c[2]/d[@e = 42]

XQuery:
for $a in a
where $a/@b = "foo"
return for $c at $p in $a/c
        where $p = 2
        return for $d in $c/d
                where $d/@e = 42
                return $d

Ránézésre mindkettő ugyanazt eredményezni, de a különbség a kontextus elem illetve elemek között van. Míg az XPath minden egyes lépés (/) esetén lecseréli a kontextus elemet, addig az XQuery esetén mindvégig ugyanaz marad!

A rendezés sajátosságai

A FLWOR kifejezések alapvetően a szekvencia eredeti elemsorrendjét követve készítik el az eredmény szekvenciát. Ennek felülbírálására az SQL-ből jól ismert order by záradék használható minimális kiegészítéssel. Használhatóak a jól ismert [ascending|descending] módosítók, és akár több elem szerint is rendezhetőek (ekkor vesszővel elválasztva kell felsorolni). Ami a kiegészítés, hogy megadható az üres szekvenciát adó kifejezések helye: vagy a lista elején vagy a végén szerepeljenek: [empty greatest|least].

Példák:

for $i in doc("companys.xml")/persons/person[id lt 10]
 where $i/@sex = "female"
 order by $i/name
 return $i/name

let $a := <a>
<b id="0">42</b>
<b id="1">5</b>
<b id="2"/>
<b id="3">3</b>
<b id="4">1</b>
</a>
for $b in $a/descendant::b
order by $b/text() empty greatest
return $b/@id

A rendezésnél még egy dolgoz figyelembe kell venni. Tekintsük az alábbi példát:

let $authors := for $a in doc("books.xml")//author
                order by $a/last, $a/first
                return $a
return $authors/last

Azaz szeretnénk a szerzőket vezetéknév szerint rendezett listában látni. A probléma az lesz, hogy az eredmény továbbra is a dokumentumsorrendnek megfelelően érkezik, mert a / és a // mindig eszerint működik. Ez szintén egy érdekes működés, de jó számolni vele, hogy a / és a // mindig felülbírálja az order by sorrendjét!

A helyes megoldás:
for $a in doc("books.xml")//author
order by $a/last, $a/first
return $a/last

A rendezés kapcsán még egy dolog merülhet fel: az azonos értékek kezelése. Alapvetően ha csomópontok ismétlődnek azonos tartalommal az a rendezés szempontjából irreleváns, egymás után következnek dokumentumsorrendben és így kerülnek át az eredmény szekvenciába. Mindazonáltal ha szeretnénk ezeket az ismétlődő részfákat kiszedni, akkor az XQuery rendelkezik egy distinct-values() függvénnyel, amely a csomópontok értékét összehasonlítva már csak az egyedieket engedi tovább.

for $l in distinct-values(doc("books.xml")//author/last)
return <last>{ $l }</last>

// Ugyanezt megtehetjük kicsit összetettebben is, már teljes nevet vizsgálva:
let $a := doc("books.xml")//author
for $l in distinct-values($a/last),
    $f in distinct-values($a[last=$l]/first)
return
<author>
<last>{ $l }</last>
<first>{ $f }</first>
</author>

Változók használata

Miután minden FLWOR kifejezés egy n-esen (tuple) dolgozik, ezért szükségszerűen ezeket meg kell adni. Erre az XQuery két lehetőséget is biztosít. Ami eddig nem derülhetett ki egyértelműen, hogy a for mellett a let is erre szolgál. Ennek értelmében legalább az egyiknek szerepelnie kell minden XQuery kifejezésben legalább egyszer, arra pedig semmilyen megkötés nincs, hogy melyiknek is. Továbbmenve, a sorrendjükre sincs semmilyen megkötés, azaz akármilyen variációt elfogad az XQuery.

Ami a legfontosabb, hogy átlássuk miként is állítják elő ezen elem n-eseket a for és let kulcsszavak. A for esete tisztább, mert ha megadok egy szekvenciát, akkor annak minden elemére végrehajtódik:

for $i in (1, 2, 3)
return <tuple><i>{ $i }</i></tuple>

eredménye:
<tuple><i>1</i></tuple>
<tuple><i>2</i></tuple>
<tuple><i>2</i></tuple>

Ezzel szemben a let egy elemként tekinti az értékét:

let $i := (1, 2, 3)
return <tuple><i>{ $i }</i></tuple>

eredménye:
<tuple><i>1 2 3</i></tuple>

Kombinálva a kettőt:

for $i in (1, 2, 3)
let $j := (1, 2, 3)
return <tuple><i>{ $i }</i><j>{ $j }</j></tuple>

eredménye:
<tuple><i>1</i><j>1 2 3</j></tuple>
<tuple><i>2</i><j>1 2 3</j></tuple>
<tuple><i>3</i><j>1 2 3</j></tuple>

egy sokkal életszerűbb példán tekintve:
megadjuk minden egyes paciens esetén a kezelőorvosok számát
for $b in doc("patients.xml")//patient
let $c := $b/therapist
return <patient>{ $b/name, <count>{ count($c) }</patient>}</book>

A változók használata mellett még egy nagyon fontos jellemzőjüket meg kell említeni: megváltoztathatatlanok. A let által kötött változókat úgy tekinthetjük, mint nevesített értékeket és épp ezért nem módosíthatóak. Az alábbi példa jól demonstrálja az intuitív használatot felülbíráló viselkedést:

Ahogy elképzelnénk imperatív módon:                   Ahogy meg kell oldani XQuery módon:
let $x :=                                             let $x := ... ua ...
<x>
 <y>12</y>
 <y>10</y>
 <y>7</y>
 <y>13</y>
</x>
let $sum := 0                                         for $y in $x//y
for $y in $x//y                                       return 0 + $y
let $sum := $sum + $y
return $sum

Kvantált kifejezések

Egyes lekérdezéseknél meg kell állapítanunk, hogy a sorozat legalább egy eleme megfelel-e a feltételnek, vagy akár minden eleme megfelel-e. Ezt tehetjük meg a kvantorokkal. Egzisztenciális kvantorral azt vizsgáljuk, hogy legalább egy tétel megfelel-e egy állapotnak. Az alábbi lekérdezés egy egzisztenciális kvantort mutat:

// megkapjuk azon cégeket, melyeknek van bejegyzett magyarországi telefonjuk
for $x in doc("companys.xml")/company
where some $y in $x/phone satisfies
      starts-with($y/text(),"06") or starts-with($y/text(),"+36")
return $x/name

Ezzel szemben az univerzális kvantor azt vizsgálja, hogy a szekvencia összes eleme kielégíti-e a feltételt. Átírva az előző példát:

// megkapjuk azon cégeket, melyeknek CSAK magyarországi bejegyzett telefonjuk van
for $x in doc("companys.xml")/company
where every $y in $x/phone satisfies
      starts-with($y/text(),"06") or starts-with($y/text(),"+36")
return $x/name

Az univerzális kvantor esetén fontos megjegyezni, hogy olyan esetekben, ahol ezt üres szekvenciára használjuk, ott mindig igazat kapunk, mert az üres szekvencia effektív igazságértéke igaz (azaz olyan cégek is felkerülhetnek a listára, amelyeknél nincs kitöltött phone érték - mind üres)