Databasetilknytning ved bruk av JDBC™

Innledning

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.

Typer av databasedrivere

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:

  1. Type 1 oversetter JDBC til ODBC og er avhengig av ODBC-driveren for kommunikasjonen med databasen. En slik driver, JDBC/ODBC-brua, ble inkludert i de første javaversjonene. Men den krever installasjon og konfigurering av en egen ODBC-driver på brukermaskinen som skal kommunisere med databasen. Da JDBC ble lansert første gangen, var brua grei å bruke for testing, men det var fra Sun sin side, den daværende Java-utvikleren, aldri intensjonen at den skulle brukes i profesjonelle sammenhenger. Den var bare ment som en midlertidig løsning. Nå til dags (per 2012) er det Oracle som har overtatt utviklingen av Java. De ser ikke lenger på JDBC/ODBC-brua som en teknologi som de støtter eller videreutvikler. For så og si alle mulige databasesystemer som finnes på markedet finnes det drivere av minst én av de andre typene som bør brukes isteden.
  2. Type 2 er dels skrevet i java, dels i det språk som selve databasesystemet er skrevet i. Driveren kommuniserer med brukergrensesnittet for det databasesystemet som den skal jobbe mot. På grunn av dette er det på den klientmaskinen som skal kommunisere med databasen nødvendig å installere noe plattformspesifikk kode i tillegg til javabiblioteket. Dermed blir også portabiliteten noe begrenset. Eksempel på en slik driver på klientsida er Oracle's OCI (Oracle Call Interface).
  3. Type 3 er programmert i rein java og bruker en databaseuavhengig protokoll til å overføre databaseforespørsler til en serverkomponent som i sin tur oversetter forespørselen til databasespesifikk kode og kommuniserer med databasen. Det blir altså brukt et mellomlag mellom brukerprogrammet og databasen. Dette opplegget kan blant annet bidra til å forenkle installering og oppsett, siden den plattformspesifikke koden befinner seg på en server.
  4. Type 4 er et reint javabibliotek som oversetter JDBC-spørringer direkte til en databasespesifikk protokoll. Driveren kommuniserer direkte med databasen.

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.

Eksempel

For databasetypen MySQL finnes driveren Connector/J. Det er en driver av Type 4.

Hva oppnår vi med JDBC?

Det som ønskes oppnådd ved hjelp av JDBC kan vi kort summere opp på følgende måte:

Merknad

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:

Typisk bruk av JDBC

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:

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.

Opprette forbindelse med databasen

Det finnes i Javas klassebibliotek følgende to typer klasser som kan brukes til å opprette forbindelse:

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.

Bruk av klassen 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]...

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;
  }

Hvordan behandle en 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).

Sammenkjeding av unntaksobjekter

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);

Tilleggsegenskaper for en 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:

Eksempel

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();
      }
    }
  }

Behandling av advarsler

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.

Utførelse av SQL-setninger

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 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 >
  }

Merknad

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");

Merknader

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.

Eksempel

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.

Kompilerte SQL-setninger

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.

Eksempel

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.

Merknad

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.

Lesing og skriving av bilder og andre store objekter

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.

Eksempel

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.

Eksempel

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();

Skrollbare og oppdaterbare 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.

Flytting av markøren i et 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.

Oppdatering av data i et 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.

Eksempel

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.

Merknad

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.

Innsetting av nye rader

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.

Eksempel

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.

Fjerning av rader

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.

Merknader

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 egenskaper

Foran 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:

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.

Bruk av 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.

Opprette og initialisere et CachedRowSet-objekt

Det 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.

Meta-data

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:

Eksempel: opplysninger om selve databasen

  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);
    ...
  }

Eksempel: opplysninger om et 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.

Transaksjoner

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.

Eksempel

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.

Metoden 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.

Eksempel

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:

kaffenavnlevIdprisukesalgtotalsalg
(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-hendelser

Jeg 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.

Hvordan vise data fra et 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.

Hvordan definere en radsetteditor

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:

  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 }

Database-logg

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();

Opprette tabell i database

Dersom en database allerede eksisterer, kan vi bruke JDBC til å opprette tabeller i den.

Eksempel

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);

Legge inn data i en tabell

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.

Oppdatere tabeller

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.

Returverdier for metoden 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