Link Cerca Menu Expand Document

Persistència POO

Resultats d’aprenentatge:

  1. Gestiona informació emmagatzemada en bases de dades relacionals mantenint la integritat i la consistència de les dades.
  2. Gestiona informació emmagatzemada en bases de dades objecte-relacionals mantenint la integritat i la consistència de les dades.
  3. Utilitza bases de dades orientades a objectes, analitzant-ne les característiques i aplicant tècniques per mantenir la persistència de la informació.

Persistència

Localització de les dades

Una aplicació conté codi i dades. El codi interactua amb les dades que hi ha a la memòria principal, que és volàtil, el tipus de memòria més ràpida que existeix i la més senzilla de llegir i escriure. A POO, les dades són als objectes.

Un problema de la memòria principal és que perdem el seu contingut si l’aplicació finalitza, ja sigui de forma correcta o per un problema. El context necessari per a l’execució d’una aplicació és el seu estat, i cal recuperar-lo cada cop que les executem. Això es fa utilitzant memòria no volàtil basada en un o diversos arxius físics.

Quan parlem de recuperar el context d’una aplicació per a la seva execució, no sempre és possible fer-ho amb totes les dades que es gestionen. Pot ser perquè la mida sigui superior a la de la memòria principal, però també perquè no calgui i sigui millor ser mandrós en la seva càrrega per raó de rendiment.

Model de dades

El disseny del model de dades ha de tenir en compte el millor rendiment possible quan s’utilitza memòria no volàtil, molt més lenta, i facilitar la futura escalabilitat de les nostres dades.

Un mecanisme per a minimitzar temps és la memòria cache: mantenim un nombre limitat d’objectes a la memòria principal per evitar haver-los de carregar des de la memòria no volàtil cada cop, i els invalidem si es modifiquen.

També els SGBD poden proporcionar-nos mecanismes per a processar les dades sense necessitat de carregar-les en memòria, gràcies a llenguatges declaratius com SQL: demanem quines dades necessitem, no com processar-les.

Si parlem de disseny, és convenient crear una capa de persistència que encapsuli el procés. Això ens permetrà substituir-la sense afectar la resta de l’aplicació. Si parlem d’usabilitat, la persistència hauria de ser transparent per a l’usuari.

Què, quan i com persistir

Tenim dos mètodes de persistència: d’estat, el més habitual, i basat en esdeveniments.

Persistència d’estat

Parlant de POO, ens podríem fer la pregunta de si allò que volem persistir és un objecte o una estructura de dades. Els objectes tenen comportament i encapsulen les dades. Les estructures de dades exposen les dades i no tenen comportament. Aquesta distinció ens ajudarà a decidir la nostra estratègia de persistència.

Per definir la persistència, cal repassar els camps de l’objecte i decidir quins necessitarem per tal de tornar a instanciar l’objecte en memòria principal. Alguns camps innecessaris poden ser calculats o tenir una funció temporal. Podem ajudar-nos de l’estudi dels constructors i setters de l’objecte.

A més, ens caldrà fer referències entre els objectes. Quan dissenyem mecanismes ad-hoc, podem utilitzar un identificador d’objecte en el moment de la persistència. En el cas dels SGBD, els objectes es relacionen amb claus foranes.

Tenim diversos àmbits de persistència:

  • Context general de l’aplicació: configuracions generals, per usuari, etc.
  • Documents, basats en serialització. Persistim en base a un document que pot intercanviar-se.
  • Registres, habitualment relacionats i associats a SGBD. No se sol restaurar completament al començament, només quan cal.

Persistència d’esdeveniments

La persistència per esdeveniments (Event Sourcing) és un mecanisme que permet restaurar l’estat d’una aplicació a partir de tots els esdeveniments que s’han produït. Es tracta d’una mena de log on es guarden els esdeveniments per ordre d’ocurrència, i que permet reconstruir l’estat en qualsevol moment del temps.

Quan l’aplicació s’inicia, reconstrueix l’estat actual aplicant tots els esdeveniments en l’ordre en que es val produir. Com que podria haver molts d’esdeveniments, aquest mecanisme se sol combinar amb altres, com per exemple emmagatzemar un estat intermedi (snapshot) i aplicar els esdeveniments a partir d’aquell instant.

Quan i com

Hem de persistir sempre que hi hagi un canvi en alguna dada? La resposta general és que sí. Així evitem perdre dades si el nostre programa falla abans d’haver persistit. Però de vegades no seria òptim, parlant de rendiment, haver de persistir tot l’estat d’una aplicació. Alternativament, es podria endarrerir el moment, o fer-ho periòdicament de forma automàtica. També podem deixar aquesta responsabilitat en mans de l’usuari (File > Save).

La persistència per esdeveniments permet només guardar l’esdeveniment que es produeix en lloc de totes les dades, i també pot ser una solució per millorar el rendiment.

Quin tipus de persistència?

El format i organització dependrà de les nostres necessitats.

Criteris per decidir la forma de persistència:

  • És una aplicació monousuari o multiusuari?
  • Es comparteix informació a la xarxa amb altres clients o aplicacions?
  • Hi ha un volum molt alt de dades?
  • Hi ha un esquema estable (ben estructurat)?
  • Quins requisits qualitatius tenim: disponibilitat, escalabilitat, latència, rendiment, consistència…?

Si la resposta està a prop d’una aplicació monousuari, sense connexió amb altres clients, i poques dades, segurament podem gestionar-ho mitjançant persistència en fitxer. Caldria, en tot cas, decidir quin és el format d’aquest fitxer, ja que podem utilitzar solucions existents sense necessitar inventar-nos un format a mida. En POO, utilitzem el concepte de serialització d’objectes: generem una representació del graf d’objectes que permet restaurar-los quan l’aplicació torna a executar-se. El format podria ser a mida, utilitzant JSON, un arxiu de preferències clau-valor, etc.

Si la resposta s’assembla a aplicació multiusuari amb informació compartida per la xarxa i grans volums de dades, estem a prop de necessitar un sistema de gestió de base de dades (SGBD). En aquest cas, parlem de mapatge d’objectes relacional (ORM). Aquest procés consisteix a interposar una capa entre la lògica i la persistència de l’aplicació, de tal forma que podem persistir utilitzant el paradigma d’orientació a objecte en lloc del llenguatge SQL. És un procés complex i no exempte de problemes. A Java es pot implementar a mida (JDBC) o bé utilitzar llibreries de mapatge (JPA).

Els SGBD resolen problemes habituals que ens trobem en el desenvolupament d’aplicacions. Entre ells:

  • Definició, creació, manteniment i control d’accés a una base de dades.
  • Gestió de transaccions i concurrència (segons el model).
  • Facilitats per recuperar dades en cas de danys.
  • Gestió d’autoritzacions i accés remot.
  • Regles de comportament de les dades en funció de la seva estructura.

Base de dades d’aplicació o d’integració

Una base de dades d’integració és una base de dades que actua com a magatzem de dades de diverses aplicacions i, per tant, integra dades d’aquestes aplicacions.

Una base de dades d’aplicació es controla i accedeix des d’una sola aplicació. Per a compartir dades amb altres aplicacions, l’aplicació que controla la base de dades hauria de proporcionar serveis.

La recomanació general és la d’evitar bases de dades d’integració. En general, les bases de dades d’integració comporten problemes greus perquè la base de dades esdevé un punt d’acoblament entre les aplicacions que hi accedeixen. Generalment es tracta d’un acoblament profund que augmenta significativament el risc que suposa canviar aquestes aplicacions i dificulta la seva evolució.

Model relacional vs NoSQL

Les bases de dades actuals responen, moltes d’elles, al model relacional. Aquest model ha triomfat segurament gràcies a l’establiment d’un estàndard per a la gestió de dades, l’SQL. Existeixen alguns altres models que tenen sentit per a certs nínxols, i que cal considerar: bases de dades en graf, multivalor o orientades a objecte. Però han anat perdent ressò en favor de les bases de dades NoSQL. El creixement d’aquestes es veu afavorit pel Big Data i les aplicacions en temps real. Són sistemes habitualment no estructurats (sense esquema), i poden persistir al costat de solucions relacionals en models de persistència poliglota.

Sense sortir del model relacional, solem tenir extensions sense esquema. Per exemple, camps amb contingut JSON. O taules d’atributs que permeten fer JOINs addicionals. Aquestes extensions ens permeten tenir dades sense esquema dins d’un esquema, tot i que aquestes dades no són tan accessibles des de consultes SQL.

El que cal evitar és tenir esquemes implícits al codi d’accés. Només en pocs casos pot tenir sentit no tenir-ne un esquema:

  • Camps a mida imposats per l’usuari
  • Objectes sense un tipus uniforme (esdeveniments)
  • Pot ser més fàcil fer migracions d’esquemes (implícits)

Punts a considerar a l’hora de decidir-se:

  • És una BBDD amb o sense esquema (relacionals vs NoSQL)?
  • Existeixen relacions, que utilitzarem per navegar aquesta informació?
  • La velocitat és un aspecte crític? Les relacionals sacrifiquen la velocitat en favor de la normalització.
  • És important tenir escalabilitat? Les NoSQL escalen millor horitzontalment.
  • Les relacionals ofereixen les propietats ACID, mentre les NoSQL són BASE.

Característiques ACID

En el context de bases de dades, ACID (acrònim anglès de Atomicity, Consistency, Isolation, Durability) són tot un seguit de propietats que ha de complir tot sistema de gestió de bases de dades per tal de garantir que les transaccions (operacions sobre les dades) siguin fiables.

Concretament, l’acrònim ACID significa:

  • Atomicitat: Una transacció o bé finalitza correctament i confirma o bé no deixa cap rastre de la seva execució.
  • Consistència: La concurrència de transaccions no pot produir resultats anòmals.
  • llament (o Isolament): Cada transacció del sistema s’ha d’executar com si fos l’única que s’executa en aquell moment en el sistema.
  • Durabilitat: Si es confirma una transacció, el resultat d’aquesta ha de ser definitiu i no es pot perdre.

Característiques BASE

Les característiques BASE estan associades a les BBDD NoSQL. Es basen en el teorema CAP (Consistency-Availability-Partition Tolerance), que afirma que és molt difícil tenir més de dues d’aquestes propietats alhora.

Són l’acrònim de:

  • Basic Availability: la base de dades funciona la majoria del temps.
  • Soft-State: no cal tenir consistència a l’escriptura, ni les rèpliques han de ser consistents.
  • Eventual consistency: la consistència es pot tenir més tard en el temps (funcionament mandrós).

Fitxers en Java

Podem utilitzar fitxers en un format a mida. Aquests són alguns possibles casos d’ús.

Properties

Les Properties són un format pla per a configuració d’una aplicació en format text. S’utilitzen parelles clau/valor que poden llegir-se (load) i escriure’s (store).

JSON

Podem escriure un arxiu de text en format JSON, un format estructurat, i processar-lo utilitzant una de tantes llibreries existents. Per exemple, JSON-java. Poden servir per llegir i persistir l’estat d’una aplicació, quan la quantitat de dades no és massa gran.

YAML

YAML és un format d’entrada estructurat que també té algunes llibreries que permeten llegir-lo, com snakeyaml, però no està tan indicat per persistir.

Arxius d’accés aleatori

Podem utilitzar RandomAccessFile per a crear arxius binaris o de text amb registres de mida fixa. Això ens permet operar amb un índex sense haver de llegir tot l’arxiu com un stream. Hi ha utilitats a Java per exportar i importar tipus primitius utilitzant bytes, com ByteBuffer.

Serialització

La serialització i la deserialització són processos que permeten convertir un objecte (Java) en un format fàcil de persistir i a la inversa.

El mecanisme de la JRE es basa en l’utilització de ObjectInputStream i ObjectOutputStream. Permeten utilitzar un stream per llegir i escriure objectes. Cal que l’objecte implementi Serializable, i es pot fer serializació a mida implementat els mètodes privats writeObject i readObject.

Utilitzar els mecanismes Java de serializació pot fer que la nostra solució estigui tancada a altres llenguatges. Pot no ser un problema, si són arxius que no es compartiran fora.

JSON pot ser una solució de format més intercanviable. Les llibreries existents s’encarreguen de la serialització i deserialització.

Java Database Connectivity (JDBC)

JDBC (Java DataBase Connectivity): API estàndard que permet llençar consultes a una BD relacional.

JDBC és el model de persistència bàsic a Java. En funció de la mida i el tipus de projecte és possible que necessitem ajuda per implementar aspectes recurrents al codi:

  • Operacions CRUD: si necessitem fer CRUD per moltes taules, seria una feina molt feixuga.
  • Generació de queries SQL: ens facilita tenir queries universals ben formades (dialectes).
  • Gestió de transaccions: gestió per threads de transaccions i connexions a la BBDD.
  • Control de concurrència (optimista, pessimista): mecanismes amb versions/timestamps o amb bloqueig de files.

Els paquets java.sql y javax.sql formen part de Java SE i contenen un bon nombre d’interfícies i algunes classes concretes, que conformen l’API de JDBC. Els components principals de JDBC són: els controladors, les connexions, les sentències i els resultats.

Controladors (drivers)

Un controlador JDBC és una col·lecció de classes Java que us permet connectar-vos a una determinada base de dades. Per exemple, MySQL té el seu propi controlador JDBC. Un controlador JDBC implementa moltes de les interfícies JDBC. Quan el codi utilitza un controlador JDBC determinat, en realitat només utilitza les interfícies estàndard JDBC. El controlador JDBC concret que s’utilitza s’amaga darrere de les interfícies JDBC. D’aquesta manera podeu connectar un nou controlador JDBC sense que el vostre codi ho noti.

Els drivers estan disponibles quan la llibreria (jar) corresponent està al classpath. Per exemple, per a MySQL 8.x podem comprovar-ho amb:

try {
    Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
    System.out.println(name + "Falta llibreria");
}

Connexions (connections)

Una vegada carregat i inicialitzat un controlador JDBC, heu de connectar-vos a la base de dades. Es fa obtenint una connexió a la base de dades mitjançant l’API JDBC i el controlador carregat. Tota comunicació amb la base de dades es fa a través d’una connexió. Una aplicació pot tenir més d’una connexió oberta a una base de dades alhora, però cal estalviar connexions, ja que són cares, i tancar-les sempre.

Per obtenir una connexió, necessitem una URL, que és dependent de la BBDD concreta. Per exemple:

jdbc:mysql://localhost:3306/test
jdbc:sqlite:path/test.db

Podem obtenir una connexió:

Connection connection = DriverManager.getConnection(url, user, password);
// sentències ...
connection.close();

Una manera alternativa, i recomanable, d’obtenir una connexió és utilitzant el try-with-resources, ja que Connection és un AutoCloseable.

try (Connection connection = DriverManager.getConnection(url, user, password)) {
    // sentències ...
} catch (SQLException e) {
    e.printStackTrace();
}

Sentències (statements)

Una setència és el que utilitzeu per executar consultes i actualitzacions a la base de dades. Podeu utilitzar alguns tipus diferents d’enunciats. Cada declaració correspon a una sola consulta o actualització. Tenim bàsicament dos tipus de sentències en funció de si la sentència SQL té o no paràmetres (comodins amb ?).

Statement

try (Statement st = connection.createStatement()) {
    int count = st.executeUpdate(sql1); // INSERT, UPDATE o DELETE
    // o bé...
    try (ResultSet rs = st.executeQuery(sql2)) { // SELECT
        // processament...
    }
}

PreparedStatement

try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setType1(1, valor1); // 1 a N, on Type pot ser Int, String...
    ps.setType2(2, valor2);
    int count = ps.executeUpdate(); // INSERT, UPDATE o DELETE
    // o bé...
    try (ResultSet rs = ps.executeQuery()) { // SELECT
        // processament...
    } 
}

Conjunts de resultats (ResultSets)

Quan es realitza una consulta a la base de dades, s’obté un conjunt de resultats. A continuació, podeu recórrer aquest ResultSet per llegir el resultat de la consulta.

try (ResultSet rs = st.executeQuery(sql)) { 
    while (rs.next()) {
        Type1 valor1 = rs.getType1(1);
        Type2 valor2 = rs.getType2(2);
        // o bé...
        Type1 valor1 = rs.getType1("nom_columna1");
        Type2 valor2 = rs.getType2("nom_columna2");
    }
}

Inserció i obtenció d’una clau generada

De vegades es vol obtenir la clau que s’acaba de generar a una columna automàticament. Això es pot definir a MySQL amb AUTO_INCREMENT o bé a PostgreSQL amb SERIAL. Es pot aconseguir en dos pasos:

  1. utilitzant el paràmetre Statement.RETURN_GENERATED_KEYS quan es crea el PreparedStatement.
  2. Obtenint el ResultSet mitjançant PreparedStatement.getGeneratedKeys(). Habitualment només hi haurà una columna, la posició 1.
int key;
String sql = "INSERT INTO taula (valor) VALUES (?)";       
try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {                
    ps.setString(1, valor);
    ps.executeUpdate();
    try (ResultSet rs = ps.getGeneratedKeys()) {
        if (rs.next()) {
            key = rs.getInt(1);
        }
    }
}

java.sql.Date, java.sql.Time, and java.sql.Timestamp

Aquestes classes estenen la funcionalitat d’altres Java per representar l’equivalent en SQL. Per exemple, java.sql.Date expressa el dia, mes i any. I java.sql.Time representa hora, minuts i segons. Finalment, java.sql.Timestamp representa java.util.Date fins als nanosegons.

Conversions entre java.sql.Date i java.util.Date: java.sql.Date estén (extends) java.util.Date. Per tant: tots els java.sql.Date són també java.util.Date.

En general, als nostres programes sempre es pot utilitzar java.util.Date, la data genèrica a Java.

Però a JDBC s’utilitza java.sql.Date en dos casos:

  • void PreparedStatement.setDate(int index, Date sdate). Per crear un java.sql.Date a partir d’un java.util.Date:
    • sdate = new java.sql.Date(udate.getTime()). Per exemple:
      • preparedStatement.setDate(3, new java.sql.Date(tasca.getDataInici().getTime()));
    • sdate = java.sql.Date.valueOf(LocalDate.of(yyyy, mm, dd)).
  • Date ResultSet.getDate(...) retorna un java.sql.Date. Però no cal fer res especial: es pot assignar a un java.util.Date.
    • udate = sdate. Per exemple:
      • tasca.setDataInici(result.getDate("data_inici"));

Excepcions

Les sentències SQL sobre JDBC poden generar excepcions SQLException.

Aquest tipus d’excepció té mètodes que poden ajudar a entendre el problema que s’ha produït:

  • getErrorCode(): codi específic del proveïdor
  • getSQLState(): codi SQLState stàndard

Les excepcions poden passar per diferents motius:

  • Problemes de comunicació: connectivitat o servidor parat.
  • Problemes d’autenticació: credencials incorrectes.
  • Errors de sintaxi SQL, associats a la subclasse SQLSyntaxErrorException. Són errors de programació.
  • Errors de violació de constraints, associats a la subclasse SQLIntegrityConstraintViolationException. Indiquen que alguna constraint del DDL no es respectaria, com per exemple una Foreign Key, una Unique/Primary Key, etc. Aquest tipus es gestiona habitualment a l’aplicació per a detectar condicions recuperables.

Transaccions

Una transacció és un conjunt d’accions que s’han de dur a terme com una única acció atòmica. O totes es fan, o cap.

Quan es vol implementar una transacció, és important que totes les operacions de lectura i escriptura que es fan sobre la base de dades es facin compartint una única Connection. Per altra banda, no és correcte compartir una Connection entre diferents fils.

Inicieu una transacció per aquesta invocació:

connection.setAutoCommit(false);

Ara podeu continuar fent consultes i actualitzacions de taules. Totes aquestes accions formen part de la transacció.

Si alguna acció intentada dins de la transacció falla, haureu de desfer la transacció. Això es fa així:

connection.rollback();

Si totes les accions tenen èxit, hauríeu de confirmar la transacció. Un cop es fa, les accions són permanents a la base de dades i no hi ha marxa enrera. Es fa així:

connection.commit();

Exemple amb try/catch:

try (Connection conn = factory.getConnection()) {
    conn.setAutoCommit(false);
    try {
        // sentències...
        conn.commit();
    } catch (Exception e) {
        conn.rollback();
        throw e; // excepcio original
    }
} catch (SQLException e) {
    throw new RuntimeException("error SQL", e);
}

En el cas que la mateixa Connection es vulgui utilitzar després sense transacció, caldria fer:

// no creem la connection
conn.setAutoCommit(false);
try {
    // sentències...
    conn.commit();
} catch (Exception e) {
    conn.rollback();
    throw e; // excepcio original
} finally {
    conn.setAutoCommit(true);
}

Pool de connexions

El pool de connexions és un conegut patró d’accés a dades, que té com a objectiu principal reduir les despeses implicades en la realització de connexions de bases de dades i en operacions de bases de dades de lectura / escriptura.

En poques paraules, un pool de connexions és, al nivell més bàsic, una implementació de memòria cau de connexió de bases de dades, que es pot configurar per adaptar-se a requisits específics.

Es poden implementar adhoc, utilitzar llibreries de tercers o bé les de les implementacions del controlador JDBC. Sempre que sigui possible, és millor utilitzar un pool de connexions que fer-ho amb DriverManager.getConnection(…). Un pool ens crea un objecte javax.sql.DataSource, que permet obtenir connexions amb el seu mètode getConnection(). A més, el mètode close() de la connexió no la tanca, per poder reutilitzar-la un altre cop.

DataSource ds = // construir-lo segons la base de dades
Connection conn = ds.getConnection(); // obté una nova connexió
...
conn.close(); // retorna la connexió al pool (no la tanca)

Patrons de disseny

Model de capes d’una aplicació

Les capes d’una aplicació que necessita persistència podrien establir-se de la següent manera:

  • Presentació: la part que s’encarrega de la interacció amb l’usuari. En el patró MVC, inclouria la vista i el controlador.
  • Domini: lògica de negoci, relacionant les dades amb el seu comportament.
  • Dades: comunicació amb altres sistemes que fan tasques necessàries per a la nostra aplicació. Per exemple, la base de dades.

On s’executa cada capa?

  • Presentació: per a clients rics, al client. Per a B2C, al servidor (HTML).
  • Domini: al servidor, més fàcil manteniment. Al client, si és desconnectat. Si cal dividir-la, cal aïllar les dues parts.
  • Dades: al servidor, excepte si és un client desconnectat, llavors cal gestionar sincronitzacions.

Patrons del model

Dos estils d’implementació:

  • Senzill: similar al disseny de DB, un objecte de domini per taula. Ús del patró Active record (objecte que embolica una fila d’una taula, encapsula l’accés i afegeix lògica de domini en aquestes dades).
  • Ric: disseny diferent de la DB, amb herència, estratègies i altres patrons. Ús del patró Data Mapper (una capa de Mappers que mou les dades entre objectes i una base de dades mantenint-les independents les unes de les altres i del mateix mapper).

Patrons de la base de dades relacional

Cal fer un mapeig entre objectes i el món relacional perquè quan programem bases de dades relacionals, el seu model és diferent del dels objectes en memòria. Aquesta és una de les principals dificultats quan treballem amb els dos mons.

El patró general es diu Gateway: un objecte que encapsula l’accés a un recurs o sistema extern. Hi ha dues possibles implementacions:

  • Row data gateway: una instància del gateway per cada registre que retorna una consulta.
  • Table data gateway (o DAO): una instància gestiona tots els registres en una taula. Els registres es retornen a Record Sets.

Patró DAO (Data Access Object)

A continuació es pot veure el diagrama de classes del patró DAO i un exemple de seqüència.

Aquest patró utilitza un objecte de transferència, TransferObject, per intercanviar informació entre el client i la base de dades. Aquest objecte és una estructura de dades sense lògica de processament i habitualment immutable.

Cada objecte DAO realitza operacions sobre una taula: creació, lectura, modificació i esborrat.

Disseny d’un DAO

Aquests són els consells a l’hora de dissenyar un DAO:

  • És convenient utilitzar sempre una interfície per definir un DAO, per tal d’aïllar millor el domini de les dades.
  • Els mètodes públics, si utilitzen altres privats per a executar l’operació, han de compartir la mateixa Connection.
  • No retornar mai objectes associats a la capa de base de dades, per exemple, evitant fer visible els ResultSet o els SQLException.
  • Si una excepció SQL no és recuperable, utilitzeu una RuntimeException embolcall d’aquesta.
  • No utilitzar camps al marge del DataSource, ja que un DAO hauria de dissenyar-se sense estat per poder ser thread-safe.

Estratègies DAO

Objecte de transferència

L’objecte de transferència es pot utilitzar com es pot veure al següent exemple.

// suposem que a resultSet hi ha un registre d'una cerca
Persona persona = new Persona(); // transfer object
persona.setId(resultSet.getInt("id"));
persona.setName(resultSet.getString("name"));
...
return persona;

Els objectes de transferència poden implementar-se de diferents maneres, en particular, poden ser mutables o immutables. La preferència general seria que fossin immutables al client que els utilitza, malgrat que això pot significar complicar-los pel que fa a la seva programació.

  • Com a l’exemple, amb getters i setters. La més convencional.
  • Amb una interfície immutable, encara que la seva implementació sigui mutable. La més correcta.
  • Amb camps públics i sense getters ni setters. La més senzilla.
  • Alternativament, es pot utilitzar un Map<String, Object>, similar al concepte schemaless. No requereix classes, però es perd la validació en temps de compilació. Relacionat amb els JSON.

Col·lecció d’objectes de transferència

Com ja s’ha comentat, és millor no exposar objectes associats a la capa de dades al client. Per exemple, evitar fer visible els ResultSet. Així encapsulem i evitem dependències i haver de gestionar excepcions de tipus SQLException. En aquesta estratègia el DAO crea una sentència SQL i executa una consulta per obtenir un ResultSet. Llavors el DAO processa el ResultSet per recuperar tantes files de resultats coincidents com ho sol·liciti el client que fa la crida. Per a cada fila, el DAO crea un objecte de transferència i l’afegeix a una col·lecció que es retorna al client.

List<Persona> persones = new ArrayList<>();
while (resultSet.next()) {
    Persona persona = new Persona(); // transfer object
    persona.setId(resultSet.getInt("id"));
    persona.setName(resultSet.getString("name"));
    persones.add(to);
}
return persones;

Factoria de DAO

Quan tenim diversos DAO que cal crear per diferents taules, un patró habitual és la factoria de DAO.

public class DAOFactory {
    private static DAOFactory instance;
    private DAOFactory() {
        // init ConnectionFactory
    }
    public static DAOFactory getInstance() {
        if (instance == null)
            instance = new DAOFactory();
        return instance;
    }
    public CustomerDAO getCustomerDAO() {
        // implementar-ho
    }
    public AccountDAO getAccountDAO() {
        // implementar-ho
    }
    public OrderDAO getOrderDAO() {
        // implementar-ho
    }
}

Control de concurrència

Quan tractem de llegir i modificar dades, podríem afrontar alguns dilemes sobre la integritat i la validesa de la informació. Aquests dilemes sorgeixen a causa de les operacions de bases de dades xocant entre elles; per exemple, dues operacions d’escriptura o una operació de lectura i escriptura que col·lideixen.

Les estratègies per resoldre aquesta situació poden ser:

  • Bloqueig pessimista: s’espera una col·lisió, i llavors es fa un bloqueig dels recursos implicats. Cap altre client pot accedir-los fins que no es lliurin.
  • Bloqueig optimista: la col·lisió és poc probable. Es deixa fer, i quan acaba el processament, es comprova si hi ha hagut un problema.
  • Bloqueig massa optimista: no s’esperen col·lisions. Potser és un sistema monousuari.

Les transaccions dels SGBD resolen aquests problemes gràcies a la implementació de les característiques ACID. Primer, totes les sentències individuals que s’executen són atòmiques, és a dir, o bé s’executen o no ho fan, però mai provoquen problemes de consistència. Segon, podem estendre aquesta capacitat per a un conjunt de sentències mitjançant l’ús de transaccions.

Les estratègies de resolució de col·lisions poden ser:

  • Rendir-se.
  • Mostrar el problema, i deixar que decideixi l’usuari.
  • Barrejar els canvis.
  • Registrar el problema, i que ho resolgui algú altre més tard.
  • Ignorar la col·lisió.

Tanmateix, les transaccions no sempre són la solució en l’àmbit de les aplicacions. Les lectures/escriptures de dades separades en el temps poden provocar contenció de dades. Per exemple, una transacció no pot esperar que un usuari modifiqui les dades després d’haver-les llegit en un formulari.

Els mecanismes del bloqueig pessimista s’implementen mitjançant ordres que permeten bloquejar registres de la base de dades. Es fa perquè hi ha alta contenció i el cost de bloquejar és menor que el de fer enrere una transacció.

En canvi, el bloqueig optimista s’implementa fent que hi hagi un error si hi ha hagut col·lisió, i llavors cal que l’usuari torni a fer l’operació. Pot implementar-se amb un control de timestamps, comptadors o versions d’un registre. La modificació dels registres actualitza aquests valors, i pot utilitzar-se com a condició per fer fallar una transacció. Aquest podria ser una possible transacció:

  • Inici: guardar un timestamp/versió que marca l’inici de la transacció
  • Fer canvis: llegir i intentar escriure a la base de dades
  • Validar: veure si les dades modificades són les marcades inicialment
  • Confirmar/Desfer: si no hi ha conflicte, fer els canvis; si hi ha, desfer-los.

Referències