====== 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 ====== {{ :laboratoare:aipi2014-lab02-slides.pdf }} {{ :laboratoare:aipi2014-lab02-slides-3pp.pdf }} ===== 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: - conectarea (respectiv, deconectarea) la o sursă de date, cel mai frecvent o bază de date; - 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: - 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; - 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). - suita de teste JDBC oferă o serie de utilitare care verifică dacă "driverele" JDBC sunt compatibile cu o aplicaţie Java; - 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: - î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;\\ {{ :laboratoare:laborator02_figura01a.png?nolink&300 }} - î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.\\ {{ :laboratoare:laborator02_figura01b.png?nolink&300 }} 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: - 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; - 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; - 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; - 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 {{ :laboratoare:laborator02_figura02.png?nolink&600 }} 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: - un API JDBC responsabil de comunicaţia dintre aplicaţia Java şi modulul de gestiune al driver-ului; - 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; {{ :laboratoare:laborator02_figura03.png?nolink&700 }} 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: - [î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''; - deschiderea conexiunii la baza de date; - 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; - procesarea rezultatelor obţinute cu propagarea modificărilor realizate înapoi în baza de date; - î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 [[http://www.mysql.com/downloads/connector/j/|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 .java aipi2014@ubuntu:~$ javac -classpath .:mysql-connector-java-5.1.33-bin.jar .java * rulare: C:\Users\Aipi2014> java -classpath .;mysql-connector-java-5.1.33-bin.jar .java aipi2014@ubuntu:~$ java -classpath .:mysql-connector-java-5.1.33-bin.jar .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 Path// → //Add to Build Path//\\ {{ :laboratoare:laborator02_figura04_eclipse01.png?nolink&500 }} * în cazul în care arhiva .jar nu există în structura proiectului: click dreapta pe numele proiectului → //Build Path// → //Add External Libraries//\\ {{ :laboratoare:laborator02_figura04_eclipse02.png?nolink&500 }} * 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//\\ {{ :laboratoare:laborator02_figura04_eclipse03.png?nolink&300 }} * NetBeans 8.0.1 * din meniul corespunzător proiectului, se selectează //Libraries//, click dreapta şi se accesează opţiunea //Add JAR/Folder//\\ {{ :laboratoare:laborator02_figura04_netbeans01.png?nolink&300 }} * î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\\ {{ :laboratoare:laborator02_figura04_netbeans02.png?nolink&600 }} * 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//\\ {{ :laboratoare:laborator02_figura04_netbeans03.png?nolink&300 }} 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: - ''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; - ''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, username, password); * 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: |< 100% >| ^ **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_NAME'' – ''SYSTEM'', ''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: - o descriere a erorii care poate fi obţinută din metoda ''getMessage()'' a obiectului de tip eroare asociat; - 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; - 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(); } - 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: * void addBatch(String sql) throws SQLException * void clearBatch() throws SQLException * int[] executeBatch() throws SQLException Î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 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''. |< 100% >| ^ **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''. - 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. - 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''. - 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''. - 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''. 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 '' ... ''; * metadate, specificate în secţiunea '' ... '', care conţin structura tabelei: * '''' indică numărul atributelor; * un număr corespunzător de secţiuni '''' 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 '' ... '' care conţine, pentru fiecare înregistrare preluată din baza de date * o secţiune '' ... '' * un număr adecvat de elemente '' ... '' * în cazul în care o valoare este modificată, ea este urmată de o secţiune ''''; * înregistrările adăugate, respectiv şterse sunt marcate prin secţiuni '''', respectiv ''''. Structura documentului XML nu contează pentru utilizator, căci metodele ''writeXml()'' / ''readXml()'' operează în mod transparent. ===== Activitate de Laborator ===== {{ :laboratoare:aipi2014-lab02-eclipse.zip }} (actualizat 22.10.2014, 18:40) {{ :laboratoare: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. - [**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. - [**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; - [**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. - [**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(); - [**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'''). - [**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 attributes, ArrayList 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. - [**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''. - [**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. - [**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 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. - [**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. - [**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 ====== [[http://docs.oracle.com/javase/tutorial/jdbc/index.html|Java Tutorial – JDBC(TM) Database Access]] ===== Soluții ===== [[https://github.com/aipi2014/Laborator02/tree/master/Eclipse/|Proiect Eclipse]]\\ [[https://github.com/aipi2014/Laborator02/tree/master/NetBeans/|Proiect NetBeans]]