Arbeiten mit XQuery

(Auszug aus "Oracle SQL: Das umfassende Handbuch", Kapitel 17 "XML-Abfragen", von Jürgen Sieben)

Sollten Sie sich bereits bestens mit XQuery auskennen, werden Sie mir vorwerfen, wie der Farbenblinde über Farben zu räsonieren, und ich muss zugeben, dass meine Kenntnisse über XQuery nicht eben tiefgreifend sind. Ich benutze diese Technologie eher an der Oberfläche, nehme das andererseits aber billigend in Kauf, weil ich bislang nicht durch entsprechende Problemstellungen in eine tiefe Beschäftigung mit XQuery getrieben worden bin.

XQuery, soviel weiß selbst ich, ist gewissermaßen ein Pendant zu SQL. Ähnlich, wie SQL daraufhin optimiert wurde, mit strukturierter Information in Tabellen zu arbeiten, ist XQuery daraufhin optimiert, mit semistrukturierten Daten in einer hierarchischen Struktur umzugehen. Es ist eine komplette Abfragesprache mit einer sequenzbasierten Ausrichtung: Alles wird in XQuery als eine Sequenz von Elementen oder skalaren Werten verstanden, ähnlich vielleicht einer Liste oder einem Array, nur, dass Werte mehrfach auftreten und die Listen sortiert sein können.

Basis der Abfragesprache ist ein FLWOR-Ausdruck (das wird ausgesprochen wie das englische Wort flower – Blume). Dieser Ausdruck ist ein Akronym für for where – let – order by – return, was den Schlüsselworten einer typischen XQuery-Abfrage entspricht. Grundsätzlich entspricht dabei for und let dem from, where und order by entsprechen den SQL-Schlüsselworten und return entspricht dem select. Wir haben also auch semantisch eine große Nähe zwischen den Abfragesprachen, wenn auch die Reihenfolge der Begriffe eine andere ist.

Dennoch ist XQuery eine grundlegend andere Abfragesprache als SQL. Die Oracle-Datenbank unterstützt als alternative Abfragesprache XQuery in Version 1.0. Ebenfalls unterstützt werden XPath in Version 2.0, was Teil der XQuery-Spezifikation ist. Es existiert auch ein SQL*Plus-Befehl XQUERY, mit dessen Hilfe SQL*Plus in einen XQuery-Modus versetzt werden kann. Offiziell ist jedoch eine Schnittstelle zwischen SQL und XQuery definiert, und diese Schnittstelle, die Teil des SQL/XML-Standards ist, wird über die Funktionen xmlquery, xmltable, xmlexists und xmlcast bereitgestellt. Diese Funktionen sind es, die ich Ihnen in diesem Abschnitt erläutern möchte, weniger die Interna von XQuery, die ich mir – wie ich zugeben muss – selbst noch weitgehend erarbeiten müsste. Ich bin lediglich in der Lage, Ihnen einfache Grundabfragen zu zeigen, die ausreichen, die Funktionen zum Leben zu erwecken. Von den weitergehenden Möglichkeiten, die geeignet sind, XSLT zu ersetzen und XML-Instanzen zu erzeugen, bin ich weit entfernt. Dieser Abschnitt ist also alles andere als ein Ersatz für ein spezialisiertes Buch über XQuery.

Funktion XMLQUERY

Beginnen wir mit der Funktion xmlquery, die dazu genutzt wird, XML aus relationalen Daten zu erzeugen oder aber XML aus anderen XML-Daten zu extrahieren. Die Funktion ist, wie bereits gesagt, Teil des SQL/XML-Standards, unterstützt aber nicht alle Verwendungsmöglichkeiten, die im Standard vorgegeben sind. Insbesondere kann diese Funktion lediglich XMLType-Instanzen zurückgeben, nicht aber Sequenzen, die ja die Bausteine von XQuery-Abfragen darstellen. Die Funktion xmlquery hat daher einige Klauseln, die zwar geschrieben werden müssen, aber ohne Alternative sind, weil die alternative Formulierung eben nicht unterstützt wird. Im Gegensatz zu sonst möchte ich Ihnen gerne den prinzipiellen Aufbau der Funktion zeigen, bevor wir ein Beispiel konstruieren. Die Funktion erwartet zunächst einen XQuery-Ausdruck als ersten Parameter. Ein XQuery-Ausdruck kann im einfachsten Fall aussehen wie ein XPath-Ausdruck, aber auch die volle Bandbreite einer FLWOR-Anweisung beinhalten. Anschließend können wir (das ist optional, aber wohl sehr häufig der Fall) einen Kontext übergeben. Dieser Kontext stellt die Rohdaten dar, auf die der XQuery-Ausdruck angewendet werden soll. Im einfachsten Fall haben Sie eine Tabelle mit einer XML-Instanz darin. Es können aber auch relationale Spalten und skalare Daten übergeben werden, allerdings keine objektrelationalen Strukturen. Diese müssten zuvor durch eine SQL/XML-Abfrage in XML überführt werden. Abschließend folgt dann die verpflichtende Klausel returning content. Ich denke, wir lassen es zunächst einmal dabei und sehen uns dies im Überblick an:

xmlquery(<XQuery-Ausdruck>
      passing <XML-Instanz>
      returning content)

Code-Beispiel: Grundsätzlicher Aufbau der Funktion XMLQUERY

Beginnen wir mit einem ganz einfachen Beispiel, bei dem statt einer FLWOR-Abfrage lediglich ein XPath-Ausdruck übergeben und unsere bereits bekannte XML-Spalte als Kontext definiert wird:

select xmlquery(
          '/Mitarbeiterliste/Mitarbeiter'
          passing liste
          returning content) ergebnis
      from dept_emp_xml;


ERGEBNIS
-------------------------------------------------------------
<Mitarbeiter einstellDatum="1981-06-09" id="7782"><Name>Clark...
<Mitarbeiter einstellDatum="1980-12-17" id="7369"><Name>Smith...
<Mitarbeiter einstellDatum="1981-02-20" id="7499"><Name>Allen...

Code-Beispiel: Ein erstes Anwendungsbeispiel für xmlquery

Sie erkennen den XPath-Ausdruck, der lediglich die Mitarbeiter von dem Wurzelelement Mitarbeiterliste befreit und als XML-Fragment wieder ausgibt. In diesem einfachen Zusammenhang ist die Mächtigkeit dieser Funktion noch nicht erkennbar. Als nächste Steigerung werden wir auf eine passing-Klausel verzichten und die Daten für unsere XQuery-Abfrage direkt aus der Datenbank gewinnen. Dazu verwenden wir eine Funktion mit dem Namen fn:collection, die eine sehr seltsame Syntax aufruft, um Zeilen einer Datenbank als XML zu erfragen. Diese Syntax lautet oradb:/SCOTT/EMP und bedeutet: Siehe die Datenbank als riesige XML-Instanz an (oradb) und gehe innerhalb dieser Instanz zum Knoten SCOTT und darin zum Knoten EMP. Dahinter steht die Oracle XML DB, die es ermöglicht, relationale Daten auf standardisierte Weise als XML-Instanz darzustellen. Stellen wir uns eine Tabelle SCOTT.EMP vor, die wir auf diese Weise darstellen möchten. Die Standarddarstellung sieht vor, dass jede Zeile der Tabelle in einem Element ROW dargestellt wird. Jede Spalte produziert ein Kindelement mit dem Namen der Spalte und dem Spaltenwert dieser Zeile. Es entsteht also eine XML-Instanz folgender Struktur:

<SCOTT>
    <EMP>
        <ROW>
            <EMPNO>7369</EMPNO>
            <ENAME>SMITH</ENAME>
            <JOB>CLERK</JOB>
            <MGR>7902</MGR>
            <HIREDATE>1980-12-17</HIREDATE>
            <SAL>800</SAL>
            <DEPTNO>20</DEPTNO>
        </ROW>
        <ROW> ... </ROW>
    </EMP>
    <DEPT>...</DEPT>
</SCOTT>

Code-Beispiel: Prinzipielles XML-Format einer Tabelle, über oradb angesprochen

SMITH ist kein SALESMAN, daher nicht kommissionsberechtigt, daher fehlt bei ihm das Kindelement COMM. Generell werden null-Werte nicht ausgegeben. Da nun unsere FLWOR-Abfrage direkt zu Tabelle EMP geht, benötigen wir die Klausel passing also nicht, die Abfrage sieht nun so aus:

select xmlquery(
         'for $emp in fn:collection("oradb:/SCOTT/EMP")
         where $emp/ROW/SAL >= 3000
         return <Mitarbeiter>
                  <Name>{$emp/ROW/ENAME/text()}</Name>
                  <Gehalt>{$emp/ROW/SAL/text()}</Gehalt>
                </Mitarbeiter>'
         returning content) ergebnis
     from dual;


ERGEBNIS
----------------------------------------------------------------
<Mitarbeiter>
    <Name>SCOTT</Name>
    <Gehalt>3000</Gehalt>
</Mitarbeiter>
<Mitarbeiter>
    <Name>KING</Name>
    <Gehalt>5000</Gehalt>
</Mitarbeiter>
<Mitarbeiter>
    <Name>FORD</Name>
    <Gehalt>3000</Gehalt>
</Mitarbeiter>

Code-Beispiel: Abfrage mit einem FLWOR-Ausdruck

Nach etwas Formatierung erkennen wir, dass wir mit dieser Funktion XML aus relationalen Daten erzeugt haben. Die Struktur wird direkt erzeugt dadurch, dass wir sie explizit hinschreiben. Das Ergebnis ist übrigens wirklich XML und sieht nicht nur so aus. Mit dieser Technik sind sogar Joins zwischen Tabellen möglich:

select xmlquery(
     'for $emp in fn:collection("oradb:/SCOTT/EMP"),
       $dept in fn:collection("oradb:/SCOTT/DEPT")
     where $emp/ROW/DEPTNO = $dept/ROW/DEPTNO
       and $emp/ROW/SAL >= 3000
     return <Mitarbeiter>
              <Name>{$emp/ROW/ENAME/text()}</Name>
              <Gehalt>{$emp/ROW/SAL/text()}</Gehalt>
              <Abteilung>{$dept/ROW/DNAME/text()}</Abteilung>
            </Mitarbeiter>'
       returning content) ergebnis        
     from dual;


ERGEBNIS
----------------------------------------------------------------
<Mitarbeiter>
    <Name>KING</Name>
    <Gehalt>5000</Gehalt>
    <Abteilung>ACCOUNTING</Abteilung>
</Mitarbeiter>
<Mitarbeiter>
    <Name>FORD</Name>
    <Gehalt>3000</Gehalt>
    <Abteilung>RESEARCH</Abteilung>
</Mitarbeiter>
<Mitarbeiter>
    <Name>SCOTT</Name>
    <Gehalt>3000</Gehalt>
    <Abteilung>RESEARCH</Abteilung>
</Mitarbeiter>

Code-Beispiel: Erweiterung um Joins

Natürlich fragen wir jeweils gegen Tabelle DUAL, weil die eigentlichen Tabellen ja in XQuery angesprochen werden. FLWOR-Ausdrücke können geschachtelt werden, um 1:n-Beziehungen von Elementen ineinander zu platzieren. Hierzu wird an der Stelle im Ausgabebaum (return-Klausel), an der die Kindelemente häufiger auftauchen sollen, der FLWOR-Ausdruck für die Kindelemente platziert. Da dies jedoch kein XQuery-Buch ist, verweise ich entweder auf dem Oracle XML DB Developers Guide, Kapitel 5, »Using XQuery with Oracle XML DB«, oder auf ein spezialisiertes XQuery-Buch.

Funktion XMLTABLE

Das Gegenteil zu xmlquery können wir mit der Funktion xmltable erreichen. Diese Funktion dient dem intuitiven Entschachteln von XML, um aus den Daten wiederum relationale Daten zu gewinnen. Wieder soll uns ein einfaches Beispiel reichen, denn auch mit dieser Funktion steht die gesamte Ausdrucksfähigkeit von XQuery zur Verfügung. Auch hier beginne ich mit einem Überblick über die Struktur der Funktion:

Zunächst erwartet die Funktion einen optionalen Parameter, der die verwendeten Namensräume der XML-Instanz deklariert. Damit ist es der Funktion dann möglich, Elemente des entsprechenden Namensraums zu verwenden. Anschließend folgen der XQuery-Ausdruck und eine optionale passing-Klausel, wie Sie das bereits kennen. Nun aber kommt der Clou der Funktion, denn nun kann in der folgenden columns-Klausel definiert werden, welcher Ausdruck als welcher Datentyp und welche Spaltenbezeichnung gelesen werden soll. Damit wird das Mapping von XML-Elementen auf eine relationale Tabelle recht intuitiv. Sehen wir uns die Funktion einmal im Überblick an:

xmltable(
   xmlnamespaces (<Namensraum-Deklaration>), 
   <xQuery-Ausdruck>
   passing <XML-Instanz>
   columns <Spaltenmapping>)

Code-Beispiel: Übersicht über die Funktion xmltable

Natürlich wird diese Funktion in der where-Klausel einer SQL-Abfrage verwendet, das Ergebnis ist ja eine relationale Tabelle.

Als etwas komplexeres Beispiel habe ich mich entschlossen, dieses Mal auch einen XML-Namensraum zu verwenden, um diese Anwendung ebenfalls zu zeigen. Als Ausgangsmaterial verwende ich eine XML-Datei, die von der Europäischen Zentralbank täglich veröffentlicht wird und die Wechselkurse einiger wichtiger Währungen zum Euro enthält. Die Datei ist unter der URL "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" frei verfügbar und enthält zwei Namensräume, zum einen den des Umschlags und dann noch zum anderen den der europäischen Zentralbank. Die Datei sieht grob so aus:

<gesmes:Envelope xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref">
    <gesmes:subject>Reference rates</gesmes:subject>
    <gesmes:Sender>
        <gesmes:name>European Central Bank</gesmes:name>
    </gesmes:Sender>
    <Cube>
        <Cube time="2012-03-29">
            <Cube currency="USD" rate="1.3272"/>
            <Cube currency="JPY" rate="109.21"/>
            <Cube currency="BGN" rate="1.9558"/>
            <Cube currency="CZK" rate="24.778"/>
            <Cube currency="DKK" rate="7.4372"/>
            <Cube currency="GBP" rate="0.83580"/>
            ...
            <Cube currency="THB" rate="40.971"/>
            <Cube currency="ZAR" rate="10.2562"/>
        </Cube>
    </Cube>
</gesmes:Envelope>

Code-Beispiel: Beispiel einer XML-Instanz mit Namensräumen

Was ich nun mache, grenzt für viele möglicherweise an Zauberei. Ich werde die XML-Datei in der aktuellen Version in der SQL-Abfrage selbst von der EZB einlesen und direkt auswerten lassen. Dazu benötige ich in Version 11 der Datenbank ein Zugriffsrecht auf die Online-Ressource mittels einer ACL. Im Skript zum Buch habe ich hinterlegt, wie das für den Benutzer SCOTT geht, das ist ein Administrationsthema und soll uns hier nicht stören. Allerdings kann ich nun eine Funktion nutzen, die mir den Zugriff auf eine http-Ressource direkt aus SQL wie im folgenden Beispiel gestattet:

select httpUriType('http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml').getXml() datei
      from dual;


DATEI
----------------------------------------------------------------
<?xml version="1.0" encoding="WINDOWS-1252"?>
<gesmes:Envelope> ...</gesmes:Envelope>

Code-Beispiel: Direkter Zugriff auf eine Online-Ressource aus SQL

Das ist natürlich schon cool. Der objektorientierte Typ httpURIType erwartet als Parameter eine URL, rennt los und besorgt die Ressource. Anschließend kann die gefundene Datei mit Hilfe der eingebauten Funktion getXml() als XMLType ausgelesen werden. Nun können wir unsere Abfrage von oben gleich so ergänzen, dass lediglich eine Tabelle mit den Umrechnungskursen ausgegeben wird. Um dieses Beispiel noch einigermaßen darstellen zu können, habe ich die Bezeichner der Namensräume verkürzt dargestellt. Zudem habe ich den gesmes-Namensraum nicht berücksichtigt, weil mich für die Ausgabe die Elemente des Nachrichtenumschlags nicht interessieren.

select k.currency, k.rate
    from (select httpUriType(
                   'http://www.ecb.....xml').getXml() msg
            from dual) ezb,
         xmltable(
            xmlnamespaces(
               default 'http://www.ecb..../eurofxref'),
            '//Cube[@currency]'
             passing ezb.msg
           columns
              currency char(3) path '@currency',
              rate number path
                  'fn:translate(@rate, ".", ",")'
       ) k;


CURRENCY  RATE
-------- -------
USD       1,3272
JPY       109,21
BGN       1,9558
...

33 Zeilen ausgewählt.

Code-Beispiel: Extraktion von Daten aus einer Online-XML-Ressource

Sehen wir uns die Bestandteile einmal an. Zunächst erkennen Sie in Zeile 2–4 die Inner View wieder, die von der EZB die Online-Ressource liest. Diese wird im Folgenden unter ezb.msg verfügbar gemacht. Die xmltable-Funktion erhält diese Nachricht in Zeile 9 durch die passing-Klausel. Ich definiere den Standardnamensraum der EZB, damit ich anschließend diese Elemente überhaupt sehen kann, und ignoriere den gesmes-Namensraum, indem ich den XPath-Ausdruck // verwende, um im gesamten Dokument nach Cube-Elementen zu fahnden. Mich interessieren andererseits lediglich die Elemente, die ein currency-Attribut haben. Das mache ich durch den XPath-Ausdruck in Zeile 8 klar. In der columns-Klausel in den Zeilen 10–13 wird dann die Abbildung der Elemente auf Spalten der resultierenden Tabelle durchgeführt. Grundsätzlich ist das Mapping einfach, denn es wird einfach der Name und Datentyp der resultierenden Spalte sowie der XPath-Ausdruck, der sich nun auf das Ergebnis der XQuery-Abfrage bezieht, angegeben. Ein kleiner Gag am Rande: Die Konvertierung in eine Zahl gelingt nicht ohne Weiteres: Entweder stellen Sie den Parameter nls_territory auf AMERICA um, oder Sie ersetzen im Attribut den Punkt durch ein Komma, denn standardmäßig wird das lokal gültige Dezimaltrennzeichen zur Konvertierung verwendet. Ich habe mich hier entschlossen, über eine fn:translate-Funktion die Konvertierung des Punktes in ein Komma ausführen zu lassen.

XML mit Namensräumen aus SQL ist schon etwas heftig. Vielleicht haben Sie insgesamt das Gefühl, solche SQL-Abfragen seien eher etwas für die Sado-Maso-Szene. Alles ist aber auch eine Frage der Gewöhnung. Die Syntax ist speziell, zum Teil auch recht empfindlich, was die Einhaltung der Regeln angeht, aber eben auch hocheffektiv. Solche Abfragen sind sicher nichts für Einsteiger, können aber dem Profi viel Programmierung ersparen und mächtige Dinge tun, eine Einarbeitung in die Konzepte von XQuery allerdings vorausgesetzt ...

Funktion XMLEXISTS

Lehnen wir uns etwas zurück und entspannen bei einer einfacheren Funktion. Diese Funktion heißt xmlexists und prüft die Existenz eines Elements in einer XML-Instanz. Auch hier sind generell XQuery-Abfragen möglich, aber da diese auch einfache XPath-Angaben sein können, ist die Funktion als schnelle Nachschlag- und Suchfunktion nicht schlecht. Nach der Abfrage des letzten Abschnitts wird Ihnen die Syntax nicht nur bekannt, sondern direkt einfach vorkommen. Ich benutze wieder die XML-Instanzen der Tabelle dept_emp_xml:

select liste
      from dept_emp_xml
   where xmlexists(
                '/Mitarbeiterliste/Mitarbeiter[Name = "King"]'
                passing liste);


LISTE
------------------------------------------------------------
<Mitarbeiterliste abteilung="10">
    <Mitarbeiter einstellDatum="1981-06-09" id="7782">
        <Name>Clark</Name>
        <Beruf>Manager</Beruf>
        <Gehalt>2.450,00€</Gehalt>
    </Mitarbeiter>
    <Mitarbeiter einstellDatum="1982-01-23" id="7934">
        <Name>Müller</Name>
        <Beruf>Clerk</Beruf>
        <Gehalt>1.300,00€</Gehalt>
    </Mitarbeiter>
    <Mitarbeiter einstellDatum="1981-11-17" id="7839">
        <Name>King</Name>
        <Beruf>PRÄSIDENT</Beruf>
        <Gehalt>5.000,00€</Gehalt>
    </Mitarbeiter>
</Mitarbeiterliste>

Code-Beispiel: Filterung über xmlexists

Die Funktion XMLCAST

Sie haben diese Funktion bereits kurz in Aktion gesehen, und die Verwendung ist ähnlich schlicht wie die vorher gezeigte Funktion xmlexists. Sie übergeben einen Ausdruck (irgendeiner Form, zum Beispiel auch eine XQuery-Abfrage) und interpretieren diese als skalaren Datentyp. Einige Typen sind nicht erlaubt, die im SQL/XML-Standard definiert sind, so zum Beispiel XMLType. Beachten Sie, dass Sie keine Formatmaske übergeben können. Ist so etwas einmal erforderlich, müssen Sie dafür Sorge tragen, dass der Ausdruck, den Sie übergeben, die nötige Konvertierung soweit vorbereitet, dass eine Standardumwandlung gelingen kann. Ich persönlich glaube, dass dies die Mächtigkeit der Funktion stark einschränkt. Hier also noch einmal ein Beispiel:

select xmlcast(
            xmlquery(
              'for $e in //Mitarbeiter
               where $e/Name = "Scott"
               return
                      fn:translate($e/Gehalt, ",.€", ",")'
              passing liste
              returning content) as number) sal
     from dept_emp_xml
   where xmlexists('/Mitarbeiterliste/Mitarbeiter[Name = "Scott"]'
             passing liste);


       SAL
----------
      3000

Code-Beispiel: Verwendung der Funktion xmlcast

In diesem Beispiel habe ich einmal alles zusammengenommen: xmlcast, xmlquery und xmlexists. Die Abfrage selbst ist wohl eher nicht so unverständlich, allerdings könnten doch einige Probleme auftauchen, wenn Sie diese Abfrage selber nachvollziehen. Fallstricke sind hier folgende:

  • der Wert der Parameter NLS_TERRITORY, aber auch NLS_NUMERIC_CHARACTERS
  • die Zeichensatzkodierung der Datenbank (bezüglich des Euro-Zeichens)

Solche Probleme können einem schon den letzten Nerv rauben ...

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Oracle & XML bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © Rheinwerk Verlag, Bonn 2013
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Oracle SQL: Das umfassende Handbuch" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, www.rheinwerk-verlag.de, service(at)rheinwerk-verlag.de