Table of Contents
Laborator 01
Gestiunea informațiilor dintr-o bază de date MySQL prin JDBC (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:
- 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
şijavax.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; pachetelejavax.naming
şijavax.sql
oferă posibilitatea realizării unei conexiuni către o sursă de date (obiect de tipDataSource
) î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;
- î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:
- 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
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;
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 tipNoClassDefFoundException
;
- 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.
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.
%System Root%\Program Files [(x86)]\MySQL\Connector.J 5.1
.
În linie de comandă, acest lucru poate fi realizat astfel:
- compilare:
C:\Users\Aipi2015> javac -classpath .;mysql-connector-java-5.1.37-bin.jar <nume_fisier>.java student@aipi2015:~$ javac -classpath .:mysql-connector-java-5.1.37-bin.jar <nume_fisier>.java
- rulare:
C:\Users\Aipi2015> java -classpath .;mysql-connector-java-5.1.37-bin.jar <nume_fisier>.java student@aipi2015:~$ java -classpath .:mysql-connector-java-5.1.37-bin.jar <nume_fisier>.java
Mai uşor, se pot folosi medii integrate de dezvoltare a aplicaţiilor.
Maven
Proiectul poate fi integrat cu Apache Maven, un utilitar automat prin care se descriu mecanismele prin care aplicațiile Java sunt compilate, instalate, rulate și testate dar și dependențele de diferite biblioteci.
Documentul în care sunt descrise proprietățile proiectului se numește pom.xml
și trebuie plasat în rădăcina sistemului de fișiere.
- pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ro.pub.cs.aipi.lab01</groupId> <artifactId>01-BookStore-JDBC</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>01-BookStore-JDBC</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</version> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency> </dependencies> </project>
Bibliotecile descrise ca dependențe ale proiectului (în cazul de față, mysql-connector-java
, versiunea 5.1.37, respectiv junit
, versiunea 4.4, doar pentru testarea aplicației Java) sunt descărcate în mod automat și referite în calea acestuia.
Diferite medii integrate de dezvoltare vor afișa bibliotecile descărcate în secțiuni ale proiectului de tipul Dependencies.
- Eclipse Mars (4.5)
- NetBeans 8.0.2
Integrarea manuală a bibliotecilor dependente
Eclipse Mars (4.5)
- clasele conţinute în arhiva .jar trebuie adăugate la calea proiectului
- în cazul în care arhiva .jar nu se găseşte în structura proiectului:
- click dreapta pe numele bibliotecii → Build Path → Configure Build Path…
- în secțiunea Java Build Path a proprietăților proiectului, în panoul de prezentare Libraries, se accesează butonul Add JARs… (dacă biblioteca se găsește în sistemul de fișiere al proiectului), respectiv butonu Add External JARs… (dacă biblioteca nu se găsește în sistemul de fișiere al proiectului)
- se selectează arhiva .jar care conține biblioteca necesară
- în cazul în care arhiva .jar există în structura proiectului: click dreapta pe numele proiectului → Build Path → Add to Build Path
- dacă biblioteca a fost adăugată în mod corect, denumirea acesteia trebuie să apară în secțiunea Package Explorer
NetBeans 8.0.2
- 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
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, clasaDriverManager
apelează metode din interfaţaDriver
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 utilizatorulroot
cu parolaStudentAipi2015
) sunt:- MySQL
jdbc:mysql://[host][,failoverhost ...][:port]/[database][?propertyName1][=propertyValue1][&propertyName2][=propertyValue2]... jdbc:mysql://localhost:3306/bookstore?user=root&password=StudentAipi2015
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 parametrulhost
. - JavaDB
jdbc:derby:[subprotocol:][databaseName][;attribute=value]* jdbc:derby:bookstore;user=root;password=StudentAipi2015
- 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=StudentAipi2015;
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 connection = 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 connection = DriverManager.getConnection(DATABASE_CONNECTION, DATABASE_USERNAME, DATABASE_PASSWORD);
Connection connection = DriverManager.getConnection(DATABASE_CONNECTION + "?user=" + DATABASE_USERNAME + "&password=" + DATABASE_PASSWORD);
Properties connectionProperties = new Properties(); connectionProperties.put("user", username); connectionProperties.put("password", password); Connection connection = 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()
:
if (connection != null && !connection.isClosed()) { connection.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 = connection.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 = connection.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()
: întoarcetrue
dacă primul obiect al interogării este de tipulResultSet
; prin această metodă pot fi obţinute unul sau mai multe (sau nici un) obiect(e) de tipulResultSet
; obiectele de tipResultSet
pot fi accesate apelând metodagetResultSet()
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
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 tipINSERT
,UPDATE
,DELETE
, dar şi pentru instrucţiuni de tip DDL precumCREATE TABLE
,ALTER TABLE
,DROP TABLE
:String query = "CREATE TABLE category ( id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL, name VARCHAR(50) NOT NULL, description VARCHAR(1000), KEY (id) );"; int result = statement.executeUpdate(query); // ... String query = "INSERT INTO publishing_house (name, registered_number, description, postal_address, zip_code, country_id, internet_address) VALUES('Aardvark Global Publishing', '385231702', '-', 'Santa Cruz, Santa Cruz County, CA', 95061, 187, null);" 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.
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.
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ă metodacommit
; un astfel de comportament este necesar atunci când obiectele de tipResultSet
sunt folosite mai mult pentru citire decât pentru scriere;CLOSE_CURSORS_AT_COMMIT
– cursorul este închis după ce este apelată metodacommit()
; 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()
.
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 = connection.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.
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).
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 = connection.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 = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet result = statement.executeQuery("SELECT issue_date, state FROM invoice_header"); 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(); }
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 personal_identifier = ?"; 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, 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 = connection.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 = connection.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
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) şiDELETE
(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ă înnull
dacă valoarea cheii primare referite se schimbă);importedKeySetDefault
(valoarea cheii străine devine cea implicită în cazul modificării cheii primare);importedKeyRestrict
(la fel cuimportedNoAction
)).
- 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ţiaSQLException
; lanţul cauzal poate fi parcurs recursiv apelând metodagetCause()
până când este returnată o valoarenull
;Throwable cause = exception.getCause(); while (cause != null) { System.out.println("Cause: " + 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
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)
.
connection.setAutoCommit(false); Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); for (ArrayList<String> row: table) { StringBuilder query = new StringBuilder("INSERT INTO book VALUES ("); for(String column: row) { query.append(column + ","); } query.append(")"); connection.addBatch(query.toString()); } int[] result = statement.executeBatch(); connection.commit(); connection.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 |
DatabaseMetaData.supportsTransactionIsolationLevel()
spre a se verifica dacă este suportat nivelul în cauză.
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(); ... connection.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.
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 cuResultSet
; - tipuri de obiecte deconectate sunt
CachedRowSet
,WebRowSet
,JoinRowSet
şiFilteredRowSet
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.
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 = connection.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 tipResultSet
, 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 metodasetCommand
, apelată prin metodaexecute()
. 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.
Metodaexecute()
realizează conexiunea cu baza de date folosind parametrii conexiunii respective, execută interogarea aferentă proprietăţiicommand
și gestionează stochează informaţiile din obiectulResultSet
reţinut în obiectul de tipJdbcRowSet
.JdbcRowSet jdbcRowSet = new JdbcRowSetImpl(); jdbcRowSet.setURL("jdbc:mysql://localhost:3306/bookstore"); jdbcRowSet.setUsername(username); jdbcRowSet.setPassword(password); jdbcRowSet.setCommand("SELECT * FROM book"); jdbcRowSet.execute();
Pentru fiecare obiect
JdbcRowSet
se pot stabili proprietăţile (url
,username
,password
,dataSourceName
). O interogare se poate specifica folosind metodasetCommand()
, iar execuţia se face folosindexecute()
, ca şi în cazulResultSet
.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 metodeinewFactory()
. InterfaţaRowSetFactory
conţine metode spre a crea diferite implementăriRowSet
: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 tipBINARY
,VARBINARY
,LONGVARBINARY
,CHAR
,VARCHAR
şiLONGVARCHAR
);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 metodacommit()
);typeMap
:null
(tipul asocierii unui obiectConnection
folosit de acest obiect de tipRowSet
estenull
).
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 obiectuluiCachedRowSet
;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 obiectulCachedRowSet
;rowSetChanged()
– defineşte comportamentul obiectului ascultător în cazul când obiectulCachedRowSet
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 invoice_headers = new CachedRowSetImpl(); invoice_headers.setURL("jdbc:mysql://localhost:3306/bookstore"); invoice_headers.setUsername(username); invoice_headers.setPassword(password); invoice_headers.setCommand("SELECT * FROM invoice_header"); invoice_headers.setMatchColumn("id"); invoice_headers.execute(); CachedRowSet invoice_lines = new CachedRowSetImpl(); invoice_lines.setURL("jdbc:mysql://localhost:3306/bookstore"); invoice_lines.setUsername(username); invoice_lines.setPassword(password); invoice_lines.setCommand("SELECT * FROM invoice_line"); invoice_lines.setMatchColumn("invoice_header_id"); invoice_lines.execute(); JoinRowSet joinRowSet = new JoinRowSetImpl(); joinRowSet.addRowSet(invoice_headers); joinRowSet.addRowSet(invoice_lines);
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("category.xml"); categories.writeXml(fileOutputStream);
java.io.FileWriter fileWriter = new java.io.FileWriter("category.xml"); categories.writeXml(fileWriter);
De asemenea, există posibilitatea populării dintr-un obiect ResultSet
înainte de reţinerea ca fişier XML:
categories.writeXml(resultSet, fileOutputStream); categories.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("category.xml"); categories.readXml(fileOutputStream);
java.io.FileReader fileReader = new java.io.FileReader("category.xml"); categories.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>
.
writeXml()
/ readXml()
operează în mod transparent.
Activitate de Laborator
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ă cărţi. Se va porni de la schema conceptuală, respectiv de la structura bazei de date construite în cadrul laboratorului anterior.
Proiectul poate fi dezvoltat folosind Apache Maven, fiind necesar să se precizeze configurațiile de rulare în mediile integrate de dezvoltare, în acest scop.
Eclipse Mars (4.5)
Pentru acțiunea Run As → Maven Build disponibilă în meniul contextual al proiectului (click dreapta în secțiunea Package Explorer), se poate defini o configurație de rulare care va realiza simultan acțiunile de curățare a fișierelor generate, compilare și testare.
O configurație de rulare este caracterizată prin următoarele caracteristici (în panoul de prezentare Main):
- denumire (eng. Name);
- locație (eng. Base directory) - de regulă, referința se face relativ la spațiul de lucru, disponibil la
${workspace_loc:}
; - acțiunile care vor fi realizate (eng. Goals): pot fi specificate acțiuni precum
clean
,build
,compile
,install
sautest
, dacă este necesar); - diferiți parametri (definiți prin nume și valoare), în situația în care este necesar un control mai exact al comportamentului, în detrimentul celui implicit.
NetBeans 8.0.2
O configurație de rulare trebuie specificată în secțiunea Configurations din meniul contextual al proiectului (disponibil prin acțiunea click dreapta pe denumirea proiectului în panoul de prezentare Projects). Crearea unei configurații de rulare noi se face prin accesarea butonului Add….
Pentru fiecare configurație de rulare trebuie specificată o denumire unică (Configuration Id). Pentru ca proiectul să poată fi utilizat și pe alte platforme este necesar să NU fie selectată opțiunea Keep private to this IDE instance.
Este necesar să se indice scopurile asociate pentru fiecare acțiune corespunzătoare configurației de rulare, existând posibilitatea de a se preciza suplimentar și o serie de proprietăți.
-D<property_name>=<property_value>
.
Accesarea unei acțiuni a proiectului se face din meniul contextual al acestuia, executându-se, de fiecare dată, scopurile care au fost specificate anterior.
0. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la https://www.github.com/aipi2015/Laborator01. În urma acestei operații, directorul Laborator01
va trebui să conțină subdirectorul labtasks
, fișierele README.md
și LICENSE
.
student@aipi2015:~$ git clone https://www.github.com/aipi2015/Laborator01.git
Constants
din pachetul ro.pub.cs.aipi.lab01.general
, trebuie să se modifice valoarea constantei DATABASE_PASSWORD
care stochează parola de conectare pentru utilizatorul root
la sistemul de gestiune al bazei de date.
Proiectul este organizat sub forma unei suite de teste unitare, corespunzătoare fiecărui exercițiu în parte. În principiu, fiecare test unitar este independent, însă având în vedere faptul că unele dintre acestea modifică starea bazei de date, este recomandat ca exercițiile să fie rezolvate în ordine.
Pentru ca rezultatul testelor să fie același, structura și conținutul bazei de date sunt recreate de fiecare dată, curățându-se și rezultatul rulărilor anterioare (această operație poate dura o perioadă de timp mai îndelungată, în funcție de performanțele mașinii pe care se rulează).
În directorul input
sunt plasate fișierele de referință cu care se face comparația pentru rezultatul fiecărui test unitar. În directorul output
sunt plasate fișierele rezultate în urma rulării testelor unitare.
test
să se specifice proprietatea -Dtest=ro.pub.cs.lab01.main.AllTests
, pentru a se forța rularea suitei de teste unitare în detrimentul rulării fiecărui test unitar în parte. Aceasta asigură faptul că testele unitare vor fi executate în ordine, precum și realizarea operațiilor de încărcare / descărcare a bazei de date înainte de fiecare rulare.
1. Să se implementeze metoda getTableNumberOfRows()
din clasa DatabaseOperationsImplementation
a pachetului ro.pub.cs.aipi.lab01.dataaccess
, ce determină numărul de înregistrări al unei tabele identificată prin denumire (transmisa ca parametru).
public int getTableNumberOfRows(String tableName) throws SQLException;
<spoiler|Indicații de Rezolvare>
Nu uitați să eliberați resursele folosite în cadrul acestei metode (obiectul de tip Statement
). De regulă, se recomandă ca o astfel de operație să fie realizată pe ramura finally
a instrucțiunii try … catch
(în situația în care nu se folosește try-with-resources
).
</spoiler>
2. Folosind metoda getTableContent()
din interfața DatabaseOperations
, să se implementeze metoda getBookList1()
din clasa BookManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
, astfel încât aceasta să furnizeze lista tuturor cărților din baza de date.
Pentru fiecare volum vor fi incluse următoarele informații:
- identificatorul;
- titlul;
- lista autorilor (în formatul
prenume nume
):- în cazul în care nu există nici un autor, se va afișa textul
* * *
; - în cazul în care există mai mulți autori, aceștia vor fi delimitați prin șirul de caractere
,
(virgulă și spațiu);
- denumirea colecției;
- denumirea editurii;
- ediția;
- anul de apariție;
- țara.
3 Folosind metoda insertValuesIntoTable()
din interfața DatabaseOperations
, să se implementeze metoda create()
din clasa EntityManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
, pentru a stoca o nouă înregistrare într-o tabelă a bazei de date.
public int create(ArrayList<String> values);
Metoda primește ca parametru lista de valori corespunzătoare tuturor atributelor din tabelă, mai puțin cheia primară (care va fi autogenerată) și întoarce numărul de înregistrări care au fost modificate în baza de date.
<spoiler|Indicații de Rezolvare>
- denumirea tabelei curente este reținută de obiectul
table
; - nu trebuie indicată lista atributelor, aceasta fiind preluată în mod automat din dicționarul de date atunci când se furnizează valoarea
null
; - valoarea parametrului
skipPrimaryKey
va fitrue
, de vreme ce cheia primară va fi autogenerată; - în caz de eroare, se furnizează valoarea -1.
</spoiler>
4. Să se implementeze metoda updateBookPresentationPriceForBooksWithMultipleFormats()
din clasa BookPresentationManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
astfel încât să mărească cu un anumit procent prețul cărților care sunt disponibile într-un număr de formate (de prezentare) mai mare decât un anumit prag.
public ArrayList<ArrayList<String>> updateBookPresentationPriceForBooksWithMultipleFormats(int numberOfFormats, double amount)
Metoda primește ca parametri procentul cu care este crescut prețul cărților precum și numărul de formate pe care trebuie să îl aibă și furnizează informații referitoare la prezentările cărților pentru care au fost actualizate:
- identificatorul din tabela
book_presentation
; - identificatorul cărții corespunzătoare (
book_id
- referința la tabelabook
); - identificatorul formatului corespunzător (
format_id
- referință la tabelaformat
); - prețul după modificare.
<spoiler|Indicații de Rezolvare> Limbajul SQL nu permite actualizarea unei tabele pentru care condiția de selecție a înregistrărilor presupune interogarea unei informații din aceeași tabelă. Din acest motiv, trebuie creată o tabelă temporară în care se vor stoca identificatorii cărților pentru care există un număr de formate care depășește pragul specificat în parametru. Ulterior se face ajustarea de preț corespunzătoare, după care se obțin informațiile solicitate și apoi se șterge tabela temporară.
Pașii care trebuie urmați sunt:
- crearea tabelei temporare
book_tmp
care conține un singur atribut,book_id
, având tipulINT(10) UNSIGNED NOT NULL
; se poate folosi metodaexecuteQuery()
din interfațaDatabaseOperations
; - se obțin identificatorii cărților pentru care numărul de formate depășește pragul specificat în parametru (interogări pe tabelele
book
șibook_presentation
); se folosește metodagetTableContent()
din interfațaDatabaseOperations
; - se populează tabela
book_tmp
cu valorile obținute; se folosește metodainsertVauluesIntoTable()
din interfațaDatabaseOperations
; - se realizează actualizarea înregistrărilor din tabela
book_presentation
; se folosește metodaupdateRecordsIntoTable()
din interfațaDatabaseOperations
; - se obțin informațiile solicitate pentru datele care au fost modificate.
</spoiler>
5. a) Să se implementeze metoda deleteRecordsFromTable()
din clasa DatabaseOperationsImplementation
a pachetului ro.pub.cs.aipi.lab01.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 int deleteRecordsFromTable(String tableName, ArrayList<String> attributes, ArrayList<String> values, String whereClause) throws SQLException, DatabaseException
Metoda va arunca o excepție de tipul DatabaseException
în cazul în care numărul de atribute este diferit de numărul de valori. În această situație se va indica și un mesaj corespunzător.
<spoiler|Indicații de Rezolvare>
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.
</spoiler>
b) Folosind metoda deleteRecordsFromTable()
, să se șteargă toți scriitorii care nu au nici o carte comercializată de librărie. În acest sens, va fi implementată metoda deleteWritersWithoutBooks()
din clasa WriterManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
. Metoda furnizează numărul de înregistrări care au fost modificate.
6. (opțional) Folosind metoda getTableContent()
din interfața DatabaseOperations
, să se implementeze metoda getWritersBibliography()
din clasa WritersManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
care furnizează lista scriitorilor care au un scris un anumit număr de cărți, dintre care la unele sunt unici autori iar pe altele le-au redactat în colaborare (oricare dintre aceste valori pot fi și nule).
public ArrayList<ArrayList<String>> getWritersBibliography(int numberOfBooksTotal, int numberOfBooksAlone, int numberOfBooksCollaboration) {
Se vor afișa prenumele și numele autorilor, lista cărților scrise (separate prin caracterul ;
), numărul total de cărți, numărul de cărți la care este unic autor, numărul de cărți redactate în colaborare cu alții.
Informațiile vor fi ordonate (crescător) în funcție de prenumele și numele scriitorilor.
7. Să se implementeze metoda executeStoredRoutine()
din clasa DatabaseOperationsImplementation
al pachetului ro.pub.cs.aipi.lab01.dataaccess
al cărui scop este de a rula o rutină stocată, transmițând parametrii de intrare și preluând informațiile de la parametrii de ieșire.
public ArrayList<String> executeStoredRoutine(String storedRoutineName, ArrayList<String> parameterTypes, ArrayList<String> inputParameterValues, ArrayList<Integer> outputParameterDataTypes) throws SQLException;
Parametrii metodei executeStoredRoutine()
sunt:
storedRoutineName
- denumirea rutinei stocate;parameterTypes
- tipul parametrilor:IN
: de intrare;OUT
: de ieșire;INOUT
: micști.
inputParameterValues
- valorile pentru parametrii de intrare (având tipulIN
sauINOUT
);outputParameterDataTypes
- tipurile de date pentru parametrii de ieșire (având tipulOUT
sauINOUT
) - din clasajava.sql.Types
.
Un exemplu de folosire al acestei metode poate fi consultat în metoda getUserListWithTotalInvoiceAmount()
din clasa UserManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
.
<spoiler|Indicații de Rezolvare>
O rutină stocată se apelează prin { CALL <stored_routine_name> (?, …, ?) }
, transmisă ca parametru unui obiect de tip CallableStatement.
Pentru parametrii de intrare, trebuie să se indice valoarea (metodea setString()
).
Pentru parametrii de ieșire, trebuie să se indice tipul de date (metoda registerOutParameter()
).
Pentru parametrii micști, trebuie precizate ambele informații.
Vor fi preluate informațiile din parametrii corespunzători. </spoiler>
8. Să se implementeze metoda getReferrences()
din clasa DatabaseOperationsImplementation
a pachetului ro.pub.cs.aipi.lab01.dataaccess
ce determină pentru o tabelă identificată prin denumire (transmisă ca parametru) care sunt tabelele pe care le referă, precum şi atributele care fac obiectul constrângerii de tip FOREIGN KEY
. Metoda întoarce o listă de obiecte de tip Referrence
care conține denumirea tabelei copil și atributul dependent din cadrul acesteia precum și denumirea tabelei părinte și atributul referit din cadrul acesteia.
<spoiler|Indicații de Rezolvare>
Se va folosi metoda getImportedKeys()
din clasa DatabaseMetaData, care furnizează următoarele informații:
PKTABLE_NAME
- denumirea tabelei părinte;FKTABLE_NAME
- denumirea tabelei copil;PKCOLUMN_NAME
- denumirea atributului în tabela părinte;FKCOLUMN_NAME
- denumirea atributului în tabela copil.
</spoiler>
9. Folosind un obiect de tip RowSet deconectat, să se implementeze funcționalitatea de la exercțiul 2. În acest scop, se va implementa metoda getBookList2()
din clasa BookManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
.
<spoiler|Indicații de Rezolvare>
Datele din fiecare tabelă implicată vor fi încărcate în obiecte de tip CachedRowSet. Asocierea dintre entități se face folosind un obiect de tipul JoinRowSet, specificând denumirea atributului prin care se realizează legătura (metoda setMatchColumn()
).
Atenție!!! Atributele pe baza cărora este realizată legătura apar în setul de date rezultat o singură dată, fiind necesar ca acest aspect să fie luat în considerare atunci când se calculează indecșii coloanelor care se doresc a fi preluați.
Se recomandă ca operația de asociere să se realizeze treptat, la aceasta participând câte 2 entități. Rezultatele obținute vor participa în continuare la realizarea legăturii cu celelalte tabele. </spoiler>
10. (opțional) Folosind un obiect de tip FilteredRowSet, să se filtrele rezultatul obținut anterior în funcție de încadrarea într-un anumit interval de preț. În acest scop, se va implementa metoda getBooksListFilteredByPriceRange()
din clasa BookManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
, care primește ca parametrii limtele inferioară și superioară.
public ArrayList<ArrayList<String>> getBooksListFilteredByPriceRange(float lowValue, float highValue);
Vor fi incluse și informații din tabela book_presentation
: codul ISBN, preț, stocul disponibil.
<spoiler|Indicații de Rezolvare>
Filtrarea se aplică pe rezultatul obținut (după ce anterior au fost realizate operațiile de asociere dintre tabele). Se poate utiliza un obiect de tipul PriceFilter
din pachetul ro.pub.cs.aipi.lab01.helper
, ca parametru al metodei setFilter()
.
</spoiler>
11. Să se implementeze metoda makeSupplyOrderBasedOnStockPile()
din clasa BookPresentationManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
care creează comenzi de aprovizionare pentru cărțile al căror stoc este inferior unui anumit prag, furnizat ca parametru. O comandă de aprovizionare este realizată pentru o anumită editură și conține toate cărțile furnizate de aceasta, având stocul care respectă condiția respectivă.
public ArrayList<ArrayList<String>> makeSupplyOrderBasedOnStockpile(int stockpile);
<spoiler|Indicații de Rezolvare> Se indentifică acele formate de prezentare ale cărților pentru care stocul nu întrunește cantitatea minimă, acestea fiind ordonate în funcție de editură. Se parcurge această listă, creându-se o comandă de aprovizionare atunci când se întâlnește o editură nouă. O entitate corespunzătoare va fi stocată pentru fiecare volum în parte, cantitatea fiind determinată ca diferență între pragul minim și valoarea aflată în stoc.
Pentru generarea unui număr de identificare se poate utiliza metoda statică generateIdentificationNumber()
din clasa Utilities
pentru care se specifică stuctura: numărul de caractere de tip literă, respectiv numărul de caractere de tip cifră.
</spoiler>
12. (opțional) Să se implementeze metoda getBookListsWithStockpile()
din clasa BookManager
a pachetului ro.pub.cs.aipi.lab01.businesslogic
care furnizează lista cărților pentru care suma stocurilor (determinate din toate formatele de prezentare posibile) depășește un anumit prag, furnizat ca parametru.
public ArrayList<ArrayList<String>> getBooksListWithStockpile(int stockpile)
Pentru fiecare volum vor putea fi consultate următoarele informații:
- identificatorul;
- titlul și subtitlul;
- lista autorilor;
- lista categoriilor;
- lista formatelor;
- lista limbilor.
În cazul în care o listă este vidă, se va afișa valoarea null
. În cazul în care o listă conține mai multe elemente, acestea vor fi delimitate prin caracterul virgulă.