Database-pakken

Opgaver
I dette kapitel præsenteres "Database pakken", der er et forsøg på at implementere en mere objektorienteret tilgang til arbejdet med en database.
Det er en fordel at kende de design mønstre der omtales i dette kapitel: Iterator, Singleton og Adapter Pattern. Alternativt kan man vælge at læse kapitlet før man kender disse patterns, overspringe design overvejelserne, og i stedet gå direkte til anvendelsen, der ikke nødvendigvis kræver kendskab til de pågældende mønstre (Hvis man da ikke stiller for mange "Hvorfor"-spørgsmål).
  1. Motivation
  Har man prøvet at arbejde med JDBC i et større projekt, kan man godt ønske et design, der er let at implementere og vedligeholde. En anvendelse af rå JDBC, som det er beskrevet i JDBC-kapitlet (når jeg engang får det skrevet) er i længden besværligt og tidskrævende.
  Hvis man skal opnå et sundt design er det nærliggende at anvende en række relevante design mønstre:
Iterator Pattern Når man gennemløber et ResultSet er det naturligt at betragte det som iteration, og derfor anvende Iterator Pattern.
Singleton Pattern Da det giver problemer at åbner mange forbindelser til den samme database er det oplagt at anvende Singleton Pattern til at lave én forbindelse til databasen, gennem hvilken al arbejde med den foregår.
Adapter Pattern Når man alligevel laver en package til at arbejde med databaser, er det nærliggende at forbedre Java's implementation af ResulSet-klassen så det bliver muligt at iterere, ikke kun frem, men også tilbage, samt at det bliver muligt at reset'e (vende tilbage til starten af ResultSet'et). Man kan i den forbindelse ty til Adapter Pattern og lave en Adapter til ResultSet, som implementerer de metoder der anvender JDBC 3.0
  [Historisk bemærkning: Den JDBC-ODBC Bridge, der fulgte med JDK/JRE da kapitlet oprindelig blev skrevet (og database pakken blev lavet) var ikke JDBC 2.0 kompatibel, og metoderne kastede derfor en exception hvis de blev kaldt, om det har ændret sig ved jeg ikke, men da database pakken i den seneste version kun understøtter MySQL og der ikke anvendes en JDBC-ODBC bridge er det usikkert om denne begrænsning stadig gælder. Det ændrer dog ikke ved at Iterator Pattern i det følgende er implementeret langt mere objektorienteret end i JDBC]
  2. Model-klasser
  Model-klasser (kaldes også "entitets-klasser") har deres navn fra MVC Pattern. Idéen med en model-klasse er, at instanser af den repræsenterer en række/record i ResultSet'et, altså data.
  Model-klasser gør anvendelsen af databaser mere objektorienteret og vil derfor bidrage til at forbedre det overordnede design.
  I forbindelse med database pakken skal alle modelklasser nedarve fra den abstracte klasse: AbstractRecordModel. At man nedarver fra denne klasse kræver at man implementerer metoden::
public boolean fromRecordHook( Record record );
  Metoden fromRecordHook skal implementeres så den opbygger objektets datakerne ud fra de data den kan hente fra den medfølgende Record. En instans af Record repræsenterer en række/record i ResultSet'et på et mere primitivt niveau end det er målet med model-klassen. Man kan aflæse de enkelte attributter i Record'en med en række get<type>-metoder, som de kendes fra ResultSet (hvis man ellers kender JDBC). Som parameter kan anvendes enten kolonnens nummer eller navn.
  Følgende liste indeholder for hver af disse metoder: returtype og navn:
  Object getObject( ... )
  String getString( ... )
    byte getByte( ... )
 boolean getBoolean( ... )
   short getShort( ... )
     int getInt( ... )
    long getLong( ... )
   float getFloat( ... )
  double getDouble( ... )
    Date getDate( ... )
Calendar getCalendar( ... )
  Som nævnt skal de tre prikker "..." ertattes med enten kolonnens nummer eller navn.
  Hvilke andre metoder model-klassen skal have, beror på den konkrete anvendelse.
  [Historisk bemærkning: Følgende afsnit er blevet stående, da det kan være væsentligt, såfremt man vil arbejde med andre databaser end MySQL (så skal man selv ændre diverse ting i implementationen). Anvender man MySQL, som database pakken er lavet til, kan man se bort fra betragtningerne om getCalendar-metoden]
Database inkompatibilitet Mht. getCalender-metoden skal man være opmærksom på en manglende kompatibilitet mellem databaser. Denne manglende kompatibilitet betyder at måned, for nogle databasers vedkommende ligger i intervallet [0..11], mens den for andres vedkommende ligger i [1..12]. getCalender-metoden er implementeret så den antager at databasen arbejder med intervallet [0..11]. Observerer man ved anvendelse af database package, at måneden altid er én for meget, skal man selv rette implementationen af getCalender-metoden i database.Record-klassen, så denne trækker én fra den måned den får fra databasen. Dette gøres ved at lave følgende ændring:
 
return new GregorianCalendar( year, month-1, day, hours, minutes, seconds );
  3. Default database
  Kendetegnende for langt de fleste database-anvendelser, er at der kun anvendes én database. Det er derfor bekvemt at implementere alle kaldene på Database-objektet (det objekt der repræsenterer data-laget, og dermed selve databasen) som statiske metoder på en Singleton i form af et klasse-objekt.
  Når man vil anvende en database, gøres det med det indledende kald:
setDefaultDB
DB.setDefaultDB( "PostnummerDB" )
  hvis vores database hedder: Postnumre.
  Hvis der ikke anvendes Windows Authentication, kræves der tillige et brugernavn og password:
DB.setDefaultDB( "postnummerDB", "fkj", "test" )
  hvis brugernavnet f.eks. er: "fkj", og password'et er: "test".
  Ovenstående metoder regner default med at database serveren kører på "localhost", hedder "SQLEXPRESS" og at portnummeret er 1433. Har man behov for at anvende andre værdier end disse, kan man bruger en tredie udgave af metoden:
DB.setDefaultDB( String dbName, String host, int port,
                 String instance, String user, String password )
  Et kald af denne metode med default-værdier vil se ud som følger:
DB.setDefaultDB( "postnummerDB", "localhost", 1433, "SQLEXPRESS", null, null )
  idet man anfører null som user og password, når der anvendes Windows Authentication.
  Metoderne returnerer boolsk om det var muligt at skabe kontakt til databasen.
  Efterfølgende kan man lave to former for kald på databasen.
  SQL-sætninger, som er kendetegnet ved, at de returnerer et ResultSet (dvs. SELECT-sætninger) laves med et kald af den statiske metode Select, der foretager SQL-forespørgslen på default-databasen. F.eks.:
Select
DB.Select( "SELECT * FROM postnummer" )
  der som bekendt returnerer et ResultSet med samtlige records fra tabellen: postnumre.
  Metoden returnerer en instans af ResultSet, som i virkeligheden er et database.ResultSet, der er en adapter til java.sql.ResultSet.
  SQL-sætninger der er kendetegnede ved at de ikke returnere et ResultSet (Det er typisk sætninger som ændrer databasen, f.eks. UPDATE) laves med et kald af den statiske metode Update, der sender SQL-sætningen videre til default-databasen. F.eks.:
Update
DB.Update( "UPDATE postnummer SET nr=8888 WHERE byen='Ikast'" )
  Man lukker default databasen ved at kalde:
Close
void Close()
  4. Gennemløb af ResultSet
  I forbindelse med Postnummer-eksemplet, vil vi lave model-klassen Postnummer, der skal repræsentere records fra ResultSet vi får fra Select-kaldet ovenfor:
Source 1:
Model-klassen fra postnummer-eksemplet.
Postnummer.java
import database.*;

public class Postnummer extends AbstractRecordModel {
  private int nr;
  private String byen;
  
  public Postnummer() { }
  
  public Postnummer( int nr, String byen ) {
    this.nr = nr;
    this.byen = byen;
  }
  
  @Override
  protected void fromRecordHook( Record record ) {
    nr = record.getInt( "nr" );
    byen = record.getString( "byen" );
  }

  @Override
  protected String getUpdateSQL() {
    return "UPDATE postnummer SET byen='" + byen + "' where nr=" + nr;
  }

  @Override
  protected String getInsertSQL() {
    return "INSERT INTO postnummer VALUES ( " + nr + ", '" + byen + "' )";
  }
  
  @Override
  protected String getDeleteSQL() {
    return "DELETE FROM postnummer WHERE nr=" + nr;
  }
  
  @Override
  public String toString() {
    return "[Postnummer: nr=" + nr + ", byen='" + byen + "']";
  }
}
  Man ser her hvordan fromRecordHook-metoden anvender get-metoderne på Record-objektet til at hente de informationer der skal placeres i objektets datakerne.
get...SQL() Man bemærker også, at der er tre get...SQL-metoder (bemærk, at disse metoder skal erklæres protected). Disse metoder er implementeret med stubbe i AbstractRecordModel, der alle returnerer null. De anvendes af database pakken til at foretage forskellige opdateringer i databasen. Man behøver kun at implementere de af metoderne der konkret vil blive brugt i ens anvendelse. De bruges i forbindelse med kalde af følgende to metoder der nedarves fra AbstractRecordModel:
boolean update()
boolean delete()
  Først nævnte vil opdatere model-objektets datakerne i databasen med enten en UPDATE-sætning eller en INSERT-sætning, alt efter om objektet i forvejen findes i databasen (dette holder database pakken selv rede på).
  Sidstnævnte sletter den tilhørende record i databasen.
  At disse to metoder fungerer, afhænger naturligvis af at man har implementeret de tre metoder korrekt. Hvis der bliver brug for at kalde en af de tre metoder, og det viser sig at den ikke er implementeret kommer der en fejlmeddelelse i form af en dialogbox.
  Af model-klassen ser man at records i tabellen: postnumre, har attributterne nummer og byen.
  Tabellen har følgende opbygning og indhold:
Figur 1:
Postnummer-tabellen
  Nu har vi en model-klasse og vi har et ResultSet som passer til. Hvordan skal det hænge sammen? Hvordan kan vi gennemløbe ResultSet'et og få en lind strøm af instanser af model-klassen?
  Vi gør det vha. en iterator. Iteratoren får vi ved at kalde metoden getIterator på vores ResultSet (Denne metode findes ikke i java.sql.ResultSet, og det er det eneste sted det afsløres for klienten af der er tale om en adapter).
  Vi foretager derfor kaldet:
getIterator
ResultSet resultSet = DB.Select( "SELECT * FROM postnummer" );
RecordIterator iterator = resultSet.getIterator( new Postnummer() );
  Vi har her medtaget Select-kaldet for at tydeliggøre sammenhængen.
  Man bemærker en speciel ting ved getIterator-metoden: Som parameter tager den en instans af model-klassen. getIterator-metoden ændrer ikke instansen på nogen måde, så man kan ubekymret give den en hvilken som helst instans, også én man er ved at bruge til noget andet. Det skal blot betragtes som en indirekte måde at fortælle iteratoren; hvilken klasse den skal retunere instanser af, når vi itererer på ResultSet'et.
Class-objektet At getIterator-metoden ikke ændrer den instans af model-klassen den får med som parameter, skyldes at den blot bruger den til at finde Class-objektet (hvis man ikke er bekendt med java.lang.reflect, vil man ikke vide hvad dette betyder, men en sådan forståelse er heller ikke nødvendigt for at anvende det.). ResultSet har en anden getIterator-metode som man kan bruge; hvis man selv ønsker at angive Class-objektet:
RecordIterator getIterator( Class modelClass )
  I så fald skulle eksemplet ovenfor i stedet være følgende:
ResultSet resultSet = DB.Select( "SELECT * FROM postnummer" );
RecordIterator iterator = resultSet.getIterator( new PostNummer().getClass() );
  RecordIterator har fire metoder, der er velkendte; hvis man tidligere har stiftet bekendskab med iteratorer:
Record-
Iterator's metoder
RecordModel current()
boolean reset()
boolean next()
boolean prev()
  Følgende anvendelse illustrerer hvordan disse metoder kan bruges:
Source 2:
Testanvendelse af Record­Iterator
/*
 *  Almindeligt gennemløb
 */
while ( iterator.next() ) {
  PostNummer p = (PostNummer) iterator.current();
  System.out.println( p );
}

System.out.println();

/*
 *  Almindeligt gennemløb - en gang til
 */
iterator.reset();

while ( iterator.next() ) {
  PostNummer p = (PostNummer) iterator.current();
  System.out.println( p );
}

System.out.println();

/*
 *  Baglæns gennemløb
 */
while ( iterator.prev() ) {
  PostNummer p = (PostNummer) iterator.current();
  System.out.println( p );
}
[Postnummer: nr=7330, byen='Brande']
[Postnummer: nr=7400, byen='Herning']
[Postnummer: nr=7430, byen='Ikast']
[Postnummer: nr=7441, byen='Bording']
[Postnummer: nr=7470, byen='Karup J.']
[Postnummer: nr=8600, byen='Silkeborg']
[Postnummer: nr=8800, byen='Viborg']

[Postnummer: nr=7330, byen='Brande']
[Postnummer: nr=7400, byen='Herning']
[Postnummer: nr=7430, byen='Ikast']
[Postnummer: nr=7441, byen='Bording']
[Postnummer: nr=7470, byen='Karup J.']
[Postnummer: nr=8600, byen='Silkeborg']
[Postnummer: nr=8800, byen='Viborg']

[Postnummer: nr=8800, byen='Viborg']
[Postnummer: nr=8600, byen='Silkeborg']
[Postnummer: nr=7470, byen='Karup J.']
[Postnummer: nr=7441, byen='Bording']
[Postnummer: nr=7430, byen='Ikast']
[Postnummer: nr=7400, byen='Herning']
[Postnummer: nr=7330, byen='Brande']
  Det skal bemærkes at man kan have lige så mange RecordIterator'er på det samme ResultSet som man ønsker, de generer ikke hinanden i forbindelse med deres individuelle anvendelse af ResultSet'et.
  5. Autonummerering
  5.1 Med model-klasser
  Anvender man autonummerering (eg. IDENTITY på SQL Server) i forbindelse med model-klasser, har man mulighed for at sætte den autogenererede nøgle i model-objektet. Dette gøres ved at override setAutoKey-metoden, der nedarves fra AbstractRecordModel.
  Har man f.eks. en Book-klasse der repræsenterer bøger; hvor der i model-klassen er en attribut: bookId, der skal indholde primær-nøglen, kan man anvende følgende:
Source 3:
setAutoKey
(Book.java)
public class Book extends AbstractRecordModel {
  private int bookId;
  
  ...
  
  public void setAutoKey( int key ) {
    bookId = key;
  }
}
  Den nedarvede metode indeholder ikke selv nogen implementation, så undlader man at override den, bliver en autogenereret nøgler ikke sat i datakernen på det indsatte objekt.
  5.2 Direkte på databasen
  Hvis man arbejder direkte på databasen, med kald af DB-klassens metoder, kan man også få den autogenererede nøgle. Det sker ved kald af:
int getAutoKey()
  på en instans af DB, eller den statiske metode:
int GetAutoKey()
  hvis man arbejder på default-databasen.
  Metoden vil returnere den autogenererede værdi fra den seneste SQL-sætning der er blevet udført. Hvis den seneste sætning ikke har afstedkommet en autogenereret værdi (f.eks. en SELECT-sætning), returneres -1.
  6. Flere databaser
  Anvender man kun én database, gør man som ovenfor: Arbejder med en default database. Skal man derimod arbejde med flere databaser samtidig, kan man ikke nøjes med det.
  Man må i stedet direkte arbejde med instanser af DB (når man arbejder med en default database, gør man det indirekte). Man kan få instanser af DB til de databaser man ønsker med metoden:
DB getDB( String dbName )
  eller hvis der skal anvendes brugernavn og password:
DB getDB( String dbName, String user, String password )
  eller hvis man vil specificere alle oplysninger (se beskrivelse af parametrene under setDefaultDB-metoden, der er bekrevet tidligere i kapitlet):
DB.getDB( String dbName, String host, int port,
          String instance, String user, password )
  I modsætning til setDefaultDB returneres her selve DB-objektet. Dette objekt kan man anvende på samme måde som default databasen med metoderne:
Med småt
ResultSet select( String sql )
boolean   update( String sql )
void      close()
  Bemærk, at disse staves med småt i modsætning til default databasens, der starter med stort (Det var desværre nødvendigt med denne forskel af implementationsgrunde!)
  De instanser af DB man får ved at kalde getDB er Singletons og DB's konstruktorer er derfor private. Når man anvender instanser af DB kan man vælge, om man vil huske dem med referencer eller kalde getDB når man bruger dem - det sidste er nemmere, men ikke så effektivt (effektivitetsproblemet er dog ubetydeligt, da det meste af tiden alligevel går med at tilgå selve databasen)
Det er ikke et enten eller! Man kan udemærket arbejde med en default database og samtidig have en række andre databaser man anvender via instanser af DB. F.eks. kan det være praktisk; hvis man bruger én af databaserne betydelig mere end de andre. I så fald lader man den hyppigst anvendte database være default, og bruger instanser af DB til de øvrige. Bemærk forøvrigt, at man frit kan skifte mellem hvilken database der er default; hvis man ønsker det.
  7. Transaktioner
  Vi skal ikke her beskrive transaktions-begrebet men blot beskrive hvordan man kan lave transaktioner med database pakken.
  Der findes følgende metoder, der kan kaldes på DB-klassen (de findes tilsvarende på DB-instanser - med sædvanligt lille startbogstav):
void SetAutocommit( boolean enable )
void Begin()
void Commit()
void Rollback()
  Den første metode bruges til at slå autocommit fra og til (default er, at autocommit er slået til på SQL Server). Hvis man slår det fra, vil udførelse af en SQL-sætning implicit markere starten på en transaktion, der fortsætter indtil man udfører commit eller rollback (med en af de sidste to metoder). Hvis man ikke slår autocommit fra, altså anvender autocommit, kan man lejlighedsvis anvende et kald af Begin-metoden, der så vil markere starten på en transaktion, der afsluttes med commit eller rollback; hvorefter man igen vil være i almindelig autocommit tilstand.
  Bemærk: Der er ikke nogen understøttelse af rollback i database pakkens model-objekter, den må man selv implementere. Er der sket ændringer i datakernen af model-objekter, der indgår i en transaktion som ender i rollback, må man selv reetablere disse objekters datakerne. En mulig løsning på dette problem, er at kassere model-objekter, der er berørt af et rollback, og reetablere dem ved påny, ved at danne dem ud fra databasen som vil have den rigtige tilstand.
  8. Debugging
  Anvendelser af JDBC er ofte vanskelige at debug'e. I database pakken er der en klasse DEBUG, med en række statiske variable, der styrer understøttelsen af debugging. Disse variable er:
boolean enabled
  Hvis denne variabel er sat til false, er al understøttelse af debugging slået fra. Er den true, vil der komme en række meddelelser via System.out, der viser kaldene til databasen. Debugging er default enabled, og kan ikke slåes fra hvis man anvender jar-filen sidst i dette kapitel.
int resultSetOpenTime
  Af hensyn til de instanser af java.sql.Statement, som instantieres i forbindelse med diverse ResultSet's skal man altid huske at lukke et ResultSet når man er færdig med at bruge det, ellers går programmet ganske enkelt ned (Det er min erfaring, at det sker ved ca.. 50+ åbne ResultSet's). Når man lukker et ResultSet frigiver man den statement, der er tilnyttet ResultSet'et.
  Da det kan være tidskrævende og træls at lede efter disse "statement-leaks" vil alle ResultSet's holde øje med sig selv i det antal sekunder man sætter variablen resultSetOpenTime til (denne er default sat til 10 sekunder, og kan ikke ændres hvis man bruger jar-filen sidst i dette kapitel). Hvis ResultSet'et ikke er blevet lukket senest ved udløbet af denne periode, kommer der en dialogbox med besked om det åbne ResultSet, og en angivelse af den SQL-sætning, der lå til grund for ResultSet'et. Det sidste skulle gerne hjælpe til at finde fejlen.
Figur 2:
Timeout
  Hvis enabled er sat til false, vil ResultSet'et ikke foretage dette check.
  Bemærk: Man har kun mulighed for at ændre disse variables værdi, hvis man arbejder med kildeteksten til database pakken, ikke hvis man anvender jar-filen!
  9. Eclipse
Access Rules Hvis man anvender Eclipse kan man blive udsat for, at den nægter at anvende klasserne i database-pakken's jar-file (den siger de ikke er tilgængelige). Dette skyldes at Eclipse har indført Access Rules når projekter tilgår jar-filer. Dette gør det muligt detaljeret at styre hvilke dele af jar-filer man ønsker at anvende (og specielt ikke anvende), men det er kun en unødvendig komplikation at skulle sætte en access rule i alle de projekter man måtte lave med database-pakken, da der kun er tale om (større eller mindre) eksperimenter med database-programmering.
Ignore Det nemmeste er helt at slå det fra under: Windows > Preferences > Java > Compiler > Error/Warning > Deprecated and resticted API, og sætte Forbidden reference (access rules) til Ignore (den er ellers sat til Error).
  Beta-version:
  Database pakken foreligger kun i en beta-version (det vil nok altid være tilfældet). Hvis man anvender den som beskrevet ovenfor skulle der ikke være nogen problemer, men lad være med selv at kalde andre metoder på database.ResultSet end getIterator() og close().
  SQL Server 2008:
 

Database pakken: database package 1.1.7 sql server 2008.jar

Kildeteksten: database package 1.1.7 sql server 2008 - source.zip

  Postnummer eksempel, inkl. script med databasen:
 
postnummer eksempel.zip