SQLExcepton
ResultSet
RowSet
-objekter: typer og egenskaperCachedRowSet
RowSet
-hendelserRowSet
i en JTable
executeUpdate
JDBC er forkortelse for Java Database Connectivity. Det er et API
(grensesnitt) som gjør det mulig for et javaprogram å kommunisere med
en database. JDBC består av mange klasser og
interface'er, samlet i pakken
java.sql
.
Ved hjelp av disse
klassene sender javaprogrammet SQL-setninger til databasen og får respons
tilbake.
De forskjellige databasesystemene som finnes på markedet har alle sitt eget API (grensesnitt) mot omverdenen. Dette er vanligvis ikke skrevet i java, men for eksempel i C. Vi trenger derfor noe som kan ta imot våre JDBC-kall og omforme dem til metodekall som passer med det databasesystemet vi skal bruke. En slik omformer kalles en databasedriver.
En tilsvarende problemstilling gjør seg gjeldende når det gjelder plattformen Microsoft Windows. Programvaren ODBC (Open Database Connectivity) gjør det mulig for Microsoft Windows å kommunisere med nær sagt et hvilket som helst databasesystem på markedet. Dersom vi kjører java på Microsoft Windows, betyr dette at dersom vi får javaprogrammet vårt til å kommunisere med ODBC, så vil veien være åpen til en stor mengde databasesystemer.
Databasedriverne er av to hovedtyper:
En driver av den første typen følger med som en del av selve javaplattformen Java SE. Driveren krever at tilhørende ODBC-driver er installert på maskinen der javaprogrammet skal bli utført. Det er en grei løsning dersom man bare skal eksperimentere med JDBC. Men for profesjonell bruk bør man bruke andre drivere. Det finnes drivere som er tilpasset de aller fleste databasehåndteringssystemer som finnes på markedet. I JDBC-spesifikasjonen blir driverne klassifisert i følgende fire typer:
De fleste databaseleverandører tilbyr en driver av type 3 eller 4 sammen med sin database. I tillegg er det mange tredjepartsselskaper som har spesialisert seg i å utvikle drivere som har bedre tilpasning, har støtte for flere plattformer, og har bedre ytelse, eller, i noen tilfeller, kort og godt har bedre pålitelighet enn driverne som blir tilbudt av databaseleverandøren. En søkeside for opplysninger om tilgjengelige drivere finnes på nettadressen http://devapp.sun.com/product/jdbc/drivers. En del informasjon om JDBC og Javas databaseteknologi finnes på adressen http://www.oracle.com/technetwork/java/javase/tech/index-jsp-136101.html.
For databasetypen MySQL finnes driveren Connector/J. Det er en driver av Type 4.
Det som ønskes oppnådd ved hjelp av JDBC kan vi kort summere opp på følgende måte:
En kan kanskje lure på hvorfor Sun, som var den opprinnelige javautvikleren, ikke valgte å bygge på ODBC-modellen, iallfall for programmer som skal kjøres på Windows-plattform. På JavaOne-konferansen i mai 1996 ble dette begrunnet med følgende:
void
-pekere
og andre C-teknikker som det ikke er naturlig å bruke i java.JDBC kan brukes i både applikasjoner og appleter. For appleter gjelder imidlertid de vanlige sikkerhetsbegrensningene. Det betyr i dette tilfelle at appleter som bruker JDBC bare kan åpne en databaseforbindelse til serveren som de selv hentes fra. Det betyr at nettserveren og databaseserveren må være på samme maskin, noe som ikke er et typisk oppsett. Applikasjoner derimot, har full frihet til å aksessere fjerne databaseservere. Dersom en skal lage et tradisjonelt klient/server-program, er det derfor mest aktuelt å bruke et program av type applikasjon for databaseaksess. Klienten kommuniserer da direkte med databasen via JDBC. Tendensen nå til dags er imidlertid at en går bort fra klient/server-modellen og over til en modell som består av tre eller flere lag. I en trelagsmodell vil ikke klienten gjøre databasekall. Isteden gjør klienten kall på et lag i midten, som i sin tur foretar databasekallene. Dette gir et mer fleksibelt opplegg på den måten at det midterste laget kan kontaktes på forskjellig måte, uavhengig av kommunikasjonen som dette har med databasen. For denne kommunikasjonen kan en bruke JDBC. Den vanligste måten å kommunisere på fra klient til det midterste laget, er å bruke http-protokollen via nettlesere. Dette kan derfor kombineres med bruk av appleter, siden slike kan inngå i html-dokumenter. I det midterste laget kan det være aktuelt å bruke en java-servlet.
I denne lille introduksjonen er det bruk av JDBC vi skal fokusere på. For enkelhets skyld skal vi derfor bare ta for oss et tradisjonelt tolags-program der en applikasjon kommuniserer direkte med en database via JDBC.
Grunnstrukturen til et JDBC-program består av følgende skritt:
Statement
-objekt som kan sende SQL-setninger til
databasen.ResultSet
-objekt som ble returnert fra
databasen.Disse skrittene, unntatt lukking av databaseforbindelsen, er skissert i den lille kodeskissen nedenfor.
public void opprettForbindelseOgSpørDatabase(String brukernavn, String passord) throws SQLException { Connection con = DriverManager.getConnection("jdbc:minDriver:minDatabase", brukernavn, passord) try (Statement stmt = con.createStatement()) { ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM Tabell1"); while (rs.next()) { int x = rs.getInt("a"); String s = rs.getString("b"); float f = rs.getFloat("c"); } } catch (SQLException sqle) { //behandle unntak } }
Vi bruker altså klassen
DriverManager
for å opprette forbindelse til databasen ved å gjøre kall på dens
static
-metode getConnection
. Den vil automatisk prøve å velge en passende
databasedriver blant dem som er installert på maskinen. For at det skal lykkes å opprette
forbindelse, er det selvsagt en forutsetning at databaseserveren er i drift.
Forbindelsen er representert av
Connection
-objektet
som blir returnert fra metoden.
For å utføre databasespørringer bruker vi et
Statement
-objekt.
Det oppretter vi
ved hjelp av Connection
-objektet som representerer forbindelsen:
Statement stmt = con.createStatement();
For å utføre spørringen mot databasen, bruker vi Statement
-objektet til å gjøre
kall på en av dets execute
-metoder med ønsket SQL-setning som parameter.
Resultatet av spørringen får vi tilbake i form av et
ResultSet
-objekt.
Dette gjennomløper vi rad for rad ved hjelp av en
while
-løkke for å hente ut enkeltdata
fra radene.
Ett og samme Statement
-objekt kan brukes til å utføre gjentatte spørringer
og kommandoer. Når vi er ferdige med å bruke et objekt av type ResultSet
,
Statement
eller Connection
, så bør vi lukke det umiddelbart
for å frigjøre ressurser. I kodeskissen ovenfor er lukking av Statement
-objektet
sikret ved at det er brukt kodetypen
'try
med ressurer' for å opprette det.
Når et Statement
-objekt blir lukket, så vil også et tilhørende
ResultSet
-objekt automatisk bli lukket. For øvrig så har alle de tre nevnte
objekttypene sin close
-metode. For å sikre at den blir utført,
når vi ikke bruker 'try
med ressurer',
kan det være
lurt å plassere kallet på den i en finally
-blokk.
Kall på close
-metoden til et Connection
-objekt vil automatisk
sørge for kall på tilsvarende metode for alle Statement
-objekter som er
knyttet til forbindelsen.
Vi skal etter tur se nærmere på de enkelte skrittene som er listet opp ovenfor. Framstillingen er dels basert på det som er skrevet om dette i The Java Tutorials, dels på framstillingen i boka Core Java, Volume II, Eighth Edition, av Cay S. Horstmann og Gary Cornell. De konkrete eksemplene som blir brukt er også hentet fra disse to kildene, med mindre noe annet er nevnt. De er til en viss grad tilpasset. Vi skal ikke ta for oss detaljer omkring oppbygging av databasene som er nevnt i eksemplene, men begrense oss til nødvendig omtale av tabellene som blir brukt i eksemplene for å kunne illustrere hvordan JDBC kan brukes til å kommunisere og manipulere med databasen.
Det finnes i Javas klassebibliotek følgende to typer klasser som kan brukes til å opprette forbindelse:
DriverManager
:
Dette er en fullt implementert klasse som når den blir brukt, automatisk vil laste inn
eventuelle drivere som den finner på installasjonens class path.DataSource
:
Klasser som implementerer dette interface
anbefales brukt i profesjonelle sammenhenger. For bruken av det vises det til
beskrivelsen i The Java Tutorials.
Applikasjonsutvikling med denne typen forbindelse er beskrevet i
Tutorial for Java Enterpise Edition.I eksemplene vi skal ta for oss vil det bli brukt typen DriverManager
fordi den er enklest å bruke og fordi det i eksemplene ikke er bruk for den
utvidete funksjonaliteten som tilbys av typen DataSource
.
DriverManager
Metoden DriverManager.getConnection
krever som parameter
en database-URL. Hvordan denne ser ut, avhenger av hvilket databasehåndteringssystem
som blir brukt. Dersom det brukes MySQL, kan den for eksempel se ut slik:
"jdbc:mysql://localhost:3306/"
,
der localhost
er navnet på databaseserveren og 3306
er portnummeret.
Det vanligste er vel imidlertid å opprette forbindelse til en navngitt database
som allerede eksisterer. Dersom den heter minsql
, så må dette
tilføyes i URL-en, slik at den kan bli seende ut slik:
"jdbc:mysql://localhost:3306/minsql"
Dersom det brukes Javas innebygde databasehåndteringssystem, Java DB, kan
URL-en se ut for eksempel slik:
jdbc:derby:testdb;create=true"
, der
testdb
er navnet på databasen som det skal opprettes forbindelse til og
create=true
instruerer databasehåndteringssystemet om å opprette databasen.
Generelt så er en database-URL en streng som JDBC-driveren bruker for å opprette forbindelse med databasen. Den kan inneholde informasjon om hvor det skal letes etter databasen, navnet på den, og konfigurasjonsegenskaper. Den eksakte syntaksen for database-URL-en vil være spesifisert i dokumentasjonen for vedkommende databasehåndteringssystem.
Syntaksen for database-URL-en for driveren Connector/J for MySQL ser ut som følger:
jdbc:mysql://[host][,failoverhost...] [:port]/[database] [?propertyName1][=propertyValue1] [&propertyName2][=propertyValue2]...
host:port
er vertsnavnet og portnummeret for
datamaskinen som databasen ligger på.
Dersom de ikke er spesifiserte, så er default-verdi for host
lik 127.0.0.1 og for port
lik 3306.database
er navnet på databasen det skal opprettes forbindelse til.
Dersom det ikke er spesifisert, vil det ikke bli opprettet forbindelse til noen
default-database.failover
er navnet på en standby-database
(MySQL Connector/J har support for failover).propertyName=propertyValue
representerer
en frivillig, ampersand-separert liste av egenskaper.
Disse attributtene kan brukes til å instruere MySQL Connector/J til å utføre diverse oppgaver.Se MySQL Reference Manual for mer informasjon.
En metode som oppretter forbindelse med en MySQL-database og returnerer et
Connection
-objekt som representerer forbindelsen kan dermed se ut
for eksempel på denne måten:
public Connection getConnection(String userName, String password) throws SQLException { Connection conn = null; Properties connectionProps = new Properties(); connectionProps.setProperty("user", userName); connectionProps.setProperty("password", password); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/minsql", connectionProps); return conn; }
SQLExcepton
Når et javaprogram skal jobbe mot en database, er det selvsagt mange
kilder til at unntakssituasjoner kan oppstå. Det vil da bli kastet ut
unntaksobjekt av type
SQLException
.
I mange tilfeller vil det da være snakk om en hel kjede av unntaksobjekter.
For å få forståelse for hvordan dette virker og hvordan vi skal behandle denne
typen unntaksobjekter, må vi derfor først se nærmere på hvordan unntaksobjekter
kan kjedes sammen. Dette temaet ble ikke behandlet i notatet
Unntakshåndtering (exceptions).
For å fange opp unntaksobjekter bruker vi
try
-blokker. I noen tilfeller
kan det være aktuelt i en try
-blokk
å kaste ut et nytt unntaksobjekt av en annen type. Det kan for eksempel være
tilfelle dersom vi programmerer et undersystem til et annet system og feilen skyldes
at noe gikk galt i undersystemet. Det er da fornuftig å kunne informere om at
det var i undersystemet feilen oppsto. Men dersom vi bare kaster ut et nytt
unntaksobjekt som i følgende skisse, så vil den informasjonen som lå i det
opprinnelige unntaksobjektet gå tapt:
try { < gjør aksess på database > } catch (SQLException e) { throw new AnnenExceptiontype("..."); }
Det er ønskelig både å kunne videreføre informasjonen som lå i det opprinnelige unntaksobjektet og kunne informere om at det var dette som var årsaken til at det nye unntaksobjektet ble kastet ut. Det oppnår vi ved å modifisere skissen ovenfor til følgende variant:
try { < gjør aksess på database > } catch (SQLException e) { Throwable se = new AnnenExceptiontype("..."); se.initCause(e); throw se; }
Når det nye unntaksobjektet blir fanget opp, kan vi nå også få tak i det opprinnelige:
Throwable e = se.getCause();
Istedenfor å bruke metoden initCause
for å angi årsak, kan vi
bruke unntaksobjektet som er årsak som en konstruktørparameter for det nye
unntaksobjektet:
Throwable se = new AnnenExceptiontype("...", e);
SQLException
Sammenkjeding av unntaksobjekter som er årsak til hverandre slik det er beskrevet
ovenfor er mulig å gjøre for alle typer unntaksobjekter. Det er funksjonalitet som
er bygget inn i klassen
Throwable
som alle typer unntaksobjekter arver fra. Unntaksobjektene som kan bli
kastet ut fra JDBC-metoder er altså av typen SQLException
.
Et slikt objekt er utstyrt med en kjede av andre SQLException
-objekter
som vi suksessivt kan få tak i ved bruk av objektenes getNextException
-metode.
Hvert objekt kan også ha en "årsakskjede" som beskrevet ovenfor. Vi får dermed
en dobbelt kjede. Før vi tar for oss kode for å gjennomløpe dobbeltkjeden, skal
vi se nærmere på hva slags informasjon et SQLException
-objekt inneholder
og som dermed kan være til nytte for å avgjøre hva en unntakssituasjon skyldes.
Et SQLException
-objekt inneholder følgende informasjon:
getMessage
-metode.DatabaseMetaData.getSQLStateType
.
Returverdien fra denne vil være en av
int
-konstantene
sqlStateXOpen
eller sqlStateSQL
.) SQL-statuskoden består
av fem alfanumeriske tegn som vi kan få tak i ved å gjøre kall på
SQLException
-objektets getSQLState
-metode.int
-verdi
som skal identifisere feilen
som var årsak til den SQLException
som oppsto. Den er bestemt av
leverandøren for driveren. Vi kan få tak i den ved å gjøre kall på
SQLException
-objektets metode getErrorCode
.SQLException
-objekt
være knyttet en årsakskjede bestående av ett eller flere Throwable
-objekter
som suksessivt var årsak til at SQLException
-objektet ble kastet ut. Objektene
i denne kjeden kan vi få tak i ved rekursivt å gjøre kall på metoden
getCause
inntil denne returnerer
null
, som altså
må brukes som test for å avgjøre om rekursjonen skal fortsette.SQLException
i kjeden av slike, se ovenfor.
Siden SQLException
implementerer
interface Iterable<Throwable>
,
kan vi gjennomløpe kjeden ved å bruke en utvidet
for
-løkke, som skissert
i eksemplet nedenfor.Her er eksempel på en metode som gjennomløper og skriver ut
innholdet av et SQLException
-objekt og kjeden av
andre slike objekter knyttet til dette, samt årsakskjeden for hvert objekt:
public static void printSQLException(SQLException ex) { for (Throwable e : ex) { e.printStackTrace(System.err); System.err.println("SQL-status: " + e.getSQLState()); System.err.println("Feilkode: " + e.getErrorCode()); System.err.println("Melding: " + e.getMessage()); Throwable t = ex.getCause(); while (t != null) { System.out.println("Årsak: " + t); t = t.getCause(); } } }
En feil av type SQLException
er så alvorlig at
det ikke er mulig å fortsette med videre kjøring av programmet.
I tillegg til å rapportere om slike feil, så kan databasedriveren
rapportere unormale tilstander som ikke er så alvorlige
at de hindrer videre kjøring av programmet, dette i form av advarsler. Slike advarsler er objekter av type
SQLWarning
.
Denne klassen er subklasse til SQLException
, men typen regnes ikke
til å være noen unntakstype. Slike advarsler kan hentes ut av objekter av type
Connection, Statement
og ResultSet
ved bruk av deres metode
getWarning
. I likhet med SQLException
-objekter, så er
SQLWarning
-objekter sammenkjedet. Dersom vi for eksempel har
et Statement
-objekt stat
, så kan vi få hentet ut
alle advarsler fra dette ved å bruke en slik løkke:
SQLWarning w = stat.getWarning(); while (w != null) { < gjør et eller annet med w > w = w.nextWarning(); }
Den vanligste typen advarsel skjer når det foregår en trunkering av data,
det vil si at det ikke lykkes å overføre mellom databasen og klientprogrammet
alle de data som det var hensikten å overføre i en operasjon mot databasen.
Det blir da gitt en advarsel av typen
DataTruncation
,
som er en subklassetype til SQLWarning
.
DataTruncation
-objekter har SQL-status lik 01004
, som
indikerer at det var et problem med enten lesing eller skriving av data.
DataTruncation
-klassen har metoder som kan brukes til å finne ut
om de data som ble trunkert var en parameterverdi eller en kolonneverdi, og
i hvilken kolonne det eventuelt var. Man kan også finne ut om trunkeringen
skjedde i en skrive- eller en leseoperasjon, hvor mange byte som skulle vært
overført og hvor mange byte som virkelig ble overført.
Vi skal ta for oss en oversikt over de objekter og metoder som kan brukes for å få utført SQL-setininger.
For å få utført en SQL-setning, oppretter vi først et
Statement
-objekt. Til det bruker vi det Connection
-objektet
som representerer forbindelsen med databasen, se
foran:
Statement stat = conn.createStatement();
Dernest skriver vi ønsket SQL-setning i form av en streng:
String kommando = "< SQL-setning >";
Når vi så videre ønsker å få utført vedkommende SQL-setning på databasen, må vi skille mellom to hovedgrupper av SQL-setninger:
SELECT
-setninger for å søke i databasen,SELECT
-setninger sender vi til databasen ved kall på
Statement
-objektets metode executeQuery
. Alle andre
typer SQL-setninger sender vi til databasen ved kall på
Statement
-objektets metode executeUpdate
.
Det kan være SQL-setninger av type
INSERT, UPDATE
og DELETE
, samt setninger for å definere
tabeller, slik som CREATE TABLE
og DROP TABLE
.
Det finnes også en execute
-metode som
kan brukes til å utføre alle typer SQL-setninger, men denne blir som regel bare
brukt for spørringer som blir tilført interaktivt under kjøring.
Er det executeUpdate
som er den relevante metoden,
vil dette bli instruksjonen som skal utføres mot databasen:
stat.executeUpdate(kommando);
Returverdien fra executeUpdate
er en
int
-verdi som enten er det
antall rader som ble endret som følge av SQL-setningen, eller 0
dersom
det var en SQL-setning som ikke returnerer noe, slik som for eksempel
setninger for tabelldefinisjon.
Når en spørring er utført, er vi interessert i resultatet. Metoden
executeQuery
returnerer et objekt av type
ResultSet
som vi kan fange opp, slik som for eksempel i spørringen
ResultSet rs = stat.executeQuery("SELECT * FROM Tabellnavn");
Innholdet i et ResultSet
-objekt består som regel av flere
rader fra en tabell.
Objektet inneholder også det vi kan forestille oss som en
markør (cursor). Denne kan vi bruke til å bevege oss fra rad til rad. For å
flytte markøren til neste rad bruker vi metoden next
. I
utgangspunktet står markøren foran første rad. Men også for å
komme til første rad må vi gjøre kall på metoden next
.
Når det ikke er flere rader å gjennomløpe, vel next
-metoden
returnere false
.
En standardløkke for gjennomløping av resultatene fra spørringen vil dermed se ut slik:
while (rs.next()) { < se på en tabellrad returnert av spørringen > }
Merk deg at den iteratoren som gjennomløper et ResultSet
, slik
den er brukt i løkka ovenfor, er litt forskjellig fra den som
brukes for gjennomløping av en Collection
, se
Iteratorer.
Iteratoren for et ResultSet
blir initialisert til en posisjon som er
foran første raden i settet. Vi må derfor gjøre et kall på
next
-metoden for å bevege iteratoren til første rad, slik det er
gjort i løkka ovenfor. Vi fortsetter med å gjøre kall på next
-metoden
inntil den returnerer false
.
For ResultSet
-iteratoren finnes det heller ingen
hasNext
-metode.
---
Rekkefølgen for radene i et ResultSet
er helt vilkårlig.
Med mindre du har spesifisert rekkefølgen ved å bruke et ORDER BY
-direktiv,
er det ingen grunn til å legge noen som helst betydning i den rekkefølgen som radene har.
Når vi gjennomløper de enkelte radene, er vi som regel interessert i å hente
ut enkeltverdier fra radene. Til dette bruk finnes det i
ResultSet
-klassen et stort antall get
-metoder for de
forskjellige datatypene, slik som getString
og getDouble
.
Hver get
-metode finnes dessuten i to former: én som har en
kolonneindeks som parameter, og én som har et kolonnenavn i form av en streng som parameter.
Her er et par kodeeksempler på å hente ut verdier fra en tabellrad fra
ResultSet
-objektet rs
:
String isbn = rs.getString(1); //returner verdien i første kolonne i aktuell rad double pris = rs.getDouble("Pris");
Pass på at dersom du bruker kolonneindekser for å referere til tabellkolonnene,
så starter disse på 1
, til motsetning fra arrayindekser i Java som starter på 0
.
Merk deg dessuten at indeksnumrene referer seg til kolonnenumrene i
ResultSet
-objektet, ikke til kolonnenumrene i
vedkommende tabell i databasen.
---
Den siste instruksjonen i eksemplet ovenfor henter ut verdien fra kolonnen
med navn Pris
i den raden i resultatsettet som behandles for øyeblikket.
I strengparametre blir det ikke gjort noe skille mellom bruk av store eller små bokstaver.
Når det gjelder effektivitet av kode,
så er det litt mer effektivt å bruke kolonneindekser enn kolonnenavn når resultater
skal hentes ut, men bruk av navn gir kode som er mer lesbar og lettere å vedlikeholde.
De forskjellige get
-metodene utfører automatisk rimelig grad av typekonvertering
dersom returtypen fra metoden ikke passer med datatypen som er i vedkommende tabellkolonne.
For eksempel så vil kallet rs.getString("Pris")
konvertere desimaltallsverdien som er i priskolonnen til en streng.
Metoden getString
, som primært er anbefalt for å hente ut SQL-typene
CHAR
og VARCHAR
, kan brukes til å hente ut alle de
grunnleggende SQL-typene, noe som jo kan være nyttig, men som også har sine begrensninger.
For dersom den blir brukt til å hente ut en numerisk type og denne etterpå skal brukes
i en numerisk beregning, så må String
-verdien som ble returnert fra
getString
først konverteres tilbake til en numerisk type. Men skal verdien
som hentes ut likevel behandles som en streng, er det ingen ulempe med å bruke
getString
.
Følgende metode fra The Java Tutorials henter ut og gjennomløper et resultatsett fra en tabell i den databasen som der brukes som eksempel. Det blir brukt indekser for å referere til kolonnene.
public static void alternateViewTable(Connection con) throws SQLException { Statement stmt = null; String query = "select COF_NAME, SUP_ID, PRICE, SALES, " + "TOTAL from COFFEES"; try { stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String coffeeName = rs.getString(1); int supplierID = rs.getInt(2); float price = rs.getFloat(3); int sales = rs.getInt(4); int total = rs.getInt(5); System.out.println(coffeeName + ", " + supplierID + ", " + price + ", " + sales + ", " + total); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); //skriver ut feilmelding } finally { if (stmt != null) { stmt.close(); } } }
Som nevnt ovenfor, er det mulig å la en bruker skrive
inn SQL-setninger direkte og få disse utført. Siden det for programmereren da
ikke vil være mulig å vite om det er riktig å bruke executeQuery
eller
executeUpdate
for å få utført den SQL-setningen som blir lest inn,
finnes det en generell metode execute
til dette bruk. Den returnerer
en logisk verdi som forteller hva slags spørring som ble utført,
true
dersom det var en utvalgsspørring,
false
dersom
det var en oppdateringsspørring. På grunnlag av denne returverdien kan vi
avgjøre om vi videre skal bruke metoden
getResultSet
eller getUpdateCount
for vedkommende
Statement
-objekt for å få tak i henholdsvis returnert
ResultSet
-objekt eller antall berørte rader.
En SQL-setning må kompileres av databasesystemet før den kan kjøres. Databasesystemet setter også opp en plan slik at et søk kan gjøres på en mest mulig effektiv måte. Dersom den samme setningen blir sendt mange ganger, vil databasesystemet spare tid ved å gjøre disse forberedelsene bare én gang. Parameterverdier kan endres i en setning uten at den behøver å bli kompilert på nytt.
Vi kan lage et ferdig kompilert setningsobjekt ved å bruke setninger av typen
PreparedStatement
.
Objekt av type PreparedStatement
blir, slik som Statement
-objekt, opprettet ved hjelp av
Connection
-objektet som representerer den åpne forbindelsen med
databasen. Men et PreparedStatement
-objekt blir, i motsetning til
et Statement
-objekt, gitt en SQL-setning som
konstruktørparameter når det blir opprettet. Fordelen med dette er at i de
fleste tilfeller blir da SQL-setningen med det samme sendt til databasen der
den blir kompilert. Kompilert setning blir mottatt i retur og lagret i
PreparedStatement
-objektet. Dette betyr at når objektet skal
foreta en aksess på databasen, kan den ferdigkompilerte SQL-setningen
utføres med det samme, det er ikke nødvendig å kompilere den først.
PreparedStatement
-objekter kan brukes på SQL-setninger uten
parametre. Men mest brukt er de for SQL-setninger som tar parametre.
Fordelen med å bruke SQL-setninger med parametre er at en kan bruke samme
setningen supplert med forskjellige parameterverdier hver gang den blir
utført, uten å måtte kompilere setningen på nytt hver gang. Hver parameter
i et PreparedStatement
blir indikert med et ?
-tegn.
Dersom det er flere av dem, må vi, når vi skal tilordne dem verdi, passe på
hvilken posisjon de har i rekkefølgen, indikert med tallverdier fra
1
og oppover.
Vi tenker oss at vi har en bokdatabase der det er en tabell
forfattere
med kolonnner forfatterID, fornavn
og
etternavn
. Vi skal foreta gjentatte søk i tabellen etter fornavn og
etternavn lik de navn som blir lest inn fra brukeren. Vi kan da bruke følgende
SQL-setning med parametre for fornavn og etternavn:
String sql = "SELECT * FROM forfattere " + "WHERE fornavn LIKE ? AND etternavn LIKE ?";
Vi antar at Connection
-objektet forbindelse
representerer en åpen forbindelse til databasen og oppretter en kompilert
SQL-setning:
PreparedStatement setning = forbindelse.prepareStatement(sql);
Før vi kan få utført et PreparedStatement
på databasen,
må vi tilordne verdi til parametrene ved å bruke passende set
-metoder
på PreparedStatement
-objektet. Som det er tilfelle med
get
-metoder for et ResultSet
-objekt, er det
forskjellige set
-metoder for de forskjellige datatypene.
I vårt tilfelle skal vi tilordne strengverdier til fornavn og etternavn.
Vi tenker oss da at String
-variablene fornavn
og
etternavn
er blitt tilordnet verdi på en eller annen måte.
For å overføre disse til PreparedStatement
-objektet kan
vi da skrive:
setning.setString(1, fornavn); // setter parameterverdi setning.setString(2, etternavn); // setter parameterverdi
En søkeløkke etter forfattere med gitt for- og etternavn kan vi skrive etter følgende mal:
do { String fornavn = < les inn fornavn >; String etternavn = < les inn etternavn >; setning.setString( 1, fornavn ); // setter parameterverdi setning.setString( 2, etternavn ); // setter parameterverdi ResultSet res = setning.executeQuery(); // Obs: ingen parameter! < vis resultat > } while ( < flere søk > );
Ovenfor ble setningobjektets metode setString
brukt til å
gi String
-verdi til de to parametrene i SQL-setningen.
PreparedStatement
-klassen inneholder tilsvarende
set
-metoder for alle standard datatyper i java
(setInt, setDouble
etc.). Første parameter i metodene angir hvilken
SQL-parameter som skal få verdi. Andre parameter angir verdien. Denne må
selvsagt ha en datatype som er kompatibel med hvilken set
-metode
det dreier seg om.
Når det er satt en verdi på en parameter, vil den beholde denne verdien
inntil den får satt en annen verdi eller metoden
clearParameters
blir kalt. Dette betyr at du fra spørring til spørring
bare trenger å gjøre kall på set
-metode for de parametrene som trenger
oppdatering. Kall på clearParameters
kan det være aktuelt å gjøre
dersom en for eksempel ønsker å frigjøre ressurser som er bundet opp av
parameterverdiene.
Det blir anbefalt å bruke PreparedStatement
alltid når en har
en SQL-setning som inneholder én eller flere variable.
Et PreparedStatement
-objekt blir ugyldig etter at det tilhørende
Connection
-objektet er lukket. Men det er imidlertid slik at mange
databasedrivere automatisk vil "cache" PreparedStatement
-objekter.
Dersom samme spørring blir forberedt om igjen, vil databasen kort og godt bruke
om igjen samme spørrestrategi. En trenger derfor ikke å bekymre seg om den ekstra
"overhead" som bruk av prepareStatement
medfører.
I tillegg til å kunne lagre tall, tekst og datoer, er det mange databaser
som kan lagre det som kalles store objekter, på engelsk forkortet
til LOBs (large objects), slik som bilder eller andre data. I SQL
blir binære, store objekter kalt BLOB
'er, og store tegnobjekter kalt
CLOB
'er.
For å håndtere slike store objekter i JDBC, utfører vi først passende
SELECT
-setning og gjør så kall på getBlob
- eller
getClob
-metoden for det ResultSet
-objektet vi får
i retur. Da vil vi få returnert et objekt av type
Blob
eller
Clob
.
For å få hentet ut de binære data fra en Blob
, kan vi gjøre kall på
objektets getBytes
- eller getInputStream
-metode.
Vi tenker oss at vi har en tabell bokforsider
som inneholder
bilder av bøkers forsider i en kolonne forside
, samt en kolonne for
ISBN-numre for identifikasjon av bøkene.
For å få tak i et slikt bilde, kan vi da skrive
kode etter dette mønster, der forbindelse
representerer forbindelsen
og isbn
er blitt tilordnet et ISBN-nummer:
PreparedStatement stat = forbindelse.prepareStatement( "SELECT forside FROM bokforsider WHERE ISBN=?"); stat.set(1, isbn); ResultSet resultat = stat.executeQuery(); if (resultat.next()) { Blob forsideblob = resultat.getBlob(1); Image forsidebilde = ImageIO.read(forsideblob.getInputStream()); }
På tilsvarende måte kan vi, dersom vi henter ut et Clob
-objekt,
få tak i tegndata ved å gjøre kall på
getSubString
- eller
getCharacterStream
-metoden.
For å plassere et LOB
i en database, bruker vi først
Connection
-objektet for å gjøre kall på dets
createBlob
- eller createClob
-metode. Får så tak i en
OutputStream
eller en Writer
på det objektet som er opprettet,
skriver dataene, og lagrer til slutt objektet i databasen.
Vi tenker oss at vi har den samme tabellen med bokforsider som er omtalt
ovenfor. Vi skal lagre Image
-objektet
forsidebilde
som en ny forside i denne tabellen:
Blob forsideblob = forbindelse.createBlob(); int startpos = 1; OutputStream ut = forsideblob.setBinaryStream(startpos); ImageIO.write(forsidebilde, "PNG", ut); PreparedStatement stat = forbindelse.prepareStatement("INSERT INTO bokforsider VALUES (?, ?)"); stat.set(1, isbn); stat.set(2, forsideblob); stat.executeUpdate();
ResultSet
Et ResultSet
består vanligvis av flere rader med tabelldata.
Ovenfor er det beskrevet hvordan
next
-metoden kan brukes til å gjennomløpe radene.
Denne funksjonaliteten er tilstrekkelig dersom vi bare har behov for
å gå gjennom radene for å analysere data. Men i mange sammenhenger
er det aktuelt å vise resultatene av en spørring mot databasen på skjermen i form av en tabell, og da
ønsker vi vanligvis at brukeren skal kunne bevege seg forover og bakover
i tabellen, om nødvendig også ved å bruke skrolling. Denne funksjonaliteten
kan vi få til ved å opprette et ResultSet
som er skrollbart.
I et slikt er det mulig å få posisjonsmarkøren til å bevege seg både forover
og bakover, og dessuten til å hoppe til en hvilken som helst posisjon.
I en tabellvisning av søkeresultater på skjermen kan det også tenkes at
vi ønsker at brukeren skal ha adgang til å redigere og oppdatere de dataene
som vises, slik at det på grunnlag av dette også foretas en oppdatering av
databasen. Det kan vi få til ved å opprette et ResultSet
som er
oppdaterbart.
Default-funksjonaliteten er at et ResultSet
verken er
skrollbart eller oppdaterbart. For å få det, må vi bruke to ekstra parametre
når vi oppretter Statement
-objektet, som i følgende eksempel:
Statement stat; stat = forbindelse.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
Begge parametrene til createStatement
-metoden er navngitte
int
-konstanter.
Det er derfor viktig å få dem i riktig rekkefølge! Den første parameteren
kan ha en av disse verdiene:
ResultSet.TYPE_FORWARD_ONLY ResultSet.TYPE_SCROLL_INSENSITIVE ResultSet.TYPE_SCROLL_SENSITIVE
Et skrollbart ResultSet
-objekt får vi bare dersom
første parameter har en av de to verdiene
ResultSet.TYPE_SCROLL_INSENSITIVE
eller
ResultSet.TYPE_SCROLL_SENSITIVE
. Forskjellen mellom de to har å
gjøre med om eventuelle endringer som gjøres i
ResultSet
-objektet mens det er åpent skal reflektere seg
umiddelbart på skjermen i form av oppdaterte verdier eller ikke. For alle
de tre mulige verdiene av den første parameteren vil det være slik at dersom
ResultSet
-objektet lukkes, og så åpnes igjen, så vil endringene
være synlige.
Den andre parameteren kan
alternativt ha verdien ResultSet.CONCUR_UPDATABLE
. Parameteren
spesifiserer om ResultSet
-objektet bare skal være lesbart eller
om det skal kunne brukes til å oppdatere databasen. Det en må huske på, er at
dersom en spesifiserer skrollbarhet (med den første parameteren), så må en også
spesifisere om det skal være bare lesbart eller oppdaterbart. Og siden
begge parametrene er av type int
,
vil kompilatoren ikke protestere dersom en bytter om rekkefølgen!
Dersom en som første parameter bruker
ResultSet.TYPE_FORWARD_ONLY
, vil ResultSet
-objektet
ikke være skrollbart, det vil si at markøren bare kan flytte seg forover.
Dersom vi ikke bruker parametre i det hele tatt, slik vi har gjort tidligere, vil vi automatisk
få et ResultSet
-objekt som er
ResultSet.TYPE_FORWARD_ONLY
og
ResultSet.CONCUR_READ_ONLY
. For øvrig må en være klar over at
databasesystemet og driveren for dette kan sette begrensninger på hva som
er mulig å gjøre.
ResultSet
For å kunne flytte markøren på annen måte enn bare forover, ved hjelp
av next
-metoden, må vi altså ha et skrollbart ResultSet
,
se ovenfor.
For å flytte markøren én rad bakover i det skrollbare ResultSet
-objektet
rs
, bruker vi koden
if (rs.previous())
...
Metoden previous
returnerer true
dersom markøren nå er plassert på en rad, false
dersom den nå er plassert foran første rad.
Det er også mulig å flytte markøren bakover eller forover et ønsket antall rader med metodekallet
rs.relative(n);
Dersom n
er positiv, vil markøren bli flyttet forover. Er n
negativ, blir markøren flyttet bakover, og er n
lik 0
, har
instruksjonen ingen effekt. Dersom vi prøver å flytte markøren utenfor det aktuelle
settet med rader, blir den satt til å peke enten til etter den siste raden, eller
foran den første, avhengig av fortegnet til n
. Metoden returnerer dessuten
false
. Metoden returnerer
true
dersom markøren er blitt plassert
på en eksisterende rad.
Vi har også muligheten til å plassere markøren på et ønsket radnummer ved å bruke instruksjonen
rs.absolute(n);
Aktuelt radnummer får vi ved hjelp av metodekallet
int aktuellRad = rs.getRow();
Første rad i et ResultSet
har radnummer 1. Dersom returverdien fra
metoden er 0
, er ikke markøren på en eksisterende rad, men enten foran den
første eller etter den siste raden.
Vi kan for øvrig plassere markøren ved bruk av metodene first, last, beforeFirst
og
afterLast
. Navnene på metodene indikerer hvor markøren da blir plassert.
For å teste om markøren er i noen av disse spesielle posisjonene, har vi dessuten
metodene isFirst, isLast, isBeforeFirst
og isAfterLast
.
ResultSet
Dersom vi ønsker å redigere dataene i et ResultSet
slik at
databasen samtidig blir oppdatert med endringene som foretas, så må vi altså
bruke et ResultSet
av den typen som er oppdaterbart. Det trenger
ikke samtidig å være skrollbart, men dersom dataene samtidig blir presentert
for en bruker for editering, ønsker vi vanligvis det.
For å få et oppdaterbart ResultSet
som samtidig er skrollbart, kan
vi opprette Statement
-objektet på følgende måte:
Statement stat = forbindelse.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
Det ResultSet
vi nå får returnert ved kall på
executeQuery
vil være oppdaterbart, og skrollbart.
Vi tenker oss at vi har en database for bøker med en Boktabell
og
en kolonne Pris
i denne. Vi ønsker å oppdatere prisen på noen av
bøkene, men har ikke noe enkelt kriterium å bruke i en UPDATE
-setning
for å få oppdatert akkurat de bøker vi ønsker og med de prisene vi ønsker.
En løsning kan da være å gjennomløpe alle bøkene for oppdatering av priser
ved at en operatør oppdaterer de prisene som skal oppdateres etter hvert som
boktabellen blir vist på skjermen. En skisse for dette kan se ut som følger:
Statement stat = forbindelse.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); String spørring = "SELECT * FROM Boktabell"; ResultSet rs = stat.executeQuery(spørring); while (rs.next()) { if (...) { double økning = ...; double pris = rs.getDouble("Pris"); rs.updateDouble("Pris", pris + økning); rs.updateRow(); //pass på å oppdatere rad etter oppdatering av kolonner! } }
Det finnes updateXxx
-metoder for alle datatyper som svarer til
SQL-typer, slik som updateDouble, updateString
, etc. Tilsvarende
som for getXxx
-metoder, må vi spesifisere enten kolonnenavn
eller kolonnenummer, og i tillegg spesifisere den nye verdien som skal inn. Men
pass på at dersom du bruker kolonnenummer, så vil det være kolonnenummer i
ResultSet
-objektet, og dette kan godt være forskjellig fra kolonnenummeret
i databasen.
Vær oppmerksom på at update
-metodene for kolonneverdier bare
endrer kolonneverdier i ResultSet
-objektet, de endrer ikke databasen! Når du er
ferdig med å oppdatere kolonneverdiene i en rad, må du gjøre kall på
updateRow
-metoden. Den sender alle oppdateringene i aktuell rad
til databasen. Dersom du flytter markøren til en annen rad uten å ha kalt
updateRow
, vil alle endringene bli kansellert og aldri bli
kommunisert til databasen. Det finnes for øvrig også en
cancelRowUpdates
-metode som kan brukes til å kansellere de
oppdateringene som er foretatt av kolonneverdier i en rad, men denne må vi
i så fall gjøre kall på før vi har gjort kall på updateRow
.
Eksemplet ovenfor skisserer hvordan du kan modifisere en eksisterende rad.
Dersom du ønsker å tilføye en ny rad til databasen, er det nødvendig å gå fram på en litt
annen måte. For ResultSet
-objektet må du da først bruke metoden
moveToInsertRow
for å flytte markøren til en spesiell posisjon
kalt innsettingsraden. I denne posisjonen kan du bygge opp den nye
raden ved å bruke passende update
-metoder for kolonneverdiene,
slik det er forklart ovenfor.
Til slutt, når alle ønskede verdier er satt inn i den nye raden, gjør du kall
på insertRow
-metoden for å få lagt den nye raden inn i databasen.
Når dette er gjort, gjør du kall på metoden moveToCurrentRow
for
å få flyttet markøren tilbake til den posisjonen den hadde før kallet på
moveToInsertRow
ble foretatt.
Vi tenker oss at vi har en Boktabell
med kolonner for
Tittel, ISBN, ForlagsId
og Pris
. Fra denne
tabellen har vi hentet ut et ResultSet
-objekt rs
og ønsker å sette inn en ny rad i tabellen. Vi tenker oss at variablene
tittel, isbn, forlag
og pris
skal brukes til å
tilføre data til de kolonnene som navnene deres indikerer, og at variablene
er blitt tilordnet verdier på en eller annen måte. Instruksjonene som behøves
for selve innsettingen kan da bli noe slikt som dette:
rs.moveToInsertRow(); rs.updateString("Tittel", tittel); rs.updateString("ISBN", isbn); rs.updateString("ForlagsId", forlag); rs.updateDouble("Pris", pris); rs.insertRow(); rs.moveToCurrentRow();
Merk deg at du har ikke noen mulighet til å bestemme hvor den nye raden
blir satt inn i ResultSet
-objektet eller i databasen.
Dersom det ikke blir spesifisert noen kolonneverdi i en rad som skal settes inn,
så vil denne kolonnen få SQL-verdi NULL
. Men dersom kolonnen har et
NOT NULL
-krav, så vil det isteden bli kastet ut et unntaksobjekt og raden
blir ikke satt inn.
Vi har mulighet til å fjerne den raden som markøren står på for øyeblikket ved å bruke instruksjonen
rs.deleteRow();
der rs
er ResultSet
-objektet. Instruksjonen har som
virkning at vedkommende rad øyeblikkelig blir fjernet fra både
ResultSet
-objektet og fra databasen.
Vi ser at ResultSet
-klassens metoder updateRow, insertRow
og deleteRow
gir samme muligheter som SQL-instruksjonene
UPDATE, INSERT
og DELETE
. Vi kan derfor velge hva
vi vil bruke. Java-programmerere vil kanskje finne det mer naturlig å manipulere
databaseinnhold ved å gå veien om ResultSet
-objekter enn å
konstruere SQL-setninger. Men samtidig bør en være klar over at effektiviteten
av koden kan være ganske forskjellig. Det er mye mer effektivt å utføre
en UPDATE
-setning enn det er å utføre enn spørring, for så å
iterere gjennom resultatene og oppdatere data underveis. Oppdatering via
ResultSet
er det fornuftig å bruke i interaktive programmer der
brukeren skal kunne gjøre tilfeldige dataendringer, mens det for de fleste
programmatiske endringer vil være best å bruke en SQL-setning av type
UPDATE
.
RowSet
-objekter: typer og egenskaperForan har vi sett at skrollbare ResultSet
-objekter gir store
muligheter for å manipulere med databasen. Men de har den ulempen at databaseforbindelsen
må holdes kontinuerlig åpen mens vi bruker dem. Siden databaseforbindelser er
knapphetsressurser, er dette uheldig for brukere som skal jobbe interaktivt
mot en database dagen lang. Men heldigvis finnes det for slike situasjoner
en spesialtype resultatsett definert av
interface RowSet
,
som er subinterface til ResultSet
. Et RowSet
kan være knyttet med en åpen forbindelse til en database mens det
eksisterer og kalles da et tilknyttet RowSet
, men det
trenger ikke å ha en slik åpen forbindelse, og kalles da et
fraknyttet RowSet
.
RowSet
-objekter passer det også å bruke i tilfeller der vi
trenger å flytte et spørreresultat til et annet lag i en kompleks applikasjon
bestående av flere lag, eller å flytte det til en annen komponent, slik som for eksempel en
mobiltelefon. Det er aldri aktuelt å flytte et ResultSet
-objekt,
det kan inneholde en stor datastruktur, og det er fastbundet til en
databaseforbindelse.
I pakken
javax.sql.rowset
finnes det følgende subinterface til RowSet
for spesialiserte oppgaver:
CachedRowSet
:
Dette er et fraknyttet RowSet
, der operasjoner kan utføres
uten at det er en åpen tilknytning til databasen. Bruken av det blir
nærmere omtalt i det følgende.WebRowSet
:
Et CachedRowSet
som kan lagres som en XML-fil. Fila kan flyttes til
et annet lag i applikasjonen der den kan åpnes med et annet
WebRowSet
-objekt.FilteredRowSet
og
JoinRowSet
:
Disse typene støtter operasjoner på RowSet
-objekter som svarer til
SQL-operasjonene SELECT
og JOIN
, uten at det er nødvendig å
ha en databaseforbindelse.JdbcRowSet
:
Et tilknyttet RowSet
-objekt. Kan oppfattes som en innpakning rundt et
ResultSet
-objekt som gjør det skrollbart og oppdaterbart, og
utstyrer det med get- og set-metoder slik at det kan brukes som en
JavaBeans
-komponent.Oracle har utviklet referanseimplementasjoner av de
interface
som er listet opp ovenfor. De er inneholdt i pakken
com.sun.rowset
.
Men det er fritt fram for programmerere til å lage
sine egne implementasjoner.
CachedRowSet
Et CachedRowSet
inneholder rader av data fra en datakilde og
lagrer disse i maskinens memory, slik at man kan operere på dataene uten
kontinuerlig forbindelse med datakilden.
Objektet er dessuten skrollbart, oppdaterbart
og serialiserbart, og det kan brukes som en JavaBeans
-komponent.
Det typiske er at et CachedRowSet
inneholder rader fra et
ResultSet
fra en databasespørring, men det kan også
inneholde rader fra en hvilken som
helst fil med et tabellformat, slik som for eksempel et regneark.
Siden CachedRowSet
er subinterface til ResultSet
,
kan du bruke det på akkurat samme måte som du bruker et ResultSet
.
I tillegg har det den fordelen at du kan lukke forbindelsen med databasen og
likevel fortsette å bruke CachedRowSet
-objektet. Dette gjør det
mulig å implementere interaktive applikasjoner på en mye enklere måte ved at
man for hver brukerkommando kort og godt kan åpne databaseforbindelsen, foreta
en spørring, putte spørreresultatet i et CachedRowSet
, og så
lukke forbindelsen med databasen.
Det er altså også tillatt å foreta endringer i de dataene som et
CachedRowSet
inneholder. Det gjør vi på samme måte som for
et ResultSet
, ved kall på metoden updateRow
,
deleteRow
eller
insertRow
. Men siden et CachedRowSet
ikke er knyttet
til sin datakilde mens det blir oppdatert, er det nødvendig å utføre en
tilleggsoperasjon. Det er kall på en av variantene av dets metode
acceptChanges
. CachedRowSet
-objektet vil da igjen bli satt i
forbindelse med databasen og utføre nødvendige SQL-setninger for å få skrevet
endringene til databasen.
Et CachedRowSet
er altså bare kortvarig knyttet med en forbindelse til datakilden,
når det leser inn data i forbindelse med at det blir opprettet, og til slutt når
det skal oppdatere datakilden med de endringene som er blitt foretatt underveis.
Resten av tida er det fraknyttet datakilden, også mens endringer av data blir foretatt.
Siden objektet dessuten er "slankt" og serialiserbart, kan det lett sendes til
andre, tynne komponenter som for eksempel mobiltelefoner.
CachedRowSet
-objektDet finnes flere muligheter for å opprette et CachedRowSet
-objekt
og fylle det med data. For å opprette objektet, kan vi bruke default-konstruktøren
til Oracle's referanseimplementasjon CachedRowSetImpl
:
import com.sun.rowset.CachedRowSetImpl; ... CachedRowSet crs = new CachedRowSetImpl();
Videre er det flere muligheter for å fylle objektet med ønskede data.
Én mulighet er å fylle det med data fra et eksisterende
ResultSet
:
ResultSet resultat = ...;
crs.populate(resultat);
forbindelse.close(); //kan nå lukke forbindelsen til databasen
En annen mulighet er å la CachedRowSet
-objektet knytte seg
til databasen automatisk. Men det forutsetter at vi på forhånd har satt opp
nødvendige databaseparametre for objektet:
crs.setURL(<database-URL>);
crs.setUsername(brukernavn);
crs.setPassword(passord);
Videre må vi ha satt ønsket SQL-setning for spørringen og
eventuelle parametre. Dersom vi for eksempel ønsker å hente ut bøker
utgitt på et bestemt forlag fra en boktabell, og vi har lagt inn vedkommende
forlagsnavn i String
-variabelen forlagsnavn
,
kan det være noe slikt som
crs.setCommand("SELECT * FROM Boktabell WHERE Forlag=?");
crs.setString(1, forlagsnavn);
Nå kan vi fylle CachedRowSet
-objektet med data ved å skrive
crs.execute();
Dette metodekallet vil opprette databaseforbindelse, foreta spørringen
mot databasen, fylle CachedRowSet
-objektet med data, og så
lukke forbindelsen. Vi trenger altså ikke egne instruksjoner verken for å åpne
eller lukke forbindelsen med databasen.
Siden et CachedRowSet
-objekt lagrer sine data i maskinens
memory, vil det være størrelsen på tilgjengelig lagerplass som til enhver tid
vil avgjøre hvor mye data det kan inneholde. For at dette ikke skal bli noe
problem i tilfeller der vi vet at et resultatsett kan bli ganske stort, har vi
mulighet for å splitte opp resultatsettet i passe store enheter kalt sider.
For uansett så vet vi at en interaktiv bruker sannsynligvis bare vil å se på et
begrenset antall rader om gangen. Vi kan da spesifisere en sidestørrelse på denne måten:
CachedRowSet crs = ...; crs.setCommand(kommando); crs.setPageSize(20); ... crs.execute();
I dette tilfelle vil bare 20 rader om gangen være tilgjengelige for inspeksjon
og eventuelle endringer. For å få hvert knippe med rader, gjør vi kall på
metoden nextPage
. Den vil opprette et nytt CachedRowSet
-objekt
og fylle det med neste side med data. Dersom for eksempel resultatsettet fra
execute
-kallet ovenfor inneholdt 100 rader med data og sidestørrelsen er satt
til 20, så vil første kallet på nextPage
opprette et CachedRowSet
som inneholder de første 20 radene. Etter at vi har gjort det vi ønsker med disse,
gjør vi et nytt kall på nextPage
for å få et nytt
CachedRowSet
-objekt som inneholder de neste 20 radene. Dataene fra det
første CachedRowSet
-objektet vil da ikke lenger befinne seg i memory,
siden de er blitt erstattet av dataene fra det andre objektet, og slik fortsetter det.
Metoden nextPage
returnerer true
så lenge som det er mer data å hente. En løkke for gjennomløping av alle data kan derfor
lages etter dette mønster:
while (crs.nextPage()) { while (crs.next()) { <behandler rad på denne side> } ... //eventuelt kall på crs.acceptChanges(forbindelse) eller //crs.acceptChanges() for oppdatering av database }
Kallet crs.acceptChanges()
uten parameter kan vi bare
utføre dersom CachedRowSet
-objektet er tilført informasjon om
database-URL, brukernavn og passord som behøves for tilknytning til databasen,
slik det ble skissert ovenfor.
Vær dessuten oppmerksom på at dersom CachedRowSet
-objektet ble
fylt med data fra et ResultSet
ved hjelp av populate
-metoden,
så vil det ikke ha kjennskap til navnet på den tabellen det skal oppdatere.
I så fall må vi gjøre kall på dets setTable
-metode for å få satt
tabellnavn.
Det er klart at dersom innholdet i databasen har endret seg etter at vi
hentet inn data til vårt resultatsett, og vi ønsker å oppdatere databasen
med de endringene vi har foretatt i resultatsettet, så kan det bli trøbbel
ved at dataene i databasen ikke lenger vil bli konsistente. Dette må derfor
hindres. I den referanseimplementasjonen som er nevnt ovenfor
blir dette taklet ved at det blir sjekket om de opprinnelige radverdiene i
resultatsettet, det vil si verdiene før eventuell redigering, er
identiske med de nåværende verdiene i databasen. Dersom dette er tilfelle, så
vil databasen bli oppdatert med de redigerte dataene. I motsatt fall vil det bli kastet ut
en SyncProviderException
og ingen endringer blir foretatt.
Når det gjelder bruk av WebRowSet
, FilteredRowSet
,
JoinRowSet
og JdbcRowSet
henvises det til
The Java Tutorials.
JDBC kan brukes til å hente opplysninger om strukturen til en database og dens tabeller. Slik informasjon er av stor nytte når man skal programmere verktøy som skal jobbe mot en database.
Data som beskriver en database eller en av dens deler blir
i SQL kalt meta-data, for å skille dem fra data som lagres i databasen.
Vi kan hente ut tre typer av meta-data: om en database, om et
ResultSet
-objekt, og om parametrene til en kompilert SQL-setning
(et PreparedStatement
).
Til bruk for uthenting av meta-data har JDBC definert
interface
DatabaseMetaData
.
Dette må utviklere av databasedrivere implementere slik at dets metoder returnerer
informasjon om driveren og databasehåndteringssystemet som den er driver for.
Det finnes for eksempel et stort antall metoder som forteller om driveren gir støtte
for en bestemt funksjonalitet.
For å kunne få tak i meta-data, må vi først bruke databaseforbindelsen
til å få tak i et objekt av type DatabaseMetaData
, slik at
vi kan bruke det til å gjøre kall på dets metoder:
DatabaseMetaData meta = forbindelse.getMetaData();
Vi ønsker opplysninger om alle tabellene i databasen. Da kan vi bruke følgende metodekall:
String[] typer = {"TABLE"}; ResultSet mrs = meta.getTables(null, null, null, typer);
Hver rad i resultatsettet som blir returnert vil inneholde informasjon om en tabell i databasen. Tredje kolonne i raden inneholder tabellens navn. En løkke som henter ut alle tabellnavnene kan vi derfor skrive slik:
while (mrs.next())
{
String tabellnavn = mrs.getString(3);
...
}
ResultSet
Et objekt av type DatabaseMetaData
som ble brukt i eksemplet ovenfor
gir opplysninger om selve databasen. Dersom vi ønsker opplysninger om et
ResultSet
, må vi isteden bruke et objekt av type
ResultSetMetaData
.
Når vi har et ResultSet
fra en spørring mot databasen, kan vi
for eksempel få tak i opplysninger om antall kolonner i det og hver kolonnes navn,
type og feltbredde. Som eksempel skal vi gjøre bruk av meta-data for å
lage en label for hvert kolonnenavn
og et tekstfelt av passe bredde for hver verdi. Vi gjør bruk av
Statement
-objektet setning
som er opprettet på
forhånd.
ResultSet res = setning.executeQuery("SELECT * FROM " + tabellnavn ); ResultSetMetaData meta = res.getMetaData(); for (int i = 1; i <= meta.getColumnCount(); i++) { String kolonnenavn = meta.getColumnLabel(i); int kolonnebredde = meta.getColumnDisplaySize(i); JLabel label = new JLabel(kolonnenavn); JTextField tf = new JTextField(kolonnebredde); ... }
For indekseringen i for
-løkka ovenfor
må du passe på at kolonneindeksene i et ResultSetMetaData
går fra 1
og
oppover til og med antall kolonner, til forskjell fra en array i java,
der de går fra 0
og oppover til, men ikke til og med antall plasser
i arrayen.
En transaksjon er en samling av en eller flere setninger som skal utføres som en enhet, slik at enten blir alle setningene utført, eller ingen av dem blir utført.
Dersom penger skal overføres fra én konto til en annen, er det viktig at begge kontoene endrer saldo. Dersom feil inntreffer under utførelsen, kan vi ikke risikere at bare én av kontoene har endret saldo.
***
Når en forbindelse til en database blir opprettet, er den i
auto-commit-modus. Det betyr at hver enkelt SQL-setning blir behandlet som
en transaksjon og vil bli automatisk stadfestet (committed) rett etter at
den er blitt utført. For å kunne gruppere to eller flere SQL-setninger sammen
til en transaksjon, må vi gå ut av auto-commit-modus. Dersom
Connection
-objektet forbindelse
representerer en
åpen forbindelse med databasen, gjør vi det slik:
forbindelse.setAutoCommit(false);
Når vi er ute av auto-commit-modus, vil ikke noen SQL-setninger bli
stadfestet før vi eksplisitt gjør kall på commit
-metoden:
forbindelse.commit();
Alle setninger som er utført siden forrige kall på commit
vil bli inkludert i inneværende transaksjon og vil bli stadfestet sammen som
en enhet.
rollback
Metoden rollback
(i klasse Connection
) har som
virkning at en transaksjon blir abortert og eventuelle verdier som er blitt
endret i løpet av transaksjonen blir tilbakeført til sine tidligere
verdier. Dersom en prøver å utføre én eller flere setninger i en transaksjon
og får en SQLException
, så bør en gjøre kall på rollback
for å abortere transaksjonen og starte transaksjonen på nytt. Det er eneste
måten å være sikker på hva som er stadfestet (committed) og hva som ikke er det.
Oppfanging av en SQLException
forteller oss at noe er galt, men
det forteller ikke hva som er stadfestet og hva som ikke er det. Siden vi ikke
kan stole på at ikke noe er blitt stadfestet, er et kall på
rollback
eneste måten å sikre seg på.
***
Gangen i å få utført en transaksjon blir dermed som følger, etter at vi har gått ut av auto-commit-modus som forklart ovenfor:
Opprett et Statement
-objekt på vanlig måte:
Statement stat = forbindelse.createStatement();
Gjør kall på executeUpdate
så mange ganger som behøves
for å få utført de SQL-setningene som skal tilhøre transaksjonen:
stat.executeUpdate(kommando1); stat.executeUpdate(kommando2); stat.executeUpdate(kommando3); ...
Dersom alle setningene er blitt utført uten at det har oppstått noe feil,
det vil si uten at det er kastet ut noen SQLException
,
så gjør du kall på commit
-metoden:
forbindelse.commit();
Dersom det i motsatt tilfelle oppsto en feil under utførelsen av
SQL-setningene (kalt kommandoer ovenfor) ved at det er kastet ut en
SQLException
, så gjør du isteden kall på
rollback
:
forbindelse.rollback();
Da vil alle kommandoene som er blitt utført siden forrige commit
bli reversert, det vil skje både for setninger som er utført
automatisk og for setninger som er utført ved kall.
Instruksjonen
forbindelse.setAutoCommit(true);
setter databaseforbindelsen tilbake til auto-commit-modus, slik at hver
kommando igjen blir automatisk stadfestet (commited) når den er blitt fullført,
slik at vi ikke trenger å gjøre egne kall på commit
-metoden.
Du bør bare gå ut av auto-commit-modus når transaksjoner skal utføres.
Dermed unngår du å sette lås på databasen for utførelse av multiple
SQL-setninger, noe som minsker risikoen for at du kommer i konflikt med
andre brukere av den samme databasen.
Det er vanskelig å lage et fornuftig eksempel ut fra en database
som lagrer bare statiske opplysninger, slik som bokdatabasen
som vi har referert til i eksemplene hittil. Vi tenker oss derfor isteden at vi har en database
kaffesalg
som skal brukes til å registrere salg av forskjellige
kaffemerker. En av tabellene i denne databasen heter salg
og har
følgende kolonner:
kaffenavn | levId | pris | ukesalg | totalsalg |
---|---|---|---|---|
(tekst, nøkkel) | (heltall, leverandør) | (desimaltall) | (heltall, ant kg) | (heltall, ant kg) |
Denne skal oppdateres for ukesalg og totalsalg i slutten av hver uke. Det er da viktig at disse to blir oppdatert samlet, ellers vil ikke databasen inneholde konsistente data. En transaksjon for dette, tilpasset fra The Java Tutorials, kan se ut slik:
Connection forbindelse = < oppretter åpen forbindelse til databasen >; PreparedStatement oppdaterSalg = null; PreparedStatement oppdaterTotal = null; String kommando1 = "UPDATE salg SET ukesalg = ? WHERE kaffenavn LIKE ?"; String kommando2 = "UPDATE salg SET totalsalg = totalsalg + ? " + "WHERE kaffenavn LIKE ?"; try { forbindelse.setAutoCommit(false); oppdaterSalg = forbindelse.prepareStatement(kommando1); oppdaterTotal = forbindelse.prepareStatement(kommando2); for (<alle kaffemerker>) { int ukesalg = ...; oppdaterSalg.setInt(1, ukesalg); String kaffemerke = ...; oppdeterSalg.setString(2, kaffemerke); oppdaterSalg.executeUpdate(); int totalsalg = ...; oppdaterTotal.setInt(1, totalsalg); String kaffemerke = ...; oppdaterTotal.setString(2, kaffemerke); oppdaterTotal.executeUpdate(); forbindelse.commit(); } } catch (SQLException e) { <skriv ut feilmelding> if (forbindelse != null) { try { <meld fra om tilbakeføring av transaksjon> forbindelse.rollback(); } catch (SQLException ex) { <skriv feilmelding> } } } finally { if (oppdaterSalg != null) { oppdaterSalg.close(); } if (oppdaterTotal != null) { oppdaterTotal.close(); } forbindelse.setAutoCommit(true); }
I dette eksemplet vil de to kompilerte SQL-setningene
oppdaterSalg
og oppdaterTotal
bli bekreftet samlet
og dermed utgjøre en transaksjon. Hver gang commit
-metoden blir
kalt (enten automatisk når vi er i auto-commit-modus eller eksplisitt ellers),
så vil alle endringer som følge av setninger i transaksjonen bli gjort
permanente.
Generelt bør en bare gå ut av auto-commit-modus når en ønsker å utføre transaksjoner. I eksemplet blir derfor forbindelsen satt tilbake igjen til auto-commit-modus når transaksjonen er fullført. Det er nemlig slik at en transaksjon medfører at en del av databasen blir låst inntil transaksjonen er avsluttet. En oppdatering fører til at andre databaseforbindelser ikke får tilgang til de berørte dataene i det hele tatt. Dersom dette skjer i stor utstrekning, kan det føre til konflikter med andre brukere av databasen.
RowSet
-hendelserJeg minner om at i utgangspunktet er et ResultSet
verken
skrollbart eller oppdaterbart, se ovenfor,
mens et RowSet
derimot er begge deler.
Hver gang det skjer en endring i et RowSet
-objekt ved at
markøren til RowSet
-objektet flytter seg, en rad har endret seg, eller hele objektet har
endret seg, vil det skje en hendelse av type
RowSetEvent
.
Denne hendelsen er det mulig å lytte på ved hjelp av et lytteobjekt av type
RowSetListener
.
Det vil være aktuelt å la et slikt lytteobjekt kommunisere med
den grafiske skjermkomponenten som viser innholdet av
RowSet
-objektet på skjermen, slik at lytteobjektet kan
sette i gang relevant oppdatering
av skjermvisningen. Et opplegg for dette blir skissert lenger ute i dette notatet,
der innholdet av et RowSet
-objekt er tenkt vist i en JTable
.
For å definere en RowSetListener
må vi
implementere de tre metodene som er listet opp i følgende skisse:
import javax.sql.*; class Radsettlytter implements RowSetListener { public void cursorMoved(RowSetEvent e) { //Markøren til RowSet-objektet har flyttet seg. //Kan være aktuelt å melde fra om dette til brukeren. } public void rowChanged(RowSetEvent e) { //En rad har endret innhold, eller er blitt fjernet. //Oppdatering av skjermbildet er aktuelt, eventuelt også en //melding til brukeren. } public void rowSetChanged(RowSetEvent e) { //Hele radsettet har endret seg, f.eks. fordi en rad er fjernet //eller lagt til. Oppdatering av skjermbildet er aktuelt, //eventuelt også en melding til brukeren. } }
Metoden rowSetChanged
vil bli aktivert blant annet av
RowSet
-metoden execute
(som er aktuell i interaktive sammenhenger),
cursorMoved
blir aktivert av alle metodene for flytting av markøren i et
RowSet
, mens rowChanged
blir aktivert av blant annet
RowSet
-metoden updateRow
.
Vi registerer en RowSetListener
for et RowSet
-objekt ved
å gjøre kall på dets metode addRowSetListener
.
RowSet
i en JTable
Siden et RowSet
inneholder rader av tabelldata, vil det
i et javaprogram være naturlig å vise dataene i en JTable
. Den
kan vi også programmere slik at den kan brukes interaktivt til oppdatering av
databasen, i kombinasjon med andre grafiske brukerkomponenter som
vi kjenner fra javaprogrammer. Bruken av de vanligste grafiske skjermkomponentene er omtalt
i notatet
Grafiske brukergrensesnitt, grunnleggende komponenter,
mens bruk av mer avanserte komponenter er omtalt i
Grafiske brukergrensesnitt, spesialiserte komponenter.
Bruk av JTable
er forklart i
Hvordan programmere tabeller av type JTable
.
Som der forklart,
vil det i de fleste tilfelle være nødvendig å definere en egen tabellmodell
for tabellen vår. Jeg minner om at en tabellmodell får vi enklest definert ved å definere
en subklasse til AbstractTableModel
. I den subklassen må vi minst redefinere
de tre metodene getRowCount, getColumnCount
og getValueAt
.
Som regel er det også ønskelig å redefinere metoden getColumnName
.
Dersom tabellen skal kunne brukes til oppdatering av RowSet
-objektet,
og dermed til oppdatering av databasen, må vi dessuten angi hvilke tabellkolonner
som skal være editerbare, ved at vi implementerer tabellmodellens metode
isCellEditable
, og for at oppdatering skal skje, må vi implementere
tabellmodellens metode setValueAt
. Denne siste må da sørge for
at både tabellcellen og RowSet
-objektet blir oppdatert.
Når tabellmodellen skal brukes av en tabell som skal vise data fra et RowSet
,
må den ha tilgang til dette i form av et datafelt.
Nedenfor er det skissert en slik tabellmodell. I modellen er det lagt inn
noen testverdier til bruk for det tilfellet at den for øyeblikket blir brukt uten å være
knyttet til en database. I modellen er det ikke implementert oppdatering av
tabellceller og databaseverdier, bare visning av eksisterende databaseinnhold.
1 import java.sql.*; 2 import javax.sql.*; 3 import javax.swing.table.AbstractTableModel; 4 5 public class RowSetTabellmodell extends AbstractTableModel 6 { 7 // RowSet-objektet som skal vises i tabellen 8 private RowSet rowSet = null; 9 10 public RowSet getRowSet() 11 { 12 return rowSet; 13 } 14 15 public void setRowSet(RowSet radsett) 16 { 17 if (radsett != null) 18 { 19 rowSet = radsett; 20 fireTableStructureChanged(); //se forklaring nedenfor 21 } 22 } 23 24 //Setter antall rader i tabellen lik antall rader i radsettet 25 public int getRowCount() 26 { 27 try 28 { 29 if (rowSet != null) 30 { 31 rowSet.last(); 32 return rowSet.getRow(); //Returnerer aktuelt radnummer 33 } 34 } 35 catch (Exception ex) 36 { 37 ex.printStackTrace(); 38 } 39 40 return 5; //Testverdi til å bruke når ingen database finnes. 41 } 42 43 //Setter antall kolonner i tabellen lik antall kolonner i radsettet 44 public int getColumnCount() 45 { 46 try 47 { 48 if (rowSet != null) 49 { 50 return rowSet.getMetaData().getColumnCount(); 51 } 52 } 53 catch (SQLException ex) 54 { 55 ex.printStackTrace(); 56 } 57 58 return 5; //Testverdi til å bruke når ingen database finnes. 59 } 60 61 //Bestemmer verdi for tabellcelle lik den verdi som er på samme 62 //rad og kolonne i radsettet. 63 public Object getValueAt(int rad, int kolonne) 64 { 65 try 66 { 67 if (rowSet != null) 68 { 69 rowSet.absolute(rad + 1); //husk at i JTable starter rad- og 70 //kolonnenummerering på 0 71 //mens den i radsettet starter på 1. 72 return rowSet.getObject(kolonne + 1); 73 } 74 } 75 catch (SQLException sqlex) 76 { 77 sqlex.printStackTrace(); 78 } 79 80 return "Testverdi"; //Til bruk når det ikke finnes noen database. 81 } 82 83 //Setter kolonnenavn i tabell lik tilsvarende kolonnenavn i radsettet 84 public String getColumnName(int kolonne) 85 { 86 try 87 { 88 if (rowSet != null) 89 return rowSet.getMetaData().getColumnLabel(kolonne + 1); 90 } 91 catch (SQLException ex) 92 { 93 ex.printStackTrace(); 94 } 95 96 return super.getColumnName(kolonne); //Når det ikke finnes 97 //noen database. 98 } 99 100 /* 101 * Dersom tabellen skal kunne brukes til oppdatering av databasen, 102 * må vi i tillegg implementere metoden isCellEditable for å 103 * angi hvilke celler som skal være editerbare, og vi må 104 * implementere metoden setValueAt for at oppdatering skal 105 * finne sted. Det kan også være aktuelt å definere egen 106 * celleeditor for enkelte av de editerbare tabellcellene. 107 */ 108 }
På linje 20 i klassen ovenfor blir det gjort kall på metoden
fireTableDataChanged
. Dette er en metode som klassen arver fra
AbstractTableModel
. Metoden varsler alle lyttere om at tabellstrukturen
har endret seg. I dette tilfelle vil det blant annet medføre at tabellmodellen vil
bli fylt på ny med data hentet fra RowSet
-objektet og den oppdaterte
tabellmodellen vil bli satt som tabellmodell for tabellen, slik at skjermvisningen
av dens innhold også vil bli oppdatert. Legg for øvrig merke til hvordan vi
bruker RowSet
-objektet til å bestemme antall rader i tabellen,
kolonnenavnene, og hvilken verdi som skal vises i hver tabellcelle.
For å få tak i antall kolonner trenger vi radsettets metadata.
Når vi bestemmer verdi for tabellcellene, er det viktig å huske på at
indekseringen for rader og kolonner starter på 0
i tabellen, mens
den starter på 1
i radsettet.
Dersom vi bare skal vise de dataene som et RowSet
inneholder,
trenger vi ikke noe annet enn en JTable
for å gjøre det, bortsett
fra at vi selvsagt trenger tilleggsfunksjonalitet for blant annet innlesing av
av brukernavn og passord, og for å opprette forbindelse med databasen. Men dersom
vi i tillegg ønsker å kunne redigere databaseinnholdet, trenger vi noe mer.
Rett nok så vet vi at det er mulig å redigere tabellceller i en JTable
,
men da kan vi bare oppnå å endre innhold av det RowSet
som tabellen
henter sine data ifra. Som nevnt ovenfor, vil et
CachedRowSet
ikke ha kontinuerlig forbindelse med databasen.
Det er nødvendig å gjøre kall på metoden acceptChanges
for å få
overført endringene til databasen. Og for å få gjort dette kallet, trenger vi
for eksempel en knapp til å klikke på. Det samme er tilfelle dersom vi ønsker å
fjerne eller sette inn en ny rad i radsettet. For å navigere mellom radene i radsettet
kan vi velge tabellrader i tabellen, men i tillegg kan det kanskje være greit å
ha knapper for dette, slik at vi gjør et lite skille mellom navigasjon i radsettet
og navigasjon i tabellen. Uansett så må vi iallfall sørge for at navigasjon i
tabell og radsett blir synkronisert, det vil si at radene i tabellen svarer til
radene i radsettet. Her er det viktig å huske på at den interne indekseringen er
forskjellig: I tabellen starter indekseringen på 0
, mens den i
radsettet starter på 1
.
Vi skal definere et panel som kan danne grunnlag for en radsetteditor. Det skal, på grunnlag av tabellmodellen som er skissert ovenfor, inneholde en tabell som kan vise radsettdata, og knapper for navigasjon i radsettet, samt for fjerning av rad og overføring av endringer til database. Oppdatering av celleverdier og innsetting av ny rad er ikke programmert, men det er lett å supplere med det. Panelet inneholder også en statuslinje som kan informere brukeren om hva som skjer eller har skjedd. Panelet er tenkt å kunne inngå i et vindu der redigering av en database kan foretas. I tillegg til panelet må dette vinduet inneholde funksjonalitet for innlesing av brukernavn og passord, samt annet som behøves for å knytte seg til en eksisterende database. Følgende bilde viser selve panelet der tabellen er fylt med de testdata som er bestemt av tabellmodellen ovenfor.
Koden for panelet er gjengitt nedenfor. Vi merker oss her følgende:
DefaultListSelectionModel
for tabellen, og den blir på linje 44 satt som valgmodell for tabellen.
Dette er noe vi vanligvis ikke behøver å gjøre, men i dette tilfelle trenger vi
å referere til valgmodellen for å kunne synkronisere tabellradene med radene
i radsettobjektet, slik at første rad i tabellen svarer til første rad i
radsettobjektet, og tilsvarende videre utover. Slik synkronisering blir utført av metoden
setTabellmarkør
, definert på linje 72 til 77. Metoden blir kalt opp
av metoden flyttMarkør
, definert på linje 80 til 103, som utfører
flytting av radsettets markør. Metoden blir kalt opp når vi klikker på knappene
for navigering i radsettet. Synkroniseringsmetoden blir også kalt opp av metoden
velgRadsettrad
, definert på linje 132 til 147.
Denne metoden blir kalt opp når brukeren velger en
tabellrad ved å klikke på den. Synkroniseringsmetoden sørger da for at
radsettmarkøren blir flyttet til tilsvarende rad i radsettet. Synkronisering er
også nødvendig i det tilfellet at en rad blir fjernet fra tabell og radsett.
Det skjer når metoden fjernRad
, definert på linje 108 til 128 blir kalt opp
som følge av at det blir klikket på Fjern-knappen.acceptChanges
. Kall på denne blir foretatt
på linje 174 av lytteobjektet for Commit-knappen.1 import javax.swing.*; 2 import javax.swing.table.*; 3 import javax.swing.event.*; 4 import java.awt.*; 5 import java.awt.event.*; 6 import java.sql.*; 7 import javax.sql.*; 8 import com.sun.rowset.CachedRowSetImpl; 9 import java.sql.SQLException; 10 11 public class Tabelleditor extends JPanel 12 { 13 private JButton første = new JButton("Første"); 14 private JButton neste = new JButton("Neste"); 15 private JButton forrige = new JButton("Forrige"); 16 private JButton siste = new JButton("Siste"); 17 private JButton fjern = new JButton("Fjern"); 18 private JButton commit = new JButton("Commit"); 19 private JLabel statuslabel = new JLabel(); 20 21 private RowSetTabellmodell tabellmodell = new RowSetTabellmodell(); 22 private DefaultListSelectionModel listevalgmodell = 23 new DefaultListSelectionModel(); 24 private JTable tabell = new JTable(); 25 private RowSet radsett = null; 26 27 public Tabelleditor() 28 { 29 JPanel knappepanel = new JPanel(); 30 knappepanel.add(første); 31 knappepanel.add(neste); 32 knappepanel.add(forrige); 33 knappepanel.add(siste); 34 knappepanel.add(fjern); 35 knappepanel.add(commit); 36 37 setLayout(new BorderLayout()); 38 add(knappepanel, BorderLayout.PAGE_START); 39 add(new JScrollPane(tabell), BorderLayout.CENTER); 40 add(statuslabel, BorderLayout.PAGE_END); 41 42 //Setter valgmodell for tabellen. Behøves for å få synkronisert 43 //dens markør med markøren for radsettet. 44 tabell.setSelectionModel(listevalgmodell); 45 46 Knappelytter knappelytter = new Knappelytter(); 47 første.addActionListener(knappelytter); 48 neste.addActionListener(knappelytter); 49 forrige.addActionListener(knappelytter); 50 siste.addActionListener(knappelytter); 51 fjern.addActionListener(knappelytter); 52 commit.addActionListener(knappelytter); 53 54 listevalgmodell.addListSelectionListener(new Listevalgslytter()); 55 } 56 57 public void setRowSet(RowSet r) 58 { 59 radsett = r; 60 if (radsett != null) 61 radsett.addRowSetListener(new Radsettlytter()); 62 tabellmodell.setRowSet(radsett); 63 tabell.setModel(tabellmodell); 64 tabellmodell.fireTableStructureChanged(); 65 66 TableRowSorter<RowSetTabellmodell> sorterer = 67 new TableRowSorter<>(tabellmodell); 68 tabell.setRowSorter(sorterer); 69 } 70 71 //Synkroniserer tabellmarkøren med markøren for radsettet. 72 private void setTabellmarkør() throws SQLException 73 { 74 int rad = radsett.getRow(); 75 listevalgmodell.setSelectionInterval(rad - 1, rad - 1); 76 statuslabel.setText("Aktuelt radnummer: " + rad); 77 } 78 79 //Flytter radsettmarkøren til valgt posisjon. 80 private void flyttMarkør(String posisjon) 81 { 82 try 83 { 84 if (posisjon.equals("første")) 85 radsett.first(); 86 else if (posisjon.equals("neste") && !radsett.isLast()) 87 radsett.next(); 88 else if (posisjon.equals("forrige") && !radsett.isFirst()) 89 radsett.previous(); 90 else if (posisjon.equals("siste")) 91 radsett.last(); 92 setTabellmarkør(); //synkroniserer tabellmarkøren med 93 //radsettmarkøren 94 } 95 catch (NullPointerException e) 96 { 97 statuslabel.setText("Ingen database"); 98 } 99 catch (SQLException ex) 100 { 101 statuslabel.setText(ex.toString()); 102 } 103 } 104 105 //Fjerner en rad fra radsettet, oppdaterer radsettmarkør 106 //og synkroniserer tabellmarkør med denne. 107 //Radsettlytterern vil sørge for oppdatering av tabellvisning. 108 private void fjernRad() 109 { 110 try 111 { 112 int aktuellRad = radsett.getRow(); 113 radsett.deleteRow(); 114 if (radsett.isAfterLast()) 115 radsett.last(); 116 else if (radsett.getRow() >= aktuellRad) 117 radsett.absolute(aktuellRad); 118 setTabellmarkør(); 119 } 120 catch (NullPointerException e) 121 { 122 statuslabel.setText("Ingen database"); 123 } 124 catch (SQLException ex) 125 { 126 statuslabel.setText(ex.toString()); 127 } 128 } 129 130 //Synkroniserer radsettmarkør med tabellmarkøren 131 //når brukeren velger en tabellrad. 132 private void velgRadsettrad() 133 { 134 int valgtTabellrad = tabell.getSelectedRow(); 135 try 136 { 137 if (valgtTabellrad != -1 && radsett != null) 138 { 139 radsett.absolute(valgtTabellrad + 1); 140 setTabellmarkør(); 141 } 142 } 143 catch (SQLException ex) 144 { 145 statuslabel.setText(ex.toString()); 146 } 147 } 148 149 private void visMelding(String melding) 150 { 151 JOptionPane.showMessageDialog(null, melding); 152 } 153 154 private class Knappelytter implements ActionListener 155 { 156 public void actionPerformed(ActionEvent e) 157 { 158 Object kilde = e.getSource(); 159 if (kilde.equals(første)) 160 flyttMarkør("første"); 161 else if (kilde.equals(neste)) 162 flyttMarkør("neste"); 163 else if (kilde.equals(forrige)) 164 flyttMarkør("forrige"); 165 else if (kilde.equals(siste)) 166 flyttMarkør("siste"); 167 else if (kilde.equals(fjern)) 168 fjernRad(); 169 else if (kilde.equals(commit)) 170 { 171 try 172 { 173 //Oppdaterer databasen med endringene i radsettet: 174 ((CachedRowSetImpl) radsett).acceptChanges(); 175 } 176 catch (NullPointerException npe) 177 { 178 statuslabel.setText("Ingen database"); 179 } 180 catch (SQLException ex) 181 { 182 ex.printStackTrace(); 183 } 184 } 185 } 186 } 187 188 private class Listevalgslytter implements ListSelectionListener 189 { 190 public void valueChanged(ListSelectionEvent e) 191 { 192 velgRadsettrad(); 193 } 194 } 195 196 private class Radsettlytter implements RowSetListener 197 { 198 public void cursorMoved(RowSetEvent e) 199 { 200 //Markøren til RowSet-objektet har flyttet seg. 201 visMelding("Markør for radsett har flyttet seg."); 202 } 203 204 public void rowChanged(RowSetEvent e) 205 { 206 //En rad har endret innhold, eller er blitt fjernet. 207 tabellmodell.fireTableStructureChanged(); 208 visMelding("En rad er endret."); 209 } 210 211 public void rowSetChanged(RowSetEvent e) 212 { 213 //Hele radsettet har endret seg, f.eks. fordi en rad er fjernet 214 //eller lagt til. 215 tabellmodell.fireTableStructureChanged(); 216 visMelding("Radsettet er endret."); 217 } 218 } 219 }
For lettere å kunne spore opp feil etc., kan det være lurt å få skrevet en logg over det som er blitt gjort med databasen. For å opprette logg-fil kan vi skrive følgende instruksjoner:
PrintWriter loggfil = new PrintWriter(new FileWriter(filnavn)); DriverManager.setLogWriter(loggfil);
Dette blir nå en tekstfil som kan leses av en hvilken som helst editor, for eksempel NetBeans-editoren eller TextPad. For å få hentet inn fila til et program, kan vi skrive
PrintWriter logg = DriverManager.getLogWriter();
Dersom en database allerede eksisterer, kan vi bruke JDBC til å opprette tabeller i den.
Vi tenker oss at vi har en bokdatabase der vi skal opprette en tabell
forfattere
der hver rad har en heltallsverdi for forfatterID
og to strenger for fornavn
og etternavn
.
For å opprette denne tabellen, kunne vi brukt følgende
SQL-setning:
String lagForfattertabell = "CREATE TABLE forfattere " + "(forfatterID INTEGER, fornavn VARCHAR(50), " + "etternavn VARCHAR(50))";
Vi tenker oss videre at vi har opprettet forbindelse med bokdatabasen som tidligere beskrevet:
Connection forbindelse = ...;
og opprettet Statement
-objekt:
Statement setning = forbindelse.createStatement();
For å opprette tabellen forfattere
i bokdatabasen
kan vi nå skrive følgende instruksjon:
setning.executeUpdate(lagForfattertabell);
Instruksjonene som ble beskrevet ovenfor vil bare opprette strukturen til
tabellen forfattere
. Tabellen vil foreløpig ikke inneholde noe data.
Vi skal nå se på hvordan vi radvis kan legge inn data i tabellen.
Vi må da passe på å liste opp kolonnedataene i samme rekkefølge som
kolonnene ble deklarert da tabellen ble opprettet.
Et Statement
-objekt kan brukes om igjen flere ganger. For å
legge inn en rad med data der forfatterID
er lik 1
,
fornavn
er lik Donald E.
og etternavn
er lik Knuth
, kan vi derfor bruke Statement
-objektet
som ble opprettet foran og skrive følgende:
setning.executeUpdate("INSERT INTO forfattere " + "VALUES (1, 'Donald E.', 'Knuth')");
Merk at det er nødvendig med et
mellomrom mellom forfattere
og VALUES
. Merk også at
tekststrengene for verdiene som skal settes inn er avgrenset av enkle
apostrofer. Dette er fordi de befinner seg mellom de doble sitattegnene som
avgrenser tekststrengen som er parameter til executeUpdate
.
For de fleste databasesystemer så brukes regelen om å alternere mellom doble
og enkle sitattegn for å indikere at ting ligger inni hverandre.
INSERT
-setningen finnes i flere varianter. Som parameter til
executeUpdate
kan vi selvsagt bruke en hvilken som helst
variant.
Som eksempel tenker vi oss at det i tabellen forfattere
er blitt
registrert galt fornavn på forfatteren med forfatterID
lik 3 og at
riktig fornavn er Tom. For å rette opp dette, kunne vi med et
Statement
-objekt setning
, opprettet som tidligere
forklart, skrevet instruksjonen
setning.executeUpdate("UPDATE forfattere " + "SET fornavn = 'Tom' WHERE forfatterID LIKE 3");
Det er ingen ting i veien for at vi foretar oppdatering av flere kolonner samtidig på en rad. Vi skiller da de forskjellige SET-delene av UPDATE-setningen med komma.
executeUpdate
Metoden executeUpdate
returnerer en int
-verdi
som angir hvor mange rader i tabellen som ble oppdatert. Dersom setningen
som ble utført gikk ut på å definere tabellstruktur, for eksempel opprette
en tabell, vil tallet 0 bli returnert.
Copyright © Kjetil Grønning og Eva Hadler Vihovde 2012