1. Statements and CRUD operations
En aquesta secció estudiarem l'objectiu principal de treballar amb bases de dades: manipular dades. Sempre treballarem amb la mateixa plantilla:
- Connectar a la base de dades.
- Preparar la consulta.
- Executar la consulta.
- Processament de les dades, si és necessari.
El punt 1 s'ha estudiat a les seccions anteriors.
Per crear les consultes, haurem d'utilitzar les següents classes/interfaces:
Statement\(\rightarrow\) Es fa servir de manera general i és útil quan volem realitzar sentències SQL estàtiques, ja que no accepta paràmetres. Creem unStatementamb el mètodecreateStatement()de la classeConnection.PreparedStatement\(\rightarrow\) Es fa servir quan volem llançar diverses consultes i, a més, es permet realitzar sentències dinàmiques. Creem unPreparedStatementamb el mètodeprepareStatement()de la classeConnection.CallableStatement\(\rightarrow\) Es fa servir per accedir a procediments emmagatzemats a la base de dades i també accepta paràmetres d'entrada. Creem unCallableStatementamb el mètodeprepareCall()de la classeConnection.
Quan la sentència està preparada, la podem executar amb:
executeQuery\(\rightarrow\) Executa sentències que esperem que retornin dades (consultes SELECT). El valor de retorn d'aquesta execució és, com ja suposareu, un ResultSet.executeUpdate\(\rightarrow\) Executa sentències que no s'espera que retornin dades, però que s'utilitzaran per modificar la base de dades connectada (consultes INSERT, DELETE, UPDATE i CREATE TABLE).
1.1. Create (Insert)
Anem a veure un exemple senzill de declaració INSERT.
Com podeu veure, és molt fàcil d'entendre el codi. Creem una declaració buida i després executem l'SQL "per se". L'execució retorna el nombre de files inserides. Veurem millors maneres d'inserir dades, utilitzant scripts.
Info
Aquest codi és una versió reduïda, ja que quan treballem amb bases de dades poden aparèixer SQLException. Suposem que el procés de connexió ja s'ha realitzat.
1.2. Read (Select)
La lectura és el procés més important que farem, ja que només amb el procés d'inici de sessió dins d'una aplicació estem recuperant informació d'una base de dades. En tots els casos, hem d'escriure la sentència (SQL), executar-la i finalment processar les dades retornades. Depenent de com preparem la sentència, podem distingir entre:
- Sentències fixes
- Sentències variables
- Sentències preparades
Veurem amb detall ambdues maneres. Utilitzarem per això l'script Instituto.sql i la base de dades.
1.2.1. Sentències fixes
Aquestes sentències, com el seu nom indica, són sentències fixes o constants. L'SQL és fixa i no té cap variable.
En el processament de la informació, ResultSet té:
type getType(int columnIndex)\(\rightarrow\) mètode sobrecarregat que retorna el tipus de dades donat, utilitzant l'índex de columna del ResultSet. Recorda que la primera columna és 1 en comptes de 0. El tipus pot ser Int, String, Double, etc. si coneixes el tipus. Per a columnes desconegudes, pots utilitzarObjectcom a tipus genèric.type getType(String columnName)$\rightarrow` mateix que el mètode anterior, però accedint a la columna amb el nom que hem seleccionat a la consulta o el nom a la taula.
1.2.2. Sentències variables
Imagina que vols recuperar noms que continguen la cadena Ma dins seu.
| SQL | |
|---|---|
En aquest cas, aquesta consulta està codificada directament, i si vols canviar la part del text, has de modificar el teu codi. Per evitar la codificació directa, podem escriure:
Com podem veure, les dades ara estan en variables, però la construcció de l'SQL és més complexa. Cal tenir en compte que els textos han d'estar entre cometes i els números no, el que facilita cometre errors. Però pot ser pitjor, aquest tipus de codi pot incórrer en problemes d'injecció SQL, com veiem a l'exemple següent:
| Java | |
|---|---|
- Si l'usuari introdueix
4\(\rightarrow\) Es mostrarà la persona amb ID igual a 4 - Si l'usuari introdueix
4 o 1=1\(\rightarrow\) Es mostraran totes les persones
Cal evitar aquest tipus de consultes en les declaracions de validació d'usuari, per a les quals farem servir les sentències preparades i, òbviament, ser molt cautelosos en la verificació de les entrades.
1.2.3. Sentències preparades
Per evitar el problema de l'injecció SQL, sempre que tinguem paràmetres a la nostra consulta, farem servir sentències preparades. En les sentències preparades, on hem de fer servir un marcador, en lloc de compondre-la amb concatenacions dins de la cadena, l'indicarem amb un interrogant (?), un caràcter anomenat placeholder.
A continuació, haurem d'assignar valors a aquests placeholders, utilitzant els mètodes setType(int pos) on Type és el tipus de dades que assignarem i pos és la posició del placeholder, començant per 1. Veurem l'exemple:
Consell
| Text Only | |
|---|---|
1 | |
1.3. Actualització (Update) i Esborrat (Delete)
L'actualització i l'esborrat d'una fila es consideren com a actualitzacions de la base de dades, ja que modifiquen l'estat de la base de dades. De fet, també considerem la inserció com una actualització de la base de dades. Totes dues tasques s'incloent en el mètode executeUpdate(). Anem a estudiar-ho a través d'exemples:
1.3.1. Exemple d'esborrat
Anem a esborrar files d'una taula de persones entre les edats donades:
Perill
Eliminar dades és una operació molt delicada. Tingues cura de:
- No oblidis la clàusula
WHEREquan elimines, ja que eliminaràs totes les dades de la taula. - Si vols eliminar tota la taula, incloent l'estructura (definició + taula), has d'utilitzar
DROP TABLEen compte deDELETE. - Si intentes eliminar una fila que està referenciada per una clau externa, obtindràs l'excepció
SQLIntegrityConstraintViolationExceptioni un missatge com No es pot eliminar o actualitzar una fila pare: falla una restricció de clau externa.
1.3.2. Exemple d'Actualització
Anem a afegir anys a les persones amb l'ID donat:
Warning
Recorda que:
- La clàusula
UPDATEno necessitaFROMperquè indiquem directament el nom de la taula. - Apareix la clàusula
SET, amb parellscamp=valor_actualitzat, per assignar el nou valor als camps seleccionats. - Si oblidem la clàusula
FROM, s'actualitzaran totes les files.
1.4. Scripts
Un script, que normalment s'ha creat en un fitxer extern, és un conjunt d'instruccions SQL executades en ordre de dalt a baix. Podem prendre com a estratègia llegir el fitxer línia per línia i executar-lo una per una, però JDBC permet executar un conjunt d'instruccions en bloc. Per fer això, el primer que hem de fer és habilitar múltiples execucions afegint un paràmetre a la connexió, que és allowMultiQueries=true.
A continuació, hem de carregar el fitxer i compondre una cadena amb tot el script. Per normalitzar-lo i fer-lo totalment portàtil, hem de tenir cura amb els salts de línia, ja que depenent del sistema és una combinació \n o \r\n. Podem llegir línia per línia i guardar-ho en un StringBuilder, afegint System.getProperty("line.separator") com a separadors.
Després només necessitarem crear una declaració amb aquesta cadena i executar-la amb executeUpdate(). Ho veurem a continuació:
Consell
Recordes les classes Files i Paths? Reescriu l'últim exemple per obtenir un codi més net.
1.5. Transaccions
Si volem protegir la integritat de les dades, així com evitar situacions de bloqueig inesperades en aplicacions multithread, hem de protegir les nostres operacions, especialment aquelles que modifiquen les dades mitjançant l'ús de transaccions.
Una transacció defineix un entorn d'execució en què les operacions de desament es mantenen emmagatzemades a la memòria fins que la transacció es completi. Si en un moment determinat alguna cosa falla, l'estat es torna al punt inicial de la mateixa (punt inicial) o a algun punt de marca intermedi. Per defecte, obrir una connexió inicia una transacció:
- Cada execució a la connexió genera una transacció per si mateixa.
- Si volem desactivar aquesta opció perquè la transacció abasti diverses execucions, hem de marcar-ho mitjançant
Connection.setAutoCommit(false);. - Per acceptar definitivament la transacció ho farem mitjançant
Connection.commit(); - Per cancel·lar la transacció
Connection.rollback();
1.6. ResultSets actualitzables
Els mètodes de treball revisats a les seccions anteriors, especialment quan s'actualitzen o esborren files, funcionen directament a la base de dades. Això significa que esborreu o actualitzeu una fila sense carregar-la prèviament a la memòria. Què passa si voleu mostrar les dades a l'usuari i després l'usuari decideix esborrar o actualitzar la fila? En aquest cas, és millor utilitzar ResultSets actualitzables.
Hem estudiat ResultSet com una col·lecció de files i l'hem utilitzat només per a la lectura. També podem utilitzar-lo per editar i esborrar dades. Per fer-ho, hem de obrir el ResultSet d'una manera diferent de la que hem vist fins ara. I, per últim, però no menys important, dependrà de la base de dades la disponibilitat de crear aquest tipus de ResultSets.
Per aconseguir-ho, no depèn del ResultSet ni de com es va crear l'Statement. Hem d'utilitzar aquest constructor:
| Java | |
|---|---|
Com podeu veure, hi ha 3 arguments per indicar quin tipus de ResultSet donem al final. Aquests tres arguments poden ser:
resultSetType\(\rightarrow\) aquestes són opcions sobre com moure's i passar per les files del ResultSet:TYPE_FORWARD_ONLY\(\rightarrow\) opció per defecte. El ResultSet només es pot passar una vegada.TYPE_SCROLL_INSENSITIVE\(\rightarrow\) Permet rebobinar i saltar a una posició absoluta o relativa.TYPE_SCROLL_SENSITIVE\(\rightarrow\) Igual que abans, però permet veure els canvis realitzats a la base de dades.
ResultSetConcurrency\(\rightarrow\) aquestes són opcions sobre si es pot actualitzar el ResultSet o no:CONCUR_READ_ONLY\(\rightarrow\) opció per defecte. Només lectura. Si volem actualitzar alguna cosa, només podem utilitzar les clàusulesDELETEoUPDATE.CONCUR_UPDATABLE\(\rightarrow\) Les files del ResultSet es poden actualitzar o esborrar.
ResultSetHoldabilityaquestes opcions són sobre el comportament en tancar el ResultSet:HOLD_CURSORS_OVER_COMMIT\(\rightarrow\) El ResultSet es manté obert després de confirmar la transacció.CLOSE_CURSORS_AT_COMMIT\(\rightarrow\) El ResultSet es tanca després de confirmar la transacció. Millora el rendiment.
En el següent exemple, podem preguntar al nostre DBMS si aquests tipus de ResultSet estan suportats:
1.6.1. Moviment del ResultSet
Com podem veure, no només podem moure el cursor cap endavant:
next,previous,firstilast\(\rightarrow\) com el seu nom indica, es mouen cap endavant, cap enrere, al principi i a l'última fila. Retorna true si el cursor apunta a una fila i false en cas contrari.beforeFirstiafterLast\(\rightarrow\) el cursor apuntarà fora de les files, abans de la primera o després de l'última fila.relative(int n)\(\rightarrow\) el cursor es mou n files (cap endavant o cap enrere, depenent del signe).absolute(int n)\(\rightarrow\) el cursor apunta a la fila n.
1.6.2. Eliminació de files en un ResultSet
Després de col·locar el cursor a la fila que es vol eliminar, es pot eliminar del ResultSet (i de la base de dades) amb el mètode deleteRow(). Quan s'elimina una fila, el cursor apuntarà a la fila anterior a la que s'ha eliminat.
1.6.3. Actualització de files en un ResultSet
Després de col·locar el cursor a la fila desitjada, s'ha de:
- Actualitzar les columnes desitjades, utilitzant el mètode
updateType(int column, type newValue), on la columna i-èssima (o amb el seu nom) s'assigna el nou valor del tipus donat. - Un cop s'hagin modificat totes les columnes desitjades, es guarden els canvis amb
updateRow().
Consell
No podem actualitzar una fila sencera, hem de actualitzar columna per columna i després actualitzar la fila.
1.6.4. Inserció de files en un ResultSet
Si volem inserir una nova fila en un ResultSet, hem de:
- Generar una nova fila buida, i això s'aconsegueix amb el mètode
moveToInsertRow(). - En aquesta fila, apliquem el mètode
updateType(int column, type newValue)a totes les columnes a les quals volem assignar un valor, i finalment - Procedim a inserir la nova fila amb
insertRow().
Important
- Aquestes operacions d'actualització, eliminació i inserció només es poden realitzar en consultes que provenen d'una taula sense agrupacions.
- Per evitar complexitat en els nostres programes, val la pena avaluar la conveniència de traduir les actualitzacions del ResultSet a SQL pur i executar-les directament a les bases de dades mitjançant noves sentències.