Prikaži sadržaj

Programiranje baza podataka

8. Osnovni koncepti programiranja Java/SQL aplikacija sa dinamičkim SQL naredbama (JDBC)

Ova stranica je pod konstrukcijom!
Ako pronađete grešku ili propust, molimo Vas da nam skrenete pažnju otvaranjem primedbe na zvaničnom GitHub repozitorijumu.

Java Database Connectivity (JDBC) predstavlja Java interfejs za programiranje aplikacija (API) koji standardizuje načine za pristup i upravljanje bazama podataka. JDBC kod je jednostavno portabilizan između nekoliko RSUBP proizvođača. Jedina promena u kodu koja se očekuje kada se JDBC kod portuje za neki drugi RSUBP u odnosu na onaj za koji je prvobitno napisan jeste koji JDBC drajver se učitava i niska za konekciju na bazu podataka. JDBC koristi isključivo dinamičko izvršavanje SQL naredbi i veoma je popularan u praksi.

Pre nego što se izvršavaju SQL naredbe u bilo kom JDBC programu, aplikacija se mora povezati na bazu podataka.

Iako postoji nekoliko tipova JDBC drajvera kao što su tipovi 1, 2, 3 i 4, tipovi 1 i 3 nisu često korišćeni, i Db2 podrška za ove tipove je uklonjena iz novijih verzija ovog SUBP. Za tip 2 postoje dva drajvera, kao što ćemo videti, ali jedan od njih je takođe zastareo, te bi ga trebalo izbegavati.

Kao što je vidljivo iz naredne tabele, tipovi 2 i 4 JDBC drajvera su podržani Db2 SUBP. Tip 2 drajver zahteva instalaciju Db2 klijenta, s obzirom da se on koristi kako bi drajver kreirao komunikacioni kanal ka bazi podataka. Sa druge strane, tip 4 je čist Java klijent, te nije potrebno instalirati Db2 klijent, ali sam drajver mora biti instaliran na mašini gde se JDBC aplikacija pokreče.

Tip drajvera Ime drajvera Datoteka Podržava Najmanja verzija Java SDK
Tip 2 DB2 JDBC Type 2 Driver for Linux, UNIX® and Windows (Deprecated) db2java.zip JDBC 1.2, JDBC 2.0 1.4.2
Tip 2, Tip 4 IBM Data Server Driver for JDBC and SQLJ db2jcc.jar i sqlj.zip JDBC 3.0 saglasni 1.4.2
Tip 2, Tip 4 IBM Data Server Driver for JDBC and SQLJ db2jcc4.jar i sqlj4.zip JDBC 4.0 i ranije 6

Kao što je pomenuto ranije, a takođe je prikazano u tabeli iznad, JDBC drajver tipa 2 je dostupan u dva različita drajvera. Ipak, Db2 JDBC Type 2 Driver for Linux, UNIX and Windows, čija je implementacija data datotekom db2java.zip je zastareo.

Kada se instalira Db2 server, Db2 klijent ili IBM Data Server Driver za JDBC i SQLJ, tj. datoteke db2jcc.jar i sqlj.zip koje su saglasne sa JDBC 3.0 automatski su dodate u “classpath”.

8.1 Kreiranje konekcije

Kolekcija ka bazi podataka se može dobiti korišćenjem klase DriverManager iz java.sql paketa.

Paket java.sql definiše klase i interfejse koji su neophodni kako bi JDBC program pristupio podacima koji su skladišteni u relacionim bazama podataka. Ovaj API se može koristiti za povezivanje na relacionu bazu podataka i upravljanje podacima (čitanje, unos, ažuriranje, brisanje i dr.) koji su skladišteni u formi tabela prema SQL standardu. Interfejsi koji su definisani u ovom paketu implementirani su u klasa odgovarajućih drajvera i njihova definicija se može razlikovati između proizvođaca.

Pre ostvarivanja konekcije, potrebno je učitati klase iz drajvera i registrovati ih za upotrebu korišćenjem klase DriverManager. Proizvoljan broj drajvera se može učitati u registrovati klasom DriverManager korišćenjem metoda forName:

class ProgramName {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        }
        catch (Exception e) {
            e.printStackTrace();

            System.exit(1);
        }
    }
    // ...
}

Java podržava poseban blok, nazvan statički blok (koji se takođe naziva statička klauzula) koji se može koristiti za statičke inicijalizacije klase. Ovaj kod unutar statičkog bloka se izvršava samo jednom: prvi put kada napravite objekat te klase ili prvi put kada pristupite statičkom članu te klase (čak i ako nikada ne napravite objekat te klase). Koristićemo statičke blokove u našim programima za učitavanje i registraciju DB2 JDBC drajvera, kao što je prikazano u gornjem primeru koda.

Metod forName kao argument očekuje nisku čija vrednost je naziv paketa koji implementira interfejse definisane paketom java.sql.

Konekcija ka bazi podataka se može ostvariti pozivom metoda getConnection klase DriverManager. Ovaj metod prihvata nisku (URL) kao ulaz, koji sadrži informacije potrebne za povezivanje sa bazom podataka. Metod vraca objekat interfejsa Connection, koji se može koristiti za upravljanje konekcijom ka bazi podataka. Tipičan format URL-a za drajver tipa 4 je:

jdbc:db2://<naziv servera>:<broj porta>/<ime baze podataka>

Konekcija ka bazi podataka se raskida pozivom metoda close nad objektom interfejsa Connection:

public static void main(String argv[]) {
    Connection con = null;
    String url = "jdbc:db2://localhost:50000/stud2020";
    
    // Povezivanje na bazu podataka
    con = DriverManager.getConnection(url, "student", "abcdef");

    // Kod programa...

    // Raskidanje konekcije sa bazom podataka
    con.close();
}

8.2 Obrada SQL grešaka

Kao što znamo iz ranijih poglavlja, naredbe za povezivanje i raskidanje konekcije (kao i druge SQL naredbe), mogu proizvesti SQL greške. U JDBC aplikacijama, kao i u svim drugim Java aplikacijama, obrada grešaka se vrši pomoću izuzetaka i try-catch blokovima. JDBC aplikacija ispaljuje objekat klase SQLException svaki put kada dođe do SQL greške ili SQLWarning svaki put kada dođe do SQL upozorenja prilikom izvršavanja SQL naredbi. Takođe, svi metodi JDBC API-ja koji mogu dovesti do SQL grešaka u svom potpisu sadrže klauzulu throws SQLException, što znači da je pozive ovakvih metoda neophodno obuhvatiti nekim try-catch blokom u programu.

Objekat klase SQLException sadrži razne korisne informacije o SQL grešci koja je podignuta u SUBP, a neke od njih su navedene u narednoj tabeli:

Informacija Opis Metod koji se koristi za dohvatanje informacije
Poruka Tekstualna reprezentacija SQL greške. getMessage
SQLSTATE SQLState niska. getSQLState
SQLCODE Celobrojna vrednost koja indikuje vrstu greške koja je podignuta u SUBP. getErrorCode

Pored navedenih informacija, Db2 JCC drajver pruža dodatni interfejs com.ibm.db2.jcc.DB2Diagnosable. Ovaj interfejs daje više informacija u vezi sa greškom koja se dogodila prilikom pristupanja Db2 bazi podataka.

Primer obrade SQLExpection izuzetaka dat je narednim kodom:

Connection con = null;

try {
    // Povezivanje na bazu podataka
    con = DriverManager.getConnection(url, "student", "abcdef");

    // Kod programa...

    // Raskidanje konekcije sa bazom podataka
    con.close();
}
catch (SQLException e) {
    e.printStackTrace();

    System.out.println(
        "SQLCODE: " + e.getErrorCode() + "\n" +
        "SQLSTATE: " + e.getSQLState() + "\n" +
        "PORUKA: " + e.getMessage());

    // Treba da zatvorimo konekciju ka bazi podataka i u slučaju greške.
    // S obzirom da metod close takođe može ispaliti SQLException,
    // potrebno ga je uhvatiti, ali biramo da ga ne obrađujemo dalje.
    try {
        if (null != con) {
            con.close();
        }
    } catch (SQLException e2) {
    }

    System.exit(2);
}
catch (Exception e) {
    e.printStackTrace();

    // Isto kao u catch bloku iznad.
    try {
        if (null != con) {
            con.close();
        }
    } catch (SQLException e2) {
    }

    System.exit(3);
}

S obzirom da objekti interfejsa Connection implementiraju interfejs AutoCloseable, možemo koristiti i try-with-resources naredbu u okviru koje ćemo otvoriti konekciju. Naredba se stara o otvorenim resursima i na kraju ih automatski zatvara.

public static void main(String argv[]) {
    String url = "jdbc:db2://localhost:50000/stud2020";

    try (
        Connection con = DriverManager.getConnection(url, "student", "abcdef");
    ) {
        // Kod programa...
    } catch (SQLException e) {
        e.printStackTrace();

        System.out.println(
            "SQLCODE: " + e.getErrorCode() + "\n" +
            "SQLSTATE: " + e.getSQLState() + "\n" +
            "PORUKA: " + e.getMessage());

        System.exit(2);
    }
    catch (Exception e) {
        e.printStackTrace();

        System.exit(3);
    }
}

U slučaju da se više izuzetaka tipa SQLExceptions ispali, oni su ulančani. Informacija o narednom izuzetku se može dobiti pozivom metoda getNextException nad trenutnim SQLException objektom koji se obrađuje. Ovaj metod vraća null ako je tekući SQLException objekat poslednji u lancu izuzetaka. Zbog toga se može koristiti while petlja u catch bloku programa kako bi se obradili SQLException objekti jedan-po-jedan.

8.3 Upravljanje podacima

Nakon ostvarivanja konekcije ka bazi podataka, podaci se mogu dohvatiti, unositi, ažurirati ili obrisati iz relacionih tabela korišćenjem SQL naredbi. JDBC drajver implementira dva interfejsa, Statement i PreparedStatement za ove potrebe. Objekti jednog od ova dva interfejsa su neophodni kako bi se SQL naredbe izvršile nad bazom podataka. U nastavku govorimo o slučajevima upotrebe ovih interfejsa, ali pre toga ćemo reći nešto o tipovima podataka.

8.3.1 Tipovi podataka

Da bismo napisali efikasne JDBC programe, potrebno je da koristimo najbolja preslikavanja između Java tipova podataka i tipova kolona tabela.

Naredna tabela sumira mapiranja Db2 tipova podataka u Java tipove podataka za familiju metoda ResultSet.getXXX u JDBC programima. Ova tabela ne navodi tipove Java numeričkih omotača, koji se dohvataju pomoću metoda ResultSet.getObject.

SQL tip podataka Preporučeni Java tip podataka Drugi podržani Java tipovi podataka
SMALLINT short byte, int, long, float, double, java.math.BigDecimal, boolean, java.lang.String
INTEGER int short, byte, long, float, double, java.math.BigDecimal, boolean, java.lang.String
BIGINT long int, short, byte, float, double, java.math.BigDecimal, boolean, java.lang.String
DECIMAL(p,s) or NUMERIC(p,s) java.math.BigDecimal long, int, short, byte, float, double, boolean, java.lang.String
DECFLOAT(n) java.math.BigDecimal long, int, short, byte, float, double, java.math.BigDecimal, boolean, java.lang.String
REAL float long, int, short, byte, double, java.math.BigDecimal, boolean, java.lang.String
DOUBLE double long, int, short, byte, float, java.math.BigDecimal, boolean, java.lang.String
CHAR(n) java.lang.String long, int, short, byte, float, double, java.math.BigDecimal, boolean, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.io.InputStream, java.io.Reader
VARCHAR(n) java.lang.String long, int, short, byte, float, double, java.math.BigDecimal, boolean, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.io.InputStream, java.io.Reader
DATE java.sql.Date java.sql.String, java.sql.Timestamp
TIME java.sql.Time java.sql.String, java.sql.Timestamp
TIMESTAMP, TIMESTAMP(p), TIMESTAMP WITH TIME ZONE, TIMESTAMP(p) WITH TIME ZONE java.sql.Timestamp java.sql.String, java.sql.Date, java.sql.Time, java.sql.Timestamp

Naredna tabela sumira preslikavanja Java tipova podataka u Db2 tipove podataka za familije metoda PreparedStatement.setXXX i ResultSet.updateXXX u JDBC programima. Kada je navedeno više od jednog Java tipa podataka, prvi tip podataka je preporučeni tip podataka.

Java tip podataka Db2 tip podataka
short, java.lang.Short SMALLINT
boolean, byte, java.lang.Boolean, java.lang.Byte SMALLINT
int, java.lang.Integer INTEGER
long, java.lang.Long BIGINT
java.math.BigInteger BIGINT
java.math.BigInteger CHAR(n)
float, java.lang.Float REAL
double, java.lang.Double DOUBLE
java.math.BigDecimal DECIMAL(p,s)
java.math.BigDecimal DECFLOAT(n)
java.lang.String CHAR(n)
java.lang.String VARCHAR(n)
java.sql.Date DATE
java.sql.Time TIME
java.sql.Timestamp TIMESTAMP, TIMESTAMP(p), TIMESTAMP WITH TIME ZONE, TIMESTAMP(p) WITH TIME ZONE
java.util.Date CHAR(n)
java.util.Date VARCHAR(n)
java.util.Date DATE
java.util.Date TIME
java.util.Date TIMESTAMP, TIMESTAMP(p), TIMESTAMP WITH TIME ZONE, TIMESTAMP(p) WITH TIME ZONE

8.3.2 Interfejs Statement

Objekti interfejsa Statement mogu se iskoristiti za izvršavanje SQL naredbi koji ne sadrže parametarske oznake. Ovi objekti se kreiraju pozivom metoda createStatement nad objektima interfejsa Connection. Moguće je kreirati proizvoljan broj objekata Statement nad jednim objektom Connection i svi ti objekti će biti vezani za konekciju koja je opisana istim objektom Connection (drugim rečima, sve te SQL naredbe će biti izvršene nad istom bazom podataka).

Interfejs Statement definiše dve vrste metoda za izvršavanje SQL naredbi. Oba metoda kao prvi parametar očekuju nisku koja sadrži validnu SQL naredbu. U zavisnosti od vrste SQL naredbe, koriste se naredni metodi:

U nastavku dajemo dve procedure koje opisuju upotrebu opisanih metoda.

Procedura: Obrada kursora u JDBC aplikacijama (slučaj bez parametarskih oznaka)

Da bismo iterirali kroz rezultujuću tabelu naredbe SELECT koja ne sadrži parametarske oznake, potrebno je da ispratimo naredne korake:

  1. Napisati SQL naredbu kao nisku.
    String sql = "SELECT ...";
    
  2. Kreirati objekat interfejsa Statement pozivom metoda Connection.createStatement().
    Statement stmt = con.createStatement();
    
  3. Kreirati objekat interfejsa ResultSet pozivom metoda Statement.executeQuery(String sql).
    ResultSet kursor = stmt.executeQuery(sql);
    
  4. Pozicionirati se na red koji je potrebno pročitati metodima za prolazak kroz kursor. Na primer:
    while (kursor.next()) {
     // ...
    }
    
  5. Pozvati odgovarajući metod nad objektom interfejsa ResultSet iz familije metoda ResultSet.getXXX(int columnIndex) za dohvatanje vrednosti iz kolone rezultujuće tabele sa indeksom columnIndex. Na primer:
    while (kursor.next()) {
     int indeks = kursor.getInt(1);
     String ime = kursor.getString(2);
     // ...
    }
    
    1. Eventualno proveriti da li je dohvaćena NULL vrednost (videti sekciju o nedostajućim vrednostima).
  6. Nakon svih iteracija, pozvati metod kursor.close() radi zatvaranja kursora.
  7. Nakon zatvaranja kursora, pozvati metod stmt.close() radi zatvaranja objekta naredbe.

Procedura: Obrada ažurirajućih SQL naredbi u JDBC aplikacijama (slučaj bez parametarskih oznaka)

Da bismo izmenili podatke u bazi podataka nekom SQL naredbom bez parametarskih oznaka, potrebno je da ispratimo naredne korake:

  1. Napisati SQL naredbu kao nisku.
    String sql = "DELETE ...";
    
  2. Kreirati objekat interfejsa Statement pozivom metoda Connection.createStatement().
    Statement stmt = con.createStatement();
    
  3. Pozvati metod stmt.executeUpdate(String sql).
    int brojRedova = stmt.executeUpdate(sql);
    
  4. Pozvati metod stmt.close() radi zatvaranja objekta naredbe.

Naredni zadaci ilustruju opisane procedure na konkretnim primerima.

Zadatak 8.1: Napisati Java program u kojem se naredbe izvršavaju dinamički koji izlistava oznake i nazive svih predmeta koji imaju više od 20 ESPB bodova.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_1/Main.java:

package zadatak_8_1;

import java.sql.*;

public class Main {
    // Staticki blok koji sluzi za ucitavanje JDBC DB2 drajvera.
    // Izvrsava se kada se prvi put instancira objekat ove klase,
    // ili kada se prvi put pozove staticki metod.
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        // URL za JDBC konekciju tipa 4
        String url = "jdbc:db2://localhost:50000/stud2020";

        // Objekat koji ce sadrzati konekciju
        // Kreiramo konekciju na bazi podataka zadatoj u promenljivoj url,
        // koriscenjem metoda DriverManager.getConnection.
        // Argumenti za korisnicko ime i lozinku su obavezni!
        // try-with-resources vodi racuna o otvorenim resursima
        // i zatvara ih na kraju try-catch bloka
        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {
            // Kreiramo objekat naredbe (Statement)
            Statement stmt = con.createStatement();

            // SQL upit ili naredbu koju zelimo da izvrsimo
            // zapisujemo kao nisku <-> DINAMICKI SQL
            String queryStr = 
                "SELECT OZNAKA, " + 
                "       NAZIV " + 
                "FROM   DA.PREDMET " + 
                "WHERE  ESPB > 20";
            // Za izvrsavanje upita koristimo Statement.executeQuery metod.
            // Ovaj metod vraca objekat klase ResultSet,
            // koji sadrzi rezultate upita (kursor).
            ResultSet rs = stmt.executeQuery(queryStr);

            // Ispisujemo zaglavlje
            System.out.printf("%-15s %-50S\n\n", "OZNAKA", "NAZIV");

            // Metodom ResultSet.next pozicioniramo kursor na naredni red.
            // Ako vise nema redova, metod ce vratiti vrednost false.
            // Inace, vratice true, pa mozemo da ga koristimo kao uslov u petlji
            while (rs.next()) {
                // Izdvajamo podatke koriscenjem familije metoda
                // ResultSet.getXXX,
                // gde je XXX neki tip podataka,
                // a argument je broj kolone koja se dohvata iz projekcije
                // upita.
                String oznaka = rs.getString(1);
                String naziv = rs.getString(2);

                System.out.printf("%-15s %-50S\n", oznaka.trim(), naziv.trim());
            }

            // Zatvaramo kursor
            rs.close();
            // Zatvaramo naredbu
            stmt.close();
        }
        // Obrada SQL gresaka
        catch (SQLException e) {
            // Ispisujemo sve informacije na standardni izlaz
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            // Signaliziramo neuspesan zavrsetak programa
            System.exit(1);
        }
        // Obrada drugih gresaka
        catch (Exception e) {
            e.printStackTrace();
            
            // Signaliziramo neuspesan zavrsetak programa
            System.exit(2);
        }
    }
}

Zadatak 8.2: Napisati Java program u kojem se naredbe izvršavaju dinamički koji u tabelu PREDMET unosi podatak o predmetu čiji je identifikator 2001, oznaka Pred1, naziv Predmet 1 i nosi 5 ESPB bodova.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_2/Main.java:

package zadatak_8_2;

import java.sql.*;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";

        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {
            // Zelimo da izvrsimo INSERT naredbu
            // u kojoj su sve informacije poznate.
            // To znaci da mozemo koristiti kombinaciju
            // interfejsa Statement i metoda executeUpdate().
            String sql = 
                "INSERT INTO DA.PREDMET " +
                "VALUES (2001, 'Pred1', 'Predmet 1', 5)";

            Statement stmt = con.createStatement();

            System.out.println("Unosim podatke u tabelu PREDMET...");

            // Metod executeUpdate() se koristi ne samo za azuriranje,
            // vec i za unos, brisanje, i slicne naredbe.
            int insertCount = stmt.executeUpdate(sql);

            System.out.println("Broj unetih redova: " + insertCount);

            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }
}

8.3.3 Interfejs PreparedStatement

Objekti interfejsa PreparedStatement obavezno se koriste u slučaju kada SQL naredbe sadrže parametarske oznake. PreparedStatement objekti se kreiraju pozivom metoda prepareStatement nad objektima Connection. Za razliku od metoda createStatement, metod prepareStatement očekuje jedan parametar, a to je niska koja predstavlja SQL naredbu sa parametarskim oznakama, za koju će biti izvršena priprema. Pun potpis ovog metoda je:

PreparedStatement prepareStatement(String sql) throws SQLException

Postavljanje vrednosti parametarskim oznakama se vrši pozivom nekog metoda iz familije PreparedStatement.setXXX, gde XXX označava tip parametarske oznake koja se postavlja. Neki od primera setXXX metoda su:

Nakon postavljanja vrednosti parametara, SQL naredba se može izvršiti pozivom nekog od već opisanih metoda executeQuery ili executeUpdate, zavisno od vrste SQL naredbe. Za razliku od istoimenih metoda interfejsa Statement, ovi metodi se pozivaju bez prosleđivanja SQL naredbe (s obzirom da smo naredbu već prosledili prilikom konstrukcije PreparedStatement objekta).

Naredne procedure opisuju korake neophodne za korišćenje interfejsa PreparedStatement.

Procedura: Obrada kursora u JDBC aplikacijama (slučaj sa parametarskim oznakama)

Da bismo iterirali kroz rezultujuću tabelu naredbe SELECT koja sadrži parametarske oznake, potrebno je da ispratimo naredne korake:

  1. Napisati SQL naredbu kao nisku.
    String sql = "SELECT ... WHERE INDEKS = ? AND IME = ? ...";
    
  2. Kreirati objekat interfejsa PreparedStatement pozivom metoda Connection.prepareStatement(String sql).
    PreparedStatement stmt = con.prepareStatement(sql);
    
  3. Pozvati odgovarajući metod iz familije metoda PreparedStatement.setXXX(int parameterIndex, XXX x) za postavljanje vrednosti parametarske oznake u naredbi. Na primer:
    stmt.setInt(1, 20200134);
    stmt.setString(2, "Ivana");
    
  4. Kreirati objekat interfejsa ResultSet pozivom metoda PreparedStatement.executeQuery().
    ResultSet kursor = stmt.executeQuery();
    
  5. Pozicionirati se na red koji je potrebno pročitati metodima za prolazak kroz kursor. Na primer:
    while (kursor.next()) {
     // ...
    }
    
  6. Pozvati odgovarajući metod iz familije metoda ResultSet.getXXX(int columnIndex) za dohvatanje vrednosti iz kolone rezultujuće tabele sa indeksom columnIndex. Na primer:
    while (kursor.next()) {
     int indeks = kursor.getInt(1);
     String ime = kursor.getString(2);
     // ...
    }
    
    1. Eventualno proveriti da li je dohvaćena NULL vrednost (videti sekciju o nedostajućim vrednostima).
  7. Nakon svih iteracija, pozvati metod kursor.close() radi zatvaranja kursora.
  8. Nakon zatvaranja kursora, pozvati metod stmt.close() radi zatvaranja objekta naredbe.

Procedura: Obrada ažurirajućih SQL naredbi u JDBC aplikacijama (slučaj sa parametarskim oznakama)

Da bismo izmenili podatke u bazi podataka nekom SQL naredbom sa parametarskim oznakama, potrebno je da ispratimo naredne korake:

  1. Napisati SQL naredbu kao nisku.
    String sql = "DELETE ... WHERE INDEKS = ? ...";
    
  2. Kreirati objekat interfejsa PreparedStatement pozivom metoda Connection.prepareStatement(String sql).
    PreparedStatement stmt = con.prepareStatement(sql);
    
  3. Pozvati odgovarajući metod iz familije metoda PreparedStatement.setXXX(int parameterIndex, XXX x) za postavljanje vrednosti parametarske oznake u naredbi. Na primer:
    stmt.setInt(1, 20200134);
    
  4. Pozvati metod stmt.executeUpdate().
    int brojRedova = stmt.executeUpdate();
    
  5. Pozvati metod stmt.close() radi zatvaranja objekta naredbe.

Naredni zadaci demonstriraju upotrebu interfejsa PreparedStatement.

Zadatak 8.3: Napisati Java program u kojem se naredbe izvršavaju dinamički koji učitava dva cela broja, X i Y, a zatim svim predmetima koji imaju X ESPB bodova, postavlja broj bodova na Y. Nakon toga ispisati broj ažuriranih redova.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_3/Main.java:

package zadatak_8_3;

import java.sql.*;
import java.util.Scanner;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";

        try (
                Connection con = DriverManager.getConnection(url, "student", "abcdef");
                Scanner ulaz = new Scanner(System.in);
            ) {
            // JDBC koristi iskljucivo dinamicke SQL naredbe,
            // tako da se u JDBC ne koriste maticne promenljive.
            // S obzirom da su vrednosti za bodove u klauzama SET i WHERE
            // nepoznate do faze izvrsavanja,
            // potrebno je da koristimo parametarske oznake na tim mestima.
            String updateStr = 
                "UPDATE DA.PREDMET " + 
                "SET    ESPB = ? " + 
                "WHERE  ESPB = ?";
            // Zbog toga moramo da koristimo interfejs PreparedStatement,
            // jer interfejs Statement ne radi sa parametarskim oznakama.
            PreparedStatement pUpd = con.prepareStatement(updateStr);

            // Postavljamo odgovarajuce vrednosti za parametarske oznake
            // na osnovu procitanih vrednosti sa standardnog ulaza.
            int x, y;
            System.out.println("Unesite X:");
            x = ulaz.nextInt();
            System.out.println("Unesite Y:");
            y = ulaz.nextInt();
            
            // Prvu parametarsku oznaku menjamo celim brojem y.
            pUpd.setInt(1, y);

            // Drugu parametarsku oznaku menjamo celim brojem x.
            pUpd.setInt(2, x);

            // Izvrsavamo naredbu metodom executeUpdate() 
            // koji vraca broj azuriranih redova.
            int numRows = pUpd.executeUpdate();
            System.out.println("Broj azuriranih redova: " + numRows);

            pUpd.close();
        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }
}

Zadatak 8.4: Napisati Java program u kojem se naredbe izvršavaju dinamički koji sa standardnog ulaza učitava ceo broj N i izdvaja indeks, ime, prezime i naziv studijskog programa svih studenata koji su položili tačno N predmeta, kao i spisak tih predmeta (naziv i ocena).

Rešenje: S obzirom da je SQL upit koji se koristi u ovom zadatku nešto složeniji, da ga ne bismo zapisivali u Java kodu kao nisku, taj upit ćemo začuvati u datoteci upit.sql u istom direktorijumu (paketu) kao i Main.java datoteku koja sadrži rešenje ovog zadatka. Dodatno, u Java kodu ćemo pronaći ovu datoteku na sistemu datoteka i učitati njen sadržaj kao nisku da bismo je zatim izvršili. Posebno treba obratiti pažnju na parametarsku oznaku u samom upitu, koja će biti zamenjena odgovarajućom vrednošću u fazi izvršavanja programa.

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_4/upit.sql:

WITH BROJ_POLOZENIH_ISPITA AS (
    SELECT      INDEKS
    FROM        DA.ISPIT 
    WHERE       OCENA > 5 AND 
                STATUS = 'o'
    GROUP BY    INDEKS
    HAVING      COUNT(*) = ?
)
SELECT      D.INDEKS, 
            IME, 
            PREZIME, 
            S.NAZIV, 
            P.NAZIV, 
            I.OCENA
FROM        DA.DOSIJE D JOIN 
            BROJ_POLOZENIH_ISPITA BPI ON D.INDEKS = BPI.INDEKS JOIN
            DA.STUDIJSKIPROGRAM S ON D.IDPROGRAMA = S.ID JOIN
            DA.ISPIT I ON D.INDEKS = I.INDEKS JOIN
            DA.PREDMET P ON I.IDPREDMETA = P.ID
WHERE       I.OCENA > 5 AND
            I.STATUS = 'o'
ORDER BY    D.INDEKS,
            IME,
            PREZIME,
            S.NAZIV,
            P.NAZIV

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_4/Main.java:

package zadatak_8_4;

import java.io.File;
import java.io.FileNotFoundException;
import java.sql.*;
import java.util.Scanner;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";

        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {
            
            ispitiStudenteIIspite(con);
            
        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }

    private static void ispitiStudenteIIspite(Connection con) throws FileNotFoundException, SQLException {
        String sql = ucitajSqlIzDatoteke();
        int brojPredmeta = ucitajBrojPredmeta();

        PreparedStatement pstmt = con.prepareStatement(sql.toString());
        pstmt.setInt(1, brojPredmeta);

        ResultSet result = pstmt.executeQuery();
        int brojRedova = 0;

        int indeks = 0;
        
        while (result.next()) {
            // Ispisujemo informacije o studentu
            // samo kad naidjemo na novog studenta
            if (indeks != result.getInt(1)) {
                if (indeks != 0) {
                    System.out.print("\n\n\n");
                }

                System.out.println("Indeks: " + result.getInt(1) + "\n" + "Ime: " + result.getString(2).trim()
                        + "\n" + "Prezime: " + result.getString(3).trim() + "\n" + "Studijski program: "
                        + result.getString(4).trim() + "\n");

                brojRedova = 1;
            }

            // Ispisujemo informacije o i-tom predmetu
            System.out.println(brojRedova + ". predmet: " + result.getString(5).trim() + "\n\t" + "ocena: "
                    + result.getInt(6));

            // Uvecavamo broj reda
            ++brojRedova;
            // Pamtimo tekuci indeks za narednu iteraciju
            indeks = result.getInt(1);
        }

        pstmt.close();
    }

    private static int ucitajBrojPredmeta() {
        int brojPredmeta;
        
        System.out.println("Unesite broj predmeta:");
        try (Scanner ulaz = new Scanner(System.in)) {
            brojPredmeta = ulaz.nextInt();
        }
        return brojPredmeta;
    }

    private static String ucitajSqlIzDatoteke() throws FileNotFoundException {
        // Eclipse aplikacije se pokrecu iz direktorijuma projekta,
        // dakle, u podrazumevanom slucaju,
        // ~/IBM/rationalsdp/workspace/poglavlje_8/.
        // Mozemo koristiti sistemsko svojstvo
        // System.getProperty("user.dir")
        // da dohvatimo putanju u kojoj se pokrece aplikacija.
        // Takodje, prilikom kompiliranja .java datoteka u .class datoteke,
        // Eclipse alat ce kopirati sve druge datoteke iz src/ direktorijuma u bin/ direktorijum.
        String putanja = "./bin/zadatak_8_4/upit.sql";
        StringBuilder sql = new StringBuilder("");
        String linija = null;
        
        try (Scanner skenerFajla = new Scanner(new File(putanja), "utf-8")) {
            while (skenerFajla.hasNextLine()) {
                linija = skenerFajla.nextLine();
                sql.append(linija);
                sql.append("\n");
            }
        }

        return sql.toString();
    }
}

8.3.4 Rukovanje nedostajućim vrednostima

Što se tiče rada sa podacima koji su potencijalno nedostajući, na raspolaganju nam je metod ResultSet.wasNull, koji je potrebno pozvati odmah nakon pozivanja metoda dohvatača za neku kolonu. Ukoliko je vrednost za tu kolonu bila NULL, onda će metod wasNull vratiti vrednost true, a inače će vratiti false. Napomenimo da se poziv wasNull metoda vezuje samo za poslednji poziv metoda getXXX, te samim tim i na poslednju dohvaćenu kolonu. Zbog toga je potrebno više puta pozivati ovaj metod ukoliko ima više potencijalno nedostajućih kolona:

ResultSet res = ...

while (res.next()) {
    // Vrednosti u ovoj koloni ne mogu biti NULL,
    // pa nemamo ni proveru za nju.
    int kolona1_notnull = res.getInt(1);

    // Vrednosti u ovoj koloni mogu biti NULL,
    // pa zato odmah nakon dohvatanja vrednosti,
    // pozivamo metod wasNull() da proverimo da li je NULL.
    int kolona2_nullable = res.getInt(2);
    boolean kol2IsNull = res.wasNull();

    // Vrednosti u ovoj koloni mogu biti NULL,
    // pa zato odmah nakon dohvatanja vrednosti,
    // pozivamo metod wasNull() da proverimo da li je NULL.
    // Ovaj poziv metoda wasNull() se odnosi na kolonu 3, a ne na 2.
    Date kolona3_nullable = res.getDate(3);
    boolean kol3IsNull = res.wasNull();
    
    ...
}

Zadatak 8.5: Napisati Java program u kojem se naredbe izvršavaju dinamički koji izdvaja ime, prezime i datum diplomiranja za sve studentkinje (pol = ‘z’) programa čiji je identifikator 202 iz tabele DOSIJE. Ukoliko datum diplomiranja nije poznat, ispisati Nije diplomirala.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_5/Main.java:

package zadatak_8_5;

import java.sql.*;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";

        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {
            
            Statement stmt = con.createStatement();
            String queryStr = 
                "SELECT IME, " + 
                "       PREZIME, " + 
                "       DATDIPLOMIRANJA " + 
                "FROM   DA.DOSIJE " + 
                "WHERE  POL = 'z' AND " + 
                "       IDPROGRAMA=202";
            ResultSet rs = stmt.executeQuery(queryStr);

            System.out.printf("%-25s %-25s %-15s \n\n", "IME", "PREZIME", "DATUM DIPLOMIRANJA");

            while (rs.next()) {
                String ime = rs.getString(1).trim();
                String prezime = rs.getString(2).trim();
                String datum = rs.getString(3);
                // Ukoliko je u tekucem redu kursora datum bio NULL,
                // onda ce rs.wasNull() biti true
                boolean datumIsNull = rs.wasNull();

                System.out.printf("%-25s %-25s %-15s \n", ime, prezime, (datumIsNull) ? "Nije diplomirala" : datum.trim());
            }

            rs.close();
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }
}

8.3.5 Podešavanje kursora

Objekat interfejsa ResultSet održava kursor koji pokazuje na red u rezultujućoj tabeli. Moguće je pomerati kursor na specifičan red kako bismo dohvatili podatke o tom redu ili izmenili vrednosti kolona. Kursor može da pokazuje na tačno jedan red u nekom trenutku. Red na koji kursor pokazuje se naziva tekući red kursora. U zavisnosti od podešavanja kursora, moguće je pomerati kursor na različite načine.

Pre nego što se upustimo u konkretne primere, važno je da prodiskutujemo o narednim svojstvima svakog kursora:

Usmerenost kursora

Usmerenost definiše operacije kojima kursor prolazi kroz redove. Podrazumevano, kursor je usmeren samo unapred, odnosno, kursorom je moguće prolaziti samo od prvog do poslednjeg reda. Kada dođemo do poslednjeg reda, onda se ne možemo vratiti unazad, već je potrebno ponovo da otvorimo kursor. Ipak, moguće je definisati kursor koji je usmeren unapred i unazad, odnosno, moguće je kretati se proizvoljno kroz redove rezultujuće tabele. Ovakav kursor se često naziva dvosmerni kursor.

Dvosmerni kursor ima još jedno svojstvo koje se naziva osetljivost na izmene (eng. update sensitivity). Ovim svojstvom se definiše da li će se izmene u bazi podataka odraziti na redove rezultujuće tabele kroz koji se prolazi (otvorenim) dvosmernim kursorom. Kursor koji je osetljiv na izmene će prikazati izmene napravljene u bazi podataka, dok kursor koji nije osetljiv na izmene neće prikazati takve izmene. Naredne tri konstante interfejsa ResultSet se koriste za definisanje usmerenosti kursora:

Tipiziranost kursora

Tipiziranost se odnosi na sposobnost kursora da ažurira podatke kroz koje prolazi. Podrazumevano, kursor koji se napravi je ograničen samo za čitanje i ne dozvoljava nam da ažuriramo redove rezultujuće tabele. Ako želimo da koristimo kursor da ažuriramo podatke nad bazom podataka, potrebno je da upit koji je pridružen kursoru vrati skup podataka koji se može ažurirati od strane JDBC drajvera. Naredne dve konstante interfejsa ResultSet se koriste za definisanje tipiziranosti kursora.

Važno je napomenuti da, ako pokušamo da napravimo kursor koji je ažurirajući, ali nije osetljiv na izmene (tj. koristimo kombinaciju konstanti ResultSet.TYPE_SCROLL_INSENSITIVE i ResultSet.CONCUR_UPDATABLE), JDBC drajver će implicitno ograničiti takav kursor samo za čitanje, tako da neće podržavati operacije unosa, brisanja i izmene. Pokušaj da se ovakve operacije izvrše nad takvim kursorom rezultovaće ispaljivanjem SQLException izuzetka.

Zadrživost kursora

Zadrživost kursora se odnosi na stanje kursora nakon potvrđivanja izmena transakcije u kojoj je taj kursor otvoren. Kursor se može ili zatvoriti ili otvoriti u trenutku potvrđivanja izmena transakcije. Podrazumevano ponašanje zavisi od JDBC drajvera i može se proveriti pozivom metoda getHoldability() nad objektom interfejsa Connection. Metod vraća jednu od naredne dve konstante, koje se takođe mogu koristiti i za eksplicitno definisanje zadrživosti na nivou pojedinačnih kursora:

Definisanje svojstava kursora

Da bismo definisali ponašanje ResultSet objekta, potrebno je da prosledimo odgovarajuće opcije metodima createStatement, odnosno, prepareStatement, koji su definisani nad objektom interfejsa Connection, u zavisnosti od toga da li želimo da naredbu izvršimo kroz interfejse Statement ili PreparedStatement, redom.

U slučaju kreiranja objekta Statement, na raspolaganju su nam naredna dva preopterećenja metoda createStatement:

U slučaju kreiranja objekta PreparedStatement, na raspolaganju su nam naredna dva preopterećenja metoda prepareStatement, sa istim značenjima parametara kao i u slučaju metoda createStatement:

Zadatak 8.6: Napisati Java program u kojem se naredbe izvršavaju dinamički koji ispisuje podatke o ispitnim rokovima koristeći kursor kome je omogućeno kretanje i unazad kroz podatke. Podatke urediti po nazivu rastuće, ali ih ispisivati opadajuće.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_6/Main.java:

package zadatak_8_6;

import java.sql.*;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";

        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {
            
            String sql = 
                "SELECT     * " + 
                "FROM       DA.ISPITNIROK " + 
                "ORDER BY   NAZIV";
            Statement stmt = con.createStatement(
                // Podesavamo da je kursor bidirekcioni i nesenzitivni,
                // sto znaci da moze da se krece kroz njega u oba smera,
                // i da izmene u bazi podataka nece biti vidljive tokom
                // prolazenja kroz kursora
                ResultSet.TYPE_SCROLL_INSENSITIVE,
                // Podesavamo da se kursor koristi samo za citanje
                ResultSet.CONCUR_READ_ONLY);
            ResultSet res = stmt.executeQuery(sql);

            System.out.printf("%-10s %-15s %-20s %-20s %-20s\n\n", 
                "GODINA", "OZNAKA", "NAZIV", "DATUM POCETKA", "DATUM KRAJA");

            // Pozicioniranje na kraj kursora
            res.afterLast();

            // Citanje unazad
            while (res.previous()) {
                int godina = res.getInt(1);
                String oznaka = res.getString(2).trim();
                String naziv = res.getString(3).trim();
                Date datumPocetka = res.getDate(4);
                Date datumKraja = res.getDate(5);

                System.out.printf("%-10d %-15s %-20s %-20s %-20s\n", 
                    godina, oznaka, naziv, datumPocetka.toString(), datumKraja.toString());
            }

            res.close();
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }
}

8.3.6 Ažuriranje redova korišćenjem ResultSet kursora

Postoje dve procedure za ažuriranje informacija u bazi podataka na osnovu ResultSet kursora:

Kao što znamo, možemo koristiti pozicionirajuću UPDATE naredbu za menjanje podataka u bazi podataka, na osnovu tekućeg reda kursora, tako što se na kursor referiše u WHERE CURRENT OF klauzi naredbe. Da bismo dohvatili naziv ResultSet kursora, možemo iskoristiti metod ResultSet.getCursorName, koji vraća nisku sa nazivom kursora koji je vezan za taj ResultSet. Na primer, naredni fragment koda ilustruje kako možemo ažurirati broj bodova za sve predmete iz tabele PREDMET korišćenjem pozicionirajuće UPDATE naredbe:

String upit =
    "SELECT OZNAKA, " +
    "       NAZIV, " + 
    "       ESPB " +
    "FROM   DA.PREDMET " +
    "FOR    UPDATE OF ESPB";

Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(upit);

String nazivKursora = rs.getCursorName();
String azuriranje =
    "UPDATE DA.PREDMET " +
    "SET    ESPB = ? " +
    "WHERE  CURRENT OF " + nazivKursora;
PreparedStatement ps = con.prepareStatement(nazivKursora);

try (Scanner scanner = new Scanner(System.in)) {
    while (rs.next()) {
        System.out.println("Unesite nove ESPB bodove za predmet " + rs.getString(2));
        int newESPB = scanner.nextInt();

        ps.setInt(1, newESPB);
        ps.executeUpdate();
    }
}

Drugi pristup podrazumeva korišćenje specifičnih metoda radi ažuriranja podataka u kursoru, čime se efektivno vrši ažuriranje u bazi podataka.

Procedura: Ažuriranje slogova pomoću ažurirajućih kursora

Naredni koraci opisuju postupak ažuriranja postojećih slogova u ResultSet:

  1. Kreirati objekat interfejsa Statement pozivom metoda
    Statement stmt = con.createStatement(..., ResultSet.CONCUR_UPDATABLE, ...);
    
  2. Kreirati objekat interfejsa ResultSet pozivom metoda
    ResultSet kursor = stmt.executeQuery(sql);
    
  3. Pozicionirati se na slog koji je potrebno ažurirati metodima za prolazak kroz kursor. Na primer:
    while (kursor.next()) {
     kursor.absolute(5); // Samo ako kursor nije tipa TYPE_FORWARD_ONLY
     // ...
    }
    
  4. Pozvati odgovarajući metod iz familije metoda ResultSet.updateXXX(int columnIndex, XXX x) za ažuriranje vrednosti kolone tabele sa indeksom columnIndex novom vrednošću x.
    kursor.updateInt(1, 100);
    
    1. Ako želimo da poništimo izmene, možemo pozvati metod kursor.cancelRowUpdates()
    2. Ukoliko želimo da zapravo izvršimo izmene nad slogom u BP, potrebno je da pozovemo metod ResultSet.updateRow(). U suprotnom će, prelaskom na drugi red, sve izmene biti ignorisane.
      kursor.updateRow();
      
  5. Nakon svih iteracija, zatvoriti kursor.
    kursor.close();
    
  6. Nakon zatvaranja kursora, zatvoriti naredbu.
    stmt.close();
    

Pogledajmo primer upotrebe u narednom zadatku.

Zadatak 8.7: Napisati Java program u kojem se naredbe izvršavaju dinamički koji ispisuje sadržaj tabele PREDMET i, u istoj iteraciji, ukoliko je broj bodova jednak X, postavlja se broj bodova na Y i ispisuje se poruka da je promena izvršena, zajedno sa ispisom novih podataka o tom predmetu. Brojevi X i Y se učitavaju sa standardnog ulaza.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_7/Main.java:

package zadatak_8_7;

import java.sql.*;
import java.util.Scanner;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";

        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {

            int[] ucitaniESPB = ucitajStareINoveESPB();
            ispisiIAzurirajPredmete(con, ucitaniESPB[0], ucitaniESPB[1]);

        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }

    private static void ispisiIAzurirajPredmete(Connection con, int X, int Y) throws SQLException {
        String sql = 
            "SELECT * " + 
            "FROM   DA.PREDMET";
        Statement stmt = con.createStatement( 
                // Dovoljan nam je kursor koji prolazi unapred kroz redove.
                ResultSet.TYPE_FORWARD_ONLY,
                // Definisemo da je kursor azurirajuci,
                // pa su nam dostupni metodi
                // ResultSet.updateXXX i ResultSet.updateRow
                // koje cemo koristiti za azuriranje tekuceg reda
                ResultSet.CONCUR_UPDATABLE);
        ResultSet rs = stmt.executeQuery(sql);

        while (rs.next()) {
            System.out.println("Oznaka: " + rs.getString(2).trim() + ", " + "Naziv:" + rs.getString(3).trim() + ", "
                    + "ESPB: " + rs.getInt(4));

            if (rs.getInt(4) == X) {
                // Azuriramo vrednost u koloni broj 5 (ESPB) iz upita iznad.
                rs.updateInt(4, Y);
                // Kada zavrsimo sa azuriranjem svih kolona,
                // pozivamo metod updateRow
                // da bi se sve promene odmah oslikale.
                // Ako zaboravimo da pozovemo ovaj metod,
                // izmene se nece propagirati i bice izgubljene.
                rs.updateRow();

                // Sada kada zahtevamo vrednosti u kolonama,
                // dobicemo azurirane vrednosti:
                System.out.println("------> Izvrsena je naredna promena:");
                System.out.println("        Oznaka: " + rs.getString(2).trim() + ", " + "Naziv:" + rs.getString(3).trim()
                        + ", " + "ESPB: "
                        + rs.getInt(4) /* azurirana vrednost */ + "\n");
            }
        }

        rs.close();
        stmt.close();
    }
    
    private static int[] ucitajStareINoveESPB() {
        int[] rezultat = new int[2];
        
        try (Scanner ulaz = new Scanner(System.in)) {
            System.out.println("Unesite stare ESPB:");
            rezultat[0] = ulaz.nextInt();
            System.out.println("Unesite nove ESPB:");
            rezultat[1] = ulaz.nextInt();
        }
        
        return rezultat;
    }
}

8.3.7 Unos redova korišćenjem ResultSet kursora

Ono što nismo imali prilike da vidimo jeste da se pomoću kursora mogu uneti novi slogovi u tabelu. Za unos novog sloga u tabelu koristi se specijalni “slog za unos”. Ovaj specijalni slog zapravo predstavlja bafer za konstruisanje novog sloga koji će biti unet u odgovarajuću tabelu.

Konstrukcija novog sloga se vrši u dva koraka:

  1. Potrebno je postaviti vrednosti svih kolona rezultujuće tabele u “slogu za unos” pozivom odgovarajućih metoda iz familije metoda ResultSet.updateXXX(int columnIndex, XXX x)
  2. Kada su svim kolonama u “slogu za unos” dodeljene odgovarajuće vrednosti, tada je potrebno izvršiti unos baferisanog “sloga za unos” u rezultujuću tabelu.

Unos baferisanog “slogu za unos” se izvršava pozivom metoda void ResultSet.insertRow(). Međutim, da bismo zapravo uspeli da konstruišemo ovaj specijalni slog, neophodno je da signaliziramo kursoru da želimo da menjamo baš taj red, a ne tekući red kroz koji se iterira u kursoru. Ovo je moguće uraditi pozivom metoda void ResultSet.moveToInsertRow(). Prilikom poziva ovog metoda, kursor se pozicionira na “slog za unos” i ujedno se pamti redni broj tekućeg sloga koji se obrađuje u kursoru. Nakon što smo završili proceduru za unos opisanu prethodnim koracima, potrebno je pozvati metod void ResultSet.moveToCurrentRow() koji će ponovo pozicionirati kursor na prethodno zapamćeni tekući slog koji se obrađuje u kursoru.

Procedura: Unos slogova pomoću ažurirajućih kursora

Celokupna procedura za unos novog sloga je data narednim koracima:

  1. Kreirati objekat interfejsa Statement za rad sa ažurirajućim kursorima.
    Statement stmt = con.createStatement(..., ResultSet.CONCUR_UPDATABLE, ...);
    
  2. Kreirati objekat interfejsa ResultSet.
    ResultSet kursor = stmt.executeQuery(sql);
    
  3. Otvoriti kursor i pozicionirati se na “slog za unos”.
    while (kursor.next()) {
     kursor.moveToInsertRow();
     // ...
    }
    
  4. Pozvati odgovarajući metod iz familije metoda ResultSet.updateXXX(int columnIndex, XXX x) za postavljanje vrednosti kolone “sloga za unos” sa indeksom columnIndex novom vrednošću x. Na primer:
    kursor.updateInt(1, 100);
    
  5. Ukoliko želimo da zapravo unesemo novi slog u BP, potrebno je da pozovemo metod ResultSet.insertRow(). U suprotnom će, prelaskom na drugi red, sve izmene biti ignorisane.
    kursor.insertRow();
    
  6. Pozicionirati se na tekući slog u rezultujućoj tabeli kursora pozivom metoda ResultSet.moveToCurrentRow().
    kursor.moveToCurrentRow();
    
  7. Nakon svih iteracija, pozvati metod kursor.close() radi zatvaranja kursora.
  8. Nakon zatvaranja kursora, pozvati metod stmt.close() radi zatvaranja objekta naredbe.

Naredni zadatak ilustruje upotrebu opisane procedure.

Zadatak 8.8: Napisati Java program u kojem se naredbe izvršavaju dinamički koji:

  1. Kreira tabelu UNETIPREDMETI čije su kolone: (1) identifikator predmeta i (2) broj položenih ispita za taj predmet. Postaviti odgovarajuće primarne i strane ključeve.
  2. Za svaki predmet koji nije prethodno obrađen (tj. koji se ne nalazi u tabeli UNETIPREDMETI) pronalazi statistiku koja se sastoji od njegovog identifikator i broj položenih ispita.
  3. Za svaku pronađenu statistiku ispisuje podatke na standardni izlaz i pita korisnika da li želi da unete statistiku u tabelu UNETIPREDMETI. Ukoliko korisnik potvrdi, potrebno je uneti statistiku u datu tabelu i ispisati poruku o uspehu. U suprotnom, ispisati poruku da je korisnik poništio unos.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_8/Main.java:

package zadatak_8_8;

import java.sql.*;
import java.util.ArrayList;
import java.util.Scanner;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private static class StatistikaPolaganja {
        public int idPredmeta;
        public int brojPolaganja;
        
        public StatistikaPolaganja(int idPredmeta, int brojPolaganja) {
            this.idPredmeta = idPredmeta;
            this.brojPolaganja = brojPolaganja;
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";
        ArrayList<StatistikaPolaganja> statistike = new ArrayList<>();

        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {
            
            kreirajTabelu(con);
            sakupiStatistiku(con, statistike);
            unesiPredmeteIzStatistike(con, statistike);

        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }
    
    private static void kreirajTabelu(Connection con) throws SQLException {
        String sql = "CREATE TABLE DA.UNETIPREDMETI ( "
                + "IDPREDMETA INTEGER NOT NULL, "
                + "BROJPOLOZENIH INTEGER NOT NULL, "
                + "PRIMARY KEY (IDPREDMETA), "
                + "FOREIGN KEY (IDPREDMETA) REFERENCES DA.PREDMET "
                + ")";
        Statement stmt = con.createStatement();
        try {
            stmt.executeUpdate(sql);
        } catch (SQLException e) {
            // Ignorisemo gresku samo ako je u pitanju greska da tabela vec postoji (-601).
            // Sve ostale ispaljujemo main metodu.
            if (e.getErrorCode() != -601) {
                throw e;
            }
        }
        stmt.close();
    }

    private static void sakupiStatistiku(Connection con, ArrayList<StatistikaPolaganja> statistike) throws SQLException {
        String sql = "SELECT  IDPREDMETA, COUNT(OCENA) " +
            "FROM    DA.ISPIT " +
            "WHERE   OCENA > 5 AND " +
            "        STATUS = 'o' AND " +
            "        IDPREDMETA NOT IN (SELECT IDPREDMETA FROM DA.UNETIPREDMETI) " +
            "GROUP BY IDPREDMETA";
        Statement stmt = con.createStatement();
        ResultSet kursor = stmt.executeQuery(sql);
        while (kursor.next()) {
            int idPredmeta = kursor.getInt(1);
            int brojPolaganja = kursor.getInt(2); 
            statistike.add(new StatistikaPolaganja(idPredmeta, brojPolaganja));
        }
        kursor.close();
        stmt.close();
    }
    
    private static void unesiPredmeteIzStatistike(Connection con, ArrayList<StatistikaPolaganja> statistike) throws SQLException {
        String sql = 
            "SELECT * " + 
            "FROM   DA.UNETIPREDMETI";
        Statement stmt = con.createStatement( 
                // Dovoljan nam je kursor koji prolazi unapred kroz redove.
                ResultSet.TYPE_FORWARD_ONLY,
                // Definisemo da je kursor azurirajuci,
                // pa su nam dostupni metodi
                // ResultSet.updateXXX i ResultSet.insertRow
                // koje cemo koristiti za konstrukciju i unos novog reda
                ResultSet.CONCUR_UPDATABLE);
        ResultSet kursor = stmt.executeQuery(sql);
        Scanner ulaz = new Scanner(System.in);
        
        for (StatistikaPolaganja statistika : statistike) {
            // Pozicioniramo kursor na "slog za unos"
            kursor.moveToInsertRow();
            // Konstruisemo "slog za unos" navodjenjem vrednost za svaku kolonu
            kursor.updateInt(1, statistika.idPredmeta);
            kursor.updateInt(2, statistika.brojPolaganja);
            // Izvrsavamo ili ponistavamo unos u zavisnosti od korisnikovog odgovora
            System.out.println("Da li zelite da unete statistiku za predmet " + statistika.idPredmeta + "? [da/ne]");
            String odgovor = ulaz.nextLine();
            if (odgovor.equalsIgnoreCase("da")) {
                kursor.insertRow();
                System.out.println("\tUneta je statistika: " + statistika.idPredmeta + " (" + statistika.brojPolaganja + ")");
            }
            else {
                System.out.println("\tPonistili ste unos!");
            }
            // Vracamo kursor na zapamceni tekuci red
            kursor.moveToCurrentRow();
        }
        
        ulaz.close();
        kursor.close();
        stmt.close();
    }
}

8.3.8 Brisanje redova korišćenjem ResultSet kursora

Poput ažuriranja podataka, i za brisanje podataka postoje dve procedure:

Kao što znamo, možemo koristiti pozicionirajuću DELETE naredbu za brisanje podataka iz baze podataka, na osnovu tekućeg reda kursora, tako što se na kursor referiše u WHERE CURRENT OF klauzi naredbe. Procedura za dohvatanje naziva kursora i izvršavanje pozicionirajuće DELETE naredbe je ekvivalentna prethodno opisanoj proceduri za ažuriranje podataka korišćenjem pozicionirajuće UPDATE naredbe, te ćemo samo prikazati fragment koda koji ilustruje ovu proceduru:

String upit =
    "SELECT OZNAKA, " +
    "       NAZIV, " + 
    "       ESPB " +
    "FROM   DA.PREDMET";

Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(upit);

String nazivKursora = rs.getCursorName();
String brisanje =
    "DELETE FROM DA.PREDMET " +
    "WHERE  CURRENT OF " + nazivKursora;
Statement ps = con.prepareStatement();

while (rs.next()) {
    String nazivPredmeta = rs.getString(2);

    ps.executeUpdate(brisanje);
    System.out.println("Obrisan je predmet " + nazivPredmeta);
}

Brisanje sloga iz objekta ResultSet je jednostavnije nego ažuriranje ili unos sloga.

Procedura: Brisanje slogova pomoću ažurirajućih kursora

Naredni koraci definišu operaciju brisanja sloga iz kursora:

  1. Kreirati objekat interfejsa Statement za rad sa brišućim kursorima.
    Statement stmt = con.createStatement(..., ResultSet.CONCUR_UPDATABLE, ...);
    
  2. Kreirati objekat interfejsa ResultSet.
    ResultSet kursor = stmt.executeQuery(sql);
    
  3. Pozicionirati se na slog koji je potrebno obrisati metodima za prolazak kroz kursor. Na primer:
    while (kursor.next()) {
     kursor.absolute(5); // Samo ako kursor nije tipa TYPE_FORWARD_ONLY
     // ...
    }
    
  4. Pozvati metod kursor.deleteRow() koji će obrisati slog u bazi podataka
  5. Nakon svih iteracija, pozvati metod kursor.close() radi zatvaranja kursora
  6. Nakon zatvaranja kursora, pozvati metod stmt.close() radi zatvaranja objekta naredbe

Metod deleteRow() briše onaj red na koji je ResultSet pozicioniran. Ne postoji način da se operacija brisanja poništi (osim poništavanjem transakcije, o čemu će biti više reči u narednom poglavlju).

Zadatak 8.9: Napisati Java program u kojem se naredbe izvršavaju dinamički koji briše sve nepoložene ispite u školskoj godini koja se zadaje sa standarnog ulaza. Nakon svakog brisanja ispita, ispisati naredne informacije o njemu na standardni izlaz: indeks, oznaku roka, školsku godinu i identifikator predmeta.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_9/Main.java:

package zadatak_8_9;

import java.sql.*;
import java.util.Scanner;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";

        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {
            
            izbrisiNepolozeneIspite(con);

        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }

    private static void izbrisiNepolozeneIspite(Connection con) throws SQLException {
        int godina = ucitajGodinu();
        
        String selectSql = 
            "SELECT INDEKS, " + 
            "       OZNAKAROKA, " + 
            "       IDPREDMETA " +
            "FROM   DA.ISPIT " +
            "WHERE  SKGODINA = ? AND " +   
            "       OCENA = 5 AND " +  
            "       STATUS = 'o'";    
        PreparedStatement stmt = con.prepareStatement(selectSql,
            ResultSet.TYPE_FORWARD_ONLY,
            // Podesavamo kursor da bude za menjanje,
            // da bi smo mogli da brisemo redove metodom deleteRow() .
            ResultSet.CONCUR_UPDATABLE);
        stmt.setInt(1, godina);
        ResultSet ispiti = stmt.executeQuery();
        
        while(ispiti.next()) {
            int indeks = ispiti.getInt(1);
            String oznakaRoka = ispiti.getString(2).trim();
            int idPredmeta = ispiti.getInt(3);
            
            ispiti.deleteRow();
            
            System.out.printf("Obrisan je ispit %-10d %-10s %-5d %-10d\n", 
                indeks, oznakaRoka, godina, idPredmeta);
        }
        
        ispiti.close();
        stmt.close();
    }
    
    private static int ucitajGodinu() {
        int godina;
        
        try (Scanner ulaz = new Scanner(System.in)) {
            System.out.println("Unesite godinu roka za koju zelite da budu obrisani nepolozeni ispiti:");
            godina = ulaz.nextInt();
        }
        
        return godina;
    }
}

8.3.9 Ugnežđeni kursori

U JDBC aplikacijama se kursori jednostavno ugnežđuju: potrebno je celokupnu obradu unutrašnjeg kursora smestiti u okviru obrade jednog reda iz spoljašnjeg kursora. Naredni primer ilustruje upotrebu ugnežđenih kursora.

Zadatak 8.10: Napisati Java program u kojem se naredbe izvršavaju dinamički koji izdvaja indeks, ime, prezime i naziv studijskog programa svih studenata koji su položili tačno N predmeta, kao i spisak tih predmeta (naziv i ocena). Broj N se učitava sa standardnog ulaza. Za svakog studenta napraviti posebnu sekciju izveštaja.

Rešenje:

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_10/ispiti.sql:

SELECT      D.INDEKS, 
            TRIM(IME),
            TRIM(PREZIME), 
            TRIM(NAZIV)
FROM        DA.DOSIJE D JOIN 
            DA.STUDIJSKIPROGRAM S ON D.IDPROGRAMA = S.ID JOIN 
            DA.ISPIT I ON D.INDEKS = I.INDEKS
WHERE       OCENA > 5 AND 
            STATUS = 'o'
GROUP BY    D.INDEKS, 
            IME, 
            PREZIME, 
            NAZIV
HAVING      COUNT(*) = ?

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_10/predmeti.sql:

SELECT      TRIM(NAZIV), 
            OCENA 
FROM        DA.ISPIT I JOIN 
            DA.PREDMET P ON I.IDPREDMETA = P.ID 
WHERE       I.INDEKS = ? AND 
            OCENA > 5 AND 
            STATUS = 'o'
ORDER BY    NAZIV

Datoteka: vezbe/primeri/poglavlje_8/src/zadatak_8_10/Main.java:

package zadatak_8_10;

import java.io.File;
import java.io.FileNotFoundException;
import java.sql.*;
import java.util.Scanner;

public class Main {
    static {
        try {
            Class.forName("com.ibm.db2.jcc.DB2Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String argv[]) {
        String url = "jdbc:db2://localhost:50000/stud2020";

        try (Connection con = DriverManager.getConnection(url, "student", "abcdef")) {
            
            prodjiKrozSpoljasnjiKursor(con);
            
        } catch (SQLException e) {
            e.printStackTrace();

            System.out.println("SQLCODE: " + e.getErrorCode() + "\n" + "SQLSTATE: " + e.getSQLState() + "\n"
                    + "PORUKA: " + e.getMessage());

            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();

            System.exit(2);
        }
    }

    private static void prodjiKrozSpoljasnjiKursor(Connection con) throws SQLException, FileNotFoundException {
        int ukupnoPredmeta = ucitajBrojPredmeta();

        PreparedStatement pstmt;

        String sql = ucitajSqlIzDatoteke("ispiti.sql");
        pstmt = con.prepareStatement(sql);

        pstmt.setInt(1, ukupnoPredmeta);
        ResultSet ispiti = pstmt.executeQuery();

        while (ispiti.next()) {
            System.out.printf("\n\nIndeks: %-10d\nIme: %-10s\nPrezime: %-20s\nNaziv smera: %-30s\n\n", ispiti.getInt(1),
                    ispiti.getString(2), ispiti.getString(3), ispiti.getString(4));

            int indeks = ispiti.getInt(1);
            prodjiKrozUnutrasnjiKursor(con, indeks);
        }

        ispiti.close();
        pstmt.close();
    }

    private static void prodjiKrozUnutrasnjiKursor(Connection con, int indeks) throws SQLException, FileNotFoundException {
        PreparedStatement pstmt;

        String sql = ucitajSqlIzDatoteke("predmeti.sql");
        pstmt = con.prepareStatement(sql);

        pstmt.setInt(1, indeks);
        ResultSet predmeti = pstmt.executeQuery();

        int redniBr = 1;

        while (predmeti.next()) {
            System.out.printf("\t%d. predmet: %s\n\t\tOcena: %d\n", redniBr, predmeti.getString(1),
                    predmeti.getShort(2));

            ++redniBr;
        }

        predmeti.close();
        pstmt.close();
    }

    private static int ucitajBrojPredmeta() {
        int godina;

        try (Scanner ulaz = new Scanner(System.in)) {
            System.out.println("Unesite broj predmeta N:");
            godina = ulaz.nextInt();
        }

        return godina;
    }
    
    private static String ucitajSqlIzDatoteke(String nazivDatoteke) throws FileNotFoundException {
        String putanja = "./bin/zadatak_8_10/" + nazivDatoteke;
        StringBuilder sql = new StringBuilder("");
        String linija = null;
        
        try (Scanner skenerFajla = new Scanner(new File(putanja), "utf-8")) {
            while (skenerFajla.hasNextLine()) {
                linija = skenerFajla.nextLine();
                sql.append(linija);
                sql.append("\n");
            }
        }

        return sql.toString();
    }
}

8.4 Zadaci za vežbu

Zadatak 8.11: Napisati Java program u kojem se naredbe izvršavaju dinamički koji ispisuje podatke za predmet čiji je identifikator 2027.

Zadatak 8.12: Napisati Java program u kojem se naredbe izvršavaju dinamički koji ispisuje broj studenata koji su upisali studije u školskoj godini koja se unosi sa standardnog ulaza.

Zadatak 8.13: Napisati Java program u kojem se naredbe izvršavaju dinamički koji za svaku ocenu od 6 do 10 ispisuje ime i prezime studenta koji je poslednji položio neki ispit sa tom ocenom. U slučaju da ima više takvih studenata, klauzom LIMIT 1 naredbe SELECT INTO se osigurati da bude vraćen najviše 1 red. (Pomoć: Koristiti for petlju za menjanje vrednosti matične promenljive koja sadrži ocenu, pa u svakoj iteraciji dohvatiti informaciju za tekuću vrednost te matične promenljive.)

Zadatak 8.14: Napisati Java program u kojem se naredbe izvršavaju dinamički kojim se dodaje da je za polaganje predmeta čiji je identifikator 2327 uslov da se položi predmet čiji je identifikator 1588.

Zadatak 8.15: Napisati Java program u kojem se naredbe izvršavaju dinamički kojim se u tabelu NIVOKVALIFIKACIJE dodaje novi nivo čiji se identifikator i naziv.

Zadatak 8.16: Napisati Java program u kojem se naredbe izvršavaju dinamički kojim se, za sve položene ispite čiji se naziv predmeta unosi sa standardnog ulaza, ocena uvećava za 1.

Zadatak 8.17: Napisati Java program u kojem se naredbe izvršavaju dinamički kojim se u tabeli USLOVNIPREDMET brišu sve uslovnosti za predmet čiji se identifikator unosi sa standardnog ulaza.

Zadatak 8.18: Napisati Java program u kojem se naredbe izvršavaju dinamički kojim se brišu svi podaci o ispitima za studenta čiji se indeks unosi sa standardnog ulaza.

Zadatak 8.19: Napisati Java program u kojem se naredbe izvršavaju dinamički koji iz tabele PREDMET briše podatak o predmetu čija se oznaka unosi sa standardnog ulaza.