Lagrede programmer med PL/SQL

Lagrede programmer med PL/SQL
Bjørn Kristoffersen
Høgskolen i Telemark
[email protected]
For å utvikle databaseapplikasjoner blir SQL gjerne kombinert med generelle
programmeringsspråk, som for eksempel Java, C# eller PHP. SQL tar seg av
operasjoner mot databasen, mens beregninger og brukerkommunikasjon blir
håndtert i et generelt språk. SQL kan imidlertid brukes til mer enn INSERT
og SELECT!
SQL/PSM (Persistent Stored Modules) er en del av SQL-standarden og
beskriver en «prosedural utvidelse» av SQL – med variabler, tilordning, valg
og løkker. Dette kalles for lagrede programmer fordi koden blir lagret og
utført på databasetjeneren, og kan deles inn i to hovedkategorier:


En lagret rutine er enten en lagret funksjon eller en lagret prosedyre
og kalles opp fra klientapplikasjoner. Et Java-program kan kalle lagrede
rutiner ved å bruke metoder i JDBC-biblioteket. JDBC blir behandlet i
kapittel 15.
En trigger er en slags «hendelsesrutine». Det er et lite program som blir
lagret i databasen, og som blir utført automatisk når bestemte hendelser
inntreffer, for eksempel ved hver innsetting i en bestemt tabell.
PL/SQL er et programmeringsspråk for utvikling av lagrede programmer på
Oracle-databaser. Kapittel 14 tar for seg lagrede programmer i MySQL.
Lagrede prosedyrer
PL/SQL støtter både lagrede prosedyrer og lagrede funksjoner. Prosedyren
bytt_ut_vare vist under oppdaterer en bestemt ordre, ved å bytte ut en vare
med en annen. Antall enheter forblir uendret, mens enhetsprisen blir satt til
90 % av nåværende pris for den nye varen. Ordrenummeret, samt gammelt og
nytt varenummer er parametre til prosedyren.
Utførelse av prosedyren starter med første setning etter BEGIN, som er
en SQL-spørring. Merk at parameteren p_ny_vnr blir brukt i WHEREbetingelsen. Spørringen sjekker at det faktisk finnes en vare med dette vare-
nummeret. Hvis den nye varen ikke finnes i varetabellen blir operasjonen
avbrutt ved at det blir generert et unntak (exception). Programkontrollen
hopper i så fall til unntakshåndtereren etter EXCEPTION, og en feilmelding blir generert. Applikasjonen som kaller prosedyren vil fange opp
feilmeldingen, og kanskje be brukeren om å skrive inn et nytt varenummer.
Etter at ny pris er beregnet i setning 2 blir riktig rad i tabellen Ordrelinje
oppdatert med nytt varenummer og pris, før hele transaksjonen blir bekreftet
med COMMIT.
CREATE OR REPLACE PROCEDURE bytt_ut_vare
(
p_ordrenr NUMBER,
p_vnr VARCHAR2,
p_ny_vnr VARCHAR2
)
IS
v_pris NUMBER(10, 2);
BEGIN
SELECT Pris INTO v_pris FROM Vare
WHERE VNr=p_ny_vnr;
v_pris := v_pris*0.9;
UPDATE Ordrelinje
SET VNr=p_ny_vnr, PrisPrEnhet=v_pris
WHERE OrdreNr=p_ordrenr AND VNr=p_vnr;
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
raise_application_error(-20001, 'Ukjent varenummer');
END;
Lagrede funksjoner
«Innmaten» i lagrede funksjoner er lik lagrede prosedyrer. Forskjellen ligger i
at funksjoner returnerer en verdi. Det får to syntaktiske konsekvenser:


I toppen av funksjonen, etter parameterlisten, angir vi datatypen til returverdien.
Funksjonen blir avsluttet med en RETURN-setning som bestemmer
returverdien.
Eksempelfunksjonen under gir en returverdi av datatype NUMBER. Den får
inn et kategorinummer som parameter, finner gjennomsnittsprisen for alle
3
varer i denne kategorien med en SQL-spørring. Prisen blir lagret i variabelen
v_snitt og returnert i siste setning.
CREATE OR REPLACE FUNCTION snitt_pris
(p_kat NUMBER) RETURN NUMBER
IS
v_snitt NUMBER(10, 2);
BEGIN
SELECT AVG(Pris) INTO v_snitt
FROM Vare
WHERE KatNr = p_kat;
RETURN v_snitt;
END;
Funksjoner kan brukes fra en applikasjon på tilsvarende måte som vist for
lagrede prosedyrer. Forskjellen ligger i at returverdien nå vil bli lagret i en
programvariabel og brukt i videre beregninger.
Klient/tjener-kommunikasjon
Lagrede rutiner kan brukes fra et klientprogram. En databaseklient kan være
en selvstendig applikasjon, for eksempel et Windows-program med grafisk
brukergrensesnitt, en web-tjener, eller kanskje en applikasjonstjener.
For eksempel kan vi fra et Java-program kalle lagrede rutiner via JDBCmetoder, som vist i Figur 1. Klienten sender kall på prosedyrer og funksjoner
til DBHS ved hjelp av JDBC, og får eventuelle returverdier tilbake. Hvordan
kommunikasjonen foregår rent teknisk er skjult i JDBC-biblioteket. En fordel
med lagrede rutiner er at de kan brukes fra flere ulike applikasjoner.
Figur 1. Databaseklient og databasetjener
Anta at variabelen forbindelse representerer en åpnet forbindelse til databasen. Følgende Java-kode bruker bytt_ut_vare til å erstatte vare 33044 med
vare 33045 på ordre 20578:
CallableStatement kall =
forbindelse.prepareCall ("{CALL bytt_ut_vare(?,?,?)}");
kall.setInt(1, 20578);
kall.setString(2, "33044");
kall.setString(3, "33045");
kall.executeUpdate();
Oppsett av prosedyrekallet gjøres i flere steg, først ved bruk av metoden
prepareCall, som setter inn selve prosedyrenavnet. Parametrene blir kun antydet med plassholdere (de tre spørsmålstegnene). Kall på metodene setInt
og setString setter inn konkrete verdier for plassholderne. Utførelse av prosedyren gjøres med kall på executeUpdate i siste linje.
Markører
Spørringer som gir flere rader kan ikke håndteres med SELECT-INTO. I
stedet må vi bruke en teknikk som minner om gjennomløp av spørreresultater
med iteratormetoden next. En markør (cursor) er en navngitt spørring.
Spørreresultatet til en markør kan gjennomløpes med en løkke.
Prosedyren prisendring (se under) øker prisen på alle varene i en bestemt
kategori med et gitt beløp, og kopierer dessuten informasjon om gamle priser
til en historikktabell. En markør blir brukt for å behandle hver enkelt vare.
CREATE OR REPLACE PROCEDURE prisendring
(p_kat NUMBER, p_endring NUMBER)
IS
CURSOR c_varekategori IS
SELECT VNr, Pris FROM Vare
WHERE KatNr = p_kat;
c_rad c_varekategori%ROWTYPE;
BEGIN
OPEN c_varekategori;
LOOP
FETCH c_varekategori INTO c_rad;
IF c_varekategori%NOTFOUND THEN
EXIT;
END IF;
INSERT INTO Prishistorikk(VNr, Dato, Gammelpris)
VALUES (c_rad.VNr, SYSDATE, c_rad.Pris);
UPDATE Vare SET Pris=Pris+p_endring
WHERE VNr=c_rad.VNr;
END LOOP;
CLOSE c_varekategori;
COMMIT;
END;
5
Markøren c_varekategori blir definert i toppen av prosedyren (CURSOR-IS).
Koden som bruker markøren er omsluttet av setningene OPEN og CLOSE.
OPEN utfører spørringen og posisjonerer markøren på første rad i spørreresultatet. CLOSE lukker markøren.
Gjennomløpet av spørreresultatet blir gjort med en løkke (LOOP-END
LOOP), der hver enkelt rad blir kopiert inn i en sammensatt hjelpevariabel
c_rad med setningen FETCH. Den inneholder hele raden. Vi kan plukke ut
verdiene i hver enkelt kolonne med prikknotasjon, c_rad.VNr og c_rad.Pris.
Valgsetningen (IF-END IF) sjekker at det ikke er lest forbi slutten av spørreresultatet (%NOTFOUND). I så fall blir løkka avsluttet med EXIT. Ellers
blir nåværende pris satt inn i historikktabellen (INSERT), og varetabellen blir
oppdatert med ny pris (UPDATE). Helt til slutt i prosedyren, etter at løkka er
avsluttet, blir transaksjonen bekreftet (COMMIT).
Triggere
En trigger er et program som blir utført automatisk (av DBHS) hver gang en
bestemt hendelse inntreffer i databasen. Triggere blir blant annet brukt for å
kontrollere forretningsregler som ikke lar seg definere med mekanismer som
primærnøkler, fremmednøkler og verdimengdebeskrankninger. Uten triggere
vil alternativet være at samme regel ble bakt inn i samtlige applikasjoner som
oppdaterer databasen.
En hendelse kan være innsetting, oppdatering eller sletting mot en bestemt
tabell. Aksjonen som triggeren skal utføre blir beskrevet med standard
PL/SQL-kode. En SQL-setning som oppdaterer en tabell kan berøre flere
rader. En radtrigger blir utført for hver enkelt rad, mens en setningstrigger
blir utført en gang for hver SQL-setning. For radtriggere kan vi bestemme om
aksjonen skal bli utført før eller etter selve oppdateringen. Vi har tilgang på
før-verdier og etter-verdier, og kan for eksempel sjekke at ny verdi er større
enn gammel.
Følgende eksempel garanterer at samtlige fornavn og etternavn som blir
satt inn i tabellen Ansatt blir lagret med kun store bokstaver:
CREATE OR REPLACE TRIGGER ansatt_trg
BEFORE INSERT OR UPDATE ON Ansatt
FOR EACH ROW
BEGIN
:NEW.Fornavn := UPPER(:NEW.Fornavn);
:NEW.Etternavn := UPPER(:NEW.Etternavn);
END;
Her refererer :NEW.Fornavn til den nye verdien, og UPPER er en innebygd
funksjon som konverterer en tekststreng til store bokstaver. Fordelen med en
slik trigger er at vi ved søk ikke trenger å bry oss med om navn er registrert
med små eller store bokstaver, eller kanskje en kombinasjon. Vi kan alltid
søke etter navn med kun store bokstaver. Merk for øvrig at triggeren ikke berører data som allerede var lagret i det triggeren ble opprettet.
Triggere kan brukes til å overvåke mistenkelig adferd. For eksempel kan
man lage en trigger som blir aktivert hver gang noen oppdaterer lønnskolonnen i Ansatt-tabellen. Gamle og nye verdier og informasjon om den
som utførte oppdateringen kan skrives til en egen loggtabell.
Sekvenser
Sekvenser blir brukt for å implementere autonummerering i Oracle. En
sekvens er et objekt som genererer nye tall på forespørsel. Det er mulig å
styre både startverdi og inkrement. Eksempel:
CREATE SEQUENCE KNrSeq
START WITH 1
INCREMENT BY 1;
Sekvensen kan brukes i en PL/SQL-prosedyre for innsetting av nye kunder.
Vi antar tabellen har kolonner KundeNr, Fornavn og Etternavn. Ved å bruke
KNrSeq.NEXTVAL får vi tak i neste ledige kundenummer:
CREATE OR REPLACE PROCEDURE NyKunde
(p_fornavn VARCHAR2, p_etternavn VARCHAR2)
IS
BEGIN
INSERT INTO Kunde (KNr, Fornavn, Etternavn)
VALUES (KNrSeq.NEXTVAL, p_fornavn, p_etternavn);
END;
Fordi sekvenser er «selvstendige» objekter kan de også brukes hvis man vil at
flere tabeller skal dele den samme nummerserien. Autonummerering kan med
fordel gjøres i en trigger. Da kan vi hoppe over kundenummer i INSERTsetningen.
Pakker
En pakke er en samling lagrede funksjoner og prosedyrer, sekvenser og utsnitt som hører logisk sammen. Pakker utgjør modulbegrepet i PL/SQL, og
lagrede funksjoner og prosedyrer bør med få unntak bli organisert i pakker
for å holde orden.
Definisjon av en pakke består av en pakkespesifikasjon og en pakkekropp. Førstnevnte lister navn på lagrede funksjoner og prosedyrer, med
7
datatyper for parametre og eventuelle returverdier. Pakkespesifikasjonen definerer altså et grensesnittet til pakken, men inneholder ikke selve programkoden.
Koden under viser et eksempel på spesifikasjon av en pakke med nyttige
operasjoner mot varetabellen. Pakkekroppen vil ha samme oppbygging som
spesifikasjonen, men inneholder i tillegg selve programkoden, og man skriver
nøkkelordet BODY etter PACKAGE. Den kan også definere ekstra
funksjoner og prosedyrer som kun blir brukt internt i pakken.
CREATE OR REPLACE PACKAGE varepakke
IS
PROCEDURE ny_vare(
p_vnr VARCHAR2,
p_betegnelse VARCHAR2,
p_pris NUMBER,
p_kat VARCHAR2,
p_ant NUMBER,
p_hylle VARCHAR2);
PROCEDURE endre_pris(
p_vnr VARCHAR2,
p_ny_pris NUMBER);
PROCEDURE slett_vare(p_vnr VARCHAR2);
FUNCTION pris(p_vnr VARCHAR2) RETURN NUMBER;
END varepakke;