Penúltima part del curs Curs BBDD i Java centrada en la construcció de consultes mitjançant XPath/XQuery.

1 Introducció

Les bases de dades natives XML (XML native database) ofereixen un mecanisme adient per a la consulta sobre dades que resideixin en documents XML.

En aquest sentit, les dades estan estructurades en la base de dades (que ara és un o més documents XML) en la forma d’una estructura en arbre invertit, on el seu creador haurà definit els noms dels nodes i la seva distribució:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <!DOCTYPE mondial SYSTEM "mondial.dtd">
 3 <mondial>
 4   <country car_code="AND" area="450" capital="cty-Andorra-Andorra-la-Vella" memberships="org-CE org-ECE org-Interpol org-IFRCS org-IOC org-ITU org-UN org-UNESCO org-WIPO">
 5     <name>Andorra</name>
 6     <population>72766</population>
 7 ...
 8     <border country="F" length="60"/>
 9     <border country="E" length="65"/>
10     <city id="cty-Andorra-Andorra-la-Vella" is_country_cap="yes" country="AND">
11       <name>Andorra la Vella</name>
12       <longitude>1.3</longitude>
13       <latitude>42.3</latitude>
14       <population year="87">15600</population>
15     </city>
16   </country>
17 
18 ...
19 
20   <desert id="desert-Atacama" country="RCH">
21     <name>Atacama</name>
22 ...
23     <area>181300</area>
24     <longitude>-69.25</longitude>
25     <latitude>-24.5</latitude>
26   </desert>
27 </mondial>

El fragment mostrat correspon a la base de dades MondialDB, que pot descarregar-se lliurament aquí i que serà utilitzada aquí per als exemples i exercicis.

Les consultes sobre XND són de natura diferents respecte de les conegudes fins ara arran de l’estructura interna d’aquests documents. Per tant, els llenguatges existents per fer-hi consultes també són diferents.

Trobem així que ara no podem utilitzar llenguatges com SQL, perquè aquí no tenen sentit. Pel contrari, existeix XPath i XQuery com els llenguatges estrella per a l’extracció de dades d’arxius XML i XND.

En concret, XPath és un llenguatge centrat en el recorregut descendent a través del XML, realitzant filtrat i agrupació, mitjançant un llenguatge senzill amb una col·lecció de funcions i operacions limitada.

Per altra banda, XQuery permet construir estructures més complexes basades en un iterador (bucle) de tipus for en el que es coneix com construcció FLWOR (for-let-where-order-result).

A més, existeixen diferents API disponibles per als diversos llenguatges de programació que permeten connectar amb aquest tipus de bases de dades i arxius XML per tal d’executar consultes amb aquests dos llenguatges de programació.

2. Instal·lació de BaseX

El primer que hem de fer és disposar d’una instal·lació d’alguna aplicació que permeti treballar en aquest entorn.

De sistemes XND n’hi ha un grapat, però nosaltres ens centrarem en el producte BaseX.

Aquest producte està disponible en el mateix dipòsit estàndard d’Ubuntu, de manera que només necessitem treballar des de apt-get per gestionar la seva instal·lació.

Així doncs, els passos seran els següents:

  1. Actualitzar primer el sistema operatiu:

    sudo aptitude update && sudo aptitude upgrade
    
  2. Instal·lar JDK6 (si no estava instal·lat prèviament:

    sudo aptitude install openjdk-6-jdk
    
  3. Configurar JDK6 o JDK7 com a versió per defecte, i així disposar de les dues versions instal·lades en el sistema, deixant la desitjada com a versió per defecte de línia d’ordres:

    sudo update-alternatives --config java
    sudo update-alternatives --config javac
    sudo update-alternatives --config javadoc
    sudo update-alternatives --config javap
    sudo update-alternatives --config javah
    sudo update-alternatives --config javaws
    

    Si vostè desitja deixar JDK6 com a versió per defecte per compilar programes en línia d’ordres, només cal que així ho indiqui. Podrà comprovar quina és la versió configurada fent:

    java -version
    
  4. Instal·lar BaseX:

    sudo aptitude install basex
    
  5. Descarregar la base de dades de mostra MONDIAL (The MONDIAL Database) en format XML:

    wget http://www.dbis.informatik.uni-goettingen.de/Mondial/mondial.xml
    
  6. Obrir BaseX en mode gràfic:

    basexgui
    
  7. Crear una base de dades nova a partir de l’arxiu mondial.xml descarregat i amb nom “mondial

En aquest moment ja disposa de les eines necessàries per poder aprendre XPath/XQuery

Ara pot veure com és la base de dades amb què farem proves, tot recorrent-la gràficament amb l’aplicació i observant el document associat. Observi que quan se selecciona una branca de l’arbre XML, només aquella part serà mostrada en el visor de text i quedarà marcat en el visor gràfic.

3. Consultes XPath

XPath és, com s’ha dit, un llenguatge de consultes senzill que permet recórrer l’arbre XML de manera descendent i filtrar els elements (nodes) pels quals passem en aquest recorregut amb diversos criteris.

En aquest sentit, el resultat d’una consulta XPath és, en termes generals, un document XML. Aquest resultat és la col·lecció de nodes i elements que hagin “sobreviscut” a aquest recorregut i filtrat descendent.

3.1. Recorregut de l’arbre XML

La forma bàsica d’una sentència XPath s’assembla molt a la d’un camí de disc, on els directoris són els nodes presents al XML.

El node arrel es representa com a //. A partir d’aquí, podem incloure un camí concret descendent per l’arbre XML, tot filtrant què volem que quedi en el resultat.

Vegem alguns exemples:

  1. Tot l’arbre del document:

     //mondial
    

    El resultat serà tota la base de dades sencera.

  2. Tots els nodes del document:

    //*
    

    El resultat, tot i semblar el mateix, conceptualment és un altre: tots els nodes que es trobin penjant de l’arrel (que en aquest cas coincideix).

  3. Tots l’arbre del document si el document és un altre:

    doc('/Users/felix/Desktop/curset/biblio.xml')//biblioteca
    

    Observi que és possible especificar un arxiu determinat mitjançant la funció doc(). XPath presenta algunes funcions interessants que anirem descobrint al llarg dels exemples.

    Observi també com s’ha indicat el node arrel i el node biblioteca que és l’element inicial del XML utilitzat a l’exemple.

  4. Informació dels diferents continents:

    //mondial/continent
    
  5. Extreure només els noms dels continents:

    //mondial/continent/name
    
  6. El nom de tots els països que apareixen a la base de dades:

    //mondial/country/name

    Vegi com el resultat ha estat l’extracció de tots els nodes name que pengin d’un node country, que pengin directament de mondial. Entengui que el resultat és un XML i que hem eliminat altres camins de l’arbre en baixar per aquesta branca, però que en cap cas hem dit que ens quedem en un o un altre país (no s’ha fet filtrat). És per això que veurà els noms de tots els països. És com si baixéssim en paral·lel per totes les branques que compleixin el camí indicat.

  7. Només els noms en format text (entrant en el text del tag):

    //mondial/continent/name/text()
    

    Aquesta funció fa que només quedi el text que és contingut dins dels nodes que han arribat a aquell punt (nodes name). Com que BaseX no gestiona el formatat, veurem el text tot seguit, però en realitat el que tenim és una col·lecció (llista) de cadenes de text.

  8. Informació millor presentada:

    string-join(//mondial/continent/name/text(), ', ')
    

    Aquesta funció uneix la llista de texts obtinguda posant caràcters de coma entre cada parell, retornant una única cadena de text de resultat.

  9. Llistat de conceptes presents a la base de dades:

    distinct-values(//mondial/*/node-name())
    

    Observem diversos punts a comentar aquí:

    • Per una banda, el caràcter * farà de comodí per indicar que en aquest punt estem baixant un nivell dins del XML, però no ens importa com es digui en node concret per on baixem aquest nivell. I pensi que estem indicant que s’ha de baixar un i només un nivell. No representa una quantitat arbitrària de nivells, sinó que no ens importa com es digui el nivell.

    • Tot i que no ens importa quin node baixem, igualment volem aplicar-hi una funció. En concret, la funció node-name() retorna el nom del node del node en què estiguem situats. Així doncs, si hem baixat per country, el nom serà “country”, i si hem baixat per river, serà “river”.

    • El distinct-values() és necessari per eliminar duplicats en el resultat obtingut, fent que només es mostri una vegada cadascun dels conceptes trobats.

3.2. Filtrat de nodes

Quan volem filtrar nodes en l’arbre, ho farem indicant entre claudàtors [] una expressió que representi un criteri de filtrat. Quan l’expressió indicada es compleixi, aquell node “passarà” el filtre i romandrà al resultat de la consulta.

Els filtres podem posar-los en qualsevol node d’un recorregut descendent com els vistos fins ara, de tal manera que podem combinar filtres diferents en nivells diferents del nostre recorregut.

Observi el següent exemple:

//mondial/country[name='Spain']/province[name='Catalonia']/city/name

En aquest cas, el recorregut que fem és, en realitat, //mondial/country/province/city/name. És a dir, baixem pels nodes de l’arbre XML fins al nom de les ciutats de les províncies dels països.

Però no volem tots els països, així que afegim el filtre sobre el nom del país en el nivell de country. El criteri és clar: Només volem aquell país que es diu “Spain”. Un cop aquest filtrat s’hagi efectuat, només quedarà un node país per baixar i continuarà el processament del camí.

En el nivell de província, tornem a aplicar un filtre sobre el nom de la província, de manera que només quedi aquella amb nom “Catalonia”.

Ara bé, observi que, en realitat, el filtre ha fet un descens per l’arbre XML paral·lel al que s’estava fent per obtenir la informació resultant. Quan indiquem que miri el name en els dos casos, s’està baixant a aquell node per comprovar si es compleix o no. I, de fet, podríem baixar més d’un nivell si volguéssim.

A més, podem també filtrar pel valor d’un atribut d’un node, posant el signe @ davant del nom de l’atribut.

Els operadors que podem utilitzar per escriure aquests filtres són els següents:

Operador Descripció Exemple
| Calcula la unió de dos conjunts //llibres | //cds
+ Suma 6 + 4
- Resta 6 - 4
* Multiplicació 6 * 4
div Divisió 8 div 4
= Igualtat preu=9.80
!= Diferent preu!=9.80
< Menor que preu<9.80
<= Menor o igual preu<=9.80
> Major que preu>9.80
>= Major o igual preu>=9.80
or  o preu=9.80 or preu=9.70
and  i preu>9.00 and preu<9.90
mod Mòdul (residu) 5 mod 2

Vegem ara uns quants exemples:

  1. Informació específica d’un país, cercada pel valor d’un node fill:

    //mondial/country[name='Spain']
    

    Això funciona perquè dins del tag <country> hi ha un altre anomenat <name> amb el text corresponent al nom del país.

  2. Informació específica d’un país, cercada a través d’un atribut:

    //mondial/country[@car_code='E']
    
  3. Nom dels rius que consten a Espanya

    //mondial/river[@country="E"]/name
    
  4. Llistat dels països amb més de 50 milions d’habitants:

    //mondial/country[population>50000000]/name
    
  5. Diferents formes de govern dels països amb més de 50 milions d’habitants:

    distinct-values(//mondial/country[population>50000000]/government)
    
  6. Quantitat de països a la base de dades:

    count(//mondial/country)
    

    Aquí veu que existeixen funcions d’agregació com count(), sum(), max(), etc. Vegi la llista de funcions al final de l’article.

  7. Quantitat de ciutats espanyoles:

    count(//mondial/country[name='Spain']/province/city)
    
  8. Quantitat de diferents formes de govern dels països de més de 50 milions d’habitants:

    count(distinct-values(//mondial/country[population>50000000]/government))
    
  9. Nom de les ciutats espanyoles:

    //mondial/country[name='Spain']/province/city/name
    
  10. Ciutats espanyoles amb més de 200.000 habitants:

    //mondial/country[name='Spain']/province/city[population>200000]/name
    
  11. Noms de província espanyoles:

    //mondial/country[name='Spain']/province/name
    
  12. Països d’Europa:

    //mondial/country[encompassed/@continent='europe']/name
    

    O també:

    //mondial/country[encompassed[@continent='europe']]/name
    

    I és que podem aplicar filtres sobre filtres!

  13. Rius de països europeus:

    //mondial/river[@country=//mondial/country[encompassed/@continent='europe']/@car_code]/name
    

    I és que podem fer també subconsultes!

  14. Rius europeus amb una longitud de, com a mínim, la meitat de longitud que el riu més llarg de tot el mon:

    //mondial/river[
        @country=//mondial/country[encompassed/@continent='europe']/@car_code 
        and 
        length>=max(//mondial/river/length) div 2
    ]/name
    

    Observi com, per filtrar els rius europeus, s’ha fet que el codi de país sigui un dels presents a la llista de la subconsulta de països europeus. La igualtat com a condició és, en realitat, com un IN de SQL. És a dir, si un dels dos elements és una llista, el que fa és buscar el valor dins de la llista.

  15. La quantitat d’habitants de la ciutat espanyola amb més habitants:

    max(//mondial/country[name='Spain']/province/city/population)
    
  16. La ciutat espanyola amb més habitants:

    //mondial/country[name='Spain']/province/city[population=max(
        //mondial/country[name='Spain']/province/city/population)]/name
    

    O també:

    //mondial/country[name='Spain']/province/city[population=max(
        ../../province/city/population)]/name
    

    Observi’s que estem fent una subconsulta i que la subconsulta pot ser relativa al punt on s’aplica el filtrat (segon cas).

  17. Rius amb nom començat amb la lletra ‘G’):

    //mondial/river[matches(name,'^G.*')]/name
    

    La funció matches(text, expressió_regular) permet verificar expressions contra expressions regulars estàndard (tipus grep).

  18. Nom de totes les ciutats de més d’un milió habitants (independentment de si el país està subdividit en províncies o no):

    //mondial/country/descendant::city[population>1000000]/name
    

    L’operador descendant::node indica explorar tots i cadascun dels camins inferiors que continguin el node indicat, independentment del seu nivell dins de l’arbre. Així doncs, ens permet buscar un node fill per sota d’un punt de l’arbre quan no sabem en quin nivell de profunditat es troba.

Amb tot, pot comprovar que les consultes que es poden arribar a escriure cobriran gran part dels requeriments d’extracció d’informació que necessitarem per construir vistes d’usuari. Això ja ens està bé, però de tant en tant ens trobem amb casos específics que requereixen d’alguna cosa més que el recorregut descendent amb filtrat.

3.3. Resum de funcions XPath/XQuery agrupades per concepte

Quant a les funcions presentades ara, tingui present que algunes poden aplicar-se sobre el node actual, mentre que d’altres només accepten el pas per argument. No s’ha indicat aquí quin cas és cadascun per reduir la complexitat del llistat, tot i que els casos en què s’aplica un o l’altre cas són en general força evidents. Per exemple: //biblioteca/llibre[position()>3]/text()

Nom Descripció
fn:node-name(node) Retorna el nom del node indicat
fn:nilled(node) Retorna cert si el node és nul
fn:data(item.item,...) Pren una seqüència i retorna la seqüència de valors atòmics
fn:base-uri(node) Retorna el valor de la propietat base-uri del node indicat
fn:document-uri(node) Retorna el valor de la propietat document-uri del node indicat
Nom Descripció
fn:error([error[,descripció[,objecte-error]]]) Genera i retorna un error
fn:trace(valor,etiqueta) Usat per depurar consultes
Nom Descripció
fn:number(arg) Retorna el valor numèric de l’argument
fn:abs(num) Retorna el valor absolut
fn:ceiling(num) Retorna l’enter més petit que és major o igual a l’argument
fn:floor(num) Retorna l’enter més gran que és menor o igual a l’argument
fn:round(num) Arrodoneix a l’enter més proper
fn:round-half-to-even(num) Arrodoneix a l’enter més proper que sigui parell
Nom Descripció
fn:string(arg) Retorna el valor de text de l’argument
fn:codepoints-to-string(int,int,...) Retorna una cadena a partit d’una seqüència de codis numèrics de caràcter
fn:string-to-codepoints(string) Retorna la seqüència de codis de caràcter corresponent a la cadena
fn:codepoint-equal(comp1,comp2) Cert si els texts són iguals segons Unicode
fn:compare(comp1,comp2[,joc_caracters]) Compara les cadenes seguint el joc de caràcters indicat. Retorna -1, 0 o 1
fn:concat(string,string) Retorna la concatenació de les cadenes
fn:string-join(seqüència,sep) Retorna una cadena que conté els ítems indicats amb el separador indicat
fn:substring(string,inici[,long]) Treu una subcadena d’una cadena
fn:string-length(string) Retorna la quantitat de caràcters de la cadena
fn:normalize-space(string) Elimina espais sobrants de la cadena (inicial, final i repetits)
fn:normalize-unicode() Normalitza seguint les normes Unicode
fn:upper-case(string)  Converteix la cadena a majúscules
fn:lower-case(string)  Converteix la cadena a minúscules
fn:translate(string1,string2,string3) Substitueix en string1 cada caràcter de string2 pel corresponent de string3
fn:escape-uri(string,esc-res) Escapa la cadena seguint les normes per a URIs
fn:contains(string1,string2) Retorna cert si la primera cadena conté el text de la segona
fn:starts-with(string1,string2) Retorna cert si la primera cadena comença amb el text de la segona
fn:ends-with(string1,string2) Retorna cert si la primera cadena acaba amb el text de la segona
fn:substring-before(string1,string2) Extreu la part prèvia de la primera cadena respecte on apareix la segona
fn:substring-after(string1,string2) Extreu la part posterior de la primera cadena respecte on apareix la segona
fn:matches(string,regexp) Retorna cert si la cadena compleix l’expressió regular indicada
fn:replace(string,regexp,replace) Substitueix les troballes de l’expressió regular per la cadena de substitució
fn:tokenize(string,regexp) Retalla la cadena utilitzant l’expressió regular com a recerca de separador
Nom Descripció
fn:resolve-uri(relative,base) Resol la URI a partir de la base i la part relativa
Nom Descripció
fn:boolean(arg) Retorna el valor booleà de l’argument indicat
fn:not(arg) Avalua el contrari de l’argument
fn:true() És el valor cert
fn:false() És el valor fals
Nom Descripció
fn:dateTime(date,time) Converteix els arguments en una data i hora
fn:years-from-duration(dur) Retorna l’enter que representa els anys de l’interval
fn:months-from-duration(dur) Retorna l’enter que representa els mesos de l’interval
fn:days-from-duration(dur) Retorna l’enter que representa els dies de l’interval
fn:hours-from-duration(dur) Retorna l’enter que representa les hores de l’interval
fn:minutes-from-duration(dur) Retorna l’enter que representa els minuts de l’interval
fn:seconds-from-duration(dur) Retorna l’enter que representa els segons de l’interval
fn:years-from-dateTime(datetime) Retorna l’enter que representa els anys de la data/hora
fn:months-from-dateTime(datetime) Retorna l’enter que representa els mesos de la data/hora
fn:days-from-dateTime(datetime) Retorna l’enter que representa els dies de la data/hora
fn:hours-from-dateTime(datetime) Retorna l’enter que representa les hores de la data/hora
fn:minutes-from-dateTime(datetime) Retorna l’enter que representa els minuts de la data/hora
fn:seconds-from-dateTime(datetime) Retorna l’enter que representa els segons de la data/hora
fn:timezone-from-dateTime(datetime) Retorna el fus horari de la data/hora
fn:years-from-date(date) Retorna l’enter que representa els anys de la data
fn:months-from-date(date) Retorna l’enter que representa els mesos de la data
fn:days-from-date(date) Retorna l’enter que representa els dies de la data
fn:timezone-from-date(date) Retorna el fus horari de la data
fn:hours-from-time(time) Retorna l’enter que representa les hores de la hora
fn:minutes-from-time(time) Retorna l’enter que representa els minuts de la hora
fn:seconds-from-time(time) Retorna l’enter que representa els segons de la hora
fn:timezone-from-time(time) Retorna el fus horari de la hora
fn:adjust-dateTime-to-timezone(datetime,timezone) Associa el fus horari a la data/hora
fn:adjust-date-to-timezone(datetime,timezone) Associa el fus horari a la data
fn:adjust-time-to-timezone(datetime,timezone) Associa el fus horari a la hora
Nom Descripció
fn:name(nodeset) Retorna el nom del primer node del conjunt
fn:local-name(nodeset) Retorna el nom del primer node del conjunt sense el prefix del namespace
fn:namespace-uri(nodeset) Retorna el URI del namespace del primer node del conjunt
fn:lang(lang) Retorna cert si l’idioma del node actual correspon a l’especificat. S’indica com a text ("en")
fn:root(node) Retorna el node arrel de l’arbre a què pertany el node indicat
Nom Descripció
fn:index-of(seqüència, item) Retorna la posició de l’element cercat a la seqüència de recerca
fn:remove(seqüència, pos) Elimina l’element situat a la posició indicada
fn:empty(seqüència) Retorna cert si l’argument és una seqüència buida
fn:exists(seqüència) Retorna cert si l’argument no és una seqüència buida
fn:distinct-values(seqüència) Retorna la seqüència neta de duplicats
fn:insert-before(seqüència,pos,item) Insereix l’element indicat just abans de la posició especificada
fn:reverse(seqüència) Inverteix l’ordre dels elements de la seqüència
fn:subsequence(seqüència,pos,len) Extreu part d’una seqüència
fn:unordered(seqüència) Retorna els elements en un ordre dependent de la implementació
Nom Descripció
fn:zero-or-one(seqüència) Retorna l’argument si conté zero o un element
fn:one-or-more(seqüència) Retorna l’argument si conté un element com a mínim
fn:exactly-one(seqüència) Retorna l’argument si conté exactament un sol element
Nom Descripció
fn:deep-equal(param1,param2) Retorna cert si els arguments són exactament iguals dins de les seves jerarquies
Nom Descripció
fn:count(seqüència) Retorna la quantitat d’elements
fn:avg(seqüència) Retorna el valor mitjà dels elements de la seqüència
fn:max(seqüència) Retorna el valor màxim dels elements de la seqüència
fn:min(seqüència) Retorna el valor mínim dels elements de la seqüència
fn:sum(seqüència) Retorna el total de sumar els elements de la seqüència
Nom Descripció
fn:position() Retorna l’índex de l’element en el seu entorn
fn:last()  Retorna l’índex de l’últim element en el seu entorn
fn:current-dateTime() Retorna la data/hora actual
fn:current-date() Retorna la data actual
fn:current-time() Retorna la hora actual
fn:implicit-timezone() Retorna el fus horari implícit
fn:default-collation() Retorna el joc de caràcters actual
fn:static-base-uri() Retorna el valor de base-uri actual

Les funcions matemàtiques científiques estan definides en el namespace math, i per tant, quan són invocades, cal esmentar-ho explícitament. Per exemple: math:sqrt(42).

Nom Descripció
math:pi() Representa una aproximació al valor de $\pi$
math:exp(x) Retorna el valor de $e^x$
math:exp10(x) Retorna el valor de $10^x$
math:log(x) Retorna el logaritme natural de l’argument
math:log10(x) Retorna el logaritme en base 10 de l’argument
math:pow(x,y) Retorna el valor de $x^y$
math:sqrt(x) Retorna l’arrel quadrada de l’argument
math:sin(x) Retorna el sinus de l’angle subministrat
math:cos(x) Retorna el cosinus de l’angle subministrat
math:tan(x) Retorna la tangent de l’angle subministrat
math:asin(x) Retorna l’angle aplicant la funció inversa del sinus
math:acos(x) Retorna l’angle aplicant la funció inversa del cosinus
math:atan(x) Retorna l’angle aplicant la funció inversa de la tangent
math:atan2(y,x) Retorna l’angle aplicant la funció inversa de la tangent sobre (y/x)

Els tipus de dades presents a XPath/XQuery són: xs:float, xs:double, xs:decimal, xs:integer, xs:long, xs:int, xs:short, xs:byte, xs:unsignedLong, xs:unsignedInt, xs:unsignedShort, xs:unsignedByte, xs:duration, xs:dateTime, xs:date, xs:time, entre d’altres

Per a l’especificació d’arguments de les funcions, poden utilitzar-se els elements addicionals a més dels tipus de dades presentats:

Tipus d’element Permet
node()  Qualsevol node
element() Qualsevol element de node
element(nom) L’element de node amb el nom indicat
attribute() Qualsevol atribut de node

I pot indicar-se també la cardinalitat dels arguments:

Indicador Significat
cap (blanc) exactament un element
? Zero o un element
+ Un o més elements
*  Qualsevol quantitat d’elements (zero o més)

4. Consultes FLWOR (XQuery)

XQuery ofereix un mecanisme de consultes més avançat que es munta sobre XPath. En concret, quan treballem amb XQuery, treballem amb la construcció FLWOR:

FLWOR (XQuery)

FLWOR = for - let - where - order by - return

Aquestes construccions permeten fer el recorregut per un arbre XML i executar alguna tasca sobre cadascun dels elements trobats, incloent l’aplicació de filtrat i càlcul d’expressions. A més, incorpora la possibilitat d’aplicar ordenació dels resultats.

Un FLWOR, com a mínim, ha de contenir el iterador de bucle for i un return per obtenir algun resultat. Els altres tres elements (let, where i order by són opcionals).

Analitzem les diferents parts:

  • Amb la clàusula for especifiquem el conjunt d’elements que volem recórrer. La notació és:

    for $variable in //...consulta...XPath...
    

    Per tant, indiquem quina variable volem utilitzar per emmagatzemar els diferents elements trobats i la consulta XPath que ens generi la llista d’elements. Aquí podem posar una consulta XPath tant complexa com les vistes fins ara.

  • Amb la clàusula let podem indicar assignacions i càlculs en forma d’expressions avaluades. Normalment s’utilitza sintaxi XPath i podem utilitzar la variable de bucle o variables definides amb let. Podem disposar més d’un let si així ho volem.

    Per exemple:

    for $pais in //mondial/country
      let $num_ciutats := count($pais/descendant::city)
      let $tot_pobl := sum($pais/descendant::city/population)
      let $percent := $tot_pobl div $pais/population
      ...
    
  • Amb la clàusula where afegirem filtres sobre la llista d’iteració, de manera que els elements del for que no compleixin la condició del where seran omesos en el bucle:

    for $pais in //mondial/country
      let $num_ciutats := count($pais/descendant::city)
      let $tot_pobl := sum($pais/descendant::city/population)
      let $percent := $tot_pobl div $pais/population
      where $pais/encompassed/@continent='europe'
      ...
    
  • Amb la clàusula order by ordenem el resultat del for. Per tant, especifiquem l’ordre de recorregut del bucle:

    for $pais in //mondial/country
      let $nom := $pais/name/text()
      let $num_ciutats := count($pais/descendant::city)
      let $tot_pobl := sum($pais/descendant::city/population)
      let $percent := $tot_pobl div $pais/population * 100
      where $pais/encompassed/@continent='europe'
      order by $percent ascending
      ...
    
  • Amb la clàusula return indicarem què s’ha de mostrar com a resultat com a resposta a cada iteració (no a tot el bucle). El resultat ha de ser un únic node XML o una primitiva bàsica (com una cadena o un enter) per a cada iteració executada. El return s’executarà, per tant, n vegades:

    for $pais in //mondial/country
      let $nom := $pais/name/text()
      let $num_ciutats := count($pais/descendant::city)
      let $tot_pobl := sum($pais/descendant::city/population)
      let $percent := $tot_pobl div $pais/population * 100
      where $pais/encompassed/@continent='europe'
      order by $percent ascending
      return <pais> {$nom} : {$percent} </pais>
    

Vegem ara tot un seguit d’exemples de XQuery que demostren algunes particularitats destacables:

  1. Nom de les províncies espanyoles:

    for $p in //mondial/country[name='Spain']/province
        return $p/name
    
  2. Extracció de la llista de ciutats de cada província espanyola:

    for $p in //mondial/country[name='Spain']/province
        let $nom := $p/name/text()
        let $ciutats := string-join($p/city/name, ', ')
        return <provincia> {$nom}: {$ciutats} </provincia>
    
  3. Llista dels països que tenen més d’un 1% de la població mundial total, ordenats per població descendent i mostrant els resultats en format XML:

    for $c in //mondial/country
        let $n := $c/name/text()
        let $p := $c/population/text()
        where $p>=sum(//mondial/country/population)*0.01
        order by number($p) descending
        return <pais nom='{$n}'>{$p}</pais>
    
  4. Nom de les províncies espanyoles, amb població i quantitat de ciutats, ordenats per quantitat de ciutats en ordre descendent, amb resultat en format XML:

    for $p in //mondial/country[name='Spain']/province
        let $nom := $p/name
        let $pop := $p/population
        let $num := <count> {count($p/city)} </count>
        order by $num descending
        return <provincia> {$nom} {$pop} {$num} </provincia>
    
  5. Redreçament de la informació de les províncies, amb cada província amb atributs de nom i població i llista de ciutats interna, amb una entrada per a cada ciutat:

     for $p in //mondial/country[name='Spain']/province
         let $nom := $p/name/text()
         let $pop := $p/population/text()
         let $cities := <cities> {
             for $c in $p/city/name/text() 
                 return <city> {$c} </city>
             } </cities>
         return <provincia nom='{$nom}' population='{$pop}'> {$cities} </provincia>
    

5. Declaració de funcions en XQuery

És possible definir funcions pròpies per a ser utilitzades en les nostres consultes XQuery. La forma de fer-ho és mitjançant la següent sintaxi:

declare function nom_de_la_funció($var1 as tipus, $var2 as tipus, ...) as tipus {
  ...
  codi de la funció
  ...
}

XQuery accepta construccions diverses en el bloc de codi, com let, for i fins i tot estructures if ... then ... else ....

A més, les funcions solen definir-se en un espai de funcions concret. Normalment l’espai serà el local a la consulta (espai de l’usuari). Això es fa indicant el nom de la funció amb una especificació local: prèvia al nom de la funció.

Vegem ara alguns exemples que demostren diferents situacions útils en la construcció de funcions XQuery:

  1. Funció que calcula arrel d’índex N a partir de l’índex i el radicand:

     declare function local:sqrtN($i as xs:double, $r as xs:double) as xs:double {
       let $x := math:pow($r, 1 div $i)
       return $x
     };
    
     local:sqrtN(3, 125)
    
  2. Funció que retorna la primera paraula de la cadena indicada

     declare function local:primera-paraula($cad as xs:string) as xs:string {
       let $paraula := substring-before($cad, ' ')
       return $paraula
     };
     local:primera-paraula('Ola k ase')
    
  3. Funció recursiva per al factorial (i exemple de if)

     declare function local:factorial($n as xs:integer) as xs:integer {
       if ($n<=1)
       then $n
       else $n*local:factorial($n - 1)
     };
     local:factorial(7)
    
  4. Marge de població (diferència de població entre el país més habitat i el menys)

     declare function local:marge($seq as element()*) as xs:double {
       let $a := min($seq)
       let $b := max($seq)
       return ($b - $a)
     };
     local:marge(//mondial/country/population)
    
  5. Quantitat de ciutats per país

    declare function local:numciutats($c as element(country)*) as xs:double {
      let $n := count($c/descendant::city)
      return $n
    };
    local:numciutats(//mondial/country[name='Spain'])