Laborator 02

Gestiunea informațiilor dintr-o bază de date MySQL prin Java Database Connectivity

Obiective

  • descrierea funcționalităților puse la dispoziția programatorilor de Java Database Connectivity pentru gestiunea informațiilor reținute într-o bază de date;
  • cunoașterea modelelor de procesare pentru accesul la informații localizate în diferite surse de date;
  • identificarea tipurilor de drivere utilizate pentru interacțiunea cu un sistem de gestiune pentru baze de date;
  • familiarizarea cu principalele clase din arhitectura protocolului Java Database Connectivity;
  • utilizarea API-ului Java Database Connectivity pentru a realiza operații de bază cu informațiile stocate într-o bază de date MySQL din contextul unei aplicații Java;
  • însușirea modului în care poate fi exploatat dicționarul de date asociat unei baze de date;
  • folosirea unor mecanisme avansate oferite de Java Database Connectivity (obiecte deconectate) pentru optimizarea transferului de date între diferite niveluri ale unei aplicații.

Cuvinte Cheie

ODBC, driver, Connector/J, DataSource, DriverManager, JDBC API, JDBC Driver API, protocol, cursor sensitivity, cursor concurrency, cursor holdability, ResultSet, transaction, isolation level, dirty read, unrepeatable read, ghost read, commit, rollback, metadata, SQLException, SQLWarning, JdbcRowSet, CachedRowSet, FilteredRowSet, JoinRowSet, WebRowSet, conflicts, conflict resolution

Materiale Ajutătoare

Java Database Connectivity - aspecte generale

JDBC (Java Database Connectivity) este o interfaţă de programare Java prin intermediul căreia pot fi manipulate informaţiile dintr-o sursă de date. Operaţiile pe care le pune la dispoziţie acest API sunt:

  1. conectarea (respectiv, deconectarea) la o sursă de date, cel mai frecvent o bază de date;
  2. transmiterea de interogări către sursa de date respectivă (de tip SELECT, INSERT, UPDATE, DELETE dar şi referitoare la informaţiile din dicţionarul de date), obţinerea rezultatelor aferente comenzilor realizate şi procesarea lor, inclusiv propagarea modificărilor realizate.

Componentele pe care le include JDBC sunt:

  1. interfaţa de programare propriu-zisă (JDBC API 4.1) care oferă acces la informaţiile din baza de date folosind limbajul Java. Este conţinută de pachetele java.sql şi javax.sql, incluse atât în platforma standard (Java SE) cât şi în platforma pentru implementarea aplicaţiilor de întreprinderi (Java EE); în mediile distribuite, există posibilitatea de interacțiune cu mai multe surse de date simultan;
  2. modulul pentru gestiunea driver-elor (JDBC Driver Manager), reprezentat de clasa DriverManager în care sunt definite obiectele ce pot conecta aplicaţiile Java la un “driver” JDBC; pachetele javax.naming şi javax.sql oferă posibilitatea realizării unei conexiuni către o sursă de date (obiect de tip DataSource) înregistrată de către serviciul de nume Java Naming and Directory Interface (JNDI).
  3. suita de teste JDBC oferă o serie de utilitare care verifică dacă “driverele” JDBC sunt compatibile cu o aplicaţie Java;
  4. puntea ODBC-JDBC pentru realizarea de conexiuni JDBC prin “drivere” ODBC care vor trebui încărcate pe fiecare maşină ce le utilizează; acesrea sunt utilizate atunci când nu există alte soluţii de conectare (native). Puntea este ea însăşi un tip de driver bazat pe tehnologia JDBC, fiind conţinut de clasa sun.jdbc.odbc.JdbcOdbcDriver, definind subprotocolul odbc; o astfel de soluție este adecvată în rețelele de calculatoare ale corporațiilor în care instalarea de aplicații pe diferite mașini din cadrul acestora nu reprezintă o problemă sau pentru arhitecturile care utilizează un server de aplicații.

Arhitectura JDBC defineşte două modele de procesare pentru accesul la informaţiile din baza de date:

  1. în modelul pe două niveluri aplicaţia Java comunică în mod direct cu sursa de date, necesitând un “driver” JDBC specific acesteia care să poată accesa informaţiile. Instrucţiunile utilizatorului sunt transmise sursei de date care întoarce la rândul sau rezultatele. Cele două componente rulează de obicei pe maşini diferite conectate prin intermediul unei reţele de calculatoare (intranet/Internet), modelul fiind cunoscut şi sub numele de client-server;
  2. în modelul pe trei niveluri comenzile sunt transmise prin intermediul unor servicii puse la dispoziţie într-un nivel intermediar, care au acces la sursa de date. Ca atare, informaţiile vor trece – în ambele direcţii – prin acest nivel. Conexiunea la nivelul intermediar se poate face prin HTTP sau alte metode pentru acces la distanţă (RMI, CORBA, servicii web). Câteva avantaje pe care le oferă acest model sunt controlul centralizat al accesului la date, simplificarea procesului de dezvoltare al aplicaţiilor, performanţa.

JDBC începe să fie adoptat pe scară largă datorită suportului pentru gestiunea paralelă a conexiunilor, tranzacţii distribuite precum şi posibilităţilor de procesare a informaţiilor deconectate de la sursa de date corespunzătoare.

Drivere de Conectare la Surse de Date

Un “driver” de conectare la un sistem de gestiune al bazei de date reprezintă o bibliotecă prin care sunt transformate apelurile JDBC (din limbajul de programare Java) într-un format suportat de protocolul de reţea folosit de sistemul de gestiune al bazei de date, permiţând programatorilor să acceseze datele din medii eterogene.

Astfel, “driver-ul” pentru sistemul de gestiune al bazei de date realizează legătura între nivelul de logică a aplicaţiei şi nivelul de date (reprezentat prin baza de date propriu-zisă).

Există patru implementări pentru “drivere” JDBC:

  1. tipul 1: drivere ce implementează API-ul JDBC ca punte peste ODBC (eng. Open DataBase Connectivity), acesta fiind cel care accesează datele propriu-zise; portabilitatea lor este relativ redusă, fiind dependente de o bibliotecă scrisă în cod nativ (şi nu implementate complet în Java); de asemenea, viteza de execuţie este destul de redusă datorită transformărilor ce trebuie realizate atât la transmiterea interogărilor cât şi a rezultatelor; implică instalarea de utilitare suplimentare pe client ceea ce le poate face incompatibile cu anumite tipuri de aplicaţii; această soluţie este tranziţională şi ar trebui folosită în cazul în care sistemul de gestiune pentru baze de date respectiv nu oferă un driver JDBC scris doar Java; Oracle nu implementează acest tip de drivere; totuşi, întrucât există “drivere” ODBC pentru toate bazele de date existente, o astfel de soluţie oferă acces către orice tip de date;
  2. tipul 2: drivere care sunt scrise parţial în Java şi parţial în cod nativ, folosind o bibliotecă specifică pentru sursele de date la care se conectează, ceea ce le reduce portabilitatea şi posibilitatea utilizării în contextul reţelelor de calculatoare; de asemenea, nu toţi producătorii oferă astfel de biblioteci care trebuie instalate pe client; exemplu: Oracle Call Interface;
  3. tipul 3: drivere dezvoltate exclusiv în Java care comunică cu middleware-ul printr-un protocol independent de baza de date, comenzile fiind transformate la acest nivel în instrucţiuni specifice bazei de date care pot fi utilizate apoi pentru accesarea sursei de date; avantajele acestei soluţii constau în facilităţile oferite de middleware cum ar fi controlul încărcării, memorarea conexiunilor, stocarea rezultatelor interogărilor într-o zonă tampon de memorie, opţiuni de administrare sistemului (autentificare, analiza performanţelor); pe lângă portabilitate acest tip de “driver” este performant (cel mai eficient între toate) şi scalabil (se pot accesa mai multe tipuri de baze de date); nu trebuie încărcate pe client produse specifice producătorilor, ceea ce îl face adecvat utilizării în Internet; protocolul independent de baza de date poate determina ca încărcarea driverului să se facă rapid; dezavantajul consta în faptul că operaţiile specifice bazei de date trebuie realizate în cadrul nivelului intermediar;
  4. tipul 4: drivere scrise în Java care implementează un protocol de reţea specific sistemului de gestiune pentru baze de date, spre a se conecta la sursa de date în mod direct; se asigură astfel independenţa de platformă cât şi eficienţa întrucât nu sunt necesare niveluri suplimentare pentru translatarea codului dintr-un format într-altul; totodată, nu este necesară instalarea de utilitare suplimentare pe client şi pe server, ceea ce face ca abordarea să fie compatibilă cu utilizarea peste o reţea de calculatoare; nu pot fi procesate mai multe baze de date în paralel, fiind necesar un driver pentru fiecare astfel de conexiune; exemplu: MySQL Connector/J

Pentru driverele de tip 1 şi 2 este necesară existenţa unor biblioteci specifice pentru fiecare tip de bază de date, fiind necesar ca acestea să fie puse la dispoziţie de producătorii acestora.

Pentru driverul de tip 3 trebuie instalat un server de aplicaţii care comunică cu sistemul de gestiune pentru baze de date. De regulă acesta este configurat pentru a fi compatibil cu mai multe tipuri de baze de date, iar performanţele sale trebuie să compenseze timpul pentru transferul de informaţii de la şi către el.

Driverul de tip 4 este cel mai flexibil dintre toate întrucât nu necesită utilitare suplimentare, fiind şi independent de platformă.

Arhitectura Java Database Connectivity

Arhitectura protocolului Java Database Connectivity este structurată pe două niveluri:

  1. un API JDBC responsabil de comunicaţia dintre aplicaţia Java şi modulul de gestiune al driver-ului;
  2. un API JDBC Driver care este responsabil de comunicaţia dintre modulul de gestiune al driver-ului şi baza de date; un astfel de nivel este independent atât în raport cu baza de date la care se conectează precum şi în raport cu limbajul de programare din care este accesat;

O aplicaţie care gestionează informațiile dintr-o o bază de date folosind protocolul Java Database Connectivity trebuie să urmeze următorii paşi:

  1. [înregistrarea “driver”-ului] – opţional (începând cu JDBC 4.0, se încarcă în mod automat “driver-ul” identificat în classpath, operația nu mai trebuie realizată de programator explicit), se poate face în două moduri:
    • DriverManager.registerDriver(new com.mysql.jdbc.Driver()) - existența driver-ului este evaluată la momentul în care se realizează compilarea;
    • Class.forName(“com.mysql.jdbc.Driver”).newInstance() - existența driver-ului este evaluată la momentul în care se realizează execuția, putând genera o excepție de tip NoClassDefFoundException;
  2. deschiderea conexiunii la baza de date;
  3. realizarea de interogări către baza de date; interogarea trebuie “construită” anterior execuţiei sale; în situația în care unele elemente ale interogării nu se cunosc decât la momentul execuţiei, aceasta poate fi parametrizată, urmând ca transmiterea valorilor lipsă să se facă în momentul în care sunt cunoscuţi, fiind preluaţi direct de la utilizator sau dintr-un fişier;
  4. procesarea rezultatelor obţinute cu propagarea modificărilor realizate înapoi în baza de date;
  5. închiderea conexiunii la baza de date.

Configurare Connector/J

Pentru conectarea la baza de date MySQL, se poate folosi Connector/J, driver nativ pentru Java dezvoltat de Oracle şi distribuit gratuit utilizatorilor.

De regulă, fiecare sistem de gestiune pentru baze de date (Oracle, DB2) este livrat împreună cu driver-ele de conectare din diverse limbaje de programare sau pune la dispoziția utilizatorilor astfel de produse separat. Producătorul bazei de date este responsabil pentru dezvoltarea și întreținerea unor astfel de tipuri de mijloace de acces programatic la informațiile conținute de acestea.

Pentru utilizarea driver-ului de conectare din limbajul de programare Java împreună cu sistemul de gestiune pentru bazei de date MySQL trebuie descărcată arhiva care conţine Connector/J de la Download Connector/J, despachetată şi indicarea fişierului .jar din rădăcina sa la classpath în momentul în care se compilează aplicaţia.

Pentru sistemul de operare Windows este disponibil un fişier de tip .msi (Microsoft Installer) care va plasa conectorul respectiv (arhiva .jar) în directorul %System Root%\Program Files [(x86)]\MySQL\Connector J 5.1.

În linie de comandă, acest lucru poate fi realizat astfel:

  • compilare:
    C:\Users\Aipi2014> javac -classpath .;mysql-connector-java-5.1.33-bin.jar <nume_fisier>.java
    aipi2014@ubuntu:~$ javac -classpath .:mysql-connector-java-5.1.33-bin.jar <nume_fisier>.java
  • rulare:
    C:\Users\Aipi2014> java -classpath .;mysql-connector-java-5.1.33-bin.jar <nume_fisier>.java
    aipi2014@ubuntu:~$ java -classpath .:mysql-connector-java-5.1.33-bin.jar <nume_fisier>.java

Mai uşor, se pot folosi medii integrate de dezvoltare a aplicaţiilor, precum:

  • Eclipse Luna (4.4.1)
    • clasele conţinute în arhiva .jar trebuie adăugate la calea proiectului
      • în cazul în care arhiva .jar se găseşte în structura proiectului: click dreapta pe numele bibliotecii → Build PathAdd to Build Path
      • în cazul în care arhiva .jar nu există în structura proiectului: click dreapta pe numele proiectului → Build PathAdd External Libraries
    • dacă biblioteca externă a fost adăugată în mod corect, numele ei trebuie să apară în meniul din stânga corespunzător proiectului, secţiunea Referenced libraries
  • NetBeans 8.0.1
    • din meniul corespunzător proiectului, se selectează Libraries, click dreapta şi se accesează opţiunea Add JAR/Folder
    • în fereastra de dialog se alege referinţa drept cale relativă (Reference As:Relative Path), în situația în care arhiva este localizată în sistemul de fişiere al proiectului, respectiv drept cale absolută, altfel
    • dacă biblioteca externă a fost adăugată în mod corect, numele ei trebuie să apară în meniul din stânga corespunzător proiectului, secţiunea Libraries
Este recomandat ca arhiva .jar corespunzătoare driver-ului MySQL Connector/J să fie plasată întotdeauna în structura de fișiere a proiectului (cu referirea corespunzătoare a acestuia), astfel încât să se asigure portabilitatea aplicației.

API-ul Java Database Connectivity

Gestiunea Conexiunilor

Conexiunea unei aplicaţii la sistemul de gestiune al bazei de date prin intermediul protocolului Java Database Connectivity se poate realiza prin intermediul a două clase:

  1. DataSource – metodă transparentă de acces la informaţii, un obiect având proprietăţi specificate astfel încât să corespundă unor surse de date particulare;
  2. DriverManager – asigură accesul programului la o sursă de date specificată prin intermediul unui URL.
    Pe baza acestuia, clasa DriverManager apelează metode din interfaţa Driver pentru a identifica driver-ul necesar pentru interacţiunea cu sistemul de gestiune al bazei de date. Astfel, este încărcat în mod automat orice driver JDBC >4.0 identificat în classpath care respectă proprietățile identificate.
    De obicei, URL-ul respectă următoarea structură:
    protocol:subprotocol:[nume_baza_de_date][lista_de_proprietati]

    Câteva exemple de URL-uri specifice anumitor tipuri de baze de date (pentru o bază de date denumită bookstore, la care datele de autentificare sunt utilizatorul root cu parola StudentAipi2014) sunt:

    • MySQL
      jdbc:mysql://[host][,failoverhost ...][:port]/[database][?propertyName1][=propertyValue1][&propertyName2][=propertyValue2]...
      jdbc:mysql://localhost:3306/bookstore?user=root&password=StudentAipi2014

      Parametrul failoverhost indică adresa unei mașini pe care se află o copie a bazei de date, la care se poate realiza conexiunea, în situația în care nu s-a reușit stabilirea unei legături către mașina indicată de parametrul host.

    • JavaDB
      jdbc:derby:[subprotocol:][databaseName][;attribute=value]*
      jdbc:derby:bookstore;user=root;password=StudentAipi2014
      • parametrul subprotocol, deși în general este omis, indică locația bazei de date (director din sistemul de fișiere, memorie, classpath, arhiva .jar);
      • în cadrul listei de atribute se poate specifica crearea bazei de date, criptarea acesteia, locaţia fişierelor în care să se păstreze diferite jurnale, numele de utilizator şi parola pentru conectare;
    • Oracle
      jdbc:oracle:[protocol]:@[database_host]:[port]:[instance]
      jdbc:oracle:thin:@localhost:1521:orcl
    • Microsoft Access
      jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=path
      jdbc:sqlserver://address\\server:port;database=bookstore;user=root;password=StudentAipi2014;

Deschiderea unei conexiuni se face prin metoda statică getConnection din DriverManager, care întoarce un obiect conexiune (Connection) la baza de date, acesta putând fi folosit ulterior pentru diferite interogări:

String DATABASE_CONNECTION = "jdbc:mysql://localhost:3306/bookstore";
Connection dbConnection = DriverManager.getConnection(DATABASE_CONNECTION);

În condiţiile în care se citesc dintr-o interfaţă grafică cu utilizatorul informaţii de tip utilizator şi parolă, stocate, de exemplu în obiectele username şi password,conexiunea poate fi obținută şi astfel:

  • Connection dbConnection = 
    DriverManager.getConnection(DATABASE_CONNECTION+"?user="+username+"&password="+password);
  • Properties connectionProperties = new Properties();
    connectionProperties.put("user",username);
    connectionProperties.put("password",password);
    Connection dbConnection = DriverManager.getConnection(DATABASE_CONNECTION, connectionProperties);

Dacă driverele gestionate de interfaţa Driver recunosc URL-ul indicat drept parametru metodei getConnection, se stabileşte o legătură cu sistemul de gestiune pentru baza de date, întorcându-se o conexiune care poate fi utilizată pentru transmiterea de instrucţiuni Java Database Connection translatate ulterior în interogări către baza de date.

Închiderea unei conexiuni, prin care sunt eliberate toate resursele asociate acesteia, se face prin metoda close:

dbConnection.close();

Operații de Interogare a Bazei de Date

Conform specificației Java Database Connectivity, pot fi create mai multe obiecte de tip interogare:

  • Statement – folosite pentru interogări SQL fără parametri;
  • PreparedStatement [extends Statement] – folosite pentru interogări SQL precompilate care pot conţine parametri de intrare;
  • CallableStatement [extends PreparedStatement]– folosite pentru a executa rutine stocate care pot conţine parametri de intrare şi de ieşire.

Clasa Statement

Un obiect de tip interogare (Statement) se obţine prin metoda createStatement() a unui obiect de tip Connection:

Statement statement = dbConnection.createStatement();

Începând cu JDBC 4.1 există posibilitatea definirii conexiunii într-un bloc try-with-resources, ce eliberează toate resursele alocate în secţiunea respectivă în mod automat, indiferent dacă a fost generată sau nu o excepţie SQLException:

try (Statement statement = dbConnection.createStatement()) {
    ...
}

În continuare, obiectul de tip interogare poate fi utilizat pentru realizarea unei operaţii cu baza de date şi obţinerea unui set de date rezultat în urma executării instrucţiunii. Există mai multe moduri prin care se poate realiza execuţia unei interogări SQL:

  • metoda execute(): întoarce true dacă primul obiect al interogării este de tipul ResultSet; prin această metodă pot fi obţinute unul sau mai multe (sau nici un) obiect(e) de tipul ResultSet; obiectele de tip ResultSet pot fi accesate apelând metoda getResultSet() a obiectului interogare;
    String query = "SELECT personal_identifier, first_name, last_name FROM user";
    boolean result = statement.execute(query);
    if (result)
        ResultSet records = statement.getResultSet();
  • metoda executeQuery(): întoarce un singur obiect de tip ResultSet;
    String query = "SELECT COUNT(*) FROM invoice";
    ResultSet result = statement.executeQuery(query);
  • metoda executeUpdate(): întoarce un număr întreg având semnificaţia înregistrărilor afectate de expresia SQL; este folosită de regulă pentru instrucţiuni DML de tip INSERT, UPDATE, DELETE, dar şi pentru instrucţiuni de tip DDL precum CREATE TABLE, ALTER TABLE, DROP TABLE:
    String query = "CREATE TABLE series (
    	                             id          INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY NOT NULL,
    	                             name        VARCHAR(100) NOT NULL,
    	                             description VARCHAR(1000)
                                        );";
    int result = statement.executeUpdate(query);
    // ...
    String query = "INSERT INTO publishing_house (name, registered_number, description, town, region, country)
     VALUES('Grupo Planeta', '892687559', '-', 'Rome', 'Lazio', 'Italy');"
    int result = statement.executeUpdate(query);

Interfaţa ResultSet pune la dispoziţia utilizatorului o serie de metode pentru lucrul cu informaţiile (seturile de date) obţinute în urma interogării bazei de date. Obiectele având tipul ResultSet au anumite caracteristici care pot fi modificate între care tipul, gestiunea concurenţei şi posibilitatea de deţinere a cursorului. Caracteristicile pot fi precizate de utilizator în momentul creării unui obiect de tip interogare (Statement).

Cu privire la modalitatea în care poate fi manipulat cursorul (aspecte ce ţin şi de senzitivitatea cursorului), există următoarele constante:

  • TYPE_FORWARD_ONLY (implicit) – cursorul se poate muta doar înainte, ne-existând posibilitatea parcurgerii în ambele sensuri a setului de date obţinut ca rezultat al interogării;
  • TYPE_SCROLL_INSENSITIVE – cursorul se poate muta înainte şi înapoi, poziţionându-se în diferite locaţii relative faţă de poziţia curentă sau absolute, dar nu este afectat de modificările realizate de alţi utilizatori în timp ce este utilizat; conţine înregistrările care satisfac condiţiile interogării atunci când aceasta este executată sau pe măsură ce sunt obţinute entităţile;
  • TYPE_SCROLL_SENSITIVE – cursorul se poate muta înainte şi înapoi, poziţionându-se în diferite locaţii relative faţă de poziţia curentă sau absolute, şi este afectat de modificările realizate de alţi utilizatori.
Nu toate driverele JDBC implementează aceste aspecte legate de senzitivitatea cursorului pentru obiecte de tip ResultSet. Se poate utiliza metoda DatabaseMetaData.supportsResultSetType pentru a verifica dacă tipul respectiv este suportat sau nu.

Tipul de concurenţă indică operaţiile pe care utilizatorul are permisiunea de a le realiza:

  • CONCUR_READ_ONLY (implicit) – utilizatorul are doar dreptul de a consulta informaţiile, fără a le modifica;
  • CONCUR_UPDATABLE – utilizatorul poate citi şi poate scrie informaţiile reţinute în setul de date rezultat.
Nu toate driverele JDBC implementează concurenţa obiectelor ResultSet. Se poate utiliza metoda DatabaseMetaData.supportsResultSetConcurrency pentru a verifica dacă această caracteristică este suportată sau nu.

Deţinerea cursorului la realizarea tranzacţiilor (comportamentul cursorului în momentul în care se apelează metoda commit) se face prin constantele:

  • HOLD_CURSORS_OVER_COMMIT – cursorul nu este închis în momentul în care este apelată metoda commit; un astfel de comportament este necesar atunci când obiectele de tip ResultSet sunt folosite mai mult pentru citire decât pentru scriere;
  • CLOSE_CURSORS_AT_COMMIT – cursorul este închis după ce este apelată metoda commit(); un astfel de comportament poate genera performanţe mai bune pentru unele aplicaţii.

Comportamentul implicit referitor la deţinerea cursorului în cazul tranzacţiilor depinde de sistemul de gestiune pentru baze de date şi poate fi verificat prin metoda DatabaseMetaData.getResultSetHoldability().

Nu toate driverele JDBC implementează deţinerea cursorului în cazul tranzacţiilor pentru obiecte de tipul ResultSet. Se poate utiliza metoda DatabaseMetaData.supportsResultSetHoldability spre a verifica dacă un anumit comportament este suportat sau nu.

În cazul în care se doreşte modificarea ordinii în care sunt parcurse înregistrările, obiectul de tip ResultSet dispune de o metodă setFetchDirection() prin care se sugerează direcţia de obţinere a tuplurilor corespunzătoare:

  • FETCH_FORWARD (implicit) – de la prima înregistrare la ultima;
  • FETCH_REVERSE – de la ultima înregistrare spre prima;
  • FETCH_UNKNOWN – ordinea de parcurgere este necunoscută.

Un exemplu de creare a unui obiect de tip interogare realizat pentru obţinerea unui set de date în care cursorul poate fi mutat în ambele direcţii, dar nu poate fi modificat setul de date, menţinând cursorul după realizarea unei tranzacţii prin metoda commit() este:

Statement statement = dbConnection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                                                   ResultSet.CONCUR_READ_ONLY,
                                                   ResultSet.HOLD_CURSORS_OVER_COMMIT);

Un obiect de tip ResultSet conţine mai multe (sau nici un) tuplu(ri), în funcţie de condiţiile interogării, având asociat un cursor care indică la orice moment înregistrarea curentă. Iniţial, cursorul se găseşte deasupra primei înregistrări.

Un cursor indicând poziția curentă dintr-un obiect de tip ResultSet este diferit de obiectul de tip cursor definit în MySQL în contextul unei rutine stocate pe server.

Câteva dintre metodele care pot fi utilizate pentru a realiza poziţionări ale cursorului în cadrul setului de date sunt:

metoda descriere
next() mută cursorul pe înregistrarea următoare
previous() mută cursorul pe înregistrarea precedentă
first() mută cursorul pe prima înregistrare
last() mută cursorul pe ultima înregistrare
beforeFirst() mută cursorul înainte de prima înregistrare
afterLast() mută cursorul după prima înregistrare
relative(int n) mută cursorul la n poziţii distanţă faţă de poziţia curentă
absolute(int n) mută cursorul la poziţia n (absolută) din set

De regulă, metodele întorc rezultate de tip boolean, având valoarea true dacă s-a reuşit poziţionarea dorită şi false în caz de eşec sau în situaţia în care setul de date nu conţine înregistrări.

În cazul în care tipul cursorului este cel implicit (TYPE_FORWARD_ONLY), nu se poate apela decât metoda next.

Obţinerea informaţiilor (valorilor asociate atributelor) se realizează prin metode de tip getter (getString(), getInt(), getByte(), getBoolean(), getBlob(), getDate()) care pot primi ca parametru fie numele (respectiv aliasul) coloanei fie indexul ei în cadrul tabelei din baza de date (această metodă fiind mai eficientă, numerotarea făcându-se începând cu valoarea 1).

Metoda getString() poate fi folosită pentru preluarea oricărui tip de informaţie din baza de date, mai puţin tipul SQL3.

Metodele de tip getter nu ţin cont de capitalizarea şirului de caractere care este oferit drept parametru. Dacă există mai mult de o denumire (sau alias) de coloană care are denumirea respectivă, este întoarsă valoarea corespunzătoare primului atribut identificat. Metoda ar trebui folosită în cazul în care denumirile (sau aliasurile) coloanelor sunt specificate explicit în interogare, nu şi în situaţia când interogarea are forma SELECT * FROM ….

O rutină de parcurgere a înregistrărilor dintr-o bază de date poate fi:

ResultSet result = statement.executeQuery("SELECT name, registered_number FROM publishing_house");
while (result.next()) {
    String name = result.getString(1);
    float registeredNumber = result.getFloat("registered_number");
}

Pentru introducerea de informaţii într-o bază de date folosind un obiect de tip ResultSet se pot folosi metodele moveToInsertRow() (care mută cursorul la poziţia corespunzătoare din setul de date - în fapt o zonă de memorie în care poate fi construită o nouă înregistrare înainte de a fi adăugată propriu-zis în tabelă) urmată de specificarea atributelor în acelaşi mod ca pentru o oricare actualizare (folosind metode de tip update…()) pentru ca ulterior adăugarea să fie realizată prin metoda insertRow().

Statement statement = dbConnection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                                                   ResultSet.CONCUR_UPDATABLE);	
ResultSet result = statement.executeQuery("SELECT * FROM writer");
result.moveToInsertRow();
result.updateString(1,"Sterne");
result.updateString(2,"Laurence");
result.insertRow();

După introducerea înregistrării în setul de date, este recomandată mutarea cursorului pe o altă poziţie, întrucât realizarea altor operaţii asupra obiectului ResultSet pot avea rezultate imprevizibile în condiţiile în care cursorul indică asupra valorii care a fost adăugată în tabelă. Frecvent, este apelată metoda beforeFirst() astfel încât cursorul să se găsească în aceeaşi stare ca cea ulterioară creării sale.

Procesul de actualizare a informaţiilor într-o bază de date printr-un obiect de tip ResultSet este realizat în două etape:

  • modificarea valorilor ce se doresc actualizate, la nivel de coloană, pe rândul unde se găseşte cursorul, prin intermediul metodelor de tip updateString(), updateInt(), updateByte(), updateBoolean(), updateBlob(), updateDate(); la acest moment, nici o modificare nu este realizată la nivelul tabelei;
  • actualizarea rândului curent în care au fost marcate spre modificare valorile coloanelor prin intermediul metodei updateRow().

Un exemplu de actualizare a informaţiilor în baza de date este:

Statement statement = dbConnection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                                                   ResultSet.CONCUR_UPDATABLE);	
ResultSet result = statement.executeQuery("SELECT issue_date, state FROM invoice");
GregorianCalendar today = new GregorianCalendar();
today.setTime(new Date());
while (result.next()) {
    GregorianCalendar issueDate = result.getDate(issue_date);
    if (issueDate.before(today))
        result.updateString(state,'overdue');
    updateRow();
} 
În cazul în care se doreşte anularea modificărilor realizate, se poate apela metoda cancelRowUpdates(), înainte însă de a apela metoda updateRow().

O înregistrare poate fi ştearsă folosind metoda deleteRow().

Clasa PreparedStatement

Atunci când nu toate datele interogării sunt cunoscute la momentul în care este compilată aplicaţia (sau atunci când se impune reutilizarea frecventă a unei interogări, cu diverși parametri), există posibilitatea ca interogarea să fie generică, urmând a fi completată cu informaţii (provenite dintr-o sursă de date sau introduse chiar de către utilizator) atunci când ele sunt disponibile, şi anume la rulare, înainte de execuţia interogării asupra bazei de date.

În momentul în care acestea sunt create, interogările parametrizabile primesc în mod necesar o parte din interogarea propriu-zisă, transmisă sistemului de gestiune al bazei de date care îl precompilează, astfel încât execuţia sa va fi mai rapidă. În momentul în care va fi executată, ea va fi rulată de către sistemul de gestiune pentru baze de date fără a mai fi compilată.

Sunt folosite obiecte de tip PreparedStatement, derivate din clasa Statement, informaţiile necunoscute fiind specificate prin caracterul ?:

String query = "UPDATE user SET type = ? WHERE role = ? ";
PreparedStatement preparedStatement = dbConnection.prepareStatement(query);

Înainte de a executa o astfel de interogare, trebuie specificate valorile care corespund atributelor lipsă, lucru care se face prin metode de tip setter:

preparedStatement.setString(1, Integer.parseInt(buffer.readLine());
preparedStatement.setDate(2, Date.valueOf(buffer.readLine());

Execuţia interogării se face folosind metodele specifice clasei Statement.

preparedStatement.executeUpdate();

De această dată, metodele nu vor mai primi ca parametru comanda SQL, întrucât aceasta a fost deja asociată în momentul în care a fost creat obiectul de tip interogare parametrizată.

Rezultatul metodei executeUpdate() este o valoare întreagă având semnificaţia numărului de înregistrări care au fost actualizate. Semnificaţia unui rezultat nul este acela că interogarea nu a afectat nici o înregistrare din tabelă sau că instrucţiunea a fost de tip DDL.

Clasa CallableStatement

Pentru apelarea unei rutine stocate, se folosesc obiecte din clasa CallableStatement, derivată din PreparedStatement:

String query = "{? = CALL calculate_invoice_value(?)}";
CallableStatement callableStatement = dbConnection.prepareCall(query);
callableStatement.registerOutParameter(1, java.sql.Types.DECIMAL);
callableStatement.setString(2,buffer.readLine());
callableStatement.execute();
double result = callableStatement .getDouble(1);
callableStatement.close();

Valorile necunoscute (inclusiv rezultatul rutinei stocate, fie parametru, fie valoare întoarsă) sunt marcate în continuare prin caracterul ?.

Pentru parametrii procedurilor, în cazul în care au tipul IN sau INOUT, trebuie specificată valoarea lor pentru ca rutina să poată fi executată. În plus, dacă aceştia au tipul OUT sau INOUT, trebuie specificat şi tipul de date aşteptat, folosind metoda registerOutParameter(). Acelaşi comportament trebuie respectat şi pentru parametrii / rezultatele întoarse ale funcţiilor. Tipurile de date din interfaţa java.sql.Types au aceeași denumire ca cele din MySQL.

Execuţia rutinei se face cu metoda execute(), iar valorile întoarse sunt preluate indexat, prin metodele get…() corespunzătoare.

Lucrul cu Dicționarul de Date

Java Database Connectivity permite accesarea informaţiilor reţinute în dicţionarul de date, precum structura bazei de date şi a tabelelor ca şi restricţiile de integritate (chei primare, chei străine). Toate aceste informații sunt disponibile prin clasa DatabaseMetaData, care se obţine pornind de la obiectul Connection aferent bazei de date respective:

DatabaseMetaData dbMetaData = dbConnection.getMetaData();

Denumirea bazelor de date care pot fi accesate folosind conexiunea respectivă se obţine cu metoda getCatalogs(), care întoarce un obiect de tip ResultSet conţinând câte o singură înregistrare pentru fiecare rând, şi anume denumirea catalogului.

Pentru fiecare bază de date pot fi aflate descrierile tabelelor componente, acestea putând fi filtrate în funcţie de numele schemei sau al tabelei (oferindu-se modele pentru acestea) sau al tipului tabelei, prin metoda getTables. Tipul tabelei poate fi TABLE, VIEW, SYSTEM TABLE, GLOBAL TEMPORARY, LOCAL TEMPORARY, ALIAS, SYNONYM. În cazul când se doreşte obţinerea tuturor tabelelor dintr-o bază de date, toate aceste criterii pot fi marcate ca null, astfel încât se ignoră orice criteriu care ar fi putut limita rezultatele întoarse.

ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) 
          throws SQLException

Obiectul de tip ResultSet întors ca rezultat conţine descrierile tabelelor, constând în următoarele informaţii:

poziție atribut descriere
1 TABLE_CAT catalogul tabelei (poate fi null)
2 TABLE_SCHEM schema tabelei (poate fi null)
3 TABLE_NAME denumirea tabelei
4 TABLE_TYPE tipul tabelei
5 REMARKS comentariu explicativ asupra tabelei
6 TYPE_CAT catalogul tipurilor (poate fi null)
7 TYPE_SCHEM schema tipurilor (poate fi null)
8 TYPE_NAME numele tipului (poate fi null)
9 SELF_REFERENCING_COL_NAME numele identificatorului desemnat al unei tabele de un anumit tip (poate fi null)
10 REF_GENERATION specifică modul în care sunt create valorile din SELF_REFERENCING_COL_NAMESYSTEM, USER, DERIVED (poate fi null)

Structura unei tabele se obţine prin metoda getColumns() în care filtrarea rezultatelor se face după numele schemei, a tabelei şi a coloanelor, omiterea oricăruia dintre criterii făcându-se prin marcarea ca null a parametrului aferent:

ResultSet getColumns(String catalog, String schemaPattern, 
          String tableNamePattern, String columnNamePattern) throws SQLException

Obiectul de tip ResultSet întors ca rezultat conţine descrierile tabelelor, constând în următoarele informaţii:

poziție atribut descriere
1 TABLE_CAT catalogul tabelei (poate fi null)
2 TABLE_SCHEM schema tabelei (poate fi null)
3 TABLE_NAME numele tabelei
4 COLUMN_NAME numele coloanei
5 DATA_TYPE tipul de dată SQL (din java.sql.Types)
6 TYPE_NAME numele tipului de dată (dependent de sursa de date)
7 COLUMN_SIZE dimensiunea coloanei
♦ valori numerice – precizia maximă
♦ şiruri de caractere – lungimea (în caractere)
♦ date calendaristice – lungimea reprezentării ca şir de caractere
♦ reprezentare binară / tipul ROWID – dimensiunea (în octeţi)
null – N/A
8 BUFFER_LENGTH nu este utilizat
9 DECIMAL_DIGITS numărul de zecimale; null dacă nu este cazul
10 NUM_PREC_RADIX baza (de obicei 10 sau 2)
11 NULLABLE indică posibilitatea de a exista valori null în coloană
columnNoNulls – ar putea să nu permită null
columnNullable – sigur permite null
columnNullableUnknown – stare necunoscută
12 REMARKS comentariu ce descrie coloana (poate fi null)
13 COLUMN_DEF valoarea implicită a coloanei (poate fi null); pentru şirurile de caractere, aceasta va fi încadrată între caracterele ' şi '.
14 SQL_DATA_TYPE nu este utilizat
15 SQL_DATETIME_SUB nu este utilizat
16 CHAR_OCTET_LENGTH pentru şiruri de caractere – numărul maxim de octeţi dintr-o coloană
17 ORDINAL_POSITION indexul coloanei în cadrul tabelei (începând de la 1)
18 IS_NULLABLE indică posibilitatea de a exista valori null în coloană potrivit regulilor ISO
19 SCOPE_CATALOG catalogul tabelului spre care indică referinţa atributului (null dacă DATA_TYPE nu este REF)
20 SCOPE_SCHEMA schema tabelului spre care indică referinţa atributului (null dacă DATA_TYPE nu este REF)
21 SCOPE_TABLE numele tabelului spre care indică referinţa atributului (null dacă DATA_TYPE nu este REF)
22 SOURCE_DATA_TYPE sursa tipului de dată pentru un tip distinct sau pentru o referinţă generată de utilizator (null dacă DATA_TYPE nu este DISTINCT sau referinţă generată de utilizator)
23 IS_AUTOINCREMENT indică dacă coloana este auto-incrementală
24 IS_GENERATED_COLUMN indică dacă coloana este generată

Alte metode importante din clasa DatabaseMetadata sunt cele ce identifică rutinele stocate (funcţii şi proceduri): getFunctionColumns() şi getProcedureColumns(), ambele având definiţii similare:

ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) 
          throws SQLException
ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) 
          throws SQLException

Pentru o rutină stocată se întorc mai multe intrări în ResultSet, corespunzând parametrilor de ieşire şi parametrilor de intrare:

  • denumirea rutinei stocate poate fi obţinut de pe poziţia a treia;
  • următoarele câmpuri descriu parametrul în cauză:
    • denumirea (4);
    • tipul (5) – IN, OUT, INOUT;
    • valoarea întoarsă, tipul de dată din java.sql.Types asociat (6);
    • numele tipului de dată (7);
    • precizia (8);
    • lungimea (9);
    • scala (10);
    • baza (11);
    • proprietatea de a avea valori null (12);
    • comentarii (13);
    • lungimea şirului de caractere exprimată în octeţi (14);
    • poziţia între parametrii rutinei stocate (15);
    • proprietatea de a lua valori null conform regulilor ISO (16).

Cheile primare ale unei tabele pot fi obţinute prin metoda getPrimaryKeys():

ResultSet getPrimaryKeys(String catalog, String schema, String table) 
          throws SQLException
În cazul în care nu se specifică o tabelă anume (parametrul corespunzător este null), vor fi întoarse toate cheile primare din baza de date respectivă.

Denumirea coloanei ce reprezintă cheia primară poate fi obţinut de pe poziţia a patra, împreună cu poziţia pe care o ocupă în cheia primară compusă (într-un astfel de caz) ca şi denumirea pe care o are constrângerea de tip cheie primară.

De asemenea, se poate genera în mod automat un identificator unic pentru o tabelă, folosind metoda getBestRowIdentifier(). În acest caz, va trebui specificat şi un scop care precizează nivelul la care va fi utilizat identificatorul unic (ale cărui valori pot fi bestRowTemporary, bestRowTransaction, bestRowSession).

Constrângerile de tip cheie străină pot fi identificate în ambele sensuri, astfel că sunt definite:

  • metode ce identifică coloanele care referă cheia primară pentru o tabelă (getExportedKeys());
  • metode prin care sunt specificate atributele referite de cheile primare ale altor tabele (getImportedKeys()).
ResultSet getExportedKeys(String catalog, String schema, String table) 
          throws SQLException
ResultSet getImportedKeys(String catalog, String schema, String table) 
          throws SQLException

Metoda întoarce un set de date conţinând descrierea cheii străine şi anume:

  • denumirea tabelei cheii primare referite (3);
  • numele coloanei cheii primare referite (4);
  • numele tabelei cheii străine (7);
  • numele coloanei cheii străine (8);
  • numărul de ordine în cazul cheilor străine compuse (9);
  • regulile în cazul operaţiilor de tip UPDATE (10) şi DELETE (11), acestea putând avea valorile:
    • importedNoAction (nu permite realizarea de modificări asupra unei chei primare care este referită);
    • importedKeyCascade (propagă modificările asupra cheii primare la nivelul cheii străine);
    • importedKeySetNull (valoarea cheii străine se schimbă în null dacă valoarea cheii primare referite se schimbă);
    • importedKeySetDefault (valoarea cheii străine devine cea implicită în cazul modificării cheii primare);
    • importedKeyRestrict (la fel cu importedNoAction)).
  • numele date constrângerilor de tip cheie străină (12) şi cheie primară (13);
  • dacă există şi posibilitatea ca evaluarea cheii străine să fie întârziată până la momentul operaţiei commit (14).

Clasa DatabaseMetaData pune la dispoziţie şi alte metode pentru verificarea capabilităţilor pe care le are driver-ul Java Database Connectivity utilizat.

Tratarea Excepțiilor

În momentul când se produc erori în cazul interacţiunii cu o sursă de date este generată o excepţie de tip SQLException care oferă următoarele informaţii:

  1. o descriere a erorii care poate fi obţinută din metoda getMessage() a obiectului de tip eroare asociat;
  2. un cod reprezentând starea SQL, potrivit standardizării ISO/ANSI şi OpenGroup (X/Open), format din 5 caractere alfanumerice (unele coduri au fost rezervate pentru producătorii de baze de date); acesta poate fi vizualizat ca rezultat al metodei getSQLState() a obiectului de tip eroare asociat;
  3. o cauză, constând în unul sau mai multe obiecte de tip Throwable care au determinat excepţia SQLException; lanţul cauzal poate fi parcurs recursiv apelând metoda getCause() până când este returnată o valoare null;
    Throwable cause = exception.getCause();
    while (cause != null) {
        System.out.println("Cauza: "+cause);
        cause = cause.getClause();
    }
  4. referinţe către alte excepţii înlănţuite, în cazul în care s-a produs mai mult de o eroare; acestea pot fi obţinute prin metoda getNextException().

Clasa SQLException are mai multe subclase, corespunzând unor excepţii care sunt generate în situaţii particulare, ceea ce face procesul de gestiune a erorilor mult mai portabil.

Avertismentele, reprezentate de obiecte din clasa SQLWarning, nu opresc execuţia aplicaţiei, informând totuşi utilizatorul ca una sau mai multe operaţii nu s-au desfăşurat aşa cum ar fi trebuit. Un avertisment poate fi raportat pentru obiecte de tip Connection, Statement (PreparedStatament / CallableStatement) sau ResultSet, fiecare dintre acestea dispunând de o metodă getWarnings() care întoarce un rezultat de tip SQLWarning. În cazul în care nu este null, acesta dispune de o metoda getNextWarning() ce indică şi alte avertismente. Totusi, atunci când se execută o instrucţiune, avertismentele de la instrucţiunea precedentă se pierd în mod automat.

Metodele pe care le pune la dispoziţie clasa SQLWarning sunt: getMessage(), getSQLState() şi getErrorCode().

Cel mai frecvent avertisment este de tip DataTruncation ce indică faptul că tipul de date folosit pentru obţinerea unui rezultat nu este cel corespunzător. Un astfel de obiect oferă posibilitatea investigării coloanei asupra căreia s-a produs eroarea, dacă aceasta corespunde unei operaţii de scriere sau de citire, câţi octeţi ar fi trebuit transferaţi şi câţi au fost transferaţi efectiv.

Gestiunea Tranzacțiilor

Java Database Connectivity permite ca mai multe instrucțiuni SQL care nu produc un rezultat de tip ResultSet să poată fi executate împreună în mod atomic:

În situația în care vor fi incluse instrucțiuni SQL ce produc rezultate de tip ResultSet sau în cazul în care una dintre operaţiile tranzacţiei nu a fost executată cu succes, se va genera excepţia BatchUpdateException.
Prin urmare, interogările incluse în cadrul unei tranzacţii pot fi DDL (CREATE TABLE, ALTER TABLE, DROP TABLE), respectiv DML (INSERT, UPDATE, DELETE).

Metoda executeBatch() întoarce un vector care conţine numărul operaţiilor de tip actualizare realizate cu succes. După metoda executeBatch() aplicată unui obiect interogare, se apelează şi metoda commit(), astfel încât modificările să fie vizibile în cadrul bazei de date. Este important ca la începutul tranzacţiilor să se apeleze Connection.setAutoCommit(false) pentru a nu se produce modificări în baza de date până când acest lucru nu este specificat explicit prin metoda commit(). La sfârşitul tranzacţiilor se poate restabili comportamentul implicit (în care fiecare instrucţiune este considerată ca fiind o singură tranzacţie), apelându-se Connection.setAutoCommit(true).

dbConnection.setAutoCommit(false);
Statement statement = dbConnection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                                                   ResultSet.CONCUR_UPDATABLE);	
for (ArrayList<String> row:table) {
    String query = "INSERT INTO book VALUES (";
    for(String column: row)
        query += column+",";
    query+= ")";
    dbConnection.addBatch(query);
}
int[] result = statement.executeBatch();
dbConnection.commit();
dbConnection.setAutoCommit(true);

Utilizarea tranzacţiilor este şi un mecanism prin care este menţinută integritatea datelor, în contextul accesului concurent. Astfel, în timpul execuţiei unei tranzacţii, sunt specificate drepturile de acces la nivelul tabelei pentru alţi utilizatori care doresc să opereze pe acelaşi set de date. Acestea pot fi specificate prin metoda setTransactionInsolation() aplicabilă unui obiect de tip Connection.

Nivel Izolare Tranzacţii Citiri murdare Citiri ne-repetabile Citiri fantomă
TRANSACTION_NONE nu N/A N/A N/A
TRANSACTION_READ_UNCOMMITTED da permise permise permise
TRANSACTION_READ_COMMITTED da prevenite permise permise
TRANSACTION_REPEATABLE_READ da prevenite prevenite permise
TRANSACTION_SERIALIZABLE da prevenite prevenite prevenite
Unele drivere JDBC nu implementează toate nivelurile de izolare a unei tranzacţii. În acest sens, se foloseşte metoda DatabaseMetaData.supportsTransactionIsolationLevel() spre a se verifica dacă este suportat nivelul în cauză.
De obicei, nu trebuie modificat nivelul de izolare implicit, care este definit pentru fiecare sistem de gestiune pentru baze de date în parte.

Se consideră o citire “murdară” (eng. dirty read) o valoare a unui atribut care a fost actualizat dar pentru care nu s-a făcut încă commit() pentru că există posibilitatea de a se reveni la valorile de dinaintea tranzacţiei.

O citire ne-repetabilă este aceea în care două tranzacţii, A şi B operează asupra aceleiaşi înregistrări (una pentru citire, una pentru scriere) şi în care valorile furnizate sunt diferite.

Similar, o citire fantomă se obţine în situaţia în care obţinerea rezultatelor presupune satisfacerea unei condiţii ce este îndeplinită ca urmare a actualizării astfel că o nouă interogare va furniza mai multe valori.

În contextul tranzacţiilor, se poate salva starea bazei de date înaintea realizării unor modificări, astfel încât dacă produc efecte nedorite la nivelul informaţiilor din tabele, să se poată reveni la informaţiile anterioare:

SavePoint state = dbConnection.setSavePoint();
...
dbConnection.rollback(state);

Metoda rollback() încheie tranzacţia curentă, astfel încât aceasta va fi apelată întotdeauna la sfârşitul tranzacţiei. De regulă, o astfel de operaţie trebuie utilizată numai atunci când s-a generat o excepţie SQLException în tranzacţia curentă, astfel încât nu se poate garanta care sunt valorile care au fost stocate în baza de date.

O stare a bazei de date salvată poate fi eliminată din cadrul tranzacţiei folosind metoda releaseSavePoint() a obiectului de tip Connection corespunzător.

Alternative la Manipularea Informațiilor din Baza de Date

Java Database Connectivity permite utilizarea unor obiecte de tip RowSet, derivate din ResultSet, care oferă programatorilor posibilitatea de a accesa datele mai uşor, având comportament de componente JavaBeans.

Comportamentul unor componente JavaBeans se referă la accesarea atributelor ca proprietăţi precum şi la mecanismul de notificare, de fiecare dată când se modifică poziţia cursorului, când sunt executate operaţii de adăugare, modificare, ştergere la nivelul unei înregistrări dar şi atunci când se modifică conţinutul obiectului respectiv. Aceste notificări sunt transmise tuturor obiectelor RowSetListener care au fost asociate obiectului de tip RowSet.

Astfel de obiecte sunt considerate conectate sau deconectate de la sursa de date, după cum menţin conexiunea (printr-un “driver”) la baza de date pe parcursul ciclului de viaţă:

  • un tip de obiect conectat este JdbcRowSet, care oferă o funcţionalitate asemănătoare cu ResultSet;
  • tipuri de obiecte deconectate sunt CachedRowSet, WebRowSet, JoinRowSet şi FilteredRowSet care se vor conecta la sursa de date doar pentru operaţii de citire şi de scriere, situaţie în care vor trebui să verifice şi conflictele care pot apărea; acestea au şi proprietatea că sunt serializabile ceea ce le face ideale pentru a fi transmise prin intermediul unei reţele de calculatoare.
Este recomandată folosirea obiectelor de tip RowSet atunci când sistemele de gestiune a bazelor de date nu implementează funcţionalitatea de parcurgere sau actualizare a obiectelor de tip ResultSet, capabilităţi de care aceste clase dispun în mod implicit.

Clasa JdbcRowSet

Obiectele JdbcRowSet pot fi create folosind un obiect ResultSet, Connection, utilizând un constructor implicit sau dintr-o instanţă a clasei RowSetFactory.

În toate aceste cazuri se va folosi clasa JdbcRowSetImpl.

  1. Statement statement = dbConnection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                                                       ResultSet.CONCUR_UPDATABLE);
    ResultSet result = statement.executeQuery("SELECT * FROM book");
    JdbcRowSet jdbcRowSet = new JdbcRowSetImpl(result);

    Obiectul de tip JdbcRowSet este echivalent cu cel de tip ResultSet, având acelaşi conţinut. În cazul când interogarea ar fi fost creată cu parametrii impliciţi obiectul corespunzător nu ar fi putut fi nici parcurs, nici actualizat.

  2. JdbcRowSet jdbcRowSet = new JdbcRowSetImpl(dbConnection);
    jdbcRowSet.setCommand("SELECT * FROM book");
    jdbcRowSet.execute();

    Obiectul de tip JdbcRowSet nu conţine nici un fel de date până la momentul când nu îi este asociată o instrucţiune SQL prin metoda setCommand, apelată prin metoda execute(). Implicit, un astfel de obiect poate fi parcurs, iar informaţiile din el pot fi actualizate. Astfel de comportamente pot fi specificate însă şi explicit.
    Metoda execute() realizează conexiunea cu baza de date folosind parametrii conexiunii respective, execută interogarea aferentă proprietăţii command și gestionează stochează informaţiile din obiectul ResultSet reţinut în obiectul de tip JdbcRowSet.

  3. JdbcRowSet jdbcRS = new JdbcRowSetImpl();
    jdbcRS.setURL("jdbc:mysql://localhost:3306/bookstore");
    jdbcRS.setUsername(username);
    jdbcRS.setPassword(password);
    jdbcRS.setCommand("SELECT * FROM book");
    jdbcRS.execute();

    Pentru fiecare obiect JdbcRowSet se pot stabili proprietăţile (url, username, password, dataSourceName). O interogare se poate specifica folosind metoda setCommand(), iar execuţia se face folosind execute(), ca şi în cazul ResultSet.

  4. RowSetFactory rowSetFactory = RowSetProvider.newFactory();
    JdbcRowSet jdbcRowSet = rowSetFactory.createJdbcRowSet();
    jdbcRowSet.setURL("jdbc:mysql://localhost:3306/bookstore");
    jdbcRowSet.setUsername(username);
    jdbcRowSet.setPassword(password);
    jdbcRowSet.setCommand("SELECT * FROM book");
    jdbcRowSet.execute();

    Obiectul de tip RowSetFactory utilizează implementarea implicită, însă dacă driverul JDBC pune la dispoziţie o implementare proprie, aceasta poate fi utilizată ca parametru transmis metodei newFactory(). Interfaţa RowSetFactory conţine metode spre a crea diferite implementări RowSet: createJdbcRowSet(), createCachedRowSet(), createFilteredRowSet(), createJoinRowSet(), createWebRowSet().

Un obiect JdbcRowSet creat folosind constructorul implicit va avea următoarele proprietăţi:

  • type: ResultSet.TYPE_SCROLL_INSENSITIVE (poate fi parcurs);
  • concurrency: ResultSet.CONCUR_UPDATABLE (poate fi actualizat);
  • escapeProcessing: true (poate fi definită o sintaxă care marchează faptul că există un tip de cod care va fi procesat de baza de date);
  • maxRows: 0 (nu există limită cu privire la numărul de înregistrări);
  • maxFieldSize: 0 (nu există limită cu privire la numărul de octeţi pentru memorarea valorii unui atribut – aplicabil doar pentru atribute de tip BINARY, VARBINARY, LONGVARBINARY, CHAR, VARCHAR şi LONGVARCHAR);
  • queryTimeout: 0 (nu există nici o limită impusă referitoare la timpul de execuţie al interogării);
  • showDeleted: false (înregistrările şterse nu sunt vizibile);
  • transactionIsolation: Connection.TRANSACTION_READ_COMMITTED (pot fi citite numai datele pentru care s-a apelat metoda commit());
  • typeMap: null (tipul asocierii unui obiect Connection folosit de acest obiect de tip RowSet este null).

Modul în care pot fi manipulate datele folosind un obiect de tip JdbcRowSet este similar cu cel al obiectelor de tip ResultSet, metodele respective având aceeaşi sintaxă.

Clasa CachedRowSet

Interfaţa CachedRowSet desemnează modul de operare deconectat, din ea fiind derivate interfeţele FilteredRowSet, JoinRowSet şi WebRowSet. Denumirea sa indică faptul că datele sunt reţinute într-o zonă de memorie astfel încât procesările se fac pe acestea în loc de informaţiile reţinute în baza de date. Crearea unui obiect de tip CachedRowSet se face folosind constructorul implicit (CachedRowSetImpl) sau folosind o instanţă a RowSetFactory.

Un obiect CachedRowSet conţine implementarea implicită a SyncProvider ce este un obiect de tip RIOptimisticProvider. Acesta oferă un obiect RowSetReader şi un obiect RowSetWriter care sunt necesare atunci când trebuie citite sau scrise informaţii din sursa de date. Modul în care operează este transparent.

În cazul în care obiectul va fi folosit pentru actualizarea informaţiilor din baza de date şi se doreşte ca modificările să fie operate şi la nivelul acesteia, trebuie specificate coloanele ce identifică în mod unic setul de înregistrări respective, indicând indexul atributelor corespunzătoare

int[] keys = {1};
cachedRowSet.setKeyColumns(keys);

Obiectul CachedRowSet nu este populat până nu este apelată metoda execute(), moment în care obiectul RowSetReader asociat realizează coneciunea la baza de date folosind proprietăţile corespunzătoare (url, username, password, dataSourceName), executând interogarea specificată în proprietatea command. După ce sunt obţinute înregistrările necesare, conexiunea este închisă.

Pentru actualizarea informaţiilor reţinute (adăugare, modificare, ştergere) trebuie apelată metoda acceptChanges() pentru ca procesările să fie vizibile la nivelul sursei de date. În acest moment, obiectul RowSetWriter deschide conexiunea cu baza de date în care realizează operaţiile respective după care conexiunea este închisă. În cazul în care este detectat un conflict (informaţiile din sursa de date au fost actualizate între timp de un alt proces), este utilizată implementarea RIOptimisticProvider a clasei SyncProvider care foloseşte un model de concurenţă optimist, ce presupune faptul că nu vor exista conflicte sau că numărul acestora va fi redus. În situaţia în care nu sunt detectate conflicte, noile informaţii sunt transferate către baza de date, în cazul că există, actualizările sunt ignorate. Totuşi, există şi posibilitatea ca în cazul identificării unor conflicte, utilizatorul să poată selecta valorile reţinute în baza de date:

try {
    cachedRowSet.acceptChanges();
} catch (SyncProviderException syncProviderException ) {
    SyncResolver syncResolver = syncProviderException.getSyncResolver();
    while (syncResolver .nextConflict()) {
        if (syncResolver.getStatus() == SyncResolver.UPDATE_ROW_CONFLICT) {
            int conflictedRow = syncResolver.getRow();
            cachedRowSet.absolute(conflictedRow);
            int numberOfAttributes = cachedRowSet.getMetaData().getColumnCount();
            for (int index = 1; index <= numberOfAttributes; index++) {
                if (syncResolver.getConflictValue(index) != null) {
                    Object cachedRowSetValue = cachedRowSet.getObject(index);
                    Object resolverValue = syncResolver.getConflictValue(index);
                    // ...
                    syncResolver.setResolvedValue(index,...);
                }
             }
         }
     }
}

În cazul în care au fost detectate conflicte, metoda acceptChanges() generează o excepţie de tipul SyncProviderException care pune la dispoziţie obiectul SyncResolver, un iterator pe conflictele identificate. De fapt, acesta este un obiect RowSet care conţine doar valorile conflictuale ale unei înregistrări, restul atributelor având valoarea null. Totodată, dispune de metode precum getStatus() prin care se verifică tipul conflictului, getRow() ce identifică indexul înregistrării la care se găseşte conflictul şi getConflictedValue() ce reţine valoarea care a fost actualizată anterior şi marcată ca atare în baza de date.

Actualizările dintr-un obiect de tip CachedRowSet pot fi notificate către alte obiecte care implementează interfaţa RowSetListener, ceea ce presupune definirea metodelor:

  • cursorMoved() – defineşte comportamentul obiectului ascultător în cazul când se produc modificări în privinţa cursorului obiectului CachedRowSet;
  • rowChanged() – defineşte comportamentul obiectului ascultător în cazul când unul sau mai multe atribute dintr-o înregistrare sunt modificate, când este adăugată sau ştearsă o înregistrare din obiectul CachedRowSet;
  • rowSetChanged() – defineşte comportamentul obiectului ascultător în cazul când obiectul CachedRowSet este populat cu informaţii.

Un obiect ascultător poate fi asociat unui set de date CachedRowSet prin metoda addRowListener(). Oprirea notificărilor se face prin removeRowListener().

Clasa FilteredRowSet

Obiectele de tip FilteredRowSet oferă posibilitatea de a limita numărul de înregistrări vizibile conform unui criteriu şi de a selecta informaţiile ce pot fi consultate fără a realiza conexiuni la baza de date şi fără a opera modificări la nivelul interogării asociate.

Criteriul indicând înregistrările dintr-un obiect FilteredRowSet care vor fi vizibile este precizat printr-o clasă ce implementează interfaţa Predicate, precizând numele sau indexul coloanei după care se face filtrarea şi limitele între care trebuie să se găsească valorile. Clasa va trebui să definească metodele evaluate() (primind o valoare de comparat şi numele sau indexul coloanei sau un obiect de tip RowSet).

Asocierea unui filtru (criteriu) pentru un obiect FilteredRowSet se face prin metoda setFilter() care primeşte ca argument clasa care defineşte condiţiile respective. Filtrarea propriu-zisă are loc atunci când este apelată metoda next(), ducând la execuţia metodei evaluate() corespunzătoare. Există posibilitatea apelării mai multor filtre succesive prin apelarea metodei setFilter() de mai multe ori, după ce anterior s-a produs selecţia valorilor dorite prin metoda next(). De asemenea, eliminarea tuturor filtrelor asociate se face apelând metoda setFilter() cu parametrul null.

PriceFilter.java
public class PriceFilter implements Predicate {
 
    private int lowValue, highValue;
    private String attributeName = null;
    private int attributeIndex = -1;
 
    public PriceFilter(int lowValue, int highValue, String attributeName) {
        this.lowValue = lowValue;
        this.highValue = highValue;
        this.attributeName = attributeName;
    }
    public PriceFilter(int lowValue, int highValue, int attributeIndex) {
        this.lowValue = lowValue;
        this.highValue = highValue;
        this.attributeIndex = attributeIndex;
    }
    public boolean evaluate(Object value, String attributeName) {
        boolean result = true;
        if (attributeName.equalsIgnoreCase(this.attributeName)) {
            int attributeValue = ((Integer)value).intValue();
            if (attributeValue >= this.lowValue && attributeValue <= this.highValue)
                return true;
            return false;
        }
        return result;
    }
    public boolean evaluate(Object value, int attributeIndex) {
        boolean result = true;
        if (attributeIndex == this.attributeIndex)) {
            int attributeValue = ((Integer)value).intValue();
            if (attributeValue >= this.lowValue && attributeValue <= this.highValue)
                return true;
            return false;
        }
        return result;
    }
    public boolean evaluate (RowSet rowSet) {
        boolean result = false;
        CachedRowSet cachedRowSet = (CachedRowSet)rowSet;
        int attributeValue = -1;
        if (this.attributeName != null)
            attributeValue = cachedRowSet.getInt(this.attributeName);
        else if (this.attributeIndex > 0))
            attributeValue = cachedRowSet.getInt(this.attributeIndex);
        else
            return false;
        if (attributeValue >= this.lowValue && attributeValue <= this.highValue)
            result = true;
        return result;
    }
}

Operaţiile de adăugare, modificare sau ştergere sunt permise numai dacă acestea nu contravin filtrelor asociate obiectului de tip FilteredRowSet.

Clasa JoinRowSet

Obiectele de tip JoinRowSet permit realizarea operaţiei de asociere (JOIN) între obiecte RowSet care nu sunt conectate la sursa de date, astfel încât sunt economisite resursele necesare realizării uneia sau mai multor conexiuni.

Crearea unui obiect de tip JoinRowSet se face prin constructorul implicit JoinRowSetImpl (există şi implementări specifice anumitor drivere JDBC, însă este posibil ca acestea să aibă denumiri şi comportamente diferite faţă de standard). Acesta nu va conţine nici un fel de date până când nu sunt adăugate obiecte RowSet, specificându-se totodată şi atributul care serveşte drept legătură (cheie străină) în setul de date respectiv. Acest lucru se face prin metoda addRowSet() care primeşte ca parametru un obiect RowSet şi indexul sau denumirea coloanei care indică relaţia între tabele. De asemenea, trebuie specificat şi tipul de asociere (JOIN) care se va realiza între tabele. Implicit, acesta este INNER_JOIN, însă metoda setJoinType() poate primi drept parametri şi următoarele tipuri: CROSS_JOIN, FULL_JOIN, LEFT_OUTER_JOIN, RIGHT_OUTER_JOIN. Alternativ, la crearea unui obiect RowSet care implementează interfaţa Joinable, pot fi precizate atributele care vor fi utilizate la realizarea asocierii prin metoda setMatchColumn(), astfel încât atunci când sunt adăugate la JoinRowSet nu mai este necesară şi specificarea acestei proprietăţi. Asocierea obţinută va conţine toate atributele seturilor de date din care este formată, astfel încât selectarea anumitor coloane se face “manual”, parcurgând obiectul JoinRowSet cu afişarea valorilor dorite.

Pentru exemplul folosit, dacă se doreşte vizualizarea facturilor precum şi a conţinutului acestora, se poate folosi un obiect JoinRowSet:

CachedRowSet invoices = new CachedRowSetImpl();
invoices.setURL("jdbc:mysql://localhost:3306/bookstore");
invoices.setUsername(username);
invoices.setPassword(password);
invoices.setCommand("SELECT * FROM invoice");
invoices.setMatchColumn("id");
invoices.execute();
CachedRowSet invoices_details = new CachedRowSetImpl();
invoices_details.setURL("jdbc:mysql://localhost:3306/bookstore");
invoices_details.setUsername(username);
invoices_details.setPassword(password);
invoices_details.setCommand("SELECT * FROM invoice_detail");
invoices_details.setMatchColumn("invoice_id");
invoices_details.execute();
JoinRowSet joinRowSet = new JoinRowSetImpl();
joinRowSet.addRowSet(invoices);
joinRowSet.addRowSet(invoices_details);

Clasa WebRowSet

Un obiect WebRowSet are capabilitatea de a fi reţinut ca document XML şi totodată de a fi obţinut din acest format. Întrucât limbajul XML este folosit ca standard, mai ales în comunicaţiile între organizaţii, folosind servicii web, obiectul WebRowSet răspunde unor necesităţi reale.

Crearea unui obiect WebRowSet se face folosind constructorul implicit WebRowSetImpl. Acesta va dispune de un obiect SyncProvider care, spre diferenţă de implementarea standard, va avea asociat un obiect RIXMLProvider pentru a defini comportamentul în cazul unui conflict.

Descărcarea unui obiect WebRowSet ca document XML se face fie folosind un obiect OutputStream (caz în care scrierea se face la nivel de octeţi, suportând mai multe tipuri de date) sau un obiect Writer (caz în care scrierea se face la nivel de caractere).

java.io.FileOutputStream fileOutputStream = new java.io.FileOutputStream("genres.xml");
genres.writeXml(fileOutputStream);
java.io.FileWriter fileWriter = new java.io.FileWriter("genres.xml");
genres.writeXml(fileWriter);

De asemenea, există posibilitatea populării dintr-un obiect ResultSet înainte de reţinerea ca fişier XML:

genres.writeXml(resultSet, fileOutputStream);
genres.writeXml(resultSet, fileWriter);

Similar, încărcarea conţinutului unui document XML într-un obiect WebRowSet se face fie folosind un obiect InputStream, fie folosind un obiect Reader:

java.io.FileInputStream fileInputStream = new java.io.FileOutputStream("genres.xml");
genres.readXml(fileOutputStream);
java.io.FileReader fileReader = new java.io.FileReader("genres.xml");
genres.readXml(fileReader);

Documentele XML asociate obiectelor de tip WebRowSet conţin:

  • proprietăţi, specificate în secţiunea <properties> … </properties>;
  • metadate, specificate în secţiunea <metadata> … </metadata>, care conţin structura tabelei:
    • <column-count> indică numărul atributelor;
    • un număr corespunzător de secţiuni <column-definition> cu proprietăţile lor;
  • datele propriu-zise (se reţin atât valorile originale (cele care corespund celei mai recente consultări a bazei de date), cât şi valorile actualizate, astfel încât conflictele să poată fi detectate cu uşurinţă), în secţiunea <data> … </data> care conţine, pentru fiecare înregistrare preluată din baza de date
    • o secţiune <currentRow> … </currentRow>
    • un număr adecvat de elemente <columnValue> … </columnValue>
      • în cazul în care o valoare este modificată, ea este urmată de o secţiune <updateValue>;
      • înregistrările adăugate, respectiv şterse sunt marcate prin secţiuni <insertRow>, respectiv <deleteRow>.
Structura documentului XML nu contează pentru utilizator, căci metodele writeXml() / readXml() operează în mod transparent.

Activitate de Laborator

aipi2014-lab02-eclipse.zip (actualizat 22.10.2014, 18:40)

aipi2014-lab02-netbeans.zip (actualizat 22.10.2014, 18:40)

Rezolvarea fiecarui exercitiu va fi realizata ca metoda distincta (denumita exerciseX()) in clasa Bookstore din pachetul ro.cs.pub.aipi.main, unde X reprezinta numarul exercitiului (X = 02 … 11).
Se poate rula suita de teste unitare AllTests din directorul tests (pachetul ro.pub.cs.aipi.main), sau se poate defini metoda main() in clasa Bookstore din pachetul ro.pub.cs.aipi.main apelandu-se succesiv metodele exerciseX().

Se doreşte proiectarea unei aplicaţii care exploatează o bază de date ce urmează să fie integrată în cadrul unui sistem ERP pentru o librărie care comercializează doar cărţi. Se va porni de la schema conceptuală, respectiv de la structura bazei de date construite în cadrul laboratorului anterior.

  1. [0 puncte] În clasa Constants din pachetul ro.pub.cs.aipi.general, să se modifice valoarea constantei DATABASE_PASSWORD cu cea corespunzătoare sistemului de gestiune pentru baze de date instalat pe maşina proprie.
    • Daca nu se folosesc testele unitare, in MySQL Workbench, să se execute script-ul Laborator2l.sql (din directorul scripts) unde se crează structura şi diferite obiecte ale bazei de date bookstore;
    • Daca se folosesc testele unitare, nu sunt necesare alte configurari, intrucat inainte de rularea suitei de teste, se va instala baza de date in mod automat.
  2. [10 puncte] Să se implementeze metoda getTableNumberOfRows() din clasa DataBaseWrapperImplementation a pachetului ro.pub.cs.aipi.dataaccess, ce determină numărul de înregistrări al unei tabele identificata prin denumire (transmisa ca parametru).
    public int getTableNumberOfRows(String tableName) throws SQLException;
  3. [20 puncte] Folosind metoda getTableContent(), să se creeaze un fişier books.txt in directorul output care sa contina lista tuturor cărţilor disponibile în librărie (existente pe stoc).
    Pentru fiecare carte în parte se va preciza identificatorul, numele şi prenumele autorilor (in situatia in care o carte nu are un autor, se va folosi sirul de caractere * * *), titlul, editura precum şi anul apariţiei, separate prin caracterul TAB.
  4. [10 puncte] Să se adauge în tabela user o înregistrare ale cărei atribute sunt introduse de la tastatură.
    Pentru a citi o valoare de la tastatură în Java, se poate folosi următorul cod sursă:
    Scanner scanner = new Scanner(System.in);  
    value = scanner.nextLine();
    // ...
    scanner.close();
  5. [10 puncte] Pe baza metodei updateRecordsIntoTable(), să se modifice toate comenzile de aprovizionare către editura Harlequin (id = 50, registered_number = 510636670) astfel încât cantităţile să fie crescute cu 20%.
    Modificările nu vor fi operate decât asupra comenzilor care au fost deja livrate (state='delivered').
  6. [20 puncte] Să se implementeze metoda deleteRecordsFromTable() din clasa DataBaseWrapperImplementation a pachetului ro.pub.cs.aipi.dataaccess ce elimină dintr-o tabelă identificată prin denumire (transmisa ca parametru) acele înregistrări care au anumite valori corespunzătoare unor atribute sau care respectă o anumită condiţie.
    public void deleteRecordsFromTable(String tableName, ArrayList<String> attributes, ArrayList<String> values, String whereClause) throws SQLException, DataBaseException

    Dacă attributes = (attribute1, …, attributen), respectiv values = (value1, …, valuen), atunci vor fi şterse înregistrările pentru care attribute1=value1 AND … AND attributen=valuen sau pentru care este îndeplinită condiţia whereClause. În cazul în care sunt precizate attributes / values, parametrul whereClause va fi ignorat.
    Folosind metoda deleteRecordsFromTable() astfel implementată să se şteargă acele edituri care nu au cărţi comercializate de librarie.

  7. [10 puncte] Folosind procedura stocată calculate_user_total_invoice_value să se determine lista utilizatorilor împreună cu valoarea totala a facturilor asociate fiecăruia. Informatiile vor fi plasate sub forma cod numeric personal TAB valoarea totala a facturilor, cate o informatie pe un rand, in fisierul user_total_invoce_value.txt din directorul output.
  8. [10 puncte] Folosind funcţia stocată calculate_supply_order_value să se determine lista editurilor către care s-au efectuat cele mai mari plăţi. Informatiile vor fi plasate sub forma denumire factura TAB valoarea totala a comenzilor de aprovizionare, cate o informatie pe un rand, in fisierul publishing_house_total_supply_order_value.txt din directorul output, realizandu-se o ordonare descrescatoare in functie de valoarea totala a comenzilor de aprovizionare.
  9. [10 puncte] Să se implementeze metoda getReferrences() din clasa DataBaseWrapperImplementation a pachetului ro.pub.cs.aipi.dataaccess ce determină pentru o tabelă identificata prin denumire (transmisa ca parametru) care sunt tabelele pe care le referă, precum şi atributele care fac obiectul constrângerii de tip FOREIGN KEY. Metoda intoarce o lista de obiecte de tip Referrence care contine denumirea tabelei copil si atributul dependent din cadrul acesteia precum si denumirea tabelei parinte si atributul referit din cadrul acesteia.
    Pe baza acestei metode, sa se creeze fisierul foreign_key_constraints.txt in directorul output care contine toate constrangerile de tip cheie straina sub forma tabela_copil/atribut_copil referrences tabela_parinte/atribut_parinte, cate o inregistrare pe un rand.
  10. [10 puncte] Folosind un obiect de tip RowSet deconectat, să se afişeze cărţile comercializate de librarie (precizându-se titlul şi preţul lor), numele editurii care le-a publicat, numele colecţiei şi numele domeniului din care fac parte. Informatiile vor fi stocate in fisierul books_join.txt din directorul output sub forma identificator carte, titlu, pret, denumire editura, denumire colectie si denumire domeniu, separate intre ele prin TAB, cate o inregistrare pe linie.
  11. [10 puncte] Să se filtreze colecţia de date anterioară astfel încât să conţină numai cărţile având preţul cuprins între 1000 şi 5000. Informatiile vor fi stocate in fisierul books_filtered.txt din directorul output, formatul fiind acelasi ca in cazul precedent.
  12. [0 puncte]
    • Daca nu se folosesc testele unitare, in MySQL Workbench, să se execute script-ul Laborator2u.sql (din directorul scripts) unde se sterge baza de date bookstore impreuna cu toate obiectele sale;
    • Daca se folosesc testele unitare, nu sunt necesare alte configurari, intrucat dupa rularea suitei de teste, se va dezinstala baza de date in mod automat.

Resurse

Soluții

laboratoare/laborator02.txt · Last modified: 2014/10/31 04:14 by Andrei Roșu-Cojocaru
CC Attribution-Share Alike 4.0 International
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0