Datenbanken SoSe2015
Blatt 9
Cursor
Hier ein paar technische Grundlagen zur Bearbeitung von Blatt 9:
Folgende Prozedur baut das Select-Statement aus Blatt 6d nach (Ausgabe des Anteils der Fläche an Europa pro Land),
und erweitert es für beliebige Kontinente:
CREATE OR REPLACE PROCEDURE Blatt6d (kontinent CONTINENT.NAME%TYPE) IS
CURSOR countries_on_continent IS
SELECT * FROM Country, encompasses WHERE encompasses.continent = kontinent and country.code = encompasses.country;
current_country countries_on_continent%ROWTYPE;
--Datentyp der Variablen "groesse_europa" könnte man aus der Tabellenspalte übernehmen.
--groesse_kontinent CONTINENT.AREA%TYPE;
groesse_kontinent NUMBER;
Begin
--Zuerst Fläche von Europe ermitteln:
select continent.area into groesse_kontinent from continent where name = kontinent;
DBMS_OUTPUT.PUT_LINE('Größe von ' || kontinent || ': ' || groesse_kontinent);
OPEN countries_on_continent;
FETCH countries_on_continent INTO current_country;
WHILE countries_on_continent%FOUND LOOP
DBMS_OUTPUT.PUT_LINE('Country: '|| current_country.code || ' - ' || current_country.name ||
', Anteil an ' || kontinent || ': ' || (current_country.Area*(current_country.Percentage/100)/ groesse_kontinent) );
FETCH countries_on_continent INTO current_country;
END LOOP;
CLOSE countries_on_continent;
end;
Ausführen:
execute Blatt6d ('Asia');
Falls Compilefehler kommen:
show errors PROCEDURE Blatt6d;
Infos zur Cursor-Verwendung:
Im Variablendeklarationsbereich wird er definiert mit der Syntax "CURSOR cursorname IS abfrage;"
Außerdem definiert man sich eine Variable, in die die aktuelle Zeile des Cursors geschrieben wird. Diese hat den Datentyp "cursorname%ROWTYPE":
"zeilenvariable cursorname%ROWTYPE".
Im Prozedurinhalt wird der Cursor mit "OPEN" geöffet: "OPEN cursurname;".
Anschließend holt man die erste Zeile: "FETCH cursorname INTO zeilenvariable;".
Jetzt beginnt eine Schleife: solange das Cursor-Attribut "%FOUND" true ist, läuft man weiter:
WHILE cursorname%FOUND LOOP
..
END LOOP;
Innerhalb der Schleife kann man auf die Daten der aktuellen Zeile zugreifen über die Variable "zeilenvariable", wobei diese pro Spalte des SELECT-Ausdrucks
einen Wert hat.
Am Ende der Schleife muss man den Cursor zum nächsten Datensatz weiterschalten durch erneutes "FETCH cursorname INTO zeilenvariable;".
Am Ende wird der Cursor geschlossen: "CLOSE cursorname;"
Trigger
Eine Reihe von Tutorials für Trigger: http://www.techonthenet.com/oracle/triggers/index.php
In der Aufgabenstellung findet sich schon die allgemeine Syntax für Trigger.
Es gibt mehrere Ereignisse für das Auslösen eines Triggers:
- BEFORE DELETE: wird vor dem Löschen ausgelöst. In der automatisch vorhandenen Variable "NEW" findet sich der zur löschende Datensatz.
Gemäß Doku dürfte man diese Werte sogar manipulieren ;-). In der Variablen "OLD" befinden sich die originalen Werte des zu löschenden Satzes.
- BEFORE INSERT: wird vor dem Einfügen ausgelöst. In der automatisch vorhandenen Variable "NEW" findet sich der einzufügende Datensatz. Man kann Werte manipulieren, die
im automatisch folgenden INSERT verwendet werden.
- BEFORE UPDATE: wird vor dem Aktualisieren ausgelöst. In der automatisch vorhandenen Variable "NEW" finden sich die neuen Werte des zu aktualisierenden Datensatz.
In der Variable "OLD" befinden sich die vorherigen Werte. Man kann Werte in "NEW" manipulieren, die im automatisch folgenden UPDATE verwendet werden.
- AFTER DELETE: wird nach dem Löschen ausgelöst. Zu den Variablen siehe "BEFORE DELETE"
- AFTER INSERT: wird nach dem Einfügen ausgelöst. In der automatisch vorhandenen Variable "NEW" findet sich der einzufügende Datensatz. Man
kann diese Werte nicht mehr manipulieren.
- AFTER UPDATE: wird nach dem Aktualisieren ausgelöst. In der automatisch vorhandenen Variable "NEW" finden sich die aktualisierten Werte des Datensatz.
Man kann diese Werte nicht mehr manipulieren. In der Variablen "OLD" befinden sich vermutlich die vorherigen Werte.
Zusatz "FOR EACH ROW": diesen Zusatz beim Trigger-Erzeugen setzt man, wenn man für jeden geänderte Zeile ein Auslösen des Triggers möchte.
Das ist z.B. sinnvoll, wenn bei einem UPDATE- oder DELETE-Statement mehrere Zeilen auf einmal manipuliert werden. Nur mit diesem Zusatz macht es Sinn,
auf ":NEW" oder ":OLD" zuzugreifen. Ohne "FOR EACH ROW" könnte man z.B. einen Trigger bauen, der allgemein bei einer Löschoperation z.B. eine
Benachrichtigung "Es wurden Datensätze gelöscht" generiert, ohne genauere Informationen darüber. Die Trigger in der Aufgabenstellung benötigen meiner
Meinung nach alle "FOR EACH ROW".
Beispiel: wir haben eine Tabelle "Pizza" und "Bestellung". Ungeschickterweise ist in Bestellung nicht die "PizzaID" als Fremdschlüssel auf die Pizza definiert,
sondern der Name der Pizza ist dupliziert. D.h. beim Umbenennen einer Pizza muss man dies in allen Bestellungen nachziehen.
Deshalb wird hier ein Trigger verwendet:
drop table Bestellung;
drop table Pizza;
create table Pizza
(
ID number not null,
PizzaName varchar(20) not null,
Erzeugt Date not null,
constraint Pizza_PK primary key (ID)
);
create table Bestellung
(
PizzaName varchar(20) not null,
Besteller varchar(20) not null,
Bestelldatum date not null
);
insert into Pizza (ID, PizzaName, Erzeugt) values (1, 'Hawaii', SYSDATE );
insert into Pizza (ID, PizzaName, Erzeugt) values (2, 'Salami', SYSDATE);
insert into Pizza (ID, PizzaName, Erzeugt) values (3, 'Tonno', SYSDATE);
insert into Bestellung (PizzaName, Besteller, Bestelldatum) values ('Hawaii', 'Wolfgang Knauf', SYSDATE);
insert into Bestellung (PizzaName, Besteller, Bestelldatum) values ('Salami', 'Hans Wurst', SYSDATE-2);
insert into Bestellung (PizzaName, Besteller, Bestelldatum) values ('Salami', 'Karl Otto', SYSDATE-14);
CREATE OR REPLACE TRIGGER Pizza_TRA
AFTER UPDATE
ON Pizza
FOR EACH ROW
BEGIN
update Bestellung set PizzaName = :NEW.PizzaName where PizzaName = :OLD.PizzaName;
END;
--Hier wird jetzt auch die Bestellung geändert:
update Pizza set Pizzaname = 'Hawai' where Pizzaname = 'Hawaii';
select * from pizza;
select * from bestellung;
ACHTUNG: beim anlegen eines Triggers kommt im SQLDeveloper dieses Fenster:
Hierbei handelt es sich wohl um einen Bug im SQLDeveloper: man klickt einfach auf "Anwenden"!
Blatt 8
Hier ein paar technische Grundlagen zur Bearbeitung von Blatt 8:
Prozeduren
Eine Prozedur hat keinen Rückgabewert.
Erzeugung:
CREATE OR REPLACE PROCEDURE MeineProzedur (Parameterdeklaration) IS
Variablendeklarationen
BEGIN
Anweisungen;
END;
Aufruf:
Direkter Aufruf:
execute MeineProzedur (Parameters);
Beispiel:
CREATE OR REPLACE PROCEDURE HelloWorldProc IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Hello World');
END;
EXECUTE HelloWorldProc();
Löschen:
drop PROCEDURE HelloWorldProc;
Mit "CREATE OR REPLACE PROCEDURE MeineProzedur" kann man eine vorhandene Prozedur ersetzen, ohne sie vorher löschen zu müssen.
Funktionen
Eine Funktion hat einen Rückgabewert.
CREATE OR REPLACE FUNCTION MeineFunktion (Parameterdeklaration)
RETURN Rückgabedatentyp
IS
Variablendeklarationen
BEGIN
Anweisungen;
RETURN Rückgabewert;
END;
Eine Funktion kann z.B. als Teil einer Abfrage verwendet werden:
select Spalte1, Spalte2, MeineFunktion(Parameters), Spalte 3 from MeineTabelle where ...;
Will man sie nur aufrufen und den Rückgabewert prüfen, dann geht das durch ein SELECT auf die Oracle-spezifische Pseudo-Tabelle "dual":
select MeineFunktion(Parameters) from dual;
Beispiel:
CREATE OR REPLACE FUNCTION HelloWorldFunc
RETURN varchar
IS
BEGIN
return 'Hello World';
END;
select HelloWorldFunc() from dual;
Löschen:
drop FUNCTION HelloWorld;
Mit "CREATE OR REPLACE MeineFunktion" kann man eine vorhandene Funktion ersetzen, ohne sie vorher löschen zu müssen.
Arbeiten mit Variablen
Variablen werden zwischen "IS" und "BEGIN" deklariert, jede Deklaration muss mit einem Semikolen beendet werden.
Als Zuweisungsoperator dient ":=".
CREATE OR REPLACE FUNCTION Quadrat(zahl number)
RETURN number
IS
quadrat number;
BEGIN
quadrat := zahl * zahl;
return quadrat;
END;
select Quadrat(17) from dual;
Als Datentypen kann alles verwendet werden, was man auch für Spalten angeben kann. Allerdings werden varchar ohne Länge deklariert,
also immer "varchar" statt "varchar(20)".
Außerdem kann eine Variable vom Datentyp einer Datenbankspalte deklariert werden. Hier ist die Syntax "Tabellenname.Spaltenname%TYPE" ("TYPE"
ist hier ein konstantes Schlüsselwort). Siehe weiter unten für ein Beispiel.
IF/THEN/ELSE
Syntax:
IF condition THEN
{...statements to execute when condition is TRUE...}
ELSE
{...statements to execute when condition is FALSE...}
END IF;
Beispiel:
CREATE OR REPLACE FUNCTION IstPositiv(zahl number)
RETURN number
IS
BEGIN
if (Zahl >= 0) then
return 1;
else
return 0;
end if;
END;
Anwendungsfehler auslösen
Die Funktion "Raise_Application_Error" löst einen Anwendungsfehler aus, z.B. wenn ein Parameter einen falschen Wert hat.
Der erste Parameter gibt die Nummer des Fehlers an und darf zwischen -20.000 und -20.999 liegen.
Der zweite Parameter ist die Fehlermeldung, die dem User ausgegeben wird.
Raise_Application_Error (-20343, 'The balance is too low.');
SQL in Funktionen
In Funktionen können SQL-Statements ausgeführt werden. Dabei können Parameter-/Variablenwerte in die Query eingebaut werden,
und es können per SELECT Werte in Variablen geschrieben werden (siehe die beiden fett markierten Zeilen in der folgenden Prozedur).
Die Beispielprozedur soll einen Eintrag in der Tabelle "Bestellung" erzeugen. Ihr wird der Name einer Pizza übergeben, sie benötigt
aber die ID. Diese wird per SELECT ermittelt (wobei ich dabei in eine dicke Besonderheit gestolpert bin, siehe Kommentar in der Funktion).
Ist die ID ermittelt, wird ein INSERT in "Bestellung" gemacht.
drop table Bestellung;
drop table Pizza;
create table Pizza
(
ID number not null,
PizzaName varchar(20) not null,
Erzeugt Date not null,
constraint Pizza_PK primary key (ID)
);
create table Bestellung
(
PizzaID int not null,
Besteller varchar(20) not null,
Bestelldatum date not null,
constraint Bestellung_FKA foreign key (PizzaID) references Pizza(ID)
);
insert into Pizza (ID, PizzaName, Erzeugt) values (1, 'Hawaii', SYSDATE);
insert into Pizza (ID, PizzaName, Erzeugt) values (2, 'Salami', SYSDATE);
insert into Pizza (ID, PizzaName, Erzeugt) values (3, 'Tonno', SYSDATE);
create or replace Procedure BestellePizza (pPizzaname Pizza.PizzaName%TYPE, pBesteller Bestellung.Besteller%TYPE) is
myPizzaID number;
BEGIN
/*Ohne "min(ID)" liefert das SELECT keine Treffer zurück, wenn nix gefunden wurden. Dadurch wirft die Funktion den
Fehler "01403. 00000 - "no data found". Deshalb als doofen Workaround: min(ID) holen - das liefert ein gültiges NULL, wenn nix da ist!
Folgendes klappt also nicht:
select ID into myPizzaID from Pizza where Pizza.PizzaName = pPizzaname; */
select min(ID) into myPizzaID from Pizza where Pizza.PizzaName = pPizzaname;
if (myPizzaID is null) then
Raise_Application_Error (-20001, 'Keine Pizza namens ' || pPizzaname || ' gefunden');
end if;
insert into Bestellung (PizzaID, Besteller, Bestelldatum) values (myPizzaID, pBesteller, SYSDATE);
END;
--Korrektes Insert:
execute BestellePizza ('Hawaii', 'Hans Wurst');
--Wird Fehler auslösen:
execute BestellePizza ('Hawai', 'Hans Wurst');
select * from bestellung;
Datumsmagie
Wie im letzten Beispiel schon gezeigt: die Konstante "SYSDATE" kann in Queries verwendet werden, um das aktuelle Systemdatum abzurufen.
In der Aufgabenstellung zu Blatt 8 wird die Funktion "TO_DATE" beschrieben, die einen String in ein Datum umwandelt.
Diese ist allerdings unsauber definiert: ihr wird nur ein umzuwandelnder String übergeben. Hier hängt das verwendete Datumsformat von den Einstellungen
des Servers ab. Besser ist es, wenn man ihr als zweiten Parameter den Formatstring übergibt:
select TO_DATE ('18.06.2015', 'DD.MM.YYYY') from dual;
select TO_DATE ('2015-06-18', 'YYYY-MM-DD') from dual;
select TO_DATE ('2015-18-06', 'YYYY-DD-MM') from dual;
Und jetzt habt ihr hoffentlich alle Infos, um mit Blatt 8 loszulegen.
SQLDeveloper
Ausgabe von "dbms_output.put_line"
Die Ausgabe von "DBMS_OUTPUT.PUT_LINE('Hello World');" finden wir auf dem Karteireiter "DBMS-Ausgabe" wieder. Eventuell muss man sich das Fenster
erst über Menü "Ansicht" - "DBMS-Ausgabe" dazuschalten.
Auf diesem Karteireiter muss man über das grüne "Plus" die Verbindung wählen, für die der Output
angeschaltet werden soll:
Troubleshooting
Wenn beim Anlegen einer Prozedur die Meldung "Warnung: Prozedur wurde mit Kompilierungsfehlern erstellt" kommt,
dann hilft dieser Befehl:
show errors;
Es empfiehlt sich, hier den Namen der nicht compilierten Procedure bzw. Function anzugeben, da man sonst alle im System vorliegenden Compilefehler angezeigt bekommt:
show errors PROCEDURE myProcedure;
show errors FUNCTION myFunction;
show errors TRIGGER myTrigger;
Zugriff auf FH-Oracle
Linux
Die Anleitung sagt:
Zuerst braucht man einen Tunnel von außen zum Server. Mit Ubuntu kann man das z.B. so machen:
ssh xyz@login2.cs.hs-rm.de -L 1521:oracle2:1521
Dann muss man den SQl-Developer starten und eine neue Verbindung
einrichten. Im folgenden Bild sieht man, was eingegeben werden muss.
Wichtig ist, dass bei "SID" p001 eingegeben wird. So heißt die Datenbank.
Windows
Basierend auf: http://adamsquicklog-webissues.blogspot.de/2011/02/connecting-sqldeveloper-to-remote-db.html
- "putty.exe" herunterladen von http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html
- "putty.exe" starten.
- In "Connection" -> "SSH" -> "Tunnels" gehen. Im Feld "Source port" wird "127.0.0.1:1521" eingetragen, im Feld "Destination" wird "oracle2:1521" eingetragen - die Adresse der
FH-Oracle:
Danach auf "Add" klicken.
Jetzt taucht die Verbindung unter "Forwarded ports" auf:
Auf "Session" wechseln und als Hostname "login1.cs.hs-rm.de" eingeben. Ich empfehle, das als Session zu speichern, damit man den Tunnel nicht jedesmal neu einrichten muss:
Jetzt auf "Open" klicken.
Es kommt ein Anmeldefenster. Dort mit dem FH-Login anmelden.
Nach erfolgreichem Login belassen wir das Fenster in diesem Zustand und ...
- ...melden uns im SQLDeveloper an. Hier gelten die gleichen Einstellungen wie im obigem Linux-Beispiel: Hostname = "localhost", SID = "p001".
Übungsblätter
Blatt 4, Aufgabe 1.5
Alternativlösung aus dem heutigen Praktikum, die diesen Fall abdeckt: A1.5_Knauf.pdf
Operatorbaum aus Hr. Heimrichs Musterlösung: A1.5_Musterloesung.pdf.
Hier klappt der Fall "Eine Vorlesung hat zwar einen direkten Vorgänger, aber keinen Vor-Vorgänger" nicht - man könnte das Problem lösen,
indem man den Verbund von "vorraussetzen" und "vorraussetzen" durch ein Right Outer Join ersetzt.
Allgemeine Hinweise
Tools für ER-Diagramme:
DIA
yEd
Draw.IO (browserbasiertes Tool)
Stand 23.06.2015
Historie:
07.05.2015: Erstellt
27.05.2015: Verbindung zur FH-Oracle, Lösungen zu Blatt 4
16.06.2015: Hinweise zur Prozeduren im SQLDeveloper
18.06.2015: Hinweise zu Blatt 8
23.06.2015: Hinweise zu Blatt 9