Java ja tietokannan käsittely (JDBC) Javan tietokannan käsittely luokat (java.sql.*) Yhteys tietokantaan Tietokannan yhteyden sulkeminen Tiedon haku tietokannasta Tiedon päivitys tietokantaan Transaktio 1
Javan tietokannan käsittely luokat (java.sql.*) Driver DriverManager +getconnection(in url : String, in user : String, in pass : String) : Connection Connection +TRANSACTION_READ_UNCOMMITTED + TRANSACTION_READ_COMMITTED + TRANSACTION_REPEATABLE_READ + TRANSACTION_SERIALIZABLE + getmetadata() : DatabaseMetaData + setautocommit(in a : boolean) : void + setreadonly(in a : boolean) : void + settransactionisolation(in level: int) : void + createstatement() : Statement + preparestatement(int sql : String) : PreparedStatement + preparecall() : CallableStatement + commit() : void + rolback() : void + close() : void tietokantateoria_transaktiot/ S. Lahtinen SQLException +getsqlstate() : String +geterrorcode() : int 2
Yhteyden otto tietokantaan Javan tietokannan käsittely luokat (java.sql.*) jatkuu 1) rekisteröi ajuri Ohjelma tarvitsee JDBC ajuriluokan Luokka ladataan ohjelman ajoaikana, joten sitä ei tarvitse huomioida käännösaikana, käytämme MariaDB-ajuria String JDBCAjuri = org.mariadb.jdbc.driver ; Class.forName(JDBCAjuri).newInstance(); tietokantateoria_transaktiot/ S. Lahtinen 3
Yhteyden otto tietokantaan jatkuu.. 2) luo yhteys tietokantaan DriverManager luokka sisältää metodin, jonka avulla otetaan yhteys tietokantaan Ensin määritellään ajurin ja tietokannan nimi: Ajurin nimi Portin numero Tietokannan nimi String url = jdbc.mariadb://localhost:3306/kannannimi ; Sitten otetaan yhteys tietokantaan: Connection connection = DriverManager.getConnecion(url, user, salasana ); tietokantateoria_transaktiot/ S. Lahtinen 4
Yhteys Mariadb-tietokantaan private Connection yhdista() throws SQLExeption, Exception { Connection connection = null; String JDBCAjuri = "org.mariadb.jdbc.driver ; String url = "jdbc:mariadb://localhost:3306/kannannimi ; try { Class.forName(JDBCAjuri).newInstance(); // ajurin määritys // ota yhteys kantaan connection=drivermanager.getconnecion(url, user, salasana ); catch (SQLException e) { // tietokantaan ei saada yhteyttä connection = null; thow e; catch (Exception e ) { // JDBC ajuria ei löydy throw e; return connection; 5
Tietokantayhteyden sulkeminen Tietokantayhteys kannattaa sulkea niin pian kuin mahdollista, koska on muitakin käyttäjiä Käytetään Connection luokan metodia void close() throws SQLException Metodin kutsu tarvitsee poikkeukseen varautumisen 6
Javan tietokannan käsittely luokat (java.sql.*) DatabaseMetaData +gettables(in...) : ResultSet Statement Connection +setcursorname(in n : String) : void +executequery(in sql : String) : ResultSet +excuteupdate(in sql : String) : int +cancel() : void +close() : void PreparedStatement +executequery() : ResultSet +executeupdate() : int +setint(in index : int, in x : int) : void +setstring(in index : int, in x : String) : void +setxxx(in index : int, in x : XXX) : void +close() : void ResultSet +getmetadata():resultsetmetadata +findcolumn(in name:string): int +next() : boolean +getint(in name : String) : int +getstring(in name: String) : String +getdouble(int name : String) : double +getxxx(in name : String) : XXX +wasnull() : boolean + close() : void ResultSetMetaData CallableStatement +execute() : boolean +registeroutputparameter(in...) : void +getcolumncount() : int +getcolumnname(in column : int) : String +gtecolumnlabel(in column : int) : String 7
Javan tietokannan käsittely luokat (java.sql.*) jatkuu Tietojen haku tietokannasta (SQL kysely) Käytetään Connection-luokan metodia PreparedStatement preparestatement (String sql) Ja PreparedStatement-luokan metodia ResultSet executequery(); eli Connection connection = yhdista(); String sql = SELECT tunnus, nimi, opintopisteet FROM opintojakso ; // luo PreparedStatement-olio sql-lauseelle PreparedStatement lause = connection.preparestatement(sql); // suorita sql-lause ResultSet tulosjoukko = lause.executequery(); 8
SQL Kysely jatkuu public List<Opintojakso> haeopintojaksot() throws SQLException, Exception{ String sql = SELECT tunnus, nimi, opintopisteet FROM opintojakso ; List <Opintojakso> lista = null; PreparedStatement lause = null; Connection connection = null; ResultSet tulosjoukko = null; try { connection = yhdista(); // ota yhteys tietokantaan if ( connection!= null) { lause = connection.preparestatement(sql); // luo PreparedStatement-olio tulosjoukko = lause.executequery(); lause.close(); connection.close(); // sulje lause // sulje heti yhteys kantaa // suorita sql-lause if(tulosjoukko!= null){ // tuottiko sql-lause tulosjoukon? lista = puratulosjoukko(tulosjoukko); Jatkuu seuraavalla sinulla tulosjoukko.close(); // sulje tulosjoukko 9
SQL Kysely jatkuu catch (SQLException e) { throw e; catch (Exception e) { throw e; finally { if (connection!= null && connection.isclosed() == false ) { try { connection.close(); catch (Exception e) { throw e; return lista; 10 tietokantateoria_transaktiot/ S. Lahtinen
SQL Kysely jatkuu Miten puran tulosjoukon eli ResultSet-olion? kun ResultSet olio saa ensin arvonsa, se ei vielä viittaa tulosjoukon riviin boolean next()-metodin kutsu siirtää ResultSet-olion viittamaan ensin ensimmäiseen kyselyn riviin ja sitten seuraavaan jne. Kun viimeisen rivin jälkeen kutsutaan metodia next(), se palautta arvon false Kun ResultSet-olio viittaa kyselyn riviin, rivin arvot voidaan purkaa ResultSet -luokan metodeilla int getint (String sarakenimi) double getdouble (String sarakenimi) String getstring (String sarakenimi); XXX getxxx (String sarakenimi); 11
SQL Kysely jatkuu private List<Opintojakso> puratulosjoukko (ResultSet joukko) throws SQLEXception { List <Opintojakso> lista = null; Opintojakso opintojakso; if (joukko!= null){ try { while (joukko.next() == true ) { opintojakso = teeopintojakso(joukko); if (lista == null ) lista = new ArrayList<Opintojakso>(); lista.add(opintojakso); catch (SQLException e){ throw e, return lista; tietokantateoria_transaktiot/ S. Lahtinen 12
SQL Kysely jatkuu private Opintojakso teeopintojakso(resultset tulosjoukko) Opintojakso opintojakso = null; String tunnus, nimi; int pisteet; if (tulosjoukko!= null){ try { throws SQLException { tunnus = tulosjoukko.getstring( "tunnus ); nimi = tulosjoukko.getstring( "nimi ); pisteet = tulosjoukko.getint( "opintopisteet ); opintojakso = new Opintojakso(tunnus, nimi, pisteet); catch (SQLException e) { throw e; return opintojakso; CREATE TABLE opintojakso ( tunnus CHAR (9) NOT NULL, nimi VARCHAR (30) NOT NULL, opintopisteet INT NOT NULL, PRIMARY KEY (tunnus), )ENGINE=InnoDB; 13
SQL Kysely jatkuu Entä jos tietokantakyselyyn liittyy WHERE-ehto? sql-lauseen where-ehdon arvojen paikalle kirjoitetaan? merkki Esim. String sql = SELECT tunnus, nimi, opintopisteet + FROM opintojakso + WHERE tunnus =? ; tai String sql = SELECT tunnus, nimi, opintopisteet + FROM opintojakso + WHERE opintopisteet =? OR nimi =? ; 14
SQL Kysely jatkuu Miten saadaan ehtojen arvot kyselyyn? Käytetään PreparedStatement-luokan metodeja void setint ( int index, int arvo); void setstring ( int index, String arvo); void setxxx ( int index, XXXX arvo);,missä index tarkoittaa monennesta arvosta (1 ) on kysymys ja arvo tarkoittaa ko. arvon arvoa 15
SQL Kysely jatkuu Esim. Haetaan esimerkiksi tietyn tunnuksen omaavan opintojakson tiedot String sql = SELECT tunnus, nimi, opintopisteet + FROM opintojakso WHERE tunnus =? ; PreparedStatement lause = connection.preparestatement(sql); lause.setstring ( 1, SWD1TN002 ); ResultSet tulosjoukko = lause.executequery(); 16
SQL Kysely jatkuu Tai Haetaan kaikki Ohjelmointi-nimiset tai kolmen opintopisteen laajuiset opintojaksot String sql = SELECT tunnus, nimi, opintopisteet + FROM opintojakso + WHERE opintopisteet =? OR nimi =? ; PreparedStatement lause = connection.preparestatement(sql); lause.setint ( 1, 3 ); lause.setstring( 2, Ohjelmointi ); ResultSet tulosjoukko = lause.executequery(); 17
SQL Kysely jatkuu Tulosjoukon purkamisessa on huomioitava sisältääkö tulosjoukko yhden vai useita rivejä tietoa Jos tulosjoukko sisältää vain yhden rivin (haku on tehty relaation avaimen perusteella): private Opintojakso puratulosjoukko (ResultSet joukko) Opintojakso opintojakso = null; if (joukko!= null){ try { if (joukko.next() == true ) { throws SQLEXception { opintojakso = teeopintojakso(joukko); catch (SQLException e){ throw e, return opintojakso; 18
SQL-päivitys Tietokannan tiedon päivitys Käytetään PreparedStatement luokan metodia int executeupdate(); Metodi palauttaa päivitettyjen/lisättyjen/poistettujen rivien lukumäärän, mistä tiedetään onnistuiko päivitys vai ei Päivitettävien tietojen arvot kirjoitetaan sql-lauseeseen?-merkein Arvot saavat arvonsa PreparedStatement-metodien avulla tietokantateoria_transaktiot/ S. Lahtinen 19
SQL-päivitys jatkuu public boolean lisaaopintojakso (Opintojakso opintojakso) throws SQLEXception,Exception{ String sql = INSERT INTO opintojakso + (tunnus, nimi, opintopisteet) VALUES(?,?,?) ; Connection connection= null; PreparedStatement lause = null; int lkm = 0; boolean ok= false; if (opintojakso!= null) { try { connection = yhdista(); if (connection!= null) { lause = connection.preparestatement(sql); lause.setstring(1, opintojakso.gettunnnus()); lause.setstring(2, opintojakso.getnimi() ); lause.setint( 3, opintojakso.getopintopisteet() ); // suorita päivitys tietokantaan lkm = lause.executeupdate(); lause.close(); connection.close(); // sulje yhteys kantaan // jatkuu seuraavalla sivulla 20
SQL-päivitys jatkuu if (lkm == 1) { // lisäys onnistui ok= true; else { // lisäys epäonnistui ok= false; catch (SQLException e) { throw e; catch (Exception e) { throw e; finally { if (connection!= null && connection.isclosed() == false) { try { connection.close(); catch (Exception e) { return ok; throw e; // yhteys poikki 21
Transaktio Transaktio= Yhtenä toimenpiteenä suoritettu hakujen ja tallennusten sarja, jossa jonkin osan epäonnistuessa koko tapahtumasarja peruutetaan (Wikisanakirja) Asiakas PK numero... 1 * Tilaus PK tilausnro tilaushetki FK tilaaja 1 1..* * Tilausrivi PK FK PK FK tilausnro pizzatunnus lkm toimitushetki Esim. Ohjelman pitää lisätä tietokantaan Tilaus ja tilaukseen liittyvät Tilausrivit, jotka kertovat mitä pizzoja tilataan ja kuinka monta. Jos Tilaus-relaation rivi onnistutaan lisäämään, mutta Tilausrivin lisäyksessä tapahtuu virhe, riviä ei voitu lisätä koko tilaus on virheellinen eli koko toimenpide kokonaisuus pitää peruuttaa. Jos taas kaikki tiedot menivät virheettömästi kantaa, kokonaisuus pitää hyväksyä. Pizza PK tunnus 1... 22
Transaktio jatkuu Useat tiedonhallintajärjestelmät ovat ns. autocommit-moodissa, jolloin SQL-lauseen suorittamisen jälkeen suoritetaan automaattisesti lauseen hyväksyminen eli commit Jos tiedonhallintajärjestelmässä ei ole autocommit-moodia, lauseet pitää toteuttaa transaktion alla Käytämme MariaDB-tiedonhallintajärjestelmää, jossa on ns. autocommitmoodi päällä 23
Transaktio jatkuu Esim. Käytetään MariaDB-tiedonhallintajärjestelmää Siirretään 100 euroa tililtä 100012120 tilille 100000111: UPDATE tili SET saldo = saldo-100 WHERE tilinro= 100012120 ; --- automaattinen COMMIT UPDATE tili SET saldo = saldo+100 WHERE tilinro= 100000111 ; mutta --- automaattinen COMMIT Jos tili 100012120 on olemassa 100 euron otto onnistui. Jos tili 100000111 EI ole olemassa SQL-kielen kannalta päivitettävää riviä ei ole, joten lause ei tee mitään eikä aiheuta virhettä 100 euroa hävisi 24
Transaktio jatkuu.. Miten edellinen korjattaisi? Otamme autocommit-moodin pois Connection connection=yhdista() connection.setautocommit(false); // ota yhteys tietokantaan Valitsemme sopivan eristyvyystason (emme käytä kantaa yksin) READ UNCOMMITTED -- heikko READ COMMITTED -- sopii tiedon hakuun REPEATABLE READ -- sopii päivitykseen SERIALIZABLE -- vahvin, käytetään päivityksissä connection.settransactionisolation (Connection.TRANSACTION_SERIALIZABLE); 25
Transaktio jatkuu.. Kun transaktio on suoritettu, jos onnistui, hyväksymme transaktion connection.commit(); Jos epäonnistui, peruutamme transaktion connection.rollback(); Mistä tiedän onnistuiko tilinsiirto? SQL palauttaa päivitettyjen rivien lukumäärän Java PreparedStatement luokan metodi int executeupdate() palauttaa päivitettyjen/poistettujen rivin lukumäärän: 26
Transaktio jatkuu.. try{ Connection connection = yhdista(); connection.setautocommit(false); connection.settransactionisolation (Connection.TRANSACTION_SERIALIZABLE); String sql1 = UPDATE tili SET saldo = saldo-100 WHERE tilinro=? ; String sql2 = UPDATE tili SET saldo = saldo+100 WHERE tilinro=? ; PreparedStatement lause = connection.preparestatement(sql1); lause.setstring(1, 100012120 ); 27
Transaktio jatkuu.. int lkm = lause.executeupdate(); if (lkm == 1){ // ensimmäinen UPDATE onnistui, BINGO lause = connection.preparestatement(sql2); lause.setstring(1, 100000111 ); lkm = lause.executeupdate(); lause.close(); if (lkm == 1) { // myös toinen UPDATE onnistui, BINGO ok = true; connection.commit(); // hyväksy transaktio connection.close(); // sulje yhteys kantaan else{ ok = false; connection.rollback(); //peruuta transaktio connection.close(); // sulje yhteys kantaan 28
Transaktio jatkuu.. catch (SQLException e) { throw e; catch (Exception e){ finally { throw e; if (conn!= null && conn.isclosed() == false) { return ok; try { conn.rollback(); // peruuta transaktion conn.close(); // yhteys poikki catch (Exception e) { throw e; 29
Transaktio jatkuu Miten huomioin transaktion käsittelyn tiedon haussa tietokannasta? Miksi tiedon haku pitäisi olla transaktion alla? Tietokantaa käyttää muutkin käyttäjät, samanaikaisuuden hallintaa autetaan kun valitaan transaktion eristyvyystaso Samanaikaisuuden hallinta on sidoksissa tiedonhallintajärjestelmään, jotkut järjestelmät toimivat toisin kuin toiset (kannattaa testata ennen käyttöä) Autocommit-moodissa olevassa järjestelmässä on ns. oletus eristyvyystaso, joka yleensä on READ COMMITTED Yleensä tiedon haku toimii hyvin ilman transaktion hallintaa mutta on tilanteita, jossa tulosjoukko ei ole validi eli se ei sisällä juuri äsken päivitettyjä rivejä tai se sisältää rivejä, jotka äsken poistettiin 30