Lagrede programmer med PL/SQL Bjørn Kristoffersen Høgskolen i Telemark bjorn.kristoffersen@hit.no 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;
© Copyright 2024