Tutorium: PL/SQL

Werbung
Datenbanken & Informationssysteme II
Oracle PL/SQL Tutorium
(Version 3.7 vom 20.3.2017)
• Einleitung
Im vorliegenden Tutorium soll eine Einführung in PL/SQL erfolgen. Als Grundlage muss man sich
von der Kurshomepage die Tabellendefinition sowie einige initiale Datensätze herunterladen.
Das Schema realisiert ein einfaches Bestellungssystem. Es gibt Kunden, Artikel (repräsentieren
gleichzeitig den Lagerbestand), Bestellungen und Nachbestellungen. Eine Bestellung ist einem
Kunden zugeordnet (via Fremdschlüssel kunde_fk). Ob eine Bestellung bereits verschickt wurde kann an der Spalte „verschickt“ abgelesen werden. Ist der Wert der Spalte 0, so ist die Bestellung eingegangen ist der Wert 1 so wurde sie bereits an den Kunden versendet. Der Gesamtwert
einer Bestellung ist in der Spalte gesamtpreis abgelegt. In ihm findet sich die Summe aller
Einzelposten (inkl. MwSt.). Jede Bestellung besteht aus einzelnen Posten (Tabelle Posten), die
fortlaufend numeriert sind (Spalte position) und für jede Bestellung mit Position 1 beginnen.
In einem Posten ist erfasst welcher Artikel (Fremdschlüssel artikel_fk) in welcher Menge (Attribut
anzahl) bestellt wird.
Für einen Artikel (Tabelle Artikel) ist neben der ID und einer textuellen Bezeichnung festgehalten
was der Artikel kostet und welcher Mehrwertsteuersatz gilt (19% oder 7%). Weiterhin ist der aktuelle-, minimale und maximale Lagerbestand für diesen Artikel angegeben.
Lade dir von der Homepage die Quellen zum Tutorium 1 herunter. Wirf einen Blick in die beiden
Dateien, so dass du verstehst was in ihnen passiert. Führe anschließend die Kommandos aus den
Dateien bestellung-ddl.sql und bestellung-stammdaten.sql aus. Dass kannst du beispielsweise von
einem DOS oder Cygwin Fenster aus machen (alternativ kannst du die Statements auch mittels
Copy & Paste im SQLDeveloper ausführen):
sqlplus <dein-account>@oracledbwi @ bestellung-ddl.sql
sqlplus <dein-account>@oracledbwi @ bestellung-stammdaten.sql
Diese Befehle sollten ohne Fehler1 durchlaufen. Schau dir anschließend die Datensätze in den Tabellen Kunde und Artikel an.
1. eventuell dürfen Fehler bei drop-Table Statements auftreten, wenn die Tabellen noch nicht existiert haben
Andreas Schmidt
Oracle PL/SQL-Tutorium 1/14
Datenbanken & Informationssysteme II
Als nächstes soll eine Bestellung von Peter Pipe über zwei „Head Defiance I 160
2006“ Snowboards und ebenfalls zwei Paar der nagelneuen „Red Mens Total Impact
Shorts 2009“ Hosen eingegeben werden. Die Boards haben die Bestellposition eins,
die Hosen Position zwei. Ganz zum Schluss nimmt Peter auch noch ein Exemplar des
„Easy Riding. Snowboard Freestyle Book“ mit.
Ok, legen wir die Bestellung an. Welche Informationen brauchen wir:
Peter Pipe hat die Kunden ID 4, das Snowboard hat die Artikel ID 1, die Hose hat die
Artikel ID 3 und das Buch die ID Nr. 5.
insert into bestellung (id, bestelldatum, kunde_fk)
values(bestellung_seq.nextval, sysdate, 4);
insert into posten
values(1, bestellung_seq.currval, 1,2);
insert into posten
values(2, bestellung_seq.currval, 3,2);
insert into posten
values(3, bestellung_seq.currval, 5,1);
Als letztes müssen wir jetzt noch für die Bestellung den Gesamtpreis errechnen und
in die Spalte gesamtpreis der Tabelle Bestellung eintragen. Das geht beispielsweise so:
1. Erst mal die ID der Bestellung in einer Variablen1 (b_id) speichern und ausgeben:
var b_id number;
begin
select bestellung_seq.currval
into :b_id
from dual;
end;
/
print b_id
1. Hinweis: Das folgende Codefragment benutzt eine SQLPlus Bindevariable (b_id). Die gibt es in älteren
SQL-Developer Versionen (2.x., 3.x) nicht. Wenn ihr mit dem SQLDeveloper Version arbeitet und habt
Probleme mit der Bindungsvariablen, dann lasst euch statt das obige Statement auszuführen die aktuelle
Bestellungs-ID ausgeben und benutzt in den nächsten Statements diesen Wert direkt anstatt :b_id
Andreas Schmidt
Oracle PL/SQL-Tutorium 2/14
Datenbanken & Informationssysteme II
2. Errechnen des Gesamtpreises jedes einzelnen Postens für eine Bestellung:
select (a.preis+a.preis*a.mwst/100)*p.anzahl
from posten p
join artikel a
on a.id=p.artikel_fk
where p.bestellung_fk = :b_id;
bzw. dann entsprechend die Aufsummierung über alle Posten der Bestellung:
select sum((a.preis+a.preis*a.mwst/100)*p.anzahl)
from posten p
join artikel a
on a.id=p.artikel_fk
where p.bestellung_fk = :b_id;
Das Update-Statement für die Berechnung des Gesamtpreises sieht dann wiefolgt aus:
update bestellung b
set
gesamtpreis=(select
sum((a.preis+a.preis *
a.mwst/100)*p.anzahl)
from posten p
join artikel a
on a.id=p.artikel_fk
where p.bestellung_fk = b.id)
where b.id = :b_id;
Ok, wie schauts dann jetzt in der Datenbank aus:
select *
from bestellung b
where b.id = :b_id;
sehr löblich, genau so haben wir uns da s vorgestellt ;-).
Ok, was gefällt uns daran alles nicht ?
1.
wir müssen mitzählen um welchen Posten es sich bei der Bestellung handelt (1,
2, 3, ...).
2.
Wir müssen ein zusätzliches Statement angeben, das die Gesamtsumme der
Bestellung berechnet und ins zugehörige Feld einträgt.
Andreas Schmidt
Oracle PL/SQL-Tutorium 3/14
Datenbanken & Informationssysteme II
In der nächsten Lösung die wir erarbeiten, sollen genau diese beiden Punkte vermieden
werden. Dies geschieht dadurch, dass wir eine Reihe von PL/SQL Prozeduren und Funktionen schreiben, welche das Eintragen der Bestellungen und Posten vornehmen.
Im einzelnen wollen wir folgende Funktionen /Prozeduren implementieren:
• function neue_bestellung(kunde_id number) return number :
Legt eine neue Bestellung für den angegebenen Kunden zum aktuellen Datum an und
gibt die Bestellungs ID zurück.
• procedure neuer_bestellposten(bestellung_id in number,
artikel_id in number,
anzahl in number):
Legt eine neue Position für die durch den Parameter bestellung_id spezifizierte
Bestellung an. Die aktuelle Positionsnummer im Rahmen der Bestellung soll dabei
automatisch ermittelt werden. Weiterhin soll der Gesamtpreis der Bestellung entsprechend Preis, Mehrwertsteuersatz. und Anzahl Artikel modifiziert werden.
• procedure bestellung_verschicken(bestellung_id number):
Einfaches Ding, setzt die Spalte Bestellung.verschickt auf den Wert 1
• procedure show_bestellung(b_id in number).
liefert mittels dbms_output.put_line(...)1 Statements eine übersichtliche
Aufstellung der angegebenen Bestellung. Die Ausgabe soll in etwa wiefolgt aussehen:
Bestellung Nr.20 vom 09.03.09
POS BEZ
E-PREIS ANZ
PREIS
-------------------------------------------------001 Head Defiance I 160 2006
299.95 3
899.85
002 Salomon Prospect LTD 159 2008
003 Easy Riding. Snowboard Freesty
399.95
1
399.95
12.80
5
63.99
-------------------------------------------------Summe:
1,363.79
Status: eingegengen
Als erstes machen wir uns an die Implementierung der Funktion
neue_bestellung(...). Aufgabe dieser Funktion ist die Ermittlung einer neuen
Bestell ID sowie das Eintragen des Datensatzes in die Datenbank. Die ermittelte ID muss
zurückgegeben werden, da wir diese für das nachfolgende Eintragen von Bestellposten
benötigen.
1. damit die Ausgabe sichtbar ist, muss zuvor das Statement set serveroutput on abgesetzt werden.
Andreas Schmidt
Oracle PL/SQL-Tutorium 4/14
Datenbanken & Informationssysteme II
create or replace
function neue_bestellung(kunde_id in number)
return number
as
b_id number;
begin
select bestellung_seq.nextval
into b_id
from dual;
insert into bestellung
values(b_id, kunde_id, sysdate, 0.0, 0);
return b_id;
end;
/
show errors
Nachdem die Funktion (ohne Fehlermeldung) in die Datenbank eingetragen worden
ist, kann sie durch das folgende Testprogramm ausgeführt werden:
declare
bestell_id number;
kunde_id number := 2;
begin
bestell_id := neue_bestellung(kunde_id);
dbms_output.put_line('Neue Bestellung mit ID '||
bestell_id ||' erzeugt');
end;
/
show errors
Die Ausgabe bei Ausführung sollte dann wiefolgt aussehen1:
Andreas Schmidt
Oracle PL/SQL-Tutorium 5/14
Datenbanken & Informationssysteme II
Neue Bestellung mit ID 2 erzeugt
Als nächstes machen wir uns an die Implementierung der Prozedur
neuer_bestellposten(...). Deren Auzfgabe ist die Ermittlung der nächsten Position, dem Eintragen des Postens in die Datenbank sowie der Update der
Spalte gesamtpreis des zughörigen Bestellungs-Datensatzes. Input der Prozedur sind die Bestell-ID, die Artikel ID sowie die Anzahl der zu bestellenden Artikel.
Die Ermittlung der nächsten Position erfolgt durch ein SQL-Statement, das die
maximale Positionsnummer + 1 für diese Bestellung zurückliefert:
select max(position) + 1
from posten
where bestellung_fk = ...
Eingebaut in die Definition einer PL/SQL Routine sieht das Statement dann wiefolgt aus:
create or replace
procedure neuer_bestellposten(bestellung_id number,
artikel_id number,
anzahl number)
as
a_position number;
begin
select max(position) + 1
into a_position
from posten
where bestellung_fk=bestellung_id;
end;
/
a_position ist dabei eine Variable, in die das Ergebnis der Anfrage eingetragen wird.
1. Die Nummer der ID darf selbstverständlich abweichen ;-) - Falls du nichts siehst, hast du wahrscheinlich vergessen die Ausgabe euinzuschalten (siehe Folie Bildschirmausgabe, Foliensatz zu PL/SQL).
Andreas Schmidt
Oracle PL/SQL-Tutorium 6/14
Datenbanken & Informationssysteme II
Da das SQL-Statement immer genau einen Datensatz zurückliefert kann hier mit
der select ... into ... Zuweisung gearbeitet werden.
Es müssen jedoch noch zwei mögliche Fälle unterschieden werden:
1.
Es gibt noch keinen einzigen Posten für diese Bestellung - in diesem
Fall liefert das SQL-Statement NULL zurück.
2.
Es gibt mindestens einen Posten - in diesem Fall wird die nächste
Position zurückgeliefert.
Diesem Umstand wird insofern Rechnung getragen, dass als nächstes überprüft
wird, ob NULL zurückgeliefert wurde. In diesem Fall setzen wir die Variable
a_position explizit auf 1:
if a_position is null then
a_position := 1;
end if;
So jetzt haben wir alle Informationen für den Posten-Datensatz zusammen und
können ihn eintragen:
insert into posten (position, bestellung_fk,
artikel_fk, anzahl)
values (a_position, bestellung_id,
artikel_id, anzahl);
Zum Abschluss muss noch der Betrag in der Spalte gesamtpreis (Tabelle Bestellung) angepasst werden. Dazu benötigen wir die Information zu Preis und
MwSt. aus dem betreffenden Artikeldatensatz und multipliziern ihn mit der Anzahl der zu bestellenden Artikel für diesen Posten. Das Ergebnis schreiben wir
dann ebenfalls mittels der select ... into... Zuweisung in eine temporäre Variable (postenpreis - muss wie die Variable a_position im
Header der Routine (vor dem begin-Statement) definiert werden.):
select
into
from
where
(preis + preis * mwst/100) * anzahl
postenpreis
artikel
id=artikel_id;
Ok, damit sind wir auch fast fertig. Wir haben in der Variable postenpreis
den entsprechenden Betrag für diesen Posten stehen und müssen diesen nur noch
zum bisherigen Gesamtpreis der entsprechenden Bestellung hinzuaddieren.
update bestellung
set gesamtpreis = gesamtpreis + postenpreis
where id = bestellung_id;
Andreas Schmidt
Oracle PL/SQL-Tutorium 7/14
Datenbanken & Informationssysteme II
Die komplette Routine sieht demnach so aus:
create or replace
procedure neuer_bestellposten(bestellung_id in number,
artikel_id in number,
anzahl in number)
as
a_position number;
postenpreis number;
begin
select max(position) + 1
into a_position
from posten
where bestellung_fk=bestellung_id;
if a_position is null then
a_position := 1;
end if;
insert into posten (position, bestellung_fk,
artikel_fk, anzahl)
values (a_position,bestellung_id,artikel_id,anzahl);
select (preis+preis *mwst/100) * anzahl
into postenpreis
from artikel
where id=artikel_id;
update bestellung
set gesamtpreis = gesamtpreis + postenpreis
where id = bestellung_id;
end;
/
Nachdem wir jetzt die beiden Routinen erstellt haben, können wir mal das folgende
kleine Testprogramm ausführen:
Andreas Schmidt
Oracle PL/SQL-Tutorium 8/14
Datenbanken & Informationssysteme II
declare
bestell_id number;
kunde_id number := 1;
begin
bestell_id := neue_bestellung(kunde_id);
neuer_bestellposten(bestell_id , 1, 3);
neuer_bestellposten(bestell_id , 2, 1);
neuer_bestellposten(bestell_id , 5, 5);
end;
/
show error
Zur Kontrolle schauen wir uns mittels den beiden folgenden Statements die Einträge in den beiden Tabellen Bestellung und Posten an:
select *
from Bestellung;
select *
from Posten;
Als nächstes werden wir die Hilfroutine show_bestellung(...) implementieren. Auf Seite 4 ist das gewünschte Ausgabeformat dargestellt. Eingabeparameter ist die ID der Bestellung die ausgegeben werden soll. Die erste Zeile der
Ausgabe enthält Informationen aus der Tabelle Bestellung. Da wir die ID der
Bestellung haben ist es für uns kein Problem mittels einer select ... into
... Zuweisung die Informationen einer Variablen (best) zuzuweisen. Die
Struktur der Variablen entspricht der eines Datensatzes in der Tabelle Bestellung
(Bestellung%rowtype). Der Zugriff auf die einzelnen Felder geschieht
dann mit der Punkt-Notation ( z.B. best.bestelldatum).
create or replace
procedure show_bestellung(b_id in number)
as
best bestellung%rowtype;
begin
select *
Andreas Schmidt
Oracle PL/SQL-Tutorium 9/14
Datenbanken & Informationssysteme II
into best
from bestellung
where id=b_id;
dbms_output.put_line('Bestellung Nr.'||b_id||
' vom '||best.bestelldatum);
dbms_output.put_line('--------- ... -------');
end;
/
Anschließend müssen wir auf alle Postendatensätze mit der entsprechenden Bestellungs-ID (Fremdschlüssel bestellung_fk) zugreifen. Dies wird durch einen Cursor implementiert. Ein Cursor ist eine Variable vom Typ Cursor, die mit
einem select-Statement verknüpft ist. Die Deklaration eines Cursors erfolgt analog zu den übrigen Variablen im Kopf der Routine:
cursor posten_cur is select *
from posten p
join artikel a
on p.artikel_fk=a.id
where bestellung_fk=b_id
order by position;
Anschließend kann im Rumpf der Routine mittels des Cursors auf die dem SQLStatement zugrundeliegenden Datensätze zugegriffen werden:
for p in posten_cur loop
dbms_output.put_line(to_char(p.position,'009')||
' ' ||rpad(p.bezeichnung,30)||' '
||to_char(p.preis+p.preis*p.mwst/100
,'9,9999.99')||'
'||lpad(p.anzahl,4)||'
'
||to_char((p.preis+p.preis*p.mwst/100)*p.anzahl,
'9,9999.99'));
end loop;
Die rpad(), lpad() und to_char() Funktionsaufrufe sind hierbei für die
Ausgabeformatierung zuständig (wirf einen Blick in die Oracle Online-Dokumentation um ihre Funktionsweise nachzuschlagen).
Zum Schluss soll jetzt noch der Gesamtpreis sowie der Satus der Bestellung ausgegeben werden. Dies geschieht durch folgende Codefragmente (die Variable
status muss zuvor ebenfalls noch im Kopf der Routine deklariert werden):
Andreas Schmidt
Oracle PL/SQL-Tutorium 10/14
Datenbanken & Informationssysteme II
if best.verschickt=1 then
status := 'versandt';
else
status := 'eingegengen';
end if;
dbms_output.put_line('------ ... -------');
dbms_output.put_line('Summe: '||
lpad(to_char(best.gesamtpreis,'9,999.99'), 60));
dbms_output.put_line('Status: '||status);
So das wars. Jetzt solltest du in PL/SQL so fit sein, dass du dich ans erste Übungsblatt machen kannst (vorausgesetzt du weisst schon was Trigger sind).
Zum Abschluss und zur Überleitung aufs erste Übungsblatt jetzt noch die alternative
Realisierung der Prozedur neuer_bestellposten(...) als Trigger: Zweck
der Prozedur war es, automatisch die nächste Positionsnummer für einen Posten zu
ermitteln. Dazu wurde innerhalb der Prozedur die für diese Bestellung größte vergebene Positionssnummer ermittelt und anschließend die um eins erhöhte Positionsnummer, zusammmen mit den anderen Werten (artikel_fk,
bestellung_fk und anzahl) in Tabelle posten eingetragen.
Ok, was müssen wir tun, wenn wir uns nicht um die Position eines Postens kümmern
wollen? - 1. Versuch, wir lassen sie einfach weg ;-)
insert into posten(bestellung_fk, artikel_fk, anzahl)
values(bestellung_seq.currval1, 3, 1);
Dieses Vorgehen scheitert, da das Feld position als NOT NULL gekennzeichnet
ist. Was wir aber machen können ist, innerhalb eines Triggers, den Wert zu berechnen und zu setzen. Das Setzen des Wertes muss vor dem eigentlichen Eintrag erfolgen (BEFORE INSERT), denn sonst rennen wir wieder gegen das NOT NULL
Constraint. Da wir auf einen Wert eines Datensatzes zugreifen wollen (wir setzen
einen Wert), muss es sich weiterhin um einen Row-Trigger handeln.
Hier mal eine triviale Implementierung, welche hardcodiert die Position auf den
Wert 1 setzt:
create or replace TRIGGER set_position
before insert on posten
for each row
1. bzw. ein gültiger Primärschlüsselwert für eine Bestellung
Andreas Schmidt
Oracle PL/SQL-Tutorium 11/14
Datenbanken & Informationssysteme II
begin
dbms_output.put_line('START TRIGGER set_position');
:new.position := 1;
dbms_output.put_line('END TRIGGER set_position');
end;
/
show errors
Wenn wir jetzt noch einmal das obige insert-Statement absetzen1, bekommen wir
keine Fehlermeldung mehr (zumindest keine was das NOT NULL Constraint betrifft) und der Datensatz wird eingetragen, allerdings hat jeder Posten die
Position 1. Das ist natürlich auch quatsch und kann einfach verhindert werden.
Formuliere dazu ein Constraint, so dass es nicht vorkommen kann, dass zwei Posten einer Bestellung den selben Wert haben. Überprüfe anschließend, indem du
weitere Posten eingibst, ob dein neu formuliertes Constraint funktioniert. Ok, und
jetzt ist es ein auch nur noch ein kleiner Schritt die Implementierung des Triggers
so abzuändern, dass innerhalb des Triggers die korrekte Positionsnummer gesetzt
wird (wirf einfach einen Blick in die Implementierung der Prozedur
neuer_bestellposten(...) ).
Hinweis: Beachte, dass auf der Tabelle posten bereits ein Constraint formuliert
ist, das überprüft, dass keine zwei Posten sich auf den selben Artikel beziehen (das
wird über die Anzahl gemacht).
• Anhang A:
create table kunde (
id number primary key,
vorname varchar2(20) not null,
nachname varchar2(20) not null
);
create table bestellung (
id number primary key,
kunde_fk number not null references kunde(id),
bestelldatum date,
gesamtpreis number(7,2) default 0,
verschickt number(1) default 0 not null
1. um zu sehen, dass der Trigger auch aufgerufen wird, bitte die Ausgabe einschalten
Andreas Schmidt
Oracle PL/SQL-Tutorium 12/14
Datenbanken & Informationssysteme II
);
create table artikel (
id number primary key,
bezeichnung varchar2(80) not null,
preis number(7,2) not null,
mwst number(3) not null,
lager_ist_bestand number(3) not null,
lager_min_bestand number(3) not null,
lager_max_bestand number(3) not null
);
create table posten (
position number check (position > 0) not null,
bestellung_fk number not null
references bestellung (id)
on delete cascade,
artikel_fk number not null
references artikel(id),
anzahl number check (anzahl > 0),
constraint CONS_KEINE_GLEICHEN_ARTIKEL
unique(bestellung_fk, artikel_fk)
);
create table nach_bestellung (
id number primary key,
datum date default sysdate not null,
artikel_fk number not null
references artikel(id) on delete cascade,
anzahl number
);
• Quellen:
Kurshompage: http://www.smiffy.de/DB-IS-II
• Hinweise:
Andreas Schmidt
Oracle PL/SQL-Tutorium 13/14
Datenbanken & Informationssysteme II
• Der Oracle Server befindet sich auf dem Rechner mit dem Namen
iwi-w-vm-dbo.hs-karlsruhe.de, Port 1521.
• Die Datenbankinstanz ist ORACLEDBWI
• Der SQLDeveloper beitet einen Debugger. Details dazu:
http://www.oracle.com/webfolder/technetwork/tutorials/obe/db/
devdays2012/mod2_sqldev/mod2_sqldev.html
Andreas Schmidt
Oracle PL/SQL-Tutorium 14/14
Herunterladen