Einführendes Tutorium zu SQLX (Version 2.3 vom 24.2.2015) Einleitung In den folgenden Lektionen sollen dir die Grundzüge von SQLX näher gebracht werden. SQLX ist der Standard um aus relationalen Datenbeständen und SQL-Anfragen XML zu generieren. Die Datengrundlage bildet die Mondial Datenbank [1]. Die Übungen werden mithilfe der Oracle Implementierung von SQLX durchgeführt. Die zugrundeliegenden Daten der Mondial-Datenbank findest du im MONDIAL-Schema der Oracle Datenbank an der Hochschule. Du kannst die Übungen mittels SQLPLUS oder dem SQLDeveloper durchführen. Übung 1 - Elementares SQLX: Setze folgendes Stament ab: select xmlelement("land", name) from mondial.country; Das Ergbnis sollte aus 195 Datensätzen bestehen, die jeweils das Root-Element name besitzen und innerhalb des Elements sich ein Textknoten bedindet, der dem Namen des jeweiligen Landes entspricht. Als nächstes soll das SQLX Statement dahingehend erweitert werden, dass die Landes-ID als Attribut im name-element ausgegeben werden soll. Dazu benuten wir die Funktion xmlattribues(...), der eine Liste von Spalten (Komma separiert) mit optionalen Aliasnamen übergeben werden kann. Im konkreten Fall übergeben wir nur eine Spalte (code) aus der Tabelle city und sagen, dass das XML-Attribut den Namen ID tragen soll: select xmlelement("land", from country; xmlattributes(code as "id"), name) Spiel ein wenig mit dem Statement herum indem du weitere Attribute hinzufügst (mit und ohne Aliase). Als nächstes sollen weitere Informationen zu einem Land ausgegeben werden. Wir erweitern unsere Struktur deshalb um weitere Unterelemente name und einwohner. Dies geschieht einfach durch den Aufruf weiterer xmlelement-funktionen innerhalb der bereits existierenden xmlelement-aufrufs: Tutorium-1/12
select xmlelement("land", from mondial.country; xmlattributes(code as "id"), xmlelement("name", name), xmlelement("einwohnmer", population)) Wie man sieht, kann man an xmlelement() eine beliebige Anzahl weiterer Parameter übergeben. Eine alternative Kurzform wäre die folgende mit dem Sprachelement xmlforest(): select xmlelement("land", from mondial.country; xmlattributes(code as "id"), population as "einwohner")) Im Gegensatz zu xmlelement() nimmt xmlforest() eine Liste von Spalten mit optionalen Aliasnamen entgegen und gibt diese als Unterelemente des auf nächst höherer Ebene angegebenen Elements aus. Worin besteht die Einschränkng von xmlforest() im Vergleich zu xmlelement()? Als nächstes wollen wir jetzt Informationen zu den Provinzen der einzelnen Länder ausgeben. Dazu basteln wir uns in einem vorbereitenden Schritt ein SQLX-Statement, das XML-Datensätze mit folgender Struktur zurückliefert: <!ELEMENT provinz (#PCDATA)> Analog zum Vorgehen bei den Ländern sieht unser Statement wiefolgt aus (konkret geben wir alle Provinzen von Deutschland (D) aus: select xmlelement("provinz", from mondial.province where country='d'; population as "einwohner")) Tutorium-2/12
Das Ergebnis sollte in etwa wiefolgt aussehen: <provinz><name>sachsen Anhalt</name><einwohner>2759213 </einwohner> </provinz>... <provinz><name>thuringen</name><einwohner>2517776</einwohner> </provinz> 16 rows selected Der Versuch das soeben gebastelte Statement in unser erstes Statement einzubauen sieht dann wiefolgt aus (wir ersetzen die hardcodierte Länderkennung D durch die betreffende Spalte der äußeren SQL-Anfrage und erhalten somit eine korrelierte Subquery, die für jeden Datensatz der äußeren SQL-Anweisung erneut ausgeführt werden muss) : select xmlelement("land", from mondial.country; xmlattributes(code as "id"), population as "einwohner"), (select xmlelement("provinz", ) population as "einwohner")) from mondial.province where country=country.code) Das Ausführen des Statements liefert aber nicht das gewünsche Ergebnis sondern die folgende Fehlermeldung: Fehler beim Start in Zeile 1 in Befehl: Tutorium-3/12
... from mondial.country Fehlerbericht: SQL-Fehler: ORA-01427: Unterabfrage für eine Zeile liefert mehr als eine Zeile 01427. 00000 - "single-row subquery returns more than one row" *Cause: *Action: Ok, was will Oracle uns sagen: "single-row subquery returns more than one row" - wir haben nur eine Subquery (die haben wir grad eingebaut) und die liefert uns mehr wie einen Datensatz zurück - klar, Deutschland hat ja auch mehr wie ein Bundesland. Wenn er das nicht will, dann kriegt er halt nur ein Ergebnis. Wir müssen nur die 16 Datensätze die wir zurückzurückgeliefert bekommen zusammenfassen und das machen wir mit der SQLX-Funktion xmlagg(...): Das Statement das uns zuvor alle 16 Bundesländer zurückgeliefert hat sieht jetzt dann so aus: Ergebnis: select xmlagg( xmlelement("provinz", population as "einwohner")) ) from mondial.province where country='d' XMLAGG(XMLELEMENT("PROVINZ",XMLFOREST(NAMEAS"... -------------------------------------------------- <provinz><name>berlin</name><einwohner>3472009</ einwohner></provinz><provinz><name>hamburg</ name><einwohner>1705872</einwohner></provinz>... 1 rows selected Tutorium-4/12
Entscheidend ist hier die Tatsache, dass jetzt nicht mehr 16 Datensätze sondern nur noch ein Datensatz zurückgeliefert wird. Jetzt setzen wir das gerade eben entwickelte Statement in das erste Statement ein (und schränken die Ausgabe im äußeren SQLX-Statement auf Frankreich ein, indem wir in der where-klausel country.code= F fordern) und das Ergebnis sollte wiefolgt aussehen 1 : Ok, wir sehen zwar auf Anhieb keine Provinzen, aber die Länge des horizontalen Scrollbalken lässt hoffen... ;-) Wenn wir die Zeile jetzt mittels Copy & Paste in ein neues XML-Dokument von oxygen kopieren und das ganze dann mittels dem - Button formatieren, sieht das Ganze in etwa wiefolgt aus (alternativ kannst du auch die Funktion scan0004.format_xml(xmltype) anwenden): <land id="f"> <name>france</name> <einwohner>58317450</einwohner> <provinz> <name>alsace</name> <einwohner>1624000</einwohner> </provinz> <provinz> <name>aquitaine</name> <einwohner>2796000</einwohner> </provinz>... </land> aaaah- sehr schön ;-)!!!! 1. wenn ihr sqlplus benutzt, dann solltet ihr, damit die Ausgaben nicht abgeschnitten wird die folgenden Statements zuvor absetzen: set pagesize 0 set long 320000 Tutorium-5/12
Jetzt werden wir übermütig und probieren innerhalb der einzelnen Provinzen auch noch die passenden Städte einzubauen - das ganze soll dann beispielhaft für die Provinz Rhone Alpes so aussehen: <provinz> <name>rhone Alpes</name> <einwohner>5351000</einwohner> <staedte> <name>grenoble</name> <name>saint Etienne</name> <name>lyon</name> <name>villeurbanne</name> </staedte> </provinz> Das Vorgehen ist dabei analog zum Vorgehen beim Einbau der Provinzen innerhalb der einzelnen Länder: 1. Erstelle ein SQLX-Statement, das die Städte einer Provinz (Frankreich, Rhone Alpes) ausgibt (n-datensätze). 2. Fasse die n-datensätze mit xmlagg() zusammen. 3. Nimm das Statement und bau es in das zuvor entwickelte Statement ein. Ersetze dabei die hardkodierte Information von Rhone Alpes durch die Information der entsprechenden Provinz (Das Statement muss beim Einbau zusätzlich geklammert (... ) werden). Als nächstes soll nun noch die Information über die Hauptstadt eines Landes mit aufgenommen werden. Die Struktur soll dann wiefolgt sein: <!ELEMENT land (name, einwohner, hauptstadt, provinz*)> <!ELEMENT hauptstadt (#PCDATA)> Integriere diese Information noch in dein Statement. Anschließend nimm noch die Information über die Provinz in der sich die Hauptstadt befindet als XML-Attribut in das Element hauptstadt mit auf. Tutorium-6/12
Übung 2 - elementares SQLX: In der folgenden Übung soll ein etwas anderer Ansatz für eine mögliche Vorgehensweise zum Aufbau eines SQLX-Statements gezeigt werden. Konkret soll für alle Länder die Berge aufgelistet werden. Da ein SQLX-Statement nichts anderes ist als ein SQL-Statement mit SQLX-Funktionsaufrufen in der select-klausel können wir in einem ersten Schritt auch einfach mal das SQL-Statement formulieren das uns die notwendigen Informationen zurückliefert und erst dann die Formatierung in XML vornehmen. Die Informationen über die Länder liegen in der Tabelle mondial.country, über die Berge in mondial.mountain und die Beziehung zwischen Land und Berg ist in der beziehungstabelle geo_mountain festgehelten. Das SQL-Statement lautet demnach wiefolgt 1 : select distinct c.name, m.name, m.height from mondial.country c join mondial.geo_mountain gm on c.code=gm.country join mondial.mountain m on gm.mountain=m.name order by c.name; Ok, das sieht in der Ausgabe soweit gut aus, machen wir jetzt mal XML draus. Beginnen wir im folgenden von innen heraus, d.h. wir pressen zuerst die Berge in XML: select xmlelement("berg", m.name) from mondial.country c join mondial.geo_mountain gm on c.code=gm.country join mondial.mountain m on gm.mountain=m.name Das Ergebnis sieht in etwa wiefolgt aus: 1. Das distinct Kommando ist dehalb notwendig, da in der Tabelle geo_mountain die Zuordnung eines Berges zu einer Provinz (und nicht nur zum Land) abgelegt ist. Gehört jetzt ein Berg zu zwei Landesteilen desselben Landes würde er zweimal im Ergebnis auftauchen. Tutorium-7/12
XMLELEMENT("BERG",M.NAME) ------------------------------------------ <berg>mt McKinley</berg> <berg>jezerce</berg> <berg>korab</berg> <berg>mt Aylmer</berg>... 102 rows selected Ok, und jetzt fassen wir die Berge mittels xmlagg(...) zusammen: Ergebnis: select xmlagg(xmlelement("berg", m.name)) from mondial.country c join mondial.geo_mountain gm on c.code=gm.country join mondial.mountain m on gm.mountain=m.name XMLAGG(XMLELEMENT("BERG",M.NAME)) -------------------------------------------------- <berg>mt McKinley</berg><berg>Jezerce</berg>\ <berg>korab</berg><berg>mt Aylmer</berg><berg>\ Tahat</berg><berg>Huascaran</berg><berg>Humphreys\ Peak</berg>... 1 rows selected Noch nicht ganz was wir wollen, wir wollen nur die berge zusammengefasst haben, die in einem Land liegen, das geht mit der group by-klausel: select xmlagg(xmlelement("berg", m.name)) from mondial.country c Tutorium-8/12
Ergebnis: join mondial.geo_mountain gm on c.code=gm.country join mondial.mountain m on gm.mountain=m.name group by c.name <berg>feldberg</berg><berg>zugspitze</ berg><berg>brocken</berg><berg>grosser Arber\ </berg> <berg>oeraefajoekull</berg><berg>snoefell</berg> <berg>kinabalu</berg><berg>semeru</berg>\ <berg>leuser</berg> <berg>sabalan</berg><berg>damavand</berg>... 44 rows selected Aja, das sieht schon ganz gut aus, es scheint so, dass in 44 Ländern Berge rumstehen ;-). Und jetzt die Information über das Land noch drumherum gebastelt: select xmlelement("land", xmlelement("name", c.name), xmlagg(xmlelement("berg", m.name))) from mondial.country c join mondial.geo_mountain gm on c.code=gm.country join mondial.mountain m on gm.mountain=m.name group by c.name; Ok, jetzt siehts gut aus, so wollten wirs haben - ferdisch!!! Tutorium-9/12
Übung 3 - SQLX und Views: Der Returnwert einer SQLX-Funktion ist immer eine Instanz vom Typ XMLType, dem Typ der für die Speicherung von XML-Datensätzen in einer Oracle-Datenbank eingesetzt wird. Im folgenden soll nun ein View erstellt werden, der pro Datensatz eine Provinz enthält. Die DTD soll wiefolgt sein: <!ELEMENT provinz (name, einwohner, hauptstadt)> <!ATTLIST province provinz country CDATA #REQUIRED> <!ELEMENT name (#PCDATA)> <!ELEMENT einwohner (#PCDATA)> <!ELEMENT hauptstadt (#PCDATA)> Im konkreten Fall sehen die einzelnen Datensätze dann so aus: <provinz country="f"> <name>auvergne</name> <einwohner>1321000</einwohner> <hauptstadt>clermont Ferrand</hauptstadt> </provinz> Das zugehörige SQLX-Statement lautet dann wiefolgt: select xmlelement("provinz", xmlattributes(country as "country"), population as "einwohner", capital as "hauptstadt")) from mondial.province; Entsprechend lautet dann die Konstruktionsvorschrift für den View: Tutorium-10/12
create or replace view provinces_xml (xml_document) as select xmlelement("provinz", xmlattributes(country as "country"), population as "einwohner", capital as "hauptstadt")) from mondial.province; Um jetzt alle Provinzen eines Landes (z.b. Frankreich) im XML-Format zu bekommen, kann man wiefolgt anfragen: select xml_document from provinces_xml where extractvalue(xml_document, '/provinz/@country')='f'; Dabei erwartet die Funktion extractvalue(...) als ersten Parameter eine Instanz vom Typ XMLType und als zweiten Parameter einen XPath Ausdruck. Ergebnis ist der durch den XPath Ausdruck detektierte skalare Wert der XML- Type-Instanz. Konkret werden somit in der where-klausel alle die XML-Datensätze selektiert, deren Wert für das Attribut country im Element provinz F ist. Hinweis: Aus Effizienzgründen wird das XML-Dokument in Oracle ohne überflüssige Leerzeichen generiert. Allerdings ist das XML-Dokument so schwer zu lesen. Soll das SQL-Dokument sauber formatiert (àla oxygen-formatierung) ausgegeben werden, so kann die Funktion scan0004.format_xml(...) benutzt werden (siehe auch Beispielskript 10 zur Vorlesung v33-xml-sqlx). Beispiel: Tutorium-11/12
select scan0004.format_xml( xmlelement("cities", xmlagg(xmlelement("city", xmlforest(name, population)))) ) from mondial.city where country='dk'; Literatur: [1] Wolfgang May, Information Extraction and Integration with Florid: The Mondial Case Study, Universität Freiburg, Institut für Informatik, 1999, Daten: http://dbis.informatik.uni-goettingen.de/mondial Tutorium-12/12