Programski konstrukti za delo s podatkovnimi bazami.
Uvod

|
V tem poglavju pričakujemo poznavanje pojma podatkovne baze
in povpraševalnega jezika SQL. Obravnavali bomo, kako lahko nek uporabniški
program interaktira s podatkovno bazo.
Programi so seveda pisani v različnih programskih
jezikih in tečejo v različnih okoljih. Prav tako poznamo različne sisteme za
upravljanje podatkovnih baz. |
 |
Microsoft je zato predlagal standard ODBC ( Open Data Base Connectivity), ki je namenjen komunikaciji
uporabniških programov s podatkovnimi bazami. Standard določa poenotene
funkcijske klice, s katerimi se lahko pogovarjamo z različnimi podatkovnimi
bazami.
Zamisel je v tem, da vsi
uporabniški programi uporabljajo enake klice funkcij ne glede na to, s katero
podatkovno bazo komunicirajo. Zato pa mora biti med njimi in podatkovno bazo še
poseben modul, takoimenovani ODBC gonilnik (ODBC driver), ki te klice preslika
v obliko, ki jo dani sistem za upravljanje s podatkovno bazo razume. Vsaka Vrsta
SUPB torej zahteva svoj ODBC gonilnik. |
Programi v Javi in dostop do podatkovnih baz s pomočjo JDBC
 |
JDBC (Java Database Connection) je standardna metoda za dostop do podatkovnih
baz iz Jave. V bistvu je to aplikacijski programski vmesnik (API, Application
Programming Interface). Sun je po zgledu Microsoftove tehnologije ODBC razvil
knjižnico JDBC. Žal je ODBC kar kompleksen, ker nudi le nekaj, zato pa
kompleksnih klicev. JDBC naj bi to kompleksnost poenostavil z uvedbo več bolj
preprostih klicev |
Dostop do podatkovne baze s pomočjo JDBC terja več korakov:
- Tvoriti moramo objekt Connection , ki je povezan
s podatkovno bazo
- Tvorimo objekt Statement , s katerim odpremo
objekt Connection
- Povpraševalno izvajanje
objekta Statement's vrne objekt ResultSet
- Sedaj lahko obdelamo
vrstice v objektu ResultSet
Poleg te, običajne oblike imamo tudi njene različice. Namesto izjave Statement,
tvorimo PreparedStatement (ki omogoča vnaprej pripravljena vprašanja in
tako bolj učinkovito uporabo, na primer v zankah). Povpraševanje update lahko vrne le število vrstic, ki so bile posodobljene, vrinjene ali zbrisane,
ne vrne pa ResultSet. Na voljo imamo tudi klice, ki povedo podatke o
objektih ResultSet (na primer
število, imena in tipe stolpcev). Na voljo so tudi klici za podporo transakcij
s podatkovno bazo.
Paketi (packages)
Glavni paket, ki ga moramo uporabiti, je java.sql. Zato na začetku programa uporabimo stavek
Ta
paket spada v standardno distribucijo JDK (Java Development Kit). Obstaja pa
tudi njegova bolj napredna razširitev javax.sql,
ki pa jo redko uporabljamo.
Gonilniki (drivers)
 |
Posamezni
sistemi za upravljanje s podatkovnimi bazami zahtevajo za svoj dostop z
vmesnikom JDBC ustrezen JDBC gonilnik. Sun ponuja kot del svoje standardne
distribucije premostitveni gonilnik JDBC-ODBC. Ta nam omogoča povezavo s
katerokoli podatkovno bazo, ki razume ODBC. Ker je ta gonilnik precej počasen,
je primeren bolj za preskušanje in ni priporočljiv za poslovne aplikacije.
|
Večina ponudnikov sistemov za upravljanje s podatkovnimi bazami nudi svoje
gonilnike.
Če
želimo dostopiti do podatkovne baze, moramo najprej naložiti gonilnik. Seveda
mora biti gonilnik nameščen na sistem (kar pomeni, da mora biti nameščena
ustrezna »jar« datoteka z gonilnikom in navedena v poti (classpath)). Če
uporabljamo premostitveni gonilnik JDBC-ODBC, moramo še našo podatkovno bazo
registrirati z ODBC (Na sistemi MS
Windows uporabimo orodje "ODBC data sources" v nadzornem panoju). Ime
razreda gonilnika JDBC-ODBC je sun.jdbc.odbc.JdbcOdbcDriver. Ko je
gonilnik naložen, se samodejno registrira s pomočjo orodja »Drivermanager« in
je tako pripravljen za tvorbo objekta Connection.
Obstaja
več načinov nalaganja gonilnika. Omenimo le naslednjo:
Razred System ima statični seznam lastnosti
(Property). Če ta vsebuje lastnost jdbc.drivers nastavljeno na
seznam imenov razredov gonilnikov, ločen
z dvopičji (":") , bodo ustrezni gonilniki samodejno naloženi in
registrirani. Ko zahtevamo povezavo, sistem izbere najbolj primeren gonilnik.
Žal
pa ta metoda ni primerna za delo s takoimenovanimi »servleti«. V tem primeru je
bolje, da naložimo ustrezen razred dinamično, med samim izvajanjem programa na
naslednji način:
Properties props = new Properties() ;
FileInputStream in = new FileInputStream("Database.Properties") ;
props.load(in) ;
String drivers = props.getProperty("jdbc.drivers") ;
Class.forName(drivers) ; |
Datoteka Database.Properties izgleda tako:
# Default JDBC driver and database specification
jdbc.drivers = sun.jdbc.odbc.JdbcOdbcDriver
database.Shop = jdbc:odbc:Shop
|
Tvorba povezave (Connection)
Povezavo
dobimo tako, da navedemo URL konkretne podatkovne baze, ki jo želimo uporabiti.
Oblika tega URL je odvisna od uporabljenega gonilnika. URL podatkovne baze
lahko dobimo potem, ko je gonilnik naložen in lahko uporabimo prej omenjeno
datoteko z lastnostmi (properties). Če uporabimo premostitveni gonilnik JDBC-ODBC,
je URL podatkovne baze jdb:odbc:xxx , pri čemer je xxx podatkovni vir ODBC,
registriran za našo podatkovno bazo. (ime lastnosti, ki jo uporabimo, ni
pomembno).
String database = props.getProperty("database.Shop") ;
Connection con = DriverManager.getConnection(database) ;
|
Opozorilo: Microsoft Access, do katerega dostopamo preko
gonilnika JDBC-ODBC, lahko tudi ne
dopušča več sočasnih povezav (vsaj ne zanesljivo). To pomeni, da naša
aplikacija ne sme imeti istočasno odprtih več objektov Connection. Zato je uporabnost Microsoft
Access za spletno podatkovno bazo zelo omejena je pa še primerna za
ozobraževalne namene).
Uporaba
objektov Statement
Objekt
Statement (izjava) dobimo iz povezave z naslednjim stavkom:
Statement stmt = con.createStatement() ;
|
Z
objektom Statement lahko nato izvajamo ali nadziramo izvajanje različnih
povpraševanj SQL.
- Uporabimo stmt.executeUpdate z argumentom v obliki niza, ki vsebuje SQL stavek za posodobitev (INSERT,
DELETE or UPDATE). Stavek vrne število posodobljenih vrstic.
- Uporabimo stmt.executeQuery z argumentom v obliki niza, ki vsebuje SQL povpraševanje SELECT. Stavek
vrne objekt ResultSet, ki ga nato uporabimo za dostop do
vrstic rezultata povpraševanja.
- Uporabimo stmt.execute za izvajanje
poljubnega SQL stavka kateregakoli tipa. Je pa pri tem stavku težje
ugotavljanje rezultata, bodisi celega števila bodisi objekta ResultSet. Ta stavek uporabimo, če želimo posplošen
dostop do podatkovne baze oziroma za programirano tvorbo povpraševanj.
int count = stmt.executeUpdate("INSERT INTO Customers " +
"(CustomerFirstName, CustomerLastName, CustomerAddress) "
"VALUES ('Tony', 'Blair', '10 Downing Street, London')") ;
ResultSet rs = stmt.executeQuery("SELECT * FROM Customers") ;
// do something with count and RS
|
Sintaksa
stavkov SQL, ki jih posredujemo kot aegument v obliki niza, se mora ujemati s
sintakso, ki jo uporablja navezana podatkovna baza.
Primer:
ResultSet rs = stmt.executeQuery("SELECT * FROM Customers" +
"WHERE CustomerLastName = 'O''Neill'") ;
|
Uporaba
objektov ResultSet
Če
ne poznamo točne strukture tabel (sheme) v ResultSet, jo lahko dobimo
preko objekta ResultSetMetaData.
ResultSetMetaData rsmd = rs.getMetaData() ;
int colCount = rsmd.getColumnCount() ;
for (int i = 1 ; i <= colCount ; i++) {
if (i > 1) out.print(", ");
out.print(rsmd.getColumnLabel(i)) ;
}
out.println() ;
|
Ko
dobimo objekt ResultSet , lahko iz njega dobimo njegove vrstice oziroma
polja v njegovih vrsticah:
while (rs.next()) {
for (int i = 1 ; i <= colCount ; i++) {
if (i > 1) out.print(", ");
out.print(rs.getObject(i)) ;
}
out.println() ;
}
|
Stolpce
začnemo štedi od 1 dalje in ne od 0, kot
je sicer navada pri poljih v Javi. Lahko pa uporabimo metodo getObject na objektu ResultSet . Ta metoda sprejme kot argument ime stolpca. Na
voljo imamo tudi metode tipa getxxx , v katerih namesto številke
stolpca navedemo kot argument ime stolpca. Zato lahko zgornjo kodo zapišemo tudi tako:
while (rs.next()) {
out.println( rs.getObject("CustomerID") + ", " +
rs.getObject("CustomerFirstName") + ", " +
rs.getObject("CustomerLastName") + ", " +
rs.getObject("CustomerAddress") ) ;
}
|
Namesto
metode getObject lahko uporabimo bolj specifične metode, kot na
primer getInt, getString, itd.
Vendar ima njihova uporaba tudi pomanjkljivost. Če je tip polja primitiven, kot
na primer int, float itd., in če je v
podatkovni bazi vrednost tega polja prazna, metoda ne zna vrniti nekaj, po
čemer bi to ločili od normalne vrednosti. Če pa uporabimo metodo getObject in če je polje prazno, dobimo vrednost »null.
Objekti PreparedStatement
Te
objekte lahko uporabimo namesto objektov Statement. Objekte PreparedStatement imajo naslednje prednosti:
- Pri ponavljanih
povpraševanjih (na primer v zankah) je to bolj učinkovito, ker so stavki
SQL prevedeni le enkrat
- Mehanizem vključevanja
vrednosti parametrov poskrbi za vse posebne znake
Objekt PreparedStatement vzpostavi tekst SQL,
koga tvorimo. Parametre navedemo z znaki '?' . Po tvorbi lahko te parametre počistimo z metodo clearParameters in nastavimo z metodami setInt, setString, itd. (pozicije parametra začnemjo z 1) . Objekt
nato izvedemo z metodami execute,
executeUpdate ali executeQuery kot pri objektu Statement ,
povrnjene pa dobimo enake tipe, vendar brez argumentov (saj smo stavke SQL že
nastavili pri tvorbi objekta):
PreparedStatement pstmt = con.prepareStatement(
"INSERT INTO Customers " +
"(CustomerFirstName, CustomerLastName, CustomerAddress) "+
"VALUES (?, ?, ?)") ;
pstmt.clearParameters() ;
pstmt.setString(1, "Joan") ;
pstmt.setString(2, "D'Arc") ;
pstmt.setString(3, "Tower of London") ;
count = pstmt.executeUpdate() ;
System.out.println ("\nInserted " + count + " record successfully\n") ;
pstmt.clearParameters() ;
pstmt.setString(1, "John") ;
pstmt.setString(2, "D'Orc") ;
pstmt.setString(3, "Houses of Parliament, London") ;
count = pstmt.executeUpdate() ;
System.out.println ("\nInserted " + count + " record successfully\n") ;
|
Transakcije
Transakcije
so mehanizem za skupinske operacije v tem smislu, da uspejo vse ali pa nobena.
To prepreči nekonsistenčne probleme v podatkovni bazi, ki bi nastali, če bi se
uspešno izvedel le del operacij. Pomislimo na primer, da bi želeli prestaviti
del denarja iz enega računa na drugi in da bi dvig denarja uspel, polog na
drugi račun pa ne. Žalostno, kaj ne? Če
to izvedemo kot transakcijo, je prva operacija (dvig) veljavna le, če tudi
drugi del (polog) uspe.
Ko
smo dobili povezavo »Connection« je lastnost AutoCommit nastavljena na true. To pomeni, da je vsaka izvedba
povpraševanja zapomnjena takoj po svoji izvedbi in preden začnemo izvajati
naslednje povpraševanje. Če želimo združevati operacije v transakcije, moramo
lastnost AutoCommit izključiti:
con.setAutoCommit(false) ;
|
Sedaj
moramo dobiti nov objekt Statement (stari ne bo več deloval) in ga uporabljamo
na že znani način. Ko zaključimo v se želene operacije, zahtevamo, pnjihovo pomnenje
z naslednjim ukazom:
Nato
nadaljujemo s skupino operacij, ki predstavljajo naslednjo transakcijo, ali pa
preidemo na običajno, sprotno izvajanje operacij z ukazom:
con.setAutoCommit(true) ;
|
Če
pride med izvajanjem transakcije do težav (recimo, da nismo mogli izvesti vseh
operacij transakcije), lahko vse dotedanje operacije razveljavimo z ukazom:
Če
pride do izpada podatkovne baze ali celo računalnika se bo v bistvu operacija »rollBack«
izvedla samodejno pri ponovnem zagonu podatkovne baze.
Čas
izvajanja posameznih transakcij moramo minimizirati, saj transakcije zadržujejo
veliko virov in v bistvu zaklepajo podatkovno bazo. In tako onemogočajo druge
transakcije.
Pri
deli s transakcijami moramo biti previdni in rokovati s takoimenovanimi
izjemami (exception handling). .Zato moramo transakcije vgraditi v oddelek try, ki ga v javanskih programih dobro
poznamo. V primeru neuspešne transakcije izvedemo klic rollback.
Izjeme
in opozorila
Izjeme SQL - SQLExceptions, do
katerih pride, če je pri obdelavi klicev JDBC kaj narobe, so nekaj posebnega. Povzročijo
lahko generiranje verige dodatnih izjem
SQL. Obdelati moramo torej celo veriga takih izjem in iporabimo kodo naslednje
oblike:
try {
// do some JDBC calls
}
catch (SQLException e) {
do
System.out.println(e.getMessage()) ;
while ((e = e.getNextException()) != null) ;
}
catch (Exception e) { // handle non-SQLException exceptions }
|
Do
kakšnih problemov je prišlo, nam lahko pojasni opozorilni objekt SQLWarning.
Take opozorilne objekte lahko uporabljamo po uporabi objektov Connection,
Statement ali ResultSet. Zelo pogost tip opozorila je »Data
Truncation error«. Mnogo programerjev takih poizvedb niti ne dela.
Preprost primer uporabe JDBC
Poglejmo
si poenostavljen primer uporabe podatkovne baze MS Access. Na naslednji sliki
vidimo strukturo tabel, stolpcev in atributov.

Še
nekaj kratkih pojasnil:
- Stranka (Customer) ima lahko več naročil (Orders).
- Posamezno naročilo (Order) ima lahko več zapisov OrderDetail.
- Vsak zapis OrderDetail se nanaša na natančno
eno knjigo (Book).
- Vsi primarni ključi (polja
ID) so avtomatsko generirani in jih zato pri vrivanju zapisov ne navajamo.
- Referential integrity rules
are maintained on all foreign keys (ID fields which not in the first
position in the tables of the figure above). Hence, for example, it will
cause an error to try to insert an Order record with an Orders.CustomerID for which there is no corresponding Customers record.
- OrderDetail.Quantity je
celo tipa int.
- Books.BookPrice je tipa double .
- CustomerFirstName, CustomerLastName CustomerAddress in BookName are so tipa String.
- Orders.OrderDate je objekt tipa Date/Time object v MSAccess, JDBC pa ga preslika v objekt TIMESTAMP java.sql.Timestamp.
Objekte Timestamp lahko
nastavljamo na trenutni čas na naslednji način:
long millisecs = System.currentTimeMillis() ;
Timestamp ts = new java.sql.Timestamp(millisecs) ;
|
Za
nastavljanje na določeno vrednost pa lahko uporabljamo metodo valueOf z argumentom tipa String:
ts = Timestamp.valueOf("2001-07-06 14:25:29.9") ;
|
ResultSet ima metode getTimestamp, putTimestamp in updateTimestamp, PreparedStatement ima metodo setTimestamp.
Preprost
program, ki uporablja JDBC, je še vedno predolg, da bi ga vključili v to stran.
Vidimo ga pa lahko tu:Customer.java.
Za njegovo izvedbo potrebujemo še datoteko Database.Properties in
podatkovno bazo Shop.mdb.
Vse te datoteke naj bodo v istem direktoriju. V nadzornem panoju moramo
za Shop.mdb tvoriti vhod (entry) v naš »ODBC
data sources«
Še nekaj vaj z JDBC
- Napiši program, ki v ukazni
vrstici prejme niz in izpiše podrobnosti o knjigah, katerih naslovi
začnejo s tem nizom.
- Napiši program, ki iz
ukazne vrstice prebere ime in priimek stranke ter delni naslov knjige.
Program mora preveriti, ali ta stranka sploh obstaja. Na konzolo
računalnika naj nato izpiše vse knjige, ki ustrezajo zahtevanemu naslovu
(vsebujejo podniz, naveden kot delni naslov).