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:

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:
Bind-Variablen
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.
DBMS-Ausgabe
Auf diesem Karteireiter muss man über das grüne "Plus" die Verbindung wählen, für die der Output angeschaltet werden soll:

DBMS-Ausgabe

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.

SQLDeveloper mit Tunnel unter Linux


Windows

Basierend auf: http://adamsquicklog-webissues.blogspot.de/2011/02/connecting-sqldeveloper-to-remote-db.html

Ü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