Packages und ihre Initialisierung
Dieser Artikel ist ein weiterer einer kleinen Reihe von Fundstellen, die mich bei der Lektüre von Fachliteratur zu Widerspruch gereizt haben. Mir geht es natürlich nicht darum, mit dem Finger auf andere Autoren zu zeigen (diesmal fand ich die Stelle in meinem eigenen PL/SQL-Buch), diese Fundstellen stellen vielmehr den Anker dar, an dem ich ein Thema erläutern möchte, von dem ich glaube, dass es für viele Leser interessant sein könnte. Hier nun einige Überlegungen zu Packages und ihrer Initialisierung.
Die Fundstelle
In meinem PL/SQL-Buch behaupte ich, ein Package könne Initialisierungscode enthalten, der nur beim ersten Öffnen des Packages ausgeführt wird [1]. Das stimmt. Dann jedoch schreibe ich, dass dieser Code ein Problem habe, denn auf Ebene dieses Codes existiere keine Fehlerbehandlung. Wenn der Code fehlerhaft läuft, gilt das Package aber dennoch als initialisiert. Das stimmt nicht. Woher ich das habe, kann ich gar nicht mehr nachvollziehen, wahrscheinlich habe ich das bei Steven Feuerstein [2] vor Jahren gelesen, sicher bin ich mir aber nicht. Doch ist das ein Beispiel dafür, wie man Wissen, das einem persönlich als gesichert scheint, einfach wiedergibt, ohne es noch einmal zu prüfen. Nun ja, das habe ich dann irgendwann doch gemacht und mich bei der Gelegenheit einmal darum gekümmert, welche Optionen zum Initialisieren eines Packages existieren und welche Themen rechts und links davon hineinspielen.
Das Problem
Ein Package ist das Grundkonstrukt von PL/SQL-Programmen. Es ähnelt grob einer Klasse in objektorientierten Programmiersprachen, weil es mehrere Methoden bündeln und auch Attribute enthalten kann. Wenn Sie die Analogie zu einer Klasse weitertreiben möchten, könnten Sie private Packagevariablen über eine Prozedur schreiben (set) und über eine Funktion lesen lassen (get) und somit etwas erzeugen, was in Java eine Bean (Bohne) genannt werden würde. Dem Package fehlen allerdings weitergehende Fähigkeiten von Klassen, vornehmlich Vererbung und Interfaces, zudem können Packages nicht instanziert werden. Allerdings werden Packages mit Attributen durchaus einen Status haben, was einer instanzierten Klasse gleichkommt, dies wird aber immer nur pro Session einmal möglich.
Packages enthalten also oft Attribute (Variablen oder Cursor), die beim ersten Verwenden des Packages im Arbeitsspeicher mit Werten beladen werden müssen und die anschließend direkt zur Verfügung stehen. Beispiele könnten sein:
- Der Name eines Mandanten bei mandantenfähiger Software,
- globale Parameter, die hauptsächlich aus PL/SQL genutzt werden oder
- kleinere Listen von Referenzwerten.
Ein Package, das solche Attribute enthält, nennt Oracle stateful packages, sie sind zustandsbehaftet, was nichts anderes bedeutet, als dass sich mehrere Packages (in mehreren Sessions) durch die Werte ihrer Attribute unterscheiden können. Dieser Zustand muss also separat gespeichert werden.
Um diese Werte zu initialisieren, steht im Package die Möglichkeit offen, in der Implementierung des Packages das Schlüsselwort begin zu verwenden und anschließend Code zu schreiben, der diese Aufgaben übernimmt. Doch was passiert, wenn dieser Code nicht fehlerfrei ausgeführt werden kann? In welchem Zustand befindet sich das Package anschließend?
Mein Wissensstand war, dass in diesem Bereich kein exception-Block zur Verfügung steht und daher dieser Fehler unbemerkt bliebe. Zudem gelte das Package trotz fehlerhafter Initialisierung dennoch als initialisiert und die Funktionalität wäre anschließend letztlich unvorhersehbar. Das stimmt so nicht (und ich habe noch einmal bei Steven Feuerstein nachgelesen und bemerkt, dass ich das Problem vereinfacht in Erinnerung und dadurch falsch wiedergegeben hatte).
Sehen wir uns als das Problem einmal genauer an: Packages können Fehlerbehandlungen im Initialisierungscode enthalten, behandeln aber nur Fehler, die auch dort ausgelöst werden. Hierzu ein kurzes Beispiel:
SQL> set serveroutput on SQL> create or replace package test_pkg 2 as 3 my_global_var number; 4 5 end test_pkg; 6 / Package wurde erstellt. SQL> create or replace package body test_pkg 2 as 3 begin 4 dbms_output.put_line('Initialisierungscode berechnet'); 5 select 1 6 into my_global_var 7 from user_objects 8 where object_name = 'FOO' 9 and rownum = 1; 10 exception 11 when no_data_found then 12 my_global_var := 0; 13 end test_pkg; 14 / Package Body wurde erstellt. SQL> begin 2 dbms_output.put_line('Ergebnis: ' || test_pkg.my_global_var); 3 end; 4 / Initialisierungscode berechnet Ergebnis: 0 PL/SQL-Prozedur erfolgreich abgeschlossen. SQL> begin 2 dbms_output.put_line('Ergebnis: ' || test_pkg.my_global_var); 3 end; 4 / Ergebnis: 0 PL/SQL-Prozedur erfolgreich abgeschlossen.
Beim ersten Aufruf des Packages wurde der Initialisierungscode aufgerufen und der dort auftretende Fehler korrekt bearbeitet. Steven Feuerstein beschrieb ein Verhalten, dass einen Fehler verdeckt, wenn er außerhalb des Initialisierungscodes auftaucht:
SQL> create or replace package body test_pkg 2 as 3 char_to_small exception; 4 pragma exception_init(char_to_small, -6502); 5 my_internal_var char(1) := 'FOO'; 6 begin 7 dbms_output.put_line('Initialisierungscode berechnet'); 8 select 1 9 into my_global_var 10 from user_objects 11 where object_name = 'FOO' 12 and rownum = 1; 13 exception 14 when no_data_found then 15 my_global_var := 0; 16 when char_to_small then 17 dbms_output.put_line('Variable zu klein.'); 18 end test_pkg; 19 / Package Body wurde erstellt. SQL> begin 2 dbms_output.put_line('Ergebnis: ' || test_pkg.my_global_var); 3 end; 4 / begin * FEHLER in Zeile 1: ORA-06502: PL/SQL: numerischer oder Wertefehler: Zeichenfolgenpuffer zu klein ORA-06512: in "DOAG.TEST_PKG", Zeile 5 ORA-06512: in Zeile 2
Der Fehler wird geworfen, wenn die private, globale Variable my_internal_var durch eine zu lange Zeichenkette initialisiert wird. Diese Zuweisung wird aber außerhalb des begin-exception-Blocks innerhalb des Packages durchgeführt. Früher wurde der Fehler beim ersten Aufruf des Packages geworfen, das Package aber dennoch als korrekt initialisiert gekennzeichnet, der Wert von my_internal_var wäre früher NULL gewesen.
Doch hat sich in der Zwischenzeit das Verhalten geändert: Auch hier wird der Fehler mittlerweile korrekt geworfen, kann aber immer noch im Initialisierungsteil nicht abgefangen werden (ich kann nicht mehr nachvollziehen, ab welcher Version nicht mehr, habe aber Version 11 im Verdacht, weil hier der Compiler grundsätzlich überarbeitet wurde). Das Package gilt als nicht instanziert, der Fehler wird bei jedem Aufruf erneut geworfen.
Lösungsansätze
Eine Möglichkeit, Initialisierungsfehler außerhalb des begin-Blocks abzufangen, besteht in der Einführung einer eigenen Initialisierungsmethode, in der alle Zuweisungen durchgeführt werden:
SQL> create or replace package body test_pkg 2 as 3 my_internal_var char(1); 4 5 procedure initialize 6 as 7 char_to_small exception; 8 pragma exception_init(char_to_small, -6502); 9 begin 10 dbms_output.put_line('Initialisierungscode berechnet'); 11 my_internal_var := 'FOO'; 12 select 1 13 into my_global_var 14 from user_objects 15 where object_name = my_internal_var 16 and rownum = 1; 17 exception 18 when no_data_found then 19 my_global_var := 0; 20 when char_to_small then 21 dbms_output.put_line('Variable zu klein.'); 22 end initialize; 23 begin 24 initialize; 25 end test_pkg; 26 / Package Body wurde erstellt. SQL> SQL> begin 2 dbms_output.put_line('Ergebnis: ' || test_pkg.my_global_var); 3 end; 4 / Initialisierungscode berechnet Variable zu klein. Ergebnis: PL/SQL-Prozedur erfolgreich abgeschlossen.
Nun können alle Fehler während der Initialisierungsphase gefangen und bearbeitet werden. Zudem hat die Initialisierungsmethode noch den Vorteil, bei Bedarf veröffentlicht werden zu können. Auf diese Weise könnte das Package auch später noch re-initialisiert werden. Nun wo wir das Problem grundsätzlich im Griff haben, tauchen aber weitergehende Fragen auf: Warum machen wir das Ganze eigentlich?
Wir möchten den Package-Zustand initial festlegen und später ändern können
Eine Anwendung könnte sein, dass ein Package in einer globalen Variable festlegt, ob die Verarbeitung im Package protokolliert werden soll oder nicht. Initial könnten wir festlegen, dass keine Protokollierung erfolgt, später könnte der Status aber umgestellt werden. Es könnte sein, dass eine entsprechende Variable hierfür im Package verwendet wird.
Problematisch an diesem Ansatz ist, dass ein Package pro Session initialisiert wird. Sind also mehrere Sessions für einen Benutzer geöffnet, sind auch mehrere, voneinander unabhängige Package-"Instanzen" verfügbar. Das meint, dass ein initialisiertes Package mit entsprechenden Variablenwerten pro Session in dessen UGA vorhanden ist: Jede Session verfügt über eine unabhängige Kopie des Packages. Das führt dazu, dass eine Änderung der Packagevariablen immer nur in der jeweiligen Session zu sehen ist. Eine Session kann den Status von Packages in anderen Sessions nicht ohne weiteres einsehen oder gar ändern. Das mag gewollt sein oder auch nicht. Wenn Sie möchten, dass der Status des Packages in allen Sessions zentral geändert werden können soll, funktioniert dieser einfache Ansatz nicht, weil die Änderung einer Variablen im lokalen Package von den anderen Package-Instanzen nicht gesehen wird.
Eine eng verbundene Spielart dieses Problems besteht, wenn Sie zum Beispiel durch ein globales Attribut die Anzahl der Aufrufe von Packagemethoden zählen möchten. Auch ein solcher Zähler wird Session-bezogen zählen und eventuell nicht alle relevanten Aufrufe sehen können. Zudem ist man manchmal doch sehr überrascht, wie viele oder wenige Aufrufe einer Methode gezählt werden, wenn der Aufruf durch SQL erfolgt. Einfache Denkmodelle à la "eine select-Anweisung entspricht einem Aufruf" oder ähnlich, werden da nicht zum Ziel führen. update-Anweisungen können Methoden mehrfach aufrufen, select-Anweisungen entweder für jede Zeile einmal oder auch nur wenige Male für eine Anweisung. Wird eine Prozedur aus einem Job heraus aufgerufen, läuft sie in einem eigenen Sessionkontext und verfälscht dadurch die Zählung etc.
Zur Lösung vieler dieser Probleme verlagern wir den Zustand des Packages in einen Kontext, der als "Globaler Kontext" eingerichtet wurde. Unter einem Kontext verstehen wir bei Oracle eine Datenstruktur, in der auf leichtgewichtige Weise (die Struktur wird nur im Arbeitsspeicher verwaltet und kann ohne Umgebungswechsel aus SQL und PL/SQL angesprochen werden) boolesche Werte und kurze Zeichenketten unter einem Namen abgelegt werden können.
Sie kennen den von Oracle mitgelieferten Kontext USERENV, in dem sich Angaben zum angemeldeten Benutzer befinden und auf den mit der Funktion sys_context zugegriffen werden kann. Dieser Kontext ist Session-bezogen, speichert seine Daten also pro Session in der UGA. Ein Kontext kann aber auch global angelegt und durch ein Package unserer Wahl beschrieben werden. Wenn wir den Zustand unserer Variablen dort ablegen und auch stets nur von dort lesen, ist der Zustand des Packages in allen Sessions stets gleich, denn der Zustand wird nicht im Package, sondern im Kontext gespeichert.
Ein Beispiel führt hier ein wenig zu weit, sehen Sie sich Beispiele für globale Sessions unter dem Suchbegriff context accessed globally gern im Internet an [3]. Die Strategie besteht hier also darin, dass wir lokale Packagevariablen zu Gunsten einer sessionübergreifenden Speicherstruktur in der SGA aufgeben: Der Zustand wird, außerhalb des Packages, im Kontext in der SGA gespeichert.
SERIALLY_REUSABLE Packages
Packages mit globalen Variablen oder Cursor werden, wie bereits gesagt, von Oracle stateful packages genannt: Weil ein Package pro Session unterschiedliche Werte enthält, haben sie einen "Status" und dieser wird in der UGA gespeichert. Das führt dazu, dass viele Benutzer den jeweiligen Packagestatus in der eigenen UGA speichern. Da die Packages diesen Status zudem halten, bis die Session beendet wird, kann dieses Verhalten zu einer Belastung des Datenbankservers führen, denn der Zustand vieler instanzierter Packages muss gespeichert werden. Hieraus resultiert zunächst einmal die Empfehlung, die Speichergröße dieser globalen Variablen und Cursor im Hinterkopf zu haben und nicht exzessiv zu vergrößern.
Für eine spezielle Anwendungsform solcher Packages existiert seit Version 10g der Datenbank das Pragma serially_reusable. Wenn man weiß, worum es geht, erscheint einem der Name als Programm, wenn nicht – nicht. Worum geht es also? Stellen wir uns vor, ein Package müsste globale Variablen stets nur im Kontext eines "Server Calls", wie Oracle das nennt, vorhalten. Ein Server Call meint einen PL/SQL-Block, der zusammenhängend ausgeführt wird. Er kann weitere Packageaufrufe enthalten, beginnt aber beim ersten begin und endet irgendwann beim entsprechenden end.
Stellen wir uns nun vor, im Kontext dieses Server Calls würden alle relevanten Packageattribute auf Werte eingestellt, die in diesem Server Call benötigt werden. Nach dem Call könnten die Werte verworfen werden, denn der nächste Server Call setzt eigene Werte, die er für seine Arbeit benötigt. In einem solche Szenario könnte die Option serially_reusable helfen, die Speicherlast zu reduzieren. Denn: In einem herkömmlichen Package würde die Speicherung von Daten in einer globalen Variablen eines Packages dazu führen, dass dieser Wert für die Dauer der Session in der UGA verbleibt, obwohl ein Folgeaufruf des Packages in der gleichen Session diese gespeicherten Werte nicht verwendet, sondern durch neue ersetzt. Das Problem ist also, dass Werte in der UGA vorgehalten werden, obwohl sie durch den Folgeaufruf gar nicht mehr benötigt werden. Eigentlich könnte in diesem Szenario auf die Speicherung dieser Werte verzichtet werden, was die UGA entlasten würde.
In einem solchen Einsatzszenario ist das Package nacheinander, d. h. durch den Folgeaufruf, wiederverwendbar: serially_reusable. Dass dies so ist, kann der Compiler nicht erkennen, denn dies ergibt sich aus der Nutzung des Packages. Wenn Sie als Entwickler dieses Verhalten jedoch zusagen können und dies dem Package durch das Pragma (also einer Nachricht an den Compiler) serially_reusable mitteilen, wird die Datenbank nun folgendes tun: Der initiale Wert der Package-Variablen wandert in die SGA und dient sozusagen als Seed (Startwert) für den jeweiligen Aufruf durch die einzelnen Packages. Möchten Sie Änderungen an den Startwerten aus der SGA vornehmen, können Sie dies tun, diese Änderungen überleben aber den Server Call nicht, werden also mit Abschluss des äußersten PL/SQL-Blocks verworfen. In der SGA verbleiben nur die initialen Startwerte, in der UGA wird anschließend nichts gespeichert. Das reduziert die Speicherlast der UGA, hat aber den Nachteil, dass Ihr Code, wenn er denn doch auf den letzten geänderten Wert aus einem früheren Aufruf in der gleichen Session vertraut, nun nicht mehr (korrekt) funktionieren wird.
Finden Sie diese Option interessant, sollten Sie die Einschränkungen kennen, die festlegen, dass solche Packages nicht aus einem Trigger oder aus SQL heraus aufgerufen werden dürfen.
Vermeidung des Problems durch stateless Packages
Ist Ihnen das Ganze zu fummelig und verwickelt, kann man das Problem da und dort auch dadurch lösen, dass überhaupt keine Attribute in Packages verwendet werden. Ein Package ohne private oder öffentliche Variablen und Cursor wird als stateless angesehen und speichert von daher auch keinerlei Werte in der UGA. Konstanten, die sich ja definitionsgemäß nicht ändern können, stellen daher ebenfalls keine Attribute in diesem Sinne dar. Packages können durchaus ohne globale Attribute erstellt werden, im Regelfall wird dies jedoch zur Folge haben, dass Methoden mehr Parameter erhalten, also ohne globale Attribute, denn hierfür werden diese ja im Regelfall gebraucht: Um global gültige Informationen nicht von Methode zu Methode durch Parameter weitergeben zu müssen und andererseits nicht ständig die gleichen Initialisierungsroutinen für solche Parameter aufrufen zu müssen. Eine Abwägung also, die mit Bedacht durchgeführt werden sollte. Ich verwende globale Attribute in meinen Packages nur, wenn sie mir einen relevanten Vorteil verschaffen, aber so sparsam wie irgend möglich. Je nach Aufgabenstellung kann es aber durchaus sein, dass eine solche Selbstbeschränkung nicht aufrecht erhalten werden kann, dann muss man halt davon abweichen und hat mit den oben beschriebenen Möglichkeiten ein wenig Handlungsspielraum. Wichtiger ist jedoch, sich grundsätzlich klarzumachen, dass das Problem der Initialisierung von Packages existiert und die Auswirkungen auf den eigenen Code zu kontrollieren.
Zusammenfassung
PL/SQL-Packages sind die Grundbausteine für die Erstellung eines Programms innerhalb der Datenbank und haben große Vorteile in der Verwendung. Überladung, Kapselung von Logik durch private Methoden, Performanzoptimierung durch einmalige Kompilierung ganzer Programmteile sind nur einige Beispiele hierfür. Die Verwaltung global zugänglicher Attribute von Packages schafft allerdings eine neue Ebene der Komplexität, weil vormals zustandslose Packages nun einen Zustand erhalten. Erhält ein Package einen Zustand, sind Initialisierungen dieses Zustandes erforderlich, die durch Code im Packagekörper oder – besser – durch Initialisierungsmethoden durchgeführt werden.
Dieser Zustand ist Session-bezogen, was manchmal stören mag, und hat einen erhöhten Speicherverbrauch zur Folge, den man manchmal reduzieren kann. Techniken hierfür sind global zugängliche Kontexte und das Pragma serially_reusable. Sie stellen keine Basistechnik dar, sondern sind schon etwas abgefahrener, können aber spezielle Aufgabenstellungen hervorragend lösen.
- J. Sieben, 2014: Oracle PL/SQL: Das umfassende Handbuch, Galileo Computing
- S. Feuerstein, 2003: Oracle PL/SQL Programmierung, Seite 674 ff, O`Reilly
- Globale Sessions
Wann PL/SQL nicht verwendet werden sollte
Packages und ihre Initialisierung
Oracle Database: Deterministische Funktionen in PL/SQL
PL/SQL: Über die Bedeutung von Best Practices
Publikationen
- Oracle SQL: Das umfassende Handbuch
- Oracle PL/SQL: Das umfassende Handbuch
- Oracle APEX: Das umfassende Handbuch für Entwickler