20  Bases de dades orientades a objectes

Objectius

  • Explicar què és una base de dades orientada a objectes (BDOO) i com es diferencia del model relacional.
  • Entendre el desajust objecte-relacional i per què les BDOO l’eviten.
  • Comparar una BDOO amb una base de dades relacional en termes de model, consultes, maduresa i rendiment.
  • Conèixer ObjectDB, una BDOO per a Java, i configurar-la amb Maven.
  • Definir una entitat amb @Entity, @Id i @GeneratedValue.
  • Guardar, cercar, actualitzar i esborrar objectes amb EntityManager.
  • Fer consultes amb JPQL i modelar relacions 1:N entre objectes.

Als capítols anteriors hem fet sobreviure les dades de dues maneres. Amb serialització (Capítol 18) guardàvem objectes Java tal qual, en binari, però sense poder cercar-hi ni compartir-los. Amb JDBC (Capítol 19) teníem una base de dades relacional de debò, però cada objecte s’havia de trencar en files i columnes i traduir-lo amb SQL.

En aquest capítol veiem la tercera via: una base de dades orientada a objectes, que guarda els objectes directament, amb els seus atributs, mètodes i relacions, i els consulta sense fer-los passar per taules.

20.1 Què és una base de dades orientada a objectes?

Una base de dades orientada a objectes (BDOO) emmagatzema objectes com a unitats. Si guardes un objecte Llibre que té un atribut Autor, la BDOO guarda el llibre i la referència a l’autor, tal com eren a la memòria.

flowchart LR
    A["Objecte Llibre<br/>titol, isbn, anys"] -->|"guardar directament"| B[("BDOO<br/>llibres.odb")]
    B -->|"recuperar directament"| C["Objecte Llibre<br/>reconstruït tal qual"]

La idea és que el programa treballi sempre amb objectes, tant mentre són a la memòria com quan estan desats al disc. La base de dades no es nota: és com si la memòria del programa s’estengués més enllà de l’execució.

Exemples de BDOO per a Java: ObjectDB, db4o, Versant. En aquest capítol farem servir ObjectDB.

NotaRelaciona-ho amb el que ja saps

La serialització (Capítol 18) ja guardava objectes directament. La diferència és que una BDOO afegeix el que la serialització no pot:

  • Cercar dins de les dades sense carregar-les totes.
  • Compartir les dades entre diversos programes o usuaris.
  • Fer consultes combinant condicions (com WHERE en SQL).

La BDOO és, per dir-ho així, una serialització amb motor de consultes.

20.2 El desajust objecte-relacional

Recorda el problema que vam deixar apuntat a Capítol 19: quan guardes objectes en una base de dades relacional, has de traduir cada classe a una taula, cada objecte a una fila i cada atribut a una columna. Aquesta traducció manual es coneix com a desajust d’impedància (o object-relational impedance mismatch).

flowchart LR
    O["Objecte Llibre<br/>+ objecte Autor dins"] -->|"trencar en"| T["Taula LLIBRE<br/>+ taula AUTOR + clau forana"]
    T -->|"tornar a muntar<br/>amb SQL"| O2["Objecte Llibre<br/>reconstruït"]

Amb una BDOO no cal trencar res: el que guardes és el que tens a la memòria. Un Llibre amb el seu Autor dins es guarda sencer i es recupera sencer.

20.3 BDOO vs base de dades relacional

Abans d’escriure codi, fixem bé les diferències.

Aspecte BD orientada a objectes BD relacional
Unitat d’emmagatzematge Objecte (amb atributs i relacions) Fila d’una taula
Model El mateix que el codi: classes, herència Taules, files i columnes
Llenguatge de consulta JPQL / consultes sobre objectes SQL (estàndard)
Desajust amb el codi OO Cap: guardes objectes tal qual Cal traduir objecte ↔︎ taula
Relacions Referència directa entre objectes Claus foranes i JOIN
Maduresa i suport Nínxol, poques eines Enorme: molts SGBD, eines, comunitat
Escalabilitat i concurrència Limitada Molt provada
Exemples ObjectDB, db4o, Versant SQLite, MySQL, PostgreSQL, Oracle
ConsellAnalogia: dues maneres de guardar una biblioteca

Una BD relacional és com guardar els llibres en fitxes d’un arxivador: cada fitxa (fila) té camps fixos (columnes) i has de fer còpies quan un llibre parla d’un autor.

Una BDOO és com posar els llibres sencers en prestatgeries i, dins de cada llibre, a la contraportada, hi ha la foto de l’autor i la seva biografia. Recuperes el llibre i ja hi tens tot el que portava a dins.

20.4 ObjectDB: la primera configuració

ObjectDB és una base de dades orientada a objectes escrita en Java i pensada per a Java. Té una llicència free per a ús acadèmic i de desenvolupament, i funciona com a fitxer local (un .odb), com SQLite però amb objectes.

Per configurar-la amb Maven cal afegir dues coses al pom.xml:

  1. El repositori d’ObjectDB, perquè el jar no és a Maven Central.
  2. La dependència com.objectdb:objectdb.
<repositories>
    <repository>
        <id>objectdb</id>
        <url>https://m2.objectdb.com</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.objectdb</groupId>
        <artifactId>objectdb</artifactId>
        <version>2.9.5</version>
    </dependency>
</dependencies>
ImportantLa base de dades és un fitxer

Com amb SQLite, la base de dades sencera és un únic fitxer (per exemple llibres.odb). No cal instal·lar cap servidor. Això la fa ideal per aprendre i per a aplicacions d’un sol usuari o d’un sol procés.

20.5 La connexió: EntityManagerFactory i EntityManager

ObjectDB usa l’API JPA (Java Persistence API). Les dues classes clau són:

  • EntityManagerFactory: la “fàbrica” que gestiona les connexions. Es crea una vegada amb Persistence.createEntityManagerFactory(...).
  • EntityManager: la “connexió” cap a una base de dades concreta. Amb ella es guarden i es consulten objectes.

La manera més directa d’obrir una base de dades és passar-li la ruta del fitxer com a paràmetre:

import javax.persistence.*;

public class ProvaConnexio {

    public static void main(String[] args) {
        // La base de dades és el fitxer llibres.odb
        EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("$objectdb/db/llibres.odb");

        EntityManager em = emf.createEntityManager();

        System.out.println("Base de dades oberta correctament!");

        em.close();       // Tanquem la connexió...
        emf.close();      // ...i la fàbrica
    }
}

Si la base de dades no existeix, ObjectDB la crea automàticament. Fixa’t que em i emf són recursos que cal tancar, igual que una Connection amb JDBC.

ConsellPer què “Entity” i no “Objecte”?

L’API JPA fa servir la paraula entitat per referir-se a la classe que es pot guardar a la base de dades. És el mateix concepte que “objecte persistent”. No et confonguis: una entitat és una classe Java normal, però amb unes quantes anotacions que la marquen com a persistible.

20.6 Definir una entitat: @Entity, @Id, @GeneratedValue

Perquè ObjectDB pugui guardar objectes d’una classe, aquesta ha de ser una entitat. S’aconsegueix amb anotacions:

  • @Entity: marca la classe com a entitat (persistible).
  • @Id: indica quin atribut és la clau de l’objecte (l’identificador únic).
  • @GeneratedValue: fa que la clau es generi automàticament en guardar.
import java.io.Serializable;
import javax.persistence.*;

@Entity
public class Llibre implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private long id;                 // La clau: la genera ObjectDB

    private String titol;
    private String isbn;
    private int anyPublicacio;

    public Llibre() {}               // Constructor buit: JPA el necessita

    public Llibre(String titol, String isbn, int anyPublicacio) {
        this.titol = titol;
        this.isbn = isbn;
        this.anyPublicacio = anyPublicacio;
    }

    public long getId() { return id; }
    public String getTitol() { return titol; }

    @Override
    public String toString() {
        return titol + " (" + anyPublicacio + ") ISBN " + isbn;
    }
}
ImportantRequisits d’una entitat

Perquè ObjectDB pugui reconstruir els objectes:

  • Ha d’haver-hi un constructor sense arguments (buit). Si en declares d’altres, cal que el buit existeixi (pot ser public o protected).
  • És recomanable que implementi Serializable i declari el serialVersionUID (ja ho vam veure a Capítol 18).
  • Els atributs que es guarden han de ser accessibles (o tenir getters/setters) i de tipus persistible: primitius, String, col·leccions o altres entitats.

20.7 Guardar objectes: la transacció

Guardar objectes amb ObjectDB segueix sempre el mateix esquema de tres passos:

  1. Obrir una transacció amb em.getTransaction().begin().
  2. Guardar l’objecte amb em.persist(...).
  3. Confirmar-la amb em.getTransaction().commit().
EntityManager em = emf.createEntityManager();

// 1. Comencem la transacció
em.getTransaction().begin();

// 2. Guardem un objecte
Llibre llibre = new Llibre("El nom del vent", "978-8401352836", 2007);
em.persist(llibre);

// 3. Confirmem els canvis
em.getTransaction().commit();

System.out.println("Llibre guardat amb id " + llibre.getId());
em.close();

Fixa’t que, després de persist, l’objecte ja té l’id assignat per la base de dades.

AlertaNo t’oblidis de la transacció

Si crides em.persist(...) sense begin() i commit(), els canvis no es desen. La transacció és el contracte que li diu a ObjectDB “tot això ho vull confirmar d’un cop”. Fes servir sempre el patró begin → operacions → commit.

20.8 Cercar objectes per clau: find

Per recuperar un objecte a partir de la seva clau (l’id), fem servir em.find(Classe, clau):

Llibre llibre = em.find(Llibre.class, 1);

if (llibre != null) {
    System.out.println("Trobat: " + llibre);
} else {
    System.out.println("No hi ha cap llibre amb aquest id.");
}

find retorna l’objecte reconstruït tal com el vam guardar, o null si no existeix. No hi ha SQL, no hi ha traducció: tornes a tenir el mateix objecte Java.

flowchart LR
    A["em.find(Llibre.class, 1)"] --> B[("llibres.odb")]
    B -->|"objecte reconstruït"| C["Llibre amb tots els seus camps"]

20.9 Consultes amb JPQL

Per trobar objectes que compleixin condicions, ObjectDB fa servir JPQL (Java Persistence Query Language). És molt semblant a SQL, però:

  • Consultes classes i atributs, no taules i columnes.
  • El resultat són objectes, no files.

Una consulta que retorna una llista d’objectes es fa amb createQuery(...).getResultList():

import javax.persistence.*;
import java.util.*;

// Tots els llibres
TypedQuery<Llibre> q1 = em.createQuery("SELECT l FROM Llibre l", Llibre.class);
List<Llibre> tots = q1.getResultList();

// Llibres publicats a partir d'un any (paràmetre amb :nom)
TypedQuery<Llibre> q2 = em.createQuery(
    "SELECT l FROM Llibre l WHERE l.anyPublicacio >= :any ORDER BY l.titol",
    Llibre.class);
q2.setParameter("any", 2000);
List<Llibre> recents = q2.getResultList();

for (Llibre l : recents) {
    System.out.println(l);
}
SQL (relacional) JPQL (objectes)
SELECT * FROM llibre SELECT l FROM Llibre l
WHERE any_publicacio >= 2000 WHERE l.anyPublicacio >= 2000
ORDER BY titol ORDER BY l.titol
Retorna files (resultat pla) Retorna objectes Llibre

Les claus del JPQL són les mateixes de SQL (SELECT, FROM, WHERE, ORDER BY, GROUP BY…), però sempre operant sobre objectes i els seus atributs (puntet, com al codi Java).

20.10 Actualitzar i esborrar objectes

Actualitzar és senzill: modifiques l’objecte dins d’una transacció i el “retornes” amb em.merge(...).

em.getTransaction().begin();

Llibre l = em.find(Llibre.class, 1);
l.setTitol("El nom del vent (edició especial)");
em.merge(l);                          // Desem els canvis

em.getTransaction().commit();

Esborrar, amb em.remove(...) (cal passar-li l’objecte, que podem obtenir amb find):

em.getTransaction().begin();

Llibre l = em.find(Llibre.class, 1);
if (l != null) {
    em.remove(l);                     // L'eliminem de la base de dades
}

em.getTransaction().commit();

20.11 Relacions entre objectes: llibres i autors

El punt fort de les BDOO són les relacions: un objecte pot tenir una referència a un altre i tot es guarda junt. Amb JPA es marquen amb anotacions:

  • @ManyToOne: molts llibres → un autor (al costat del llibre).
  • @OneToMany: un autor → molts llibres (al costat de l’autor).
@Entity
public class Llibre {
    @Id
    @GeneratedValue
    private long id;

    private String titol;
    private int anyPublicacio;

    @ManyToOne
    private Autor autor;              // Referència a l'autor
}
@Entity
public class Autor {
    @Id
    @GeneratedValue
    private long id;

    private String nom;

    @OneToMany(mappedBy = "autor", cascade = CascadeType.ALL)
    private List<Llibre> llibres = new ArrayList<>();
}

Amb cascade = CascadeType.ALL, en guardar l’autor es guarden també els seus llibres automàticament. Recorda com de feixuc era fer això amb taules i claus foranes a Capítol 19: aquí només assignes la referència i la base de dades ho guarda tot.

flowchart LR
    A["Objecte Autor<br/>nom + llista de Llibre"] -->|"una peça"| B[("BDOO<br/>llibres.odb")]
    B -->|"una peça"| A2["Objecte Autor<br/>amb els seus llibres dins"]

ConsellCompara-ho amb JDBC

A Capítol 19, per relacionar un llibre amb el seu autor havíem de crear la taula autor, afegir-hi la columna autor_id com a clau forana i fer un JOIN per consultar-los junts.

Amb ObjectDB, la relació és una referència de Java com qualsevol altra. El programa no es diferencia gens d’un programa que treballa només amb memòria.

20.12 Avantatges i inconvenients

Per decidir quan fer servir una BDOO, val la pena tenir-ho clar:

Avantatges Inconvenients
Sense desajust d’impedància: objectes tal qual Tecnologia de nínxol: poques eines i comunitat
Relacions directes entre objectes Menys madura en escalabilitat i concurrència
Coherent amb l’orientació a objectes del codi Sense estàndard de consulta universal (JPQL és de facto)
Fàcil d’aprendre si ja saps POO La majoria d’empreses demanen SQL
Ideal per a prototips i models complexos Millor opció professional: BDR + ORM (com Hibernate)
NotaLa decisió pràctica

Al món professional, la base de dades relacional és l’estàndard, i el desajust objecte-relacional es resol amb frameworks ORM (Hibernate, JPA) que automatitzen la traducció objecte ↔︎ taula.

Les BDOO com ObjectDB brillen quan el model d’objectes és complex (herències, relacions profundes) i la consulta és secundària. Per aprendre, però, són una eina perfecta per entendre què significa persistir objectes sense les distraccions de SQL.

20.13 Errors típics

Alerta
  • Oblidar @Entity a la classe → ObjectDB no sap que és persistible i llança excepcions en persist.
  • No tenir constructor sense arguments → error en reconstruir l’objecte.
  • Oblidar begin() / commit() → els canvis no es desen.
  • No tancar em i emf → es queden recursos oberts.
  • Confondre persist (guardar un objecte nou) amb merge (desar els canvis d’un objecte existent).
  • En JPQL, escriure noms de taules i columnes de SQL (FROM llibre WHERE any_publicacio) en lloc de classes i atributs (FROM Llibre l WHERE l.anyPublicacio).
  • No afegir el repositori https://m2.objectdb.com al pom.xml → Maven no troba la dependència.

20.14 Per practicar

  1. Crea la classe Soci (nom, número de carnet, any d’alta) com a entitat amb @Entity, @Id i @GeneratedValue, i un constructor buit.
  2. Escriu un programa que obri la base de dades socis.odb, guardi tres socis dins d’una transacció i en mostri els id assignats.
  3. Recupera un soci amb find, modifica-li el nom amb merge i comprova-ho tornant-lo a cercar.
  4. Fes una consulta JPQL que retorni els socis donats d’alta a partir de l’any 2020, ordenats per nom.
  5. Explica amb les teves paraules, comparant amb Capítol 19, què estalvies quan la classe Soci té un atribut List<Prestec> prestecs.

20.15 Resum

  • Una BDOO guarda objectes directament, amb atributs i relacions, i evita el desajust objecte-relacional de les taules.
  • ObjectDB és una BDOO per a Java (un fitxer .odb, sense servidor) que es configura afegint el repositori i la dependència al pom.xml.
  • Una classe persistible és una entitat: @Entity, amb @Id i @GeneratedValue, constructor buit i Serializable.
  • El EntityManager és la connexió: persist guarda, find cerca per clau, merge actualitza i remove esborra, sempre dins d’una transacció (begin → operacions → commit).
  • Les consultes es fan amb JPQL, que és com SQL però sobre classes i atributs, i retorna objectes.
  • Les relacions (1:N, N:M) són referències de Java directes: @ManyToOne i @OneToMany amb cascade.
  • Al món professional domina la BDR amb ORM; les BDOO són una alternativa de nínxol, però excel·lents per aprendre persistència orientada a objectes.