13.02.2014 Aufrufe

PL/SQL- Funktionen von SQL aus aufrufen - beim O'Reilly Verlag

PL/SQL- Funktionen von SQL aus aufrufen - beim O'Reilly Verlag

PL/SQL- Funktionen von SQL aus aufrufen - beim O'Reilly Verlag

MEHR ANZEIGEN
WENIGER ANZEIGEN

Erfolgreiche ePaper selbst erstellen

Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.

In diesem Kapitel:<br />

• Das Problem<br />

• Die Syntax für den Aufruf<br />

gespeicherter <strong>Funktionen</strong> in<br />

<strong>SQL</strong><br />

• Bedingungen an gespeicherte<br />

<strong>Funktionen</strong> in <strong>SQL</strong><br />

• Einschränkungen <strong>von</strong><br />

<strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong><br />

• <strong>Funktionen</strong> in Packages <strong>von</strong><br />

<strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

• Die Präzedenz <strong>von</strong> Spaltenund<br />

Funktionsnamen<br />

• Die rauhe Wirklichkeit:<br />

<strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong><br />

<strong>aufrufen</strong><br />

• Beispiele eingebetteten<br />

<strong>PL</strong>/<strong>SQL</strong>-Codes<br />

17<br />

<strong>PL</strong>/<strong>SQL</strong>-<br />

<strong>Funktionen</strong> <strong>von</strong><br />

<strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

<strong>PL</strong>/<strong>SQL</strong> ist eine prozedurale Spracherweiterung zu <strong>SQL</strong>. Daher können Sie <strong>aus</strong> Ihren<br />

<strong>PL</strong>/<strong>SQL</strong>-Programmen auch native <strong>SQL</strong>-Anweisungen wie SELECT, INSERT oder UPDATE<br />

<strong>aufrufen</strong>. Bis zur Version 2.1 <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> (die zur Oracle-Datenbank 7.1 gehört) konnten<br />

Sie allerdings keine eigenen <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in einer <strong>SQL</strong>-Anweisung verwenden.<br />

HINWEIS<br />

Die in diesem Kapitel beschriebenen Möglichkeiten stehen erst ab <strong>PL</strong>/<strong>SQL</strong> Version<br />

2.1 zur Verfügung.<br />

Das Problem<br />

Die fehlende Möglichkeit, <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong>-Anweisungen verwenden zu<br />

können, führte oft zu häßlichen <strong>SQL</strong>-Anweisungen und redundanten Implementierungen<br />

<strong>von</strong> Geschäftsregeln. Nehmen wir beispielsweise an, Sie müssen das Gesamtgehalt<br />

eines Mitarbeiters sowohl in nativem <strong>SQL</strong> als auch in Ihren Formularen berechnen. Die<br />

Berechnung selbst ist einfach genug:<br />

Total compensation = salary + bonus<br />

Meine <strong>SQL</strong>-Anweisung würde diese Formel enthalten<br />

SELECT employee_name, salary + NVL (bonus, 0)<br />

FROM employee;<br />

579


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

während mein Post-Query-Trigger in meiner Oracle Forms-Anwendung den folgenden<br />

<strong>PL</strong>/<strong>SQL</strong>-Code verwenden würde:<br />

:employee.total_comp := :employee.salary + NVL (:employee.bonus, 0);<br />

In diesem Fall ist die Berechnung sehr einfach, aber trotzdem müssen Sie all diese hartkodierten<br />

Berechnungen sowohl in den <strong>SQL</strong>-Anweisungen als auch in den Anwendungskomponenten<br />

im Frontend einzeln ändern, wenn sich die Formel zur Berechnung<br />

des Gesamtgehalts <strong>aus</strong> irgendeinem Grunde ändern sollte.<br />

Sehr viel besser ist es, eine Funktion zu schreiben, die das Gesamtgehalt zurückgibt:<br />

FUNCTION total_comp<br />

(salary_in IN employee.salary%TYPE, bonus_in IN employee.bonus%TYPE)<br />

RETURN NUMBER<br />

IS<br />

BEGIN<br />

RETURN salary_in + NVL (bonus_in, 0);<br />

END;<br />

Diese Formel kann ich dann folgendermaßen in meinem Code einsetzen:<br />

SELECT employee_name, total_comp (salary, bonus)<br />

FROM employee;<br />

:employee.total_comp := total_comp (:employee.salary, :employee.bonus);<br />

Vor der Version 2.1 <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> führte das zu folgendem Fehler<br />

ORA-00919: invalid function<br />

weil es in <strong>SQL</strong> keinen Mechanismus gab, Referenzen auf in der Datenbank abgespeicherte,<br />

vom Programmierer definierte <strong>Funktionen</strong> aufzulösen. Inzwischen ist die Verbindung<br />

zwischen <strong>PL</strong>/<strong>SQL</strong> und <strong>SQL</strong> aber <strong>aus</strong>geglichener. Das ist auch sinnvoll so, denn<br />

die <strong>Funktionen</strong> werden ohnehin in der Datenbank abgelegt (wie nicht anders zu erwarten,<br />

in einer Tabelle) und stehen daher der <strong>SQL</strong>-Schicht über eine SELECT-Anweisung<br />

zur Verfügung.<br />

Seit der Version 2.1 <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> können Sie jetzt an jeder Stelle in einer <strong>SQL</strong>-Anweisung,<br />

in der ein Ausdruck erlaubt ist, gespeicherte <strong>Funktionen</strong> <strong>aufrufen</strong>, darunter in den<br />

SELECT-, WHERE-, START WITH-, GROUP BY-, HAVING-, ORDER BY-, SET- und<br />

VALUES-Kl<strong>aus</strong>eln (weil gespeicherte Prozeduren selbst <strong>aus</strong>führbare <strong>PL</strong>/<strong>SQL</strong>-Anweisungen<br />

sind, können diese nicht in eine <strong>SQL</strong>-Anweisung eingebettet werden).<br />

Sie können Ihre eigenen <strong>Funktionen</strong> gen<strong>aus</strong>o wie die Built-in- <strong>SQL</strong>-<strong>Funktionen</strong><br />

wie etwa TO_DATE, SUBSTR oder LENGTH verwenden. Auf der Begleitdiskette<br />

ist ein Package namens ps.parse (bestehend <strong>aus</strong> den Dateien psparse.sps und<br />

psparse.spb) enthalten, in dem es eine Funktion gibt, welche die Anzahl der Atome<br />

(Worte und/oder Begrenzungszeichen) in einem String zurückgibt. Das kann ich direkt<br />

in einer <strong>SQL</strong>-Anweisung verwenden, um die Verteilung der Worte in einer Reihe <strong>von</strong><br />

Textnotizen anzuzeigen:<br />

580


Das Problem<br />

SELECT line_number,<br />

ps_parse.number_of_atomics (line_text) AS num_words<br />

FROM notes<br />

ORDER BY num_words DESC;<br />

Beachten Sie, daß ich in diesem Fall dem Funktionsaufruf mit der »AS«-Syntax einen<br />

Spaltenalias zugewiesen habe. Diesen kann ich dann in ORDER BY verwenden, ohne<br />

die Syntax des Funktionsaufrufs selbst erneut angeben zu müssen.<br />

Die Möglichkeit, programmiererdefinierte <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> zu verwenden, ist<br />

eine sehr mächtige Erweiterung der Oracle-Entwicklungsumgebung. Mit diesen <strong>Funktionen</strong><br />

können Sie folgendes tun:<br />

• Die Logik <strong>von</strong> Geschäftsregeln in einer kleinen Anzahl optimierter und leicht wartbarer<br />

<strong>Funktionen</strong> zusammenfassen. Sie müssen diese Logik nicht über die einzelnen<br />

<strong>SQL</strong>-Anweisungen und <strong>PL</strong>/<strong>SQL</strong>-Programme verteilen. Dieser Punkt ist wahrscheinlich<br />

der weitreichendste und wichtigste Vorteil bei der Verwendung <strong>von</strong><br />

<strong>Funktionen</strong>.<br />

• Die Performanz Ihrer <strong>SQL</strong>-Anweisungen verbessern. <strong>SQL</strong> ist eine nichtprozedurale<br />

Sprache, aber die Anforderungen der Anwendung bedingen doch oft prozedurale<br />

Logik in Ihrem <strong>SQL</strong>-Code. <strong>SQL</strong> ist robust genug, so daß Sie immer irgendwie an die<br />

Antwort kommen können, aber in manchen Situationen ist das zu ineffizient. Eingebettetes<br />

<strong>PL</strong>/<strong>SQL</strong> kann den Job manchmal schneller erledigen. Natürlich kostet es<br />

auch ein wenig, diese <strong>Funktionen</strong> aufzurufen, weswegen Sie sorgfältig abwägen<br />

und <strong>aus</strong>probieren müssen, wo und wann <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> am meisten<br />

nützen.<br />

• Ihre <strong>SQL</strong>-Anweisungen vereinfachen. All die Gründe, <strong>aus</strong> denen sie <strong>PL</strong>/<strong>SQL</strong>-Code<br />

verwenden sollten, gelten auch für <strong>SQL</strong>, insbesondere wenn es darum geht, komplizierte<br />

Ausdrücke und komplizierte Logik hinter einer Funktionsspezifikation zu<br />

verstecken. Von der DECODE-Anweisung bis zu verschachtelten, korrelierten Subselects<br />

können programmiererdefinierte <strong>Funktionen</strong> die Lesbarkeit vieler <strong>SQL</strong>-<br />

Anweisungen verbessern.<br />

• Aktionen in <strong>SQL</strong> durchführen, die sonst unmöglich wären. <strong>SQL</strong> ist eine Sprache,<br />

die immer eine Menge zur Zeit bearbeitet, die <strong>aus</strong> Zeilen besteht, auf die dann<br />

irgendwelche Aktionen angewendet werden. Iterative Verarbeitung über einzelne<br />

Spaltenwerte ist nicht möglich. Wenn Sie beispielsweise die Anzahl der Vorkommnisse<br />

eines Substrings in den Namen <strong>von</strong> Firmen ermitteln wollen, dann geht das<br />

mit reinem <strong>SQL</strong> nicht. Sie können aber in einer SELECT-Liste eine <strong>PL</strong>/<strong>SQL</strong>-Funktion<br />

über den Firmennamen laufen lassen, der diese Art <strong>von</strong> iterativer Verarbeitung<br />

durchführen kann.<br />

Sie können <strong>Funktionen</strong> in einer VALUES-Liste, einer SET-Kl<strong>aus</strong>el oder einer GROUP<br />

BY-Kl<strong>aus</strong>el verwenden. Hier folgen ein paar Beispiele dazu:<br />

• VALUES-Liste. Schauen Sie sich das folgende Beispiel an:<br />

INSERT INTO notes<br />

(call_id, line_text, line_number)<br />

581


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

VALUES<br />

(:call.call_id, :note.text, next_line_number (:call.call_id));<br />

Die Funktion next_line_number holt die nächste laufende Nummer der Notizen<br />

zu diesem Vorfall (Sie könnten zwar einen Folgenummerngenerator verwenden,<br />

um die nächste eindeutige Vorfallnummer zu erhalten, aber die line_number für<br />

Notizen fängt immer wieder bei 1 an, so daß man hier keinen Folgenummerngenerator<br />

verwenden kann).<br />

• SET-Kl<strong>aus</strong>el. Die Funktion max_compensation gibt das höchstmögliche Gehalt in<br />

der Abteilung eines Angestellten wieder.<br />

UPDATE employee SET salary = max_compensation (department_id)<br />

WHERE employee_id = 1005;<br />

In diesem Fall ersetzt die Funktion max_compensation ein Subselect, das die<br />

Funktion AVG verwenden würde, um den Wert zu berechnen.<br />

• GROUP BY-Kl<strong>aus</strong>el. Meine Firma hat die Vereinheitlichung der Titel nicht besonders<br />

gut hinbekommen; es gibt 15 verschiedene Versionen <strong>von</strong> stellvertretenden<br />

Geschäftsführern, 20 verschiedene Manager usw. Die folgende SELECT-Anweisung<br />

räumt mit dieser Verwirrung auf und zeigt das Gesamtgehalt für jede Job-»Kategorie«<br />

an.<br />

SELECT job_category (job_title_id) as title, SUM (salary)<br />

FROM employee<br />

GROUP BY title;<br />

Die Funktion wird sowohl in der SELECT-Liste als auch in der GROUP BY-Kl<strong>aus</strong>el<br />

verwendet.<br />

Die Syntax für den Aufruf gespeicherter<br />

<strong>Funktionen</strong> in <strong>SQL</strong><br />

Eine gespeicherte Funktion wird <strong>aus</strong> einem <strong>SQL</strong>-Ausdruck mit der gleichen Syntax wie<br />

in einem <strong>PL</strong>/<strong>SQL</strong>-Ausdruck aufgerufen:<br />

[schema_name.][Package_name.][funktions_name[@db_link_name][parameter_liste]<br />

Hierbei ist schema_name der optionale Name des Datenbankschemas, in dem die Funktion<br />

definiert ist (üblicherweise Ihr Oracle-Benutzerkonto), Package_name ist der<br />

optionale Name des Packages, in dem die Funktion definiert ist (wenn es sich nicht um<br />

eine freistehende Funktion handelt), funktions_name ist der Name der Funktion,<br />

db_link_name ist der optionale Name der Datenbankverbindung, wenn Sie einen entfernten<br />

Prozeduraufruf (remote procedure call) <strong>aus</strong>führen, und parameter_liste ist<br />

die optionale Liste der Parameter der Funktion.<br />

Wenn die Funktion calc_sales folgendermaßen definiert ist<br />

582


Bedingungen an gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong><br />

FUNCTION calc_sales<br />

(company_id_in IN company.company_id%TYPE,<br />

status_in IN order.status_code%TYPE := NULL)<br />

RETURN NUMBER;<br />

dann kann diese Funktion <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> wie folgt aufgerufen werden:<br />

• Als freistehende Funktion:<br />

SELECT calc_sales (1001, 'O')<br />

FROM orders;<br />

• Als Funktion in einem Package:<br />

SELECT sales_pkg.calc_sales (1001, 'O')<br />

FROM orders;<br />

• Als entfernter Aufruf einer Funktion in einem Package:<br />

SELECT sales_pkg.calc_sales@NEW_YORK (1001, 'O')<br />

FROM orders;<br />

• Als freistehende Funktion in einem bestimmten Schema:<br />

SELECT scott.calc_sales (1001, 'O')<br />

FROM orders;<br />

<strong>SQL</strong> kann all diese Variationen korrekt parsen, aber Sie sollten es vermeiden, das<br />

Schema des Moduls und die Datenbankverbindung in Ihren <strong>SQL</strong>-Anweisungen hart zu<br />

kodieren (wie es im dritten und vierten Punkt gemacht wurde). Statt dessen sollten Sie<br />

Synonyme dafür anlegen, die diese Information verstecken. Damit müssen Sie nur das<br />

Synonym ändern, und nicht alle einzelnen <strong>SQL</strong>-Anweisungen, die diese Funktion <strong>aufrufen</strong>,<br />

wenn Sie jemals den Eigentümer der Funktion ändern oder zu einer anderen<br />

Datenbankinstanz wechseln sollten.<br />

Wenn Sie eine gespeicherte Funktion in einer <strong>SQL</strong>-Anweisung verwenden, dann müssen<br />

Sie die Zuordnung über Positionen verwenden, das Mischen der Zuordnung über<br />

den Namen und über die Position ist nicht erlaubt. Sie können calc_sales nur durch<br />

Angabe der beiden Argumente in der richtigen Reihenfolge <strong>aufrufen</strong>.<br />

Bedingungen an gespeicherte <strong>Funktionen</strong><br />

in <strong>SQL</strong><br />

Es gibt eine Reihe <strong>von</strong> Bedingungen, die eine programmiererdefinierte <strong>PL</strong>/<strong>SQL</strong>-Funktion<br />

erfüllen muß, damit sie <strong>von</strong> einer <strong>SQL</strong>-Anweisung <strong>aus</strong> aufgerufen werden kann.<br />

• Die Funktion muß in der Datenbank gespeichert sein. Eine Funktion, die in einer<br />

<strong>PL</strong>/<strong>SQL</strong>-Bibliothek <strong>von</strong> Oracle Developer/2000 oder einem einzelnen Formular<br />

gespeichert ist, kann nicht <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufen werden. <strong>SQL</strong> hat keine Möglichkeit,<br />

die Referenz auf eine solche Funktion aufzulösen.<br />

583


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

• Die Funktion muß eine zeilenspezifische Funktion sein, keine Spalten- oder Gruppenfunktion.<br />

Die Funktion kann nur auf eine einzelne Zeile mit Daten angewendet<br />

werden, nicht auf eine gesamte Spalte, die sich über mehrere Zeilen erstreckt.<br />

• Alle Funktionsparameter müssen den IN-Modus verwenden. Weder IN OUT- noch<br />

OUT-Parameter sind in gespeicherten <strong>Funktionen</strong> erlaubt, die <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufen<br />

werden sollen; aber Sie sollten ohnehin nie IN OUT- oder OUT-Parameter in<br />

<strong>Funktionen</strong> verwenden. Egal ob Sie die Funktion in einer <strong>SQL</strong>-Anweisung verwenden<br />

oder nicht, erzeugen solche Parameter Seiteneffekte zusätzlich zum Hauptzweck<br />

der Funktion, der ja die Rückgabe eines einzigen Wertes ist.<br />

• Die Datentypen der Parameter der Funktion wie auch der Datentyp der RETURN-<br />

Kl<strong>aus</strong>el müssen Oracle Server bekannt sein. Während alle Datentypen <strong>von</strong> Oracle<br />

Server in <strong>PL</strong>/<strong>SQL</strong> gültig sind, hat <strong>PL</strong>/<strong>SQL</strong> neue Datentypen hinzugefügt, die <strong>von</strong> der<br />

Datenbank (noch) nicht unterstützt werden. Zu diesen Datentypen gehören BOO-<br />

LEAN, BINARY_INTEGER, <strong>PL</strong>/<strong>SQL</strong>-Tabellen, <strong>PL</strong>/<strong>SQL</strong>-Datensätze und programmiererdefinierte<br />

Subtypen.<br />

• <strong>Funktionen</strong>, die in Packages definiert sind, müssen das Pragma RESTRICT_<br />

REFERENCES verwenden. Wenn Sie <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> eine in einem Package definierte<br />

Funktion <strong>aufrufen</strong> wollen, müssen Sie der Package-Spezifikation ein Pragma hinzufügen,<br />

das explizit angibt, daß diese Funktion zur Ausführung <strong>aus</strong> <strong>SQL</strong>-Anweisungen<br />

her<strong>aus</strong> zugelassen ist. Der Abschnitt »<strong>Funktionen</strong> in Packages <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> <strong>aus</strong><br />

<strong>aufrufen</strong>« enthält nähere Informationen hierzu.<br />

Die folgenden Funktionsspezifikationen würden zurückgewiesen werden, wenn sie in<br />

einer <strong>SQL</strong>-Anweisung verwendet würden:<br />

/* <strong>SQL</strong> weiss nichts ueber <strong>PL</strong>/<strong>SQL</strong>-Tabellen */<br />

TYPE string_tabtype IS TABLE OF VARCHAR2(30) INDEX BY BINARY_INTEGER;<br />

FUNCTION temp_table RETURN string_tabtype;<br />

/* <strong>SQL</strong> weiss nichts ueber Boolesche Werte */<br />

FUNCTION call_is_open (call_id_in IN call.call_id%TYPE) RETURN BOOLEAN;<br />

FUNCTION calc_sales<br />

(company_id_in IN NUMBER, use_closed_orders_in IN BOOLEAN)<br />

RETURN NUMBER;<br />

Einschränkungen <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong><br />

in <strong>SQL</strong><br />

Gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong> sind sehr mächtig. Aber Sie können sich vielleicht denken,<br />

daß diese Mächtigkeit auch die Möglichkeit zum Mißbrauch und die Notwendigkeit<br />

verantwortungsvollen Handelns mit sich bringt. Im Zusammenhang mit <strong>SQL</strong><br />

besteht der Mißbrauch in der Verwendung <strong>von</strong> Seiteneffekten in einer Funktion.<br />

Schauen Sie sich die folgende Funktion an:<br />

584


Einschränkungen <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong><br />

FUNCTION total_comp<br />

(salary_in IN employee.salary%TYPE, bonus_in IN employee.bonus%TYPE)<br />

RETURN NUMBER<br />

IS<br />

BEGIN<br />

UPDATE employee SET salary = salary_in / 2;<br />

RETURN salary_in + NVL (bonus_in, 0);<br />

END;<br />

Diese kleine Berechnung, die ich am Anfang des Kapitels schon eingeführt hatte, aktualisiert<br />

das Gehalt aller Mitarbeiter auf die Hälfte des angegebenen Wertes. Dieser Vorgang<br />

beeinflußt die Ergebnisse der Abfrage, <strong>aus</strong> der total_comp aufgerufen werden<br />

könnte, aber schlimmer noch, er beeinflußt alle anderen <strong>SQL</strong>-Anweisungen in dieser<br />

Sitzung.<br />

Neben der Modifikation <strong>von</strong> Datenbanktabellen ist die Modifikation <strong>von</strong> Package-Variablen<br />

ein anderer Seiteneffekt <strong>von</strong> gespeicherten <strong>Funktionen</strong> in <strong>SQL</strong>. Package-Variablen<br />

dienen in der jeweiligen Sitzung als globale Variablen. Eine Funktion, die eine Package-<br />

Variable verändert, könnte andere gespeicherte <strong>Funktionen</strong> oder Prozeduren beeinflussen,<br />

was wiederum die <strong>SQL</strong>-Anweisung beeinflußt, welche die gespeicherte Funktion<br />

verwendet.<br />

Eine <strong>PL</strong>/<strong>SQL</strong>-Funktion könnte auch einen Seiteneffekt in der WHERE-Kl<strong>aus</strong>el einer<br />

Abfrage verursachen. Der Anfrageoptimierer kann die Reihenfolge der Prädikate in der<br />

WHERE-Kl<strong>aus</strong>el umstellen, um die Anzahl der zu verarbeitenden Zeilen zu minimieren.<br />

Eine Funktion, die in dieser Kl<strong>aus</strong>el <strong>aus</strong>geführt wird, könnte also den Optimierungsprozeß<br />

der Anfrage unterlaufen.<br />

Ich würde ganz allgemein empfehlen, daß die Funktion sich ganz eng darauf beschränken<br />

sollte, einen Wert zu berechnen und zurückzugeben. Aber eine Empfehlung ist<br />

nicht genug, wenn es um die Integrität der Datenbank geht: Um sich gegen häßliche<br />

Seiteneffekte und unvorhersagbares Verhalten zu schützen, läßt der Oracle Server die<br />

folgenden Aktionen in Ihren gespeicherten <strong>Funktionen</strong> nicht zu:<br />

• Die gespeicherte Funktion darf keine Datenbanktabellen verändern. Sie kann<br />

keine INSERT-, DELETE- oder UPDATE-Anweisung <strong>aus</strong>führen.<br />

• Eine gespeicherte Funktion, die entfernt oder parallelisiert aufgerufen wird, darf<br />

weder lesend noch schreibend auf Package-Variablen zugreifen. Der Oracle Server<br />

erlaubt keine Seiteneffekte, welche die Grenzen der aktuellen Benutzersitzung<br />

überschreiten.<br />

• Eine gespeicherte Funktion kann die Werte <strong>von</strong> Package-Variablen nur dann<br />

ändern, wenn diese Funktion in einer SELECT-, VALUES- und SET-Kl<strong>aus</strong>el aufgerufen<br />

wird. Wenn diese Funktion in einer WHERE- oder GROUP BY-Kl<strong>aus</strong>el aufgerufen<br />

wird, kann sie keine Package-Variablen beschreiben.<br />

• Vor Oracle 8 konnten Sie nur sehr wenige Programme in Built-in Packages in einer<br />

<strong>von</strong> <strong>SQL</strong> <strong>aus</strong> verwendeten Funktion <strong>aufrufen</strong>. Zu den verbotenen Programmen<br />

bzw. Packages gehörten beispielsweise DBMS_OUTPUT.PUT_LINE, DBMS_PIPE und<br />

585


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

DBMS_<strong>SQL</strong>, um nur einige zu nennen. Das ist auch manchmal ganz gut so. Wenn<br />

Sie keine UPDATE-Anweisung <strong>aus</strong>führen dürfen, sollten Sie auch nicht über<br />

DBMS_<strong>SQL</strong> eine UPDATE-Operation »an der Zensur« vorbei einschleusen dürfen.<br />

Aber bei einigen anderen Packages sind diese Einschränkungen unnötig und nur<br />

vorhanden gewesen, weil Oracle diese Programme nicht aktiviert hatte. In Oracle8<br />

wurden einige dieser Einschränkungen aufgehoben. Sie können jetzt DBMS_<br />

OUTPUT.PUT_LINE <strong>aus</strong> einer <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufenen Funktion <strong>aufrufen</strong> und<br />

sogar Informationen durch Datenbank-Pipes schicken. 1<br />

• Vor Oracle8 konnten Sie RAISE_AP<strong>PL</strong>ICATION_ERROR nicht <strong>aus</strong> einer gespeicherten<br />

Funktion <strong>aufrufen</strong>.<br />

• In Oracle Server 7.3 können Sie keine <strong>PL</strong>/<strong>SQL</strong>-Tabellenmethoden (COUNT, FIRST,<br />

LAST, NEXT, PRIOR usw.) in einer gespeicherten Funktion verwenden, die <strong>von</strong><br />

<strong>SQL</strong> <strong>aus</strong> verwendet wird (das ist ein »bekannter Bug«, der in Oracle8 behoben<br />

wurde). Beispielsweise kann eine Funktion, die den folgenden Code enthält, in<br />

<strong>SQL</strong> nicht verwendet werden:<br />

DECLARE<br />

TYPE emptabtype IS TABLE of emp%ROWTYPE INDEX BY BINARY_INTEGER;<br />

emptab emptabtype;<br />

BEGIN<br />

IF emptab.COUNT > 0 THEN -- Wird in <strong>SQL</strong> zurueckgewiesen<br />

• Die gespeicherte Funktion darf kein anderes Modul (gespeicherte Prozedur oder<br />

Funktion) verwenden, das eine der obigen Regeln verletzt. Eine Funktion ist nur so<br />

rein wie alle Module, die sie selber aufruft.<br />

• Die gespeicherte Funktion darf keine Sicht verwenden, die eine der obigen Regeln<br />

verletzt. Eine Sicht ist eine gespeicherte SELECT-Anweisung; die SELECT-Anweisung<br />

der Sicht kann gespeicherte <strong>Funktionen</strong> verwenden.<br />

Wenn Ihre Funktion gegen eine dieser Regeln verstößt oder eine Funktion <strong>aus</strong> einem<br />

Package ist, in dem das Pragma RESTRICT_REFERENCES fehlt, bekommen Sie den<br />

gefürchteten Fehler ORA-06571:<br />

ORA-06571: Function TOTAL_COMP does not guarantee not to update database<br />

Wie im Abschnitt »Die rauhe Wirklichkeit: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> <strong>aufrufen</strong>«<br />

erwähnt, kann es manchmal sehr schwierig (oder schlichtweg unmöglich) sein, diesen<br />

Fehler zu vermeiden. In anderen Situationen gibt es aber einfache Lösungen (schauen<br />

Sie aber auf jeden Fall durch die obige Liste der Beschränkungen).<br />

1 Mein Buch Oracle Built-In Packages, 1998, O’Reilly & Associates, enthält eine umfassende Besprechung<br />

der <strong>Funktionen</strong> <strong>aus</strong> Packages, die <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> verwendet werden können.<br />

586


<strong>Funktionen</strong> in Packages <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

<strong>Funktionen</strong> in Packages <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

Wie in Kapitel 16, Packages, beschrieben, sind die Spezifikation und der Rumpf eines<br />

Packages getrennt <strong>von</strong>einander; eine Spezifikation kann (und muß) existieren, bevor<br />

der Rumpf definiert wird. Dieses Feature <strong>von</strong> Packages macht das Leben schwieriger,<br />

wenn es darum geht, <strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufzurufen. Wenn eine SELECT-Anweisung<br />

eine Funktion <strong>aus</strong> einem Package aufruft, dann ist nur die Information <strong>aus</strong> der<br />

Spezifikation des Packages verfügbar. Andererseits ist es aber der Inhalt des Package-<br />

Rumpfes, der festlegt, ob eine Funktion <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aus</strong>geführt werden darf. Als Konsequenz<br />

<strong>aus</strong> dieser Situation ergibt sich, daß Sie Code zu Ihrer Package-Spezifikation<br />

hinzufügen müssen, um eine Funktion <strong>aus</strong> einem Package <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufrufbar zu<br />

machen.<br />

Man spricht da<strong>von</strong>, daß Sie den »Reinheitsgrad« einer gespeicherten Funktion (der Grad,<br />

in dem die Funktion frei <strong>von</strong> Seiteneffekten ist) in der Package-Spezifikation »zusichern«<br />

müssen (assert the purity level). Der Oracle Server kann damit <strong>beim</strong> Kompilieren des<br />

Package-Rumpfes bestimmen, ob die Funktion diesen »Reinheitsgrad« erfüllt oder nicht.<br />

Erfüllt sie ihn nicht, wird ein Fehler gemeldet, und Sie stehen der manchmal gewaltigen<br />

Aufgabe gegenüber, her<strong>aus</strong>zufinden, wo und wie diese Verletzung vorkommt.<br />

Die Zusicherung des Reinheitsgrades einer Funktion geschieht mit dem Pragma<br />

RESTRICT_REFERENCES, das wir uns im nächsten Abschnitt ansehen werden.<br />

Das Pragma RESTRICT_REFERENCES<br />

Wie ich bereits erwähnt habe, ist ein Pragma eine Direktive für den <strong>PL</strong>/<strong>SQL</strong>-Compiler.<br />

Wenn Sie jemals eine programmiererdefinierte, benannte Ausnahme verwendet haben,<br />

dann sind Sie Ihrem ersten Pragma schon über den Weg gelaufen. Im Falle des Pragmas<br />

RESTRICT_REFERENCES teilen Sie dem Compiler den Reinheitsgrad mit, <strong>von</strong> dem Sie<br />

glauben, daß ihn Ihre Funktion mindestens erreicht.<br />

Jede Funktion im Package, die Sie in einer <strong>SQL</strong>-Anweisung verwenden wollen, benötigt<br />

ein eigenes Pragma, das nach der Funktionsdeklaration in der Package-Spezifikation<br />

stehen muß (im Package-Rumpf muß das Pragma dagegen nicht angegeben werden).<br />

Um den Reinheitsgrad mit dem Pragma zuzusichern, verwenden Sie die folgende Syntax:<br />

PRAGMA RESTRICT_REFERENCES<br />

(funktions_name, WNDS [, WNPS] [, RNDS] [, RNPS])<br />

Hierbei ist funktions_name der Name der Funktion, deren Reinheitsgrad Sie zusichern<br />

wollen. Die vier verschiedenen Codes haben die folgenden Bedeutungen:<br />

WNDS<br />

Writes No Database State. Sichert zu, daß die Funktion keine Datenbanktabellen<br />

verändert.<br />

WNPS<br />

Writes No Package State. Sichert zu, daß die Funktion keine Package-Variablen verändert.<br />

587


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

RNDS<br />

Reads No Database State. Sichert zu, daß die Funktion nicht <strong>aus</strong> Datenbanktabellen<br />

liest.<br />

RNPS<br />

Reads No Package State. Sichert zu, daß die Funktion nicht <strong>aus</strong> Package-Variablen<br />

liest.<br />

Nur WNDS muß im Pragma stehen. Das ist auch konsistent mit der Beschränkung, daß<br />

gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong> keine UPDATE-, INSERT- oder DELETE-Anweisung<br />

<strong>aus</strong>führen dürfen. Alle anderen Zusicherungen sind optional. Sie können Sie in beliebiger<br />

Reihenfolge aufführen, aber WNDS muß immer dabei sein. Kein Argument folgt <strong>aus</strong><br />

einem der anderen. Ich kann in die Datenbank schreiben, ohne <strong>aus</strong> ihr zu lesen, und<br />

<strong>aus</strong> einer Package-Variable lesen, ohne darauf zu schreiben.<br />

Hier ein Beispiel für zwei verschiedene Zusicherungen der Reinheit <strong>von</strong> <strong>Funktionen</strong> im<br />

Package company_financials:<br />

PACKAGE company_financials<br />

IS<br />

FUNCTION company_type (type_code_in IN VARCHAR2)<br />

RETURN VARCHAR2;<br />

FUNCTION company_name (company_id_in IN company.company_id%TYPE)<br />

RETURN VARCHAR2;<br />

PRAGMA RESTRICT_REFERENCES (company_type, WNDS, RNDS, WNPS, RNPS);<br />

PRAGMA RESTRICT_REFERENCES (company_name, WNDS, WNPS, RNPS);<br />

END company_financials;<br />

In diesem Package liest die Funktion company_name <strong>aus</strong> der Datenbank, um den<br />

Namen der angegebenen Firma zu ermitteln. Beachten Sie, daß ich beide Pragmas an<br />

das Ende der Package-Spezifikation gestellt habe, die Pragmas müssen den Funktionsspezifikationen<br />

nicht unmittelbar folgen. Ich habe mir außerdem die Mühe gemacht, die<br />

Argumente WNPS und RNPS bei beiden <strong>Funktionen</strong> anzugeben. Oracle Corporation<br />

empfiehlt, immer den höchstmöglichen Reinheitsgrad anzugeben, so daß der Compiler<br />

die Funktion nie unnötig zurückweist.<br />

HINWEIS<br />

Wenn eine Funktion, die Sie <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong> wollen, eine Prozedur in einem<br />

Package aufruft, dann muß auch für diese Prozedur das Pragma RESTRICT_<br />

REFERENCES angegeben werden. Sie können die Prozedur zwar nicht direkt <strong>von</strong><br />

<strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong>, aber wenn sie indirekt <strong>von</strong> <strong>SQL</strong> aufgerufen werden soll, dann<br />

muß sie sich ebenfalls an die Regeln halten.<br />

Fehler bei Pragmaverstößen<br />

Wenn Ihre Funktion gegen ihr Pragma verstößt, wird der Fehler <strong>PL</strong>S-00542 gemeldet.<br />

Wenn der Rumpf des Packages company_financials folgendermaßen <strong>aus</strong>sieht<br />

588


<strong>Funktionen</strong> in Packages <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

CREATE OR RE<strong>PL</strong>ACE PACKAGE BODY company_financials<br />

IS<br />

FUNCTION company_type (type_code_in IN VARCHAR2)<br />

RETURN VARCHAR2<br />

IS<br />

v_sal NUMBER;<br />

BEGIN<br />

SELECT sal INTO v_sal FROM emp WHERE empno = 1;<br />

RETURN 'bigone';<br />

END;<br />

FUNCTION company_name (company_id_in IN company.company_id%TYPE)<br />

RETURN VARCHAR2<br />

IS<br />

BEGIN<br />

UPDATE emp SET sal = 0;<br />

RETURN 'bigone';<br />

END;<br />

END company_financials;<br />

/<br />

dann bekomme ich <strong>beim</strong> Kompilieren den folgenden Fehler:<br />

3/4 <strong>PL</strong>S-00452: Subprogram 'COMPANY_TYPE' violates its associated pragma<br />

Der Grund dafür ist, daß die Funktion company_type <strong>aus</strong> der Datenbank liest, obwohl<br />

ich den Reinheitsgrad RNDS angegeben habe. Auch wenn ich diese dumme SELECT-<br />

Anweisung entferne, bekomme ich immer noch den folgenden Fehler<br />

11/4 <strong>PL</strong>S-00452: Subprogram 'COMPANY_NAME' violates its associated pragma<br />

weil die Funktion company_name die Daten in der Datenbank aktualisiert, obwohl ich<br />

WNDS angegeben habe. Manchmal werden Sie auf Ihre Funktion starren und sagen, »He,<br />

diesmal verletzte ich den angegebenen Reinheitsgrad wirklich nicht. Hier ist ja gar kein<br />

UPDATE, DELETE oder INSERT«. Vielleicht haben Sie recht. Aber es ist nicht unwahrscheinlich,<br />

daß Sie ein Built-in Package aufgerufen oder auf andere Weise gegen die<br />

Regeln verstoßen haben.<br />

Zusichern des Reinheitsgrades im Initialisierungsabschnitt des<br />

Packages<br />

Wenn Ihr Package einen Initialisierungsabschnitt (<strong>aus</strong>führbare Anweisungen nach einer<br />

BEGIN-Anweisung im Package-Rumpf) enthält, dann müssen Sie auch den Reinheitsgrad<br />

dieses Abschnitts zusichern. Der Initialisierungsabschnitt wird automatisch <strong>aus</strong>geführt,<br />

wenn das erste Mal auf ein Objekt <strong>aus</strong> dem Package zugegriffen wird. Wenn eine<br />

Funktion <strong>aus</strong> einem solchen Package also in einer <strong>SQL</strong>-Anweisung <strong>aus</strong>geführt wird,<br />

kann sie die Ausführung dieses Codes bewirken. Wenn der Initialisierungsabschnitt<br />

Package-Variablen oder Daten in der Datenbank modifiziert, dann muß dies dem Compiler<br />

über das Pragma mitgeteilt werden.<br />

589


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

Sie können den Reinheitsgrad des Initialisierungsabschnitts entweder explizit oder<br />

implizit zusichern. Für die explizite Zusicherung verwenden Sie die folgende Variation<br />

des Pragmas RESTRICT_REFERENCES:<br />

PRAGMA RESTRICT_REFERENCES<br />

(Package_name, WNDS, [, WNPS] [, RNDS] [, RNPS])<br />

Anstelle des Namens der Funktion geben Sie hier den Namen des Packages selbst an,<br />

worauf wieder die Zustandsargumente folgen. Im folgenden Beispiel sichere ich nur<br />

WNDS und WNPS zu, weil der Initialisierungsabschnitt Daten <strong>aus</strong> der Konfigurationstabelle<br />

und <strong>aus</strong> einer globalen Variable eines anderen Packages (session_pkg.<br />

user_id) liest.<br />

PACKAGE configure<br />

IS<br />

PRAGMA RESTRICT_REFERENCES (configure, WNDS, WNPS);<br />

user_name VARCHAR2(100);<br />

END configure;<br />

PACKAGE BODY configure<br />

IS<br />

BEGIN<br />

SELECT lname || ', ' || fname INTO user_name<br />

FROM user_table<br />

WHERE user_id = session_pkg.user_id;<br />

END configure;<br />

Warum kann ich hier WNPS zusichern, obwohl ich die Package-Variable user_name<br />

beschreibe? Die Antwort ist, daß es sich um eine Variable <strong>aus</strong> dem gleichen Package<br />

handelt, weswegen diese Aktion nicht als Seiteneffekt zählt.<br />

Sie können den Reinheitsgrad des Initialisierungsabschnitts des Packages auch implizit<br />

zusichern, indem Sie den Compiler den Reinheitsgrad <strong>aus</strong> den Reinheitsgraden aller<br />

Pragmas der einzelnen <strong>Funktionen</strong> im Package ableiten lassen. In der folgenden Version<br />

des Packages company kann der Oracle Server <strong>aus</strong> den beiden Pragmas für die<br />

<strong>Funktionen</strong> einen Reinheitsgrad <strong>von</strong> RNDS und WNPS für den Initialisierungsabschnitt<br />

ableiten. Der Initialisierungsabschnitt kann also nicht <strong>aus</strong> der Datenbank lesen und<br />

keine Package-Variablen beschreiben.<br />

PACKAGE company<br />

IS<br />

FUNCTION get_company (company_id_in IN VARCHAR2)<br />

RETURN company%ROWTYPE;<br />

FUNCTION deactivate_company (company_id_in IN company.company_id%TYPE)<br />

RETURN VARCHAR2;<br />

PRAGMA RESTRICT_REFERENCES (get_company, RNDS, WNPS);<br />

PRAGMA RESTRICT_REFERENCES (deactivate_name, WNPS);<br />

END company;<br />

590


Die Präzedenz <strong>von</strong> Spalten- und Funktionsnamen<br />

Üblicherweise ist es besser, den Reinheitsgrad des Initialisierungsabschnitts explizit<br />

anzugeben. Das macht es denjenigen, die das Package später einmal warten müssen,<br />

leichter, Ihre Absichten und Ihr Verständnis des Packages zu durchschauen.<br />

Die Präzedenz <strong>von</strong> Spalten- und<br />

Funktionsnamen<br />

Wenn Ihre Funktion den gleichen Namen wie eine Tabellenspalte in einer SELECT-<br />

Anweisung und keine Parameter hat, dann hat die Spalte Vorrang vor der Funktion.<br />

Die Tabelle employee habe eine Spalte namens salary. Wenn Sie nun auch eine<br />

Funktion namens salary erzeugen<br />

CREATE TABLE employee (employee_id NUMBER, ... , salary NUMBER, ...);<br />

FUNCTION salary RETURN NUMBER;<br />

dann bezieht sich salary in einer SELECT-Anweisung immer auf die Spalte und nicht<br />

auf die Funktion:<br />

SELECT salary INTO calculated_salary FROM employee;<br />

Wenn Sie diese Präzedenz der Spalte umgehen wollen, müssen Sie den Namen der<br />

Funktion mit dem Namen des Schemas, dem diese Funktion gehört, qualifizieren:<br />

SELECT scott.salary INTO calculated_salary FROM employee;<br />

Damit wird die Funktion <strong>aus</strong>geführt, anstatt den Spaltenwert zu holen.<br />

Die rauhe Wirklichkeit: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in<br />

<strong>SQL</strong> <strong>aufrufen</strong><br />

Die Möglichkeit, <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> aufzurufen, gibt es schon seit der Version<br />

2.1, aber trotzdem kann das in vielerlei Hinsicht (zumindest vor dem Erscheinen <strong>von</strong><br />

Oracle8) als »brandneue« Technologie betrachtet werden. Warum ist das so?<br />

• Sie müssen in Ihrem Code die RESTRICT_REFERENCES-Pragmas <strong>von</strong> Hand eintragen,<br />

und Sie müssen selbst her<strong>aus</strong>finden, wo diese nötig sind. Wie man das macht,<br />

wird in einem späteren Abschnitt beschrieben.<br />

• Die <strong>Funktionen</strong> werden außerhalb des Lesekonsistenzmodells der Oracle-Datenbank<br />

<strong>aus</strong>geführt (!). Auch das wird in einem späteren Abschnitt näher erläutert.<br />

• Es ist immer noch teuer, eine Funktion <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufzurufen. Die genauen<br />

Kosten eines Funktionsaufrufs (verglichen beispielsweise mit Inline-<strong>SQL</strong>-Code)<br />

sind schwer zu bestimmen, sie unterscheiden sich <strong>von</strong> Computer zu Computer und<br />

sogar <strong>von</strong> Instanz zu Instanz oder <strong>von</strong> aufgerufener Funktion zu aufgerufener<br />

591


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

Funktion. Ich habe <strong>von</strong> einer zusätzlichen halben Sekunde bis zu erstaunlichen<br />

zusätzlichen fünfzig Sekunden gehört (auch wenn ich im letzteren Falle vorschlagen<br />

würde, ein wenig mehr zu analysieren und zu debuggen). Aber egal wie<br />

schnell der Aufruf wirklich ist, die Verzögerung kann für den Anwender spürbar<br />

sein. Sie müssen sie also in Ihren Design- und Testplänen berücksichtigen.<br />

• Tuning-Mechanismen wie EX<strong>PL</strong>AIN <strong>PL</strong>AN berücksichtigen den <strong>SQL</strong>-Code nicht,<br />

der in <strong>Funktionen</strong> aufgerufen werden könnte, die wiederum <strong>von</strong> Ihren <strong>SQL</strong>-<br />

Anweisungen aufgerufen werden. <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> werden <strong>von</strong> EX<strong>PL</strong>AIN <strong>PL</strong>AN<br />

nicht berücksichtigt. Damit wird es schwierig, die Flaschenhälse zu erkennen, die<br />

man zur Performanzsteigerung beseitigen muß.<br />

• Ein großer Teil der Oracle-Technologie, und dabei wiederum insbesondere jener<br />

Teil, der in Built-in Packages steckt, ist für <strong>Funktionen</strong> in <strong>SQL</strong> verboten. Denken<br />

Sie nur an DBMS_OUTPUT. Sie können dieses Package nicht in <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufenen<br />

<strong>Funktionen</strong> verwenden. Normalerweise möchten Sie aber die gleichen <strong>Funktionen</strong><br />

in <strong>SQL</strong> und <strong>PL</strong>/<strong>SQL</strong> verwenden. Auch fügen Sie zum Debuggen möglicherweise<br />

am Anfang und am Ende Ihrer <strong>Funktionen</strong> und Prozeduren Aufrufe ein und<br />

verwenden sehr wahrscheinlich DBMS_OUTPUT (oder UTIL_FILE oder DBMS_PIPE,<br />

was Sie wollen, diese <strong>Funktionen</strong> sind alle – zumindest vor Oracle8 – verboten).<br />

Tja, tut mir leid. Diese <strong>Funktionen</strong> können Sie <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> nicht verwenden. Also<br />

müssen Sie Code <strong>aus</strong> der Funktion her<strong>aus</strong>nehmen. Am Ende haben Sie zwei Versionen<br />

Ihres Codes: eine für <strong>PL</strong>/<strong>SQL</strong>, eine für <strong>SQL</strong>, ein Alptraum für Software-Entwickler.<br />

Schauen wir uns jetzt zwei dieser Probleme genauer an.<br />

Manuelles Eintragen der Pragmas<br />

Sie müssen die RESTRICT_REFERENCES-Pragmas überall in Ihrem Code eintragen und<br />

auch her<strong>aus</strong>finden, wo diese hingehören. Dieser Vorgang ähnelt oft einer detektivischen<br />

Spurensuche, Sherlock Holmes läßt grüßen! Sie kompilieren ein Package und<br />

erhalten eine Meldung über einen Verstoß gegen ein Pragma. Der Grund dafür kann<br />

sein, daß Ihr Programm eine Regel verletzt (indem es beispielsweise Daten verändert),<br />

oder andere Programme aufruft, die eine Regel verletzen. In letzterem Fall stehen Sie<br />

womöglich vor fünf oder sechs anderen <strong>Funktionen</strong> oder Prozeduren, müssen also in<br />

all diesen ebenfalls die Pragmas eintragen. Damit sichern Sie aber Reinheitsgrade zu,<br />

wo das vorher nicht geschah, und handeln sich damit im besten Fall weitere Fehler ein,<br />

im schlechtesten Fall aber weitreichende Konsequenzen für die Architektur Ihrer<br />

Anwendung.<br />

Nehmen wir beispielsweise an, daß Sie auf einmal einer Prozedur im Package X ein<br />

Pragma anbeistellen müssen. Dieses Package hat aber einen Initialisierungsabschnitt, so<br />

daß Sie auch diesen mit Pragmas versehen müssen. In diesen Abschnitten werden oft<br />

<strong>PL</strong>/<strong>SQL</strong>-Tabellen für die Datenmanipulation im Speicher eingerichtet. Wenn Sie aber<br />

<strong>PL</strong>/<strong>SQL</strong>-Tabellenmethoden für die Initialisierung solcher Tabellen verwenden, wird das<br />

Pragma fehlschlagen.<br />

592


Die rauhe Wirklichkeit: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> in <strong>SQL</strong> <strong>aufrufen</strong><br />

Dieser Vorgang kann ziemlich frustrierend sein und manchmal dazu führen, daß Sie am<br />

Ende darauf verzichten, eine Funktion <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufzurufen. Ich habe die Erfahrung<br />

gemacht, daß man so weit wie möglich im vornherein die Bereiche der Anwendung<br />

festlegen sollte, die <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufen werden sollen. Sie können dann diesen<br />

Code besonders »sauber« und beschränkt auf die eigentliche Aufgabe halten und die<br />

Verstrickungen mit anderen Packages sowie die Verwendung Built-in Packages so<br />

gering wie möglich halten. Das ist weder einfach noch besonders spaßig.<br />

Komplikationen mit dem Lesekonsistenzmodell<br />

Ja, es ist kaum zu glauben, aber wahr: Wenn Sie nicht besondere Vorsichtsmaßnahmen<br />

treffen, ist es gut möglich, daß Ihre <strong>SQL</strong>-Abfrage das Lesekonsistenzmodell des Oracle-<br />

RDBMS verletzt, ein seit Jahren geheiligtes Territorium in Oracle. Um dieses Problem zu<br />

verstehen, werfen Sie bitte einen Blick auf die folgende Abfrage und die <strong>von</strong> ihr aufgerufene<br />

Funktion:<br />

SELECT name, total_sales (account_id)<br />

FROM account<br />

WHERE status = 'ACTIVE';<br />

FUNCTION total_sales (id_in IN account.account_id%TYPE)<br />

RETURN NUMBER<br />

IS<br />

CURSOR tot_cur<br />

IS<br />

SELECT SUM (sales) total<br />

FROM orders<br />

WHERE account_id = id_in<br />

AND year = TO_NUMBER (TO_CHAR (SYSDATE, 'YYYY'));<br />

tot_rec tot_cur%ROWTYPE;<br />

BEGIN<br />

OPEN tot_cur;<br />

FETCH tot_cur INTO tot_rec;<br />

RETURN tot_rec.total;<br />

END;<br />

Die Tabelle account hat fünf Millionen aktive Zeilen (offensichtlich ein sehr erfolgreiches<br />

Unternehmen!). Die Tabelle orders hat zwanzig Millionen Zeilen. Ich starte die<br />

Abfrage um 11 Uhr morgens und rechne damit, daß sie etwa eine Stunde dauert. Um<br />

viertel vor zwölf kommt jemand mit der entsprechenden Berechtigung, löscht alle Zeilen<br />

<strong>aus</strong> der Tabelle orders und gibt die Änderung frei. Nach dem Lesekonsistenzmodell<br />

<strong>von</strong> Oracle sollte die Sitzung, welche die Abfrage <strong>aus</strong>führt, all diese gelöschten Zeilen<br />

sehen, bis die Abfrage abgeschlossen ist. Aber wenn das nächste Mal die Funktion<br />

total_sales <strong>aus</strong> der Abfrage her<strong>aus</strong> aufgerufen wird, findet Sie keine Zeilen in<br />

orders mehr vor und gibt NULL zurück – und das auch bei allen weiteren Aufrufen.<br />

Sie müssen sich also über solche Lesekonsistenzprobleme im klaren sein, wenn Sie in<br />

<strong>Funktionen</strong>, die <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> aufgerufen werden, wiederum Abfragen durchführen.<br />

Wenn diese <strong>Funktionen</strong> <strong>von</strong> lange laufenden Abfragen oder Transaktionen aufgerufen<br />

593


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

werden, sollten Sie vielleicht zwischen den <strong>SQL</strong>-Anweisungen der aktuellen Transaktion<br />

den folgenden Befehl eingeben, um die Lesekonsistenz zu erzwingen:<br />

SET TRANSACTION READ ONLY<br />

Mehr dazu finden Sie in Kapitel 6, Interaktion mit der Datenbank und Cursoren.<br />

Das Arbeiten mit <strong>Funktionen</strong> in <strong>SQL</strong> ist schwieriger und komplizierter, als Sie es sich<br />

vielleicht auf den ersten Blick vorstellen können. Das hätte man sich eigentlich auch<br />

denken können, denn man kann das über fast jeden Aspekt der Oracle-Technologie<br />

sagen, insbesondere über die kürzlich hinzugekommenen Neuerungen. Ich hoffe, daß<br />

uns Oracle mit der Zeit das Leben leichter machen wird (und in Oracle 8.0 gibt es einige<br />

deutliche Verbesserungen). Was wir letztendlich brauchen, ist ein Hilfsprogramm, mit<br />

dem ein Entwickler auf eine Funktion verweisen und verlangen kann, daß »diese Funktion<br />

für <strong>SQL</strong> nutzbar gemacht wird«. Dieses Hilfsprogramm trägt dann alle Pragmas ein<br />

oder erstellt zumindest einen Bericht über die notwendigen Schritte.<br />

Man darf ja schließlich träumen, oder?<br />

Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />

Je mehr Sie über gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong> nachdenken, um so mehr Möglichkeiten<br />

werden Ihnen einfallen, diese in jeder einzelnen Ihrer Anwendungen zu verwenden.<br />

Um Ihrer Kreativität eine kleine Starthilfe zu geben, zeige ich Ihnen hier einige<br />

Beispiele, wie gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong>-Anweisungen Ihre Oracle-basierten<br />

Systeme verändern können.<br />

Eingekapselte Berechnungen<br />

In so ziemlich jeder Anwendung müssen Sie immer wieder die gleichen Berechnungen<br />

durchführen. Egal ob es sich dabei um die Berechnung des Bruttobetrags, des Standes<br />

der Hypothek, des Abstands zweier Punkte auf einer kartesischen Ebene oder einer statistischen<br />

Varianz handelt, mit nativem <strong>SQL</strong> müssen Sie diese Berechnungen in jeder<br />

<strong>SQL</strong>-Anweisung, in der sie benötigt werden, wieder neu schreiben.<br />

Für diese Redundanz zahlen Sie einen hohen Preis. Der Code, der Ihre Geschäftsregeln<br />

implementiert, ist über die gesamte Anwendung verstreut und wird immer wieder wiederholt.<br />

Sogar wenn sich diese Regeln selbst nicht ändern, wird sich wahrscheinlich die<br />

Implementierung dieser Regeln irgendwann ändern müssen. Wenn sich die Regel selbst<br />

ändert, ist die Lage natürlich noch übler, weil das oft zu ziemlich umfassenden Änderungen<br />

im Code führen kann.<br />

Um dieses Problem zu lösen, können Sie alle Formeln und Berechnungen in gespeicherten<br />

<strong>Funktionen</strong> verstecken. Diese <strong>Funktionen</strong> können dann sowohl <strong>aus</strong> <strong>SQL</strong>-<br />

Anweisungen als auch <strong>aus</strong> <strong>PL</strong>/<strong>SQL</strong>-Programmen her<strong>aus</strong> aufgerufen werden.<br />

Ein schönes Beispiel für die Nützlichkeit eingekapselter Berechnungen entstand, als<br />

eine Versicherung Analysen ihrer Konten auf Basis <strong>von</strong> Datumsangaben durchführen<br />

594


Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />

mußte. Der letzte Tag im Monat ist natürlich für die meisten Finanzinstitutionen ein sehr<br />

wichtiges Datum. Um die Datumsangaben zu manipulieren, sah die IT-Abteilung der<br />

Firma vor, die Built-in-Funktion LAST_DAY zu verwenden, um den letzten Tag eines<br />

Monats zu bekommen, und mit ADD_MONTHS <strong>von</strong> einem Monat zum nächsten zu springen.<br />

Bald fiel ihnen aber eine sehr interessante Nuance in der Funktion <strong>von</strong> ADD_<br />

MONTHS auf: Wenn Sie einen Tag an ADD_MONTH übergeben, welcher der letzte Tag<br />

des jeweiligen Monats ist, dann gibt <strong>SQL</strong> immer den letzten Tag im dar<strong>aus</strong> resultierenden<br />

Monat zurück, egal, wie viele Tage jeder der einzelnen Monate wirklich hat:<br />

Anders <strong>aus</strong>gedrückt:<br />

ADD_MONTHS ('28-FEB-1994', 2) ==> 30-APR-1993<br />

Das ist vielleicht bei manchen Anwendungen und Abfragen auch sinnvoll. Bei der Versicherung<br />

war die Anforderung aber, daß man <strong>beim</strong> Springen <strong>von</strong> Monat zu Monat<br />

immer am gleichen Tag des Monats ankommen muß (oder am letzten Tag, wenn der<br />

Tag im Ausgangsmonat nach dem letzten Tag des Zielmonats lag). Ohne gespeicherte<br />

<strong>Funktionen</strong> hätte man folgende <strong>SQL</strong>-Anweisung verwenden müssen:<br />

SELECT<br />

DECODE (payment_date,<br />

LAST_DAY (payment_date),<br />

LEAST (ADD_MONTHS (payment_date, 1),<br />

TO_DATE (TO_CHAR (ADD_MONTHS (payment_date, 1),<br />

'MMYYYY') ||<br />

TO_CHAR (payment, 'DD'),<br />

'MMYYYYDD')),<br />

ADD_MONTHS (payment_date, 1))<br />

FROM premium_payments;<br />

Auf deutsch heißt das: »Wenn das Datum der letzten Zahlung auf den letzten Tag des<br />

Monats fällt, dann gib als Datum der nächsten Zahlung entweder das Ergebnis der Addition<br />

eines Monats zum Datum der letzten Zahlung (mit ADD_MONTHS) oder den gleichen<br />

Tag wie den der letzten Zahlung im neuen Monat zurück. Falls die letzte Zahlung nicht<br />

am letzten Tag des Monats getätigt wurde, dann verwende zur Bestimmung des nächsten<br />

Zahlungsdatums einfach ADD_MONTHS.«<br />

Das ist nicht nur schwierig zu verstehen, sondern benötigt auch drei Aufrufe der Builtin-Funktion<br />

ADD_MONTHS. Stellen Sie sich jetzt noch vor, daß diese komplexe <strong>SQL</strong>-<br />

Anweisung in jeder SELECT-Liste hätte wiederholt werden müssen, in der ADD_MONTHS<br />

verwendet wurde, um die Datumsangaben herauf- oder herunterzuzählen. Sie können<br />

sich da wahrscheinlich gut vorstellen, wie froh die Programmierer in dieser Firma<br />

waren, als sie die Version 7.1 <strong>von</strong> Oracle Server installierten und die folgende Funktion<br />

in ihren <strong>SQL</strong>-Anweisungen verwenden konnten (eine vollständige Erklärung der Logik<br />

dieser Funktion finden Sie in Kapitel 12, Datumsfunktionen):<br />

FUNCTION new_add_months (date_in IN DATE, months_shift IN NUMBER)<br />

RETURN DATE<br />

IS<br />

return_value DATE;<br />

595


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

BEGIN<br />

return_value := ADD_MONTHS (date_in, months_shift);<br />

IF date_in = LAST_DAY (date_in)<br />

THEN<br />

return_value :=<br />

LEAST (return_value,<br />

TO_DATE (TO_CHAR (return_value, 'MMYYYY') ||<br />

TO_CHAR (date_in, 'DD') ,<br />

'MMYYYYDD'));<br />

END IF;<br />

RETURN return_value;<br />

END new_add_months;<br />

Damit wird die SELECT-Anweisung zur Bestimmung des nächsten Zahlungsdatums sehr<br />

einfach:<br />

SELECT new_add_months (payment_date,1)<br />

FROM premium_payments;<br />

Je mehr Sie Ihre <strong>SQL</strong>-Anweisungen durchsehen, um so mehr Möglichkeiten für den Einsatz<br />

gespeicherter <strong>Funktionen</strong> werden Sie finden, mit denen Sie Berechnungen verstekken<br />

und damit auf lange Sicht die Stabilität Ihres Codes verbessern können. Es ist zwar<br />

wenig wahrscheinlich, daß Sie die Zeit und die Ressourcen haben, um das gesamte<br />

<strong>SQL</strong>-Rückgrat Ihrer Anwendung neu zu schreiben, aber Sie können zumindest entsprechende<br />

<strong>Funktionen</strong> entwickeln und diese in Neuentwicklungen einsetzen.<br />

Kombinieren skalarer und aggregierter Werte<br />

Die folgende einfache Frage ist in <strong>SQL</strong> schwer zu beantworten: »Zeige mir den Namen<br />

und das Gehalt des Mitarbeiters jeder Abteilung, der in dieser Abteilung das höchste<br />

Gehalt hat, sowie das Gesamtgehalt aller Mitarbeiter der jeweiligen Abteilungen.«<br />

Wenn man diese Frage in zwei Teile aufteilt, ist das nicht schwer. Hier ist der erste Teil:<br />

SELECT department_id, last_name, salary<br />

FROM employee E1<br />

WHERE salary = (SELECT MAX (salary)<br />

FROM employee E2<br />

WHERE E.department_id = E2.department_id)<br />

GROUP BY department_id;<br />

Und hier der zweite:<br />

SELECT department_id, SUM (salary)<br />

FROM employee<br />

GROUP BY department_id;<br />

Es ist aber nicht so einfach, diese beiden zu kombinieren, weil man dabei sowohl skalare<br />

Werte (eine einzelne Zeile) als auch aggregierte (sich über mehrere Zeilen erstrekkende)<br />

Werte <strong>aus</strong> ein und derselben Tabelle abfragen müßte. Wie soll ich meine<br />

FROM-, WHERE- und GROUP BY-Kl<strong>aus</strong>eln schreiben, um sowohl das Gehalt eines einzelnen<br />

als auch das aller Abteilungsmitglieder zusammengenommen anzuzeigen?<br />

596


Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />

SELECT department_id, last_name, salary, SUM (salary)<br />

FROM ...?<br />

WHERE ...?<br />

GROUP BY ...?<br />

Vor der Version 2.1 <strong>von</strong> <strong>PL</strong>/<strong>SQL</strong> war die naheliegendste Lösung, eine Sicht zu erzeugen,<br />

die das Gehalt jeder Abteilung schon einmal »im vor<strong>aus</strong>« zusammenfaßte:<br />

CREATE VIEW dept_salary<br />

AS<br />

SELECT department_id, SUM (salary) total_salary<br />

FROM employee<br />

GROUP BY department_id;<br />

Mit dieser Sicht kann ich die Antwort mit einer einzigen <strong>SQL</strong>-Anweisung bekommen:<br />

SELECT E.department_id, last_name, salary, total_salary<br />

FROM employee E, dept_salary DS<br />

WHERE E.department_id = DS.department_id<br />

AND salary = (SELECT MAX (salary)<br />

FROM employee E2<br />

WHERE E.department_id = E2.department_id);<br />

Diese Lösung sieht ja gar nicht so schlecht <strong>aus</strong>, aber Sie müssen jedesmal, wenn Sie<br />

eine solche Berechnung durchführen wollen, eine spezielle Sicht erzeugen. Abgesehen<br />

da<strong>von</strong>, ist dieser <strong>SQL</strong>-Code auch für viele Programmierer nicht so besonders offensichtlich.<br />

Eine bessere Lösung ist es, eine gespeicherte Funktion zu verwenden. Anstatt eine Sicht<br />

zu erzeugen, schreiben wir eine Funktion, die genau die gleiche Berechnung durchführt,<br />

aber dieses Mal nur für die angegebene Abteilung:<br />

FUNCTION total_salary (dept_id_in IN department.department_id%TYPE)<br />

RETURN NUMBER<br />

IS<br />

CURSOR grp_cur<br />

IS<br />

SELECT SUM (salary)<br />

FROM employee<br />

WHERE department_id = dept_id_in;<br />

return_value NUMBER;<br />

BEGIN<br />

OPEN grp_cur;<br />

FETCH grp_cur INTO return_value;<br />

CLOSE grp_cur;<br />

RETURN return_value;<br />

END;<br />

Wenn eine Abteilung nicht existiert, gebe ich NULL zurück. Wenn eine Abteilung keine<br />

Mitarbeiter hat, gebe ich 0 zurück, ansonsten die Summe der Gehälter aller Mitarbeiter<br />

in dieser Abteilung.<br />

597


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

Meine Abfrage muß jetzt keine Sicht verwenden, die ein GROUP BY enthält. Statt dessen<br />

ruft sie einfach die Funktion total_salary auf und übergibt die ID-Nummer der<br />

Abteilung des Mitarbeiters als Parameter:<br />

SELECT E.department_id, last_name, salary, total_salary (E.department_id)<br />

FROM employee E<br />

WHERE salary = (SELECT MAX (salary)<br />

FROM employee E2<br />

WHERE E.department_id = E2.department_id);<br />

Die dar<strong>aus</strong> resultierende <strong>SQL</strong>-Anweisung ist nicht nur leichter zu lesen, sondern ist<br />

auch schneller, insbesondere bei größeren Tabellen. Ich könnte die <strong>SQL</strong>-Anweisung<br />

noch weiter vereinfachen, indem ich eine Funktion schreibe, die das maximale Gehalt<br />

in einer bestimmten Abteilung zurückgibt. Die SELECT-Anweisung lautet dann einfach<br />

nur:<br />

SELECT department_id, last_name, salary, total_salary (department_id)<br />

FROM employee E<br />

WHERE salary = max_sal_in_dept (department_id);<br />

Ersetzung korrelierter Subabfragen<br />

Mit gespeicherten <strong>Funktionen</strong> in <strong>SQL</strong>-Anweisungen kann man auch korrelierte Subabfragen<br />

ersetzen. Das sind SELECT-Anweisungen innerhalb der WHERE-Kl<strong>aus</strong>el einer<br />

<strong>SQL</strong>-Anweisung (SELECT, INSERT oder DELETE), die sich auf eine oder mehrere Spalten<br />

der umgebenden <strong>SQL</strong>-Anweisung beziehen. Im letzten Abschnitt habe ich eine korrelierte<br />

Subabfrage verwendet, um den Mitarbeiter mit dem höchsten Gehalt in jeder<br />

Abteilung zu bestimmen:<br />

SELECT E.department_id, last_name, salary, total_salary (E.department_id)<br />

FROM employee E<br />

WHERE salary = (SELECT MAX (salary)<br />

FROM employee E2<br />

WHERE E.department_id = E2.department_id);<br />

Die letzten drei Zeilen der Abfrage enthalten eine SELECT-Anweisung, in der die Abteilungsnummer<br />

des »inneren« Mitarbeiters (E2) mit der Abteilungsnummer in der »äußeren«<br />

Mitarbeitertabelle (E1) übereinstimmen soll. Die innere Abfrage wird für jede mit<br />

der äußeren Abfrage ermittelte Zeile einmal <strong>aus</strong>geführt.<br />

Korrelierte Subabfragen sind ein sehr mächtiges Feature <strong>von</strong> <strong>SQL</strong>, weil sie den verschachtelten<br />

Schleifen prozeduraler Programmiersprachen wie in<br />

LOOP<br />

LOOP<br />

END LOOP;<br />

END LOOP;<br />

entsprechen. Sie haben aber zwei Nachteile:<br />

• Die Logik kann ziemlich kompliziert werden.<br />

• Die resultierende <strong>SQL</strong>-Anweisung kann schwierig zu verstehen und zu verfolgen<br />

sein.<br />

598


Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />

Sie können diese Nachteile vermeiden, indem Sie gespeicherte <strong>Funktionen</strong> anstelle der<br />

korrelierten Subabfragen verwenden. Im obigen Beispiel bräuchte ich eine Funktion,<br />

die das höchste Gehalt in einer bestimmten Abteilung berechnet:<br />

FUNCTION max_salary (dept_id_in IN department.department_id%TYPE)<br />

RETURN NUMBER<br />

IS<br />

CURSOR grp_cur<br />

IS<br />

SELECT MAX (salary)<br />

FROM employee<br />

WHERE department_id = dept_id_in;<br />

return_value NUMBER;<br />

BEGIN<br />

OPEN grp_cur;<br />

FETCH grp_cur INTO return_value;<br />

CLOSE grp_cur;<br />

RETURN return_value;<br />

END;<br />

Jetzt kann ich sowohl total_salary als auch max_salary in meiner SELECT-Anweisung<br />

verwenden. Auf deutsch übersetzt würde diese etwa lauten: »Zeige mir den<br />

Namen und das Gehalt des Mitarbeiters, der in der jeweiligen Abteilung das höchste<br />

Gehalt hat, sowie das Gesamtgehalt aller Mitarbeiter in der Abteilung dieses Mitarbeiters«:<br />

SELECT E.department_id, last_name, salary, total_salary (department_id)<br />

FROM employee<br />

WHERE salary = max_salary (department_id);<br />

Vergleichen Sie dieses einfache und selbstdokumentierende Stück <strong>SQL</strong> mit der Version,<br />

die eine Sicht und eine korrelierte Subabfrage benötigt:<br />

CREATE VIEW dept_salary<br />

AS<br />

SELECT department_id, SUM (salary) total_salary<br />

FROM employee<br />

GROUP BY department_id;<br />

SELECT E.department_id, last_name, salary, total_salary<br />

FROM employee E, dept_salary DS<br />

WHERE E.department_id = DS.department_id<br />

AND salary = (SELECT MAX (salary)<br />

FROM employee E2<br />

WHERE E.department_id = E2.department_id);<br />

Ich bin sicher, Sie werden mir zustimmen, daß gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong> Ihr<br />

Leben einfacher machen können.<br />

Ihnen ist vielleicht aufgefallen, daß die Funktion total_salary <strong>aus</strong> dem letzten<br />

Abschnitt der Funktion max_salary <strong>aus</strong> diesem Abschnitt sehr ähnlich sieht. Der einzige<br />

Unterschied zwischen den beiden ist der, daß der Cursor in total_salary die<br />

599


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

Gruppierungsfunktion SUM und der Cursor in max_salary die Gruppierungsfunktion<br />

MAX verwendet. Wenn Sie immer fanatisch darauf <strong>aus</strong> sind, möglichst viel Ihres Codes<br />

in einer möglichst kleinen Anzahl unterschiedlicher »beweglicher Teile« zusammenzufassen,<br />

dann könnten Sie auch eine einzige Funktion dar<strong>aus</strong> machen, die abhängig <strong>von</strong><br />

einem zweiten Parameter, verschiedene Statistiken auf Gruppenebene zurückliefert:<br />

FUNCTION salary_stat<br />

(dept_id_in IN department.department_id%TYPE,<br />

stat_type_in IN VARCHAR2)<br />

RETURN NUMBER<br />

IS<br />

v_stat_type VARCHAR2(20) := UPPER (stat_type_in);<br />

CURSOR grp_cur<br />

IS<br />

SELECT SUM (salary) sumsal,<br />

MAX (salary) maxsal,<br />

MIN (salary) minsal,<br />

AVG (salary) avgsal,<br />

COUNT (DISTINCT salary) countsal,<br />

FROM employee<br />

WHERE department_id = dept_id_in;<br />

grp_rec grp_cur%ROWTYPE;<br />

retval NUMBER;<br />

BEGIN<br />

OPEN grp_cur;<br />

FETCH grp_cur INTO grp_rec;<br />

CLOSE grp_cur;<br />

IF v_stat_type = 'SUM'<br />

THEN<br />

retval := grp_rec.sumsal;<br />

ELSIF v_stat_type = 'MAX'<br />

THEN<br />

retval := grp_rec.maxsal;<br />

ELSIF v_stat_type = 'MIN'<br />

THEN<br />

retval := grp_rec.minsal;<br />

ELSIF v_stat_type = 'COUNT'<br />

THEN<br />

retval := grp_rec.countsal;<br />

ELSIF v_stat_type = 'AVG'<br />

THEN<br />

retval := grp_rec.avgsal;<br />

END IF;<br />

RETURN retval;<br />

END;<br />

600


Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />

Die zusätzlichen Kosten, die durch diese Ausdrücke in der SELECT-Liste entstehen –<br />

und die Verarbeitung der IF-Anweisung – sind vernachlässigbar. Mit diesem neuen,<br />

generischen Werkzeug sieht meine Gehaltsanalyse nun so <strong>aus</strong>:<br />

SELECT E.department_id, last_name, salary, salary_stat (department_id, 'sum')<br />

FROM employee<br />

WHERE salary = salary_stat (department_id, 'max');<br />

Wenn ich jemals den <strong>SQL</strong>-Code, mit dem man die Statistiken über Abteilungen erhält,<br />

ändern muß, dann muß ich nur diese eine Funktion anfassen.<br />

Ersetzen <strong>von</strong> DECODE durch IF-Anweisungen<br />

Die DECODE-Funktion stellt IF-ähnliche Möglichkeiten in der nichtprozeduralen <strong>SQL</strong>-<br />

Umgebung des Oracle Server bereit. Sie können die DECODE-Syntax verwenden, um<br />

Matrixberichte mit einer festen Anzahl <strong>von</strong> Spalten zu erzeugen, aber auch, um in einer<br />

Abfrage komplexe IF-THEN-ELSE-Logik einzubauen. Betrachten Sie das folgende Beispiel<br />

für DECODE, in dem bestimmt wird, ob ein Datum innerhalb eines vorgeschriebenen<br />

Bereichs liegt und das, wenn ja, die Anzahl der Zeilen, die diese Bedingung erfüllen,<br />

inkrementiert:<br />

SELECT FC.year_number,<br />

SUM (DECODE (GREATEST (ship_date, FC.q1_sdate),<br />

ship_date,<br />

DECODE (LEAST (ship_date, FC.q1_edate),<br />

ship_date, 1,<br />

0),<br />

0)) Q1_results,<br />

SUM (DECODE (GREATEST (ship_date, FC.q2_sdate),<br />

ship_date,<br />

DECODE (LEAST (ship_date, FC.q2_edate),<br />

ship_date, 1,<br />

0),<br />

0)) Q2_results,<br />

SUM (DECODE (GREATEST (ship_date, FC.q3_sdate),<br />

ship_date,<br />

DECODE (LEAST (ship_date, FC.q3_edate),<br />

ship_date, 1,<br />

0),<br />

0)) Q3_results,<br />

SUM (DECODE (GREATEST (ship_date, FC.q4_sdate),<br />

ship_date,<br />

DECODE (LEAST (ship_date, FC.q4_edate),<br />

ship_date, 1,<br />

0),<br />

0)) Q4_results<br />

FROM orders O,<br />

fiscal_calendar FC<br />

GROUP BY year_number;<br />

Die Ergebnismenge der Abfrage könnte so <strong>aus</strong>sehen:<br />

601


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

YEAR NUMBER Q1 RESULTS Q2 RESULTS Q3 RESULTS Q4 RESULTS<br />

------------ ---------- ---------- ---------- ----------<br />

1993 12000 14005 22000 40000<br />

1994 10000 15000 21000 55004<br />

Es ist zwar sehr nützlich, DECODE für solche Berichte zu verwenden, aber die dafür<br />

notwendige <strong>SQL</strong>-Anweisung ist doch etwas einschüchternd. Das DECODE für Q1<br />

RESULTS läßt sich etwa so übersetzen: » Wenn das Versanddatum größer oder gleich<br />

dem ersten Datum im ersten Quartal und kleiner oder gleich dem letzten Datum im<br />

ersten Quartal ist, dann addiere eins zur Summe aller Bestellungen, die in diesem Quartal<br />

<strong>aus</strong>geliefert worden sind. Ansonsten addiere 0.«<br />

Unglücklicherweise werden Sie die vertrackten <strong>SQL</strong>-Anweisungen nur mit viel Erfahrung<br />

mit DECODE entschlüsseln können. Auch die Wiederholungen im SELECT<br />

schreien geradezu nach einer Modularisierung. Diese erreichen wir mit der folgenden<br />

gespeicherten Funktion (incr_in_range steht für increment if in the range (inkrementieren,<br />

wenn im Bereich)):<br />

FUNCTION incr_in_range<br />

(ship_date_in IN DATE, sdate_in IN DATE, edate_in IN DATE)<br />

RETURN INTEGER<br />

IS<br />

BEGIN<br />

IF ship_date_in BETWEEN sdate_in AND edate_in<br />

THEN<br />

RETURN 1;<br />

ELSE<br />

RETURN 0;<br />

END IF;<br />

END;<br />

Ja, das ist wirklich schon alles! Mit der Funktion incr_in_range wird die lange und<br />

verworrene <strong>SQL</strong>-Anweisung zu einem einfachen:<br />

SELECT FC.year_number,<br />

SUM (incr_in_range (ship_date, q1_sdate, q1_edate)) Q1_results,<br />

SUM (incr_in_range (ship_date, q2_sdate, q2_edate)) Q2_results,<br />

SUM (incr_in_range (ship_date, q3_sdate, q3_edate)) Q3_results,<br />

SUM (incr_in_range (ship_date, q4_sdate, q4_edate)) Q4_results<br />

FROM orders O,<br />

fiscal_calendar FC<br />

GROUP BY year_number;<br />

Die gespeicherte Funktion beseitigt die Code-Redundanz und macht die <strong>SQL</strong>-Anweisung<br />

sehr viel lesbarer. Außerdem könnte diese Funktion auch noch in anderen <strong>SQL</strong>-<br />

Anweisungen verwendet werden, wo die gleiche Logik benötigt wird.<br />

Partielle Spaltenwerte mit GROUP BY<br />

Die GROUP BY-Kl<strong>aus</strong>el in einer SELECT-Anweisung ermöglicht es Ihnen, Daten über<br />

mehrere Datensätze hinweg zu sammeln und anhand einer oder mehrerer Spalten zu<br />

gruppieren. Die folgende SELECT-Anweisung berechnet beispielsweise den Gesamtbe-<br />

602


Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />

trag der Gehälter, die an alle Mitarbeiter mit jeweils dem gleichen Titel <strong>aus</strong>gezahlt werden:<br />

SELECT job_title_desc, SUM (salary)<br />

FROM employee E, job_title JT<br />

WHERE E.job_title_id = JT.job_title_id<br />

GROUP BY job_title_desc;<br />

Ein Auszug <strong>aus</strong> der Ergebnismenge dieser Abfrage könnte so <strong>aus</strong>sehen:<br />

ACCOUNT MANAGER 32000<br />

OFFICE CLERK 4000<br />

PROJECT MANAGER 10006<br />

SHIPPING CLERK 4200<br />

Was wäre aber, wenn Sie eine Gesamtsumme der Gehälter für jeden Typ oder jede<br />

Titelkategorie sehen wollten? Wieviel verdienen die verschiedenen Angestellten in der<br />

Firma? Und wie ist es mit den verschiedenen Managern und Geschäftsführern?<br />

Mit nativem <strong>SQL</strong> ist so etwas ziemlich schwierig zu schreiben. Was Sie eigentlich wollen,<br />

ist, mit GROUP BY die verschiedenen Kategorien zusammenzufassen, aber diese<br />

Kategorien gibt es nicht als separate Tabelle. Sie sind in die Titelbezeichnungen denormalisiert<br />

worden, die GROUP BY-Kl<strong>aus</strong>el kann aber nur auf alle vollständigen Spaltenwerte<br />

angewendet werden. Glücklicherweise bieten gespeicherte <strong>Funktionen</strong> in <strong>SQL</strong><br />

eine naheliegende Lösung für dieses Problem. Die folgende Funktion job_category<br />

erwartet als ihren Parameter den Primärschlüssel der Titeltabelle und liefert die Jobkategorie<br />

zurück. Das geschieht mittels einer LIKE-Operation über den Text dieses Jobtitels<br />

– etwas, was man in einer GROUP BY-Kl<strong>aus</strong>el nicht direkt machen kann.<br />

FUNCTION job_category (title_id_in IN job_title.job_title_id%TYPE)<br />

RETURN VARCHAR2<br />

IS<br />

CURSOR title_cur IS<br />

SELECT job_title_desc FROM job_title<br />

WHERE job_title_id = title_id_in;<br />

title_rec title_cur%ROWTYPE;<br />

BEGIN<br />

OPEN title_cur; FETCH title_cur INTO title_rec;<br />

IF title_cur%NOTFOUND<br />

THEN<br />

CLOSE title_cur;<br />

RETURN NULL;<br />

ELSE<br />

CLOSE title_cur;<br />

IF title_rec.job_title_desc LIKE '%CLERK%'<br />

THEN<br />

RETURN 'CLERK';<br />

ELSIF title_rec.job_title_desc LIKE '%VICE PRESIDENT%'<br />

THEN<br />

RETURN 'VICE PRESIDENT';<br />

ELSIF title_rec.job_title_desc LIKE '%MANAGER%'<br />

THEN<br />

603


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

RETURN 'MANAGER';<br />

END IF;<br />

END IF;<br />

END;<br />

Jetzt kann ich meine Abfrage mit der Funktion job_category neu schreiben und all die<br />

Konfusionen mit meinen denormalisierten Stellenbezeichnungen vermeiden:<br />

SELECT job_category (job_title_id) as title, SUM (salary)<br />

FROM employee<br />

GROUP BY title;<br />

Ich verwende diese Funktion sowohl in der SELECT-Liste als auch in der GROUP BY-<br />

Kl<strong>aus</strong>el. Mit dieser Abfrage kann ich nun etwa folgendes Resultat bekommen:<br />

CLERK 8200<br />

MANAGER 42006<br />

VICE PRESIDENT 75000<br />

Sequentielle Verarbeitung eines Spaltenwerts<br />

Es ist sehr leicht, mit der Funktion INSTR her<strong>aus</strong>zufinden, ob ein bestimmtes Wort oder<br />

eine Menge <strong>von</strong> Zeichen in einem String steht. Es ist ebenfalls leicht, in <strong>SQL</strong> zu bestimmen,<br />

welche Zeilen eine Spalte haben, die ein bestimmtes Wort enthält. Aber es ist sehr<br />

viel schwieriger, nur mit <strong>SQL</strong> her<strong>aus</strong>zufinden, wie oft ein bestimmtes Wort oder eine<br />

Menge <strong>von</strong> Zeichen in einem String in einer einzigen Zeile auftaucht. Die eingebaute<br />

mengenbezogene Verarbeitung <strong>von</strong> <strong>SQL</strong> kennt kein iteratives oder Schleifenverhalten<br />

innerhalb einer Zeile – nur über Zeilen hinweg.<br />

Wenn ich eine iterative Analyse oder Berechnung auf einem bestimmten Wert in einer<br />

einzigen Zeile <strong>aus</strong>führen möchte, dann muß ich eine <strong>PL</strong>/<strong>SQL</strong>-Funktion verwenden.<br />

Schauen wir uns an, wie man das macht.<br />

Zunächst der <strong>SQL</strong>-Code, der mir alle Zeilen in der Tabelle text <strong>aus</strong>gibt, die das Wort<br />

»der« enthalten:<br />

SELECT text<br />

FROM notes<br />

WHERE INSTR (text, 'der') > 0;<br />

Ich kann sogar INSTR benutzen, um alle Zeilen her<strong>aus</strong>zufinden, die das Wort »der« mindestens<br />

dreimal enthalten:<br />

SELECT text<br />

FROM notes<br />

WHERE INSTR (text, 'der', 1, 3) > 0;<br />

Und bevor man mich schlägt, könnte ich auch alle Zeilen ermitteln, die das Wort »der«<br />

genau dreimal enthalten:<br />

SELECT text<br />

FROM notes<br />

604


Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />

WHERE INSTR (text, 'der', 1, 3) != 0<br />

AND INSTR (text, 'der', 1, 4) = 0;<br />

Wenn INSTR also einen <strong>von</strong> Null verschiedenen Wert für das dritte Auftreten <strong>von</strong> »der«,<br />

aber 0 für das vierte Auftreten zurückgibt, dann heißt das, daß der Teilstring genau dreimal<br />

im String vorkommt.<br />

Wenn ich aber einen Bericht benötige, der die Anzahl <strong>von</strong> »der«-Vorkommnissen in<br />

jeder meiner Notizen aufführt, oder wenn ich die Anzahl der Vorkommnisse <strong>von</strong> »der«<br />

mit GROUP BY zusammenfassen möchte, dann läßt <strong>SQL</strong> mich im Stich. Ich kann meinen<br />

Wunsch höchstens in Pseudocode <strong>aus</strong>drücken:<br />

SELECT anzahl-<strong>von</strong>-der-im-text, text<br />

FROM notes<br />

GROUP BY anzahl-<strong>von</strong>-der-im-text;<br />

Als nichtprozedurale Sprache, die mengenorientiert (und zwar in Bezug auf Mengen<br />

<strong>von</strong> Zeilen) ist, kennt <strong>SQL</strong> keine Möglichkeit, programmatisch den Inhalt einer<br />

bestimmten Spalte analysieren zu lassen. <strong>PL</strong>/<strong>SQL</strong> ist dagegen perfekt dafür <strong>aus</strong>gerüstet,<br />

um diese Art <strong>von</strong> Problemen zu lösen. Das Package ps.parse auf der Diskette zeigt,<br />

wie man eine Funktion schreibt, welche die Anzahl <strong>von</strong> Atomen (Wörter und/oder<br />

Begrenzungszeichen) in einem String ermittelt. Ich werde die Implementierung dieses<br />

Packages hier nicht wiederholen, aber Ihnen zumindest zeigen, wie man diese Funktion<br />

in <strong>SQL</strong> einsetzen könnte:<br />

• Anzeigen, wie oft »der« in jeder Zeile <strong>von</strong> text vorkommt:<br />

SELECT text,<br />

ps_parse.number_of_atomics (text, 'der')<br />

FROM notes;<br />

• Anzeigen, wie oft das Wort »dringend« über die Tage der Woche verteilt ist, aber<br />

nur die Texte berücksichtigen, in denen dieses Wort mindestens einmal auftaucht:<br />

SELECT TO_CHAR (note_date, 'DAY')day,<br />

SUM (ps_parse.number_of_atomics (text, 'dringend')) urgent_count<br />

FROM notes<br />

WHERE urgent_count > 0;<br />

GROUP BY TO_CHAR (note_date, 'DAY');<br />

Rekursive Verarbeitung in einer <strong>SQL</strong>-Anweisung<br />

<strong>SQL</strong> unterstützt keine Rekursion, auch wenn diese mächtige Programmiertechnik<br />

manchmal doch sehr wichtig für die Lösung eines Problems ist. <strong>PL</strong>/<strong>SQL</strong> erlaubt aber das<br />

rekursive Aufrufen <strong>von</strong> <strong>Funktionen</strong>, so daß Sie die rekursiven Aufrufe bei Bedarf in eine<br />

Funktion stecken können, wenn Sie in <strong>SQL</strong> Rekursion benötigen.<br />

Nehmen wir an, Sie schreiben eine Anwendung, die Schecks <strong>aus</strong>druckt. Ein Scheck enthält<br />

sowohl die numerische Betragsangabe (beispielsweise DM 99,70) als auch die <strong>aus</strong>geschriebene<br />

Version des Betrags (neunundneunzig Mark und siebzig Pfennige). Mit<br />

<strong>SQL</strong> können Sie den numerischen Scheckbetrag (der in der Datenbank steht) nicht in<br />

die <strong>aus</strong>geschriebene Version umsetzen. Mit <strong>PL</strong>/<strong>SQL</strong> geht das aber, auch wenn wir dafür<br />

605


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

ein bißchen tiefer in die Trickkiste greifen müssen. Dabei bekommen wir Rekursion,<br />

globale Daten und <strong>PL</strong>/<strong>SQL</strong>-Tabellen zu fassen und können damit eine sehr elegante<br />

Lösung finden.<br />

Das untenstehende Package checks ermöglicht genau diese Konvertierung. 2<br />

/* Dateiname auf der Begleitdiskette: checks.spp */<br />

PACKAGE checks<br />

IS<br />

/* Zahl in Worte konvertieren */<br />

FUNCTION num2words (number_in IN NUMBER) RETURN VARCHAR2;<br />

/* Das ist ein Package, ich muss den Reinheitsgrad zusichern */<br />

PRAGMA RESTRICT_REFERENCES (num2words, WNDS);<br />

END checks;<br />

PACKAGE BODY checks<br />

IS<br />

/* Tabellenstruktur, welche die Namen der numerischen Komponenten enthaelt. */<br />

TYPE numwords_tabtype IS TABLE OF VARCHAR2(20)<br />

INDEX BY BINARY_INTEGER;<br />

words_table numwords_tabtype;<br />

/* Wird im Initialisierungsabschnitt verwendet */<br />

v_date DATE;<br />

FUNCTION num2words (number_in IN NUMBER) RETURN VARCHAR2<br />

IS<br />

my_number NUMBER;<br />

BEGIN<br />

/* Sorry, aber wir kuemmern uns hier nicht um Pfennige! */<br />

my_number := FLOOR (number_in);<br />

/* $1000+ */<br />

IF my_number >= 1000<br />

THEN<br />

/* In zwei rekursive Aufrufe <strong>von</strong> num2words aufteilen */<br />

RETURN num2words (my_number/1000) ||<br />

' Thousand ' ||<br />

num2words (MOD (my_number, 1000));<br />

END IF;<br />

/* $100-999 */<br />

IF my_number >= 100<br />

THEN<br />

/* In zwei rekursive Aufrufe <strong>von</strong> num2words aufteilen */<br />

RETURN num2words (my_number/100) ||<br />

' Hundred ' ||<br />

2 Diese Implementierung <strong>von</strong> number-to-words wurde zuerst <strong>von</strong> Mike Burnside <strong>von</strong> Oracle Australia auf<br />

der International Oracle User's Week in San Francisco im September 1994 vorgestellt. Ich habe einige kleinere<br />

Änderungen vorgenommen.<br />

606


Beispiele eingebetteten <strong>PL</strong>/<strong>SQL</strong>-Codes<br />

END IF;<br />

num2words (MOD (my_number, 100));<br />

/* $20-$99 */<br />

IF my_number >= 20<br />

THEN<br />

/* In das Zehnerwort und den letztlichen Dollarbetrag aufteilen */<br />

RETURN words_table (FLOOR (my_number/10)) ||<br />

' ' ||<br />

num2words (MOD (my_number, 10));<br />

END IF;<br />

/* Wir sind bei 19 oder weniger. Wort <strong>aus</strong> dem "oberen Register" der Tabelle<br />

holen */<br />

RETURN words_table (my_number + 10);<br />

END num2words;<br />

BEGIN<br />

/* Initialisierungsabschnitt des Packages, wird nur einmal pro Sitzung <strong>aus</strong>gefuehrt<br />

*/<br />

/* Manuell die Zehnernamen konstruieren */<br />

words_table (1) := 'Ten';<br />

words_table (2) := 'Twenty';<br />

words_table (3) := 'Thirty';<br />

words_table (4) := 'Forty';<br />

words_table (5) := 'Fifty';<br />

words_table (6) := 'Sixty';<br />

words_table (7) := 'Seventy';<br />

words_table (8) := 'Eighty';<br />

words_table (9) := 'Ninety';<br />

/* Bei 0 NULL zurueckgeben */<br />

words_table (10) := NULL;<br />

/* Zahlnamen <strong>von</strong> eins bis neunzehn erzeugen */<br />

FOR day_index IN 1 .. 19<br />

LOOP<br />

v_date := TO_DATE (to_char(day_index) || '-JAN-94');<br />

words_table (day_index+10) :=<br />

INITCAP (TO_CHAR (v_date, 'DDSP'));<br />

END LOOP;<br />

END checks;<br />

Hier einige Beispiele der Konvertierung <strong>von</strong> ganzen Zahlen in Worte:<br />

checks.num2words (99) ==> Ninety Nine<br />

checks.num2words (12345) ==> Twelve Thousand Three Hundred Forty Five<br />

checks.num2words (5) ==> Five<br />

607


Kapitel 17: <strong>PL</strong>/<strong>SQL</strong>-<strong>Funktionen</strong> <strong>von</strong> <strong>SQL</strong> <strong>aus</strong> <strong>aufrufen</strong><br />

Ich kann diese Package-Funktion auch in meiner <strong>SQL</strong>-Anweisung verwenden, um alle<br />

Informationen, die ich zum Drucken des Schecks benötige, <strong>aus</strong> der Datenbank zu<br />

holen:<br />

SELECT TO_CHAR (SYSDATE, 'Month DD, YYYY'),<br />

payee,<br />

amount,<br />

checks.num2words (amount),<br />

comment<br />

FROM bill<br />

WHERE bill_status = 'UNPAID'<br />

608

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!