Persistencia de Objetos Java utilizando db4o

Puede encontrar la versión original de este artículo en Inglés en:

http://www.onjava.com/

Introducción

Muchas aplicaciones Java necesitan tratar con datos persistentes. En la mayoría de los casos, esto significa encararse con una base de datos relacional, posiblemente un base de datos obsoleta (o antigua) o un Sistema de Manejo de Bases de Datos (DBMS) estándar industrial. El API JDBC y los drivers para la mayoría de los sistemas de bases de datos proporcionan una forma estándar de utilizar SQL para ejecutar consultas a la base de datos. Sin embargo, el interface se complica por la "diferencia de impedancia" entre el modelo de objetos de dominio de la aplicación y el modelo relacional de la base de datos. El modelo de objetos está basado en principios de ingeniería de software y modela los objetos en el dominio del problema, mientras que el modelo relacional está basado en principios matemáticos y organiza los datos para una almacenamiento y recuperación eficientes. Ninguno de estos modelos es particularmente mejor que el otro, pero el problema es que son diferentes y no siempre se acoplan de forma confortable en la misma aplicación.

Algunas soluciones a este problema, como Hibernate y Java Data Objects, están diseñados para proporcionar al desarrallodor la persistencia transparente: la aplicación trata con objetos persistentes utilizano un API orientado a objetos sin la necesidad de código SQL embebido en el código Java. La Persistencia Manejada por el Contenedor (CMP) hace un trabajo similar para contenedores EJB, pero no hay una facilidad de persistencia general para la plataforma Java. En cualquiera de estas soluciones, los objetos son mapeados a tablas en una Base de Datos Relacional (RDBMS) por el marco de trabajo subyacente, que genera el SQL requerido para almacenar atributos de objetos. Cuanto más complejo sea modelo de objetos más díficil será el mapeo. Se necesita crear Descriptores, normalmente ficheros XML, para definir estos mapeos. La herencia y las relaciones muchos-a-muchos en particular, añaden complejidad ya que estas relaciones no se pueden representar directamente en el modelo relacional. Los árboles de herencia pueden mapearse a un conjunto de tablas de varias formas, la elección resulta de un balance entre la eficiencia de almacenamiento y la complejidad de las consultas, ya que se requiere una unión de tablas separadas para implementar relaciones muchos-a-muchos.

Almacenar objetos en una base de datos, que a su vez utiliza su propio modelo de objetos, ofrece otra solución. Durante los años 90 se ha desarrollado una gran variedad de Bases de Datos Orientadas a Objetos (OODBMS), pero dichas herramientas pueden ser complejas de configurar y pueden requerir el uso de un lenguaje de definición de objetos. Los objetos se almacenan como objetos, pero no son nativos al lenguaje de la aplicación. Estos productos no han tenido un fuerte impacto en el mercado más allá de sus áreas "nicho", y el esfuerco parece concentrarse principalmente en los APIs orientados a objetos para bases de datos relacionales así como bases de datos híbridas objeto-relacional.

Bases de Datos Embebidas

En algunas aplicaciones no es necesaria la sobrecarga de una potente DMBS industrial, y los requerimientos de almacenacimiento de datos los proporciona mejor un motor de base de datos pequeño y embebible. Por ejemplo SQLite, proporciona un motor de base de datos SQL auto-contenido. Pero aún así el interface con Java es a través del driver JDBC, ya que las soluciones basadas en SQL todavía se ven afectadas por la diferencia de impedancia.

En muchos casos la persistencia se puede conseguir más fácilmente utilizando un motor de base de datos de objetos embebido. Es un buen momento para mirar db4o. Creado por Carl Rosenberg, db4o durante un tiempo solo estaba disponible de forma comercial, pero ahora es 'open source' y recientemente se ha liberado bajo la licencia GPL (con ciertas condiciones que puedes ver en su página web).

db4o tiene algunas características interesantes:

  • No hay diferencia de impedancia: los objetos se almacenan tal y como son.
  • Manejo automático del esquema de base de datos.
  • No hay que cambiar las clases para poder almacenarlas.
  • Sin junturas en la unión con el lenguaje Java (o .NET).
  • Uniones de datos automatizadas.
  • Se instala añadiendo un único fichero de librería de 250Kb (jar para Java o DLL para .NET).
  • Un único fichero de base de datos.
  • Versionado del esquema automático.
  • Consultas-por-ejemplo (Query-By-Example).
  • S.O.D.A. (Simple Object Database Access), un API de consultas open source.

¿Para qué es bueno db4o?

db4o ha sido elegida para aplicaciones en sistemas embebidos en los que las características críticas son cero administración, eficiencia, y pequeño tamaño. En Alemania, el departamento de IT de BMW, por ejemplo, lo utliza en prototipos de coches electrónicos. Die Mobilanten, también el Alemania, utiliza db4o en una solución basada en PDA para utilidades de tamaño medio. En los U.S.A. el sistema de imagen retinal de Massie Systems para el diagnóstico de ojos infantiles utiliza db4o para almacenar las imágenes de sus clientes.

La completa simplicidad de la forma en que se almacenan objetos con db4o también es atractiva para propósitos de enseñanza. La universidad de Essex y Texas A&M University utilizan db4o para enseñanza e investigación. En mi propio colegio, para los estudiantes que aprenden cómo aplicar conceptos orientados a objetos en sus propios proyectos, la necesidad de interactuar con una base de datos relacional puede tener una influencia negativa en su aproximación al diseño de sus modelos de dominio. Utilizar db4o les permite trabajar con datos persistentes sin la distracción de los modelos de datos conflictivos y sin la necesidad de emplear una significante cantidad de tiempo en aprender a utilizar herramientas como Hibernate o una compleja OODBMS. Además, aprender los conceptos de una API de consultas orientado a objetos podría resultarles muy útil en el futuro.

Mismo API, Diferente Almacenamiento

Algunas veces simplemente se tiene que utilizar una base de datos relacional. Desde el punto de vista de un desarrollador Java, la presistencia transparente es lo ideal. Si la persistencia se implementa mediante un API orientado a objetos, el desarrollador no tiene que aprender diferentes técnicas para utilizar los diferentes tipos de almacenamiento de datos. Aunque db4o no es compatible con JDO-(como resultado también es más fácil de utilizar), sus creadores tienen relaciones con otros proyectos de código abierto, como MySQL e Hibernate y están trabajando juntos en busca de un API de persistencia de objetos único y consistente que interactúe con bases de datos de objetos incluyendo la propia db4o, bases de datos relacionales, y esquemas de almacenamiento alternativos como Prevayler. Si hacer las cosas según el estándar JDO es importante para usted, debería ver ObjectDB, que es una base de datos de objetos puramente compatible con JDO.

Un Ejemplo

Este ejemplo demuestra la simple que es crear una base de datos y almacenar objetos. También ilustra dos métodos de consulta: Query-By-Example (QBE) y el más flexible S.O.D.A. Aquí puede bajarse el código fuente de este artículo. Para ejecutarlo sólo tiene que añadir el fichero JAR de db4o a su CLASSPATH y ejecutar la clase principal Db4oTest.java.

Las dos clases del ejemplo representan un Team y un Player de Baseball. Para hacer las cosas más interesantes también tenemos una clase Pitcher. La clase Pitcher es una subclase de Player y añade un campo extra sobre los heredados. Team tiene un atributo que es una lista de objetos Player, que puede, por supuesto, incluir objetos Pitcher. Los objetos Team, Player, y Pitcher son objetos "normales" de Java, sin código de persistencia. No se requieren atributos de clave única, porque la base de datos de objetos almacena automáticamente los objetos con un identificador de objeto único (OID).

La clase Player

public class Player {
    
    protected String name;
    protected int squadNumber;
    protected float battingAverage;
    protected Team team;
    
    public Player(String name, int squadNumber,
        float battingAverage){
        this.name = name;
        this.squadNumber = squadNumber;
        this.battingAverage = battingAverage;
    }
    
    public void setName(String n){this.name = n;}  
    public String getName(){return this.name;} 

    public void setSquadNumber(int s){this.squadNumber = s;}  
    public int getSquadNumber(){return this.squadNumber;}   
   
    public void setBattingAverage(final float b) {
        this.battingAverage = b; } 
    public float getBattingAverage(){
        return this.battingAverage;}

    public void setTeam(Team t) {this.team = t;}
    public Team getTeam() {return this.team;}
    
    public String toString() {
        return name + ":" + battingAverage;
    }
}

La clase Pitcher

public class Pitcher extends Player{   
    private int wins;
    
    public Pitcher(String name, int squadNumber,
                    float battingAverage, int wins) {
        super(name,squadNumber,battingAverage);
        this.wins = wins;
    }  

    public void setWins(final int w){this.wins = w;}
    public int getWins() {return this.wins;}
    
    public String toString() {
        return name + ":" + battingAverage + ", " + wins;
    }
}

La clase Team

import java.util.List;
import java.util.ArrayList;

public class Team {
    
    private String name;
    private String city;
    private int won;
    private int lost;
    private List players;
    
    public Team(String name, String city,
                int won, int lost){
        this.name = name;
        this.city = city;
        this.won = won;
        this.lost = lost;
        this.players = new ArrayList();
    }
    
    public void addPlayer(Player p) {
        players.add(p);
    }
    
    public void setName(String n){this.name = n;}   
    public String getName(){return this.name;}  

    public void setStadium(String c){this.city = c;}
    public String getCity(){return this.city;} 

    public void setPlayers(List p){players = p;}
    public List getPlayers(){return players;}
    
    public void setWon(int w) {this.won = w;}
    public int getWon(){return this.won;}

    public void setLost(int l) {this.lost = l;}
    public int getLost() {return this.lost;}

    public String toString() {
        return name;
    } 
}

Primero, creamos algunos datos de prueba con los que trabajar:

// Create Players
Player p1 = new Player("Barry Bonds", 25, 0.362f);
Player p2 = new Player("Marquis Grissom", 9, 0.279f);
Player p3 = new Player("Ray Durham", 5, 0.282f);
Player p4 = new Player("Adrian Beltre", 29, 0.334f);
Player p5 = new Player("Cesar Izturis", 3, 0.288f);
Player p6 = new Player("Shawn Green", 15, 0.266f);
        
// Create Pitchers
Player p7 = new Pitcher("Kirk Rueter",46, 0.131f, 9);
Player p8 = new Pitcher("Kazuhisa Ishii",17, 0.127f, 13);
        
// Create Teams
Team t1 = new Team("Giants", "San Francisco", 91, 71);
Team t2 = new Team("Dodgers", "Los Angeles", 93, 69);
        
// Add Players to Teams
t1.addPlayer(p1); p1.setTeam(t1);
t1.addPlayer(p2); p2.setTeam(t1);
t1.addPlayer(p3); p3.setTeam(t1);
t2.addPlayer(p4); p4.setTeam(t2);
t2.addPlayer(p5); p5.setTeam(t2);
t2.addPlayer(p6); p6.setTeam(t2);
        
// Add Pitchers to Teams
t1.addPlayer(p7); p7.setTeam(t1);
t2.addPlayer(p8); p8.setTeam(t2);

Almacenar los Datos

Un objeto Team se puede almacenar con una sóla línea de código:

db.set(t1);

Donde db es una referencia a un objeto ObjectContainer, que se haya creado abriendo un fichero de base de datos, de esta forma:

ObjectContainer db = Db4o.openFile(filename);

Una base de datos db4o es un único fichero con una extensión .yap, y se utiliza su método set para almacenar objetos.

Observe que está linea almacena el objeto Team y su colección de objetos Player. Se puede probar esto recuperando uno de esos objetos Player. La forma más simple de hacer esto es utilizando QBE.

Consulta Simple: QBE

El siguiente código lista todos los objetos Player que sean iguales al objeto de ejemplo; sólo debería haber un resultado. Los resultados se obtienen como un ObjectSet llamando al método get de ObjectContainer.

Player examplePlayer = new Player("Barry Bonds",0,0f);
ObjectSet result=db.get(examplePlayer);
        
System.out.println(result.size());
while(result.hasNext()) {
    System.out.println(result.next());
}

Se pueden obtener todos los objetos Player que se hayan almacenado creando un objeto de ejemplo vacío (todos son campos son null o 0), de esta forma:

Player examplePlayer = new Player(null,0,0f);
ObjectSet result=db.get(examplePlayer);

System.out.println(result.size());
while(result.hasNext()) {
    System.out.println(result.next());
}

La salida se parecería a esto:

8
Kazuhisa Ishii:0.127, 13
Shawn Green:0.266
Cesar Izturis:0.288
Adrian Beltre:0.334
Kirk Rueter:0.131, 9
Ray Durham:0.282
Marquis Grissom:0.279
Barry Bonds:0.362

Observe que se pueden recuperar todos los objetos de la clase Player y de todas sus subclases (como Pitcher en este ejemplo) sin ningún exfuerzo extra. Los objetos Pitcher muestran en la salida un atributo más: wins. Con una base de datos relacional tendríamos que decidir cómo mapear el árbol de herencia a tablas y posiblemente hubieramos tenido que unir tablas para recuperar todos los atributos de todos lo objetos.

Actualizar y Borrar

La actualización de objetos se pude consegir utilizando una combinación de las técnicas anteriores. El siguiente código asume que sólo se ha encontrado una correspondencia, y el objeto encontrado se fuerza a Player para poder modificar sus atributos:

Player examplePlayer = new Player("Shawn Green",0,0f);
ObjectSet result = db.get(examplePlayer);
Player p = (Player) result.next();                  
p.setBattingAverage(0.299f);
db.set(p);

De forma similar se pueden borrar objetos de la base de datos:

Player examplePlayer = new Player("Ray Durham",0,0f);
ObjectSet result = db.get(examplePlayer);
Player p = (Player) result.next();    
db.delete(p);

Un soporte de Consultas más poderoso

Una de las mayores desventajas de versiones anteriores de db4o era que QBE proporcionaba una capacidad de consulta bastante limitada. Por ejemplo, no se podia ejecutar una consulta como "todos los jugadores con un promedio de bateo superior a .300". Ahora db4o incluye el API S.O.D.A. para proporcionar consultas que se acercan mucho más al poder de SQL. Un ejemplar de la clase Query representa un nodo en un gráfico de criterios de consulta a los que se pueden aplicar restricciones. Un nodo puede representar una clase, múltiples clases o un atributo de una clase.

El siguiente código demuestra cómo crear la consulta descrita en el párrafo anterior. Definimos una nodo de consulta y lo restringiremos a la clase Player. Esto significa que la consulta sólo devolverá objetos Player. Luego descendemos la rama para encontrar un nodo que representa un atributo llamado “battingAverage” y lo restringimos a los mayores de 0.3. Finalmente se ejecuta la consulta para que devuelva todos los objetos de la base de datos que cumplen las restricciones.

Query q = db.query();
q.constrain(Player.class);
q.descend("battingAverage").constrain(new Float(0.3f)).greater();
ObjectSet result = q.execute();

A primera vista, esto realiza una consulta similar al SQL:

SELECT * FROM players WHERE battingAverage > 0.3

Sin embargo, el diseño de la clase Player permite crear relaciones inversas entre objetos Team y Player, como se vió en los datos de prueba. Un Team tiene una lista de objetos Player, mientras que cada objeto Player tiene una referencia a un Team. Esto significa que el resultado de una consulta contiene objetos Player y Team. El siguiente código demuestra esto:

System.out.println(result.size());
while(result.hasNext()) {
    // Print Player
    Player p = (Player) result.next();
    System.out.println(p);
    // Getting Player also gets Team - print Team
    Team t = p.getTeam();
    System.out.println(t);
}

Salida :

2
Adrian Beltre:0.334
Dodgers
Barry Bonds:0.362
Giants

Ahora la consulta sería similar a este SQL:

SELECT teams.name, players.name, players.battingAverage FROM teams, players 
WHERE teams.teamID = players.playerID
AND battingAverage > 0.3

Esto a funcionado debido a que la relación inversa se diseñó dentro del modelo de objetos. Las bases de datos de objetos son navegables: sólo se pueden recuperar los datos siguiendo la dirección de las relaciones predefinidas. Por otro lado, las bases de datos relacionales, no tienen direccionalidad en sus uniones de tablas y por lo tanto, permiten más flexibilidad para consultas personalizadas. Sin embargo, dando unas relaciones de objetos correctas, los objetos relacionados se pueden recuperar de la base de datos de objetos con muy poco esfuerzo de programación. Los modelos de la base de datos y los modelos de objetos de la aplicación son idénticos, por eso el programador no tiene que pensar de forma diferente en los datos. Si se pude obtener el Team de un Player dado cuando están en memoria, se puede hacer lo mismo desde la base de datos.

¿Qué más puede hacer S.O.D.A.?

SQL permite ordenar los resultados; S.O.D.A. también. Este ejemplo muestra cómo los objetos Player almacenados se pueden recuperar ordenándolos por battingAverage. (¡Ahora es bastante obvio quienes son los pitchers!)

Query q = db.query();
q.constrain(Player.class);
q.descend("battingAverage").orderAscending();
ObjectSet result = q.execute();

Salida:

7
Kazuhisa Ishii:0.127, 13
Kirk Rueter:0.131, 9
Marquis Grissom:0.279
Cesar Izturis:0.288
Shawn Green:0.299
Adrian Beltre:0.334
Barry Bonds:0.362

S.O.D.A. permite definir consultas más complejas utilizando código que es bastante simple una vez que se evita la tentación de pensar de forma 'relacional'. Para configurar las restricciones sólo hay que navegar por el gráfico de consulta para encontrar las clases o atributos a los que se quieren poner condiciones. El gráfico de consulta está muy relacionado con el modelo de objetos de dominio, que todos los desarrolladores deberían entender. Por otro lado, para conseguir un resultado similar con SQL se necesita tener en cuentra cómo se han mapeado los objetos de dominio en las tablas relacionales.

Este ejemplo muestra cómo seleccionar condiciones a dos atributos de la clase Player para encontrar "jugadores con un promedio de bateo por encima de .130 que sean pitchers con más de 5 wins". De nuevo, definimos un nodo de consulta y lo restringimos a la clase Player. Descendemos el gráfico para encontrar un nodo que represente el atributo llamado “battingAverage” y lo restringimos a los mayores de 0.13. El resultado es un objeto Constraint. Para selecconar la siguiente restricción, descendemos para encontrar el nodo que representa el atributo "wins"; esto en sí mismo significa que la consulta sólo encontrará objetos Pitcher. Este nodo está restringido a ser mayor que 5, y esto se combina utilizando un "AND" lógico con el primer objeto Constraint.

Query q = db.query();
q.constrain(Player.class);
Constraint constr = q.descend("battingAverage").constrain(new Float(0.13f)).greater();
q.descend("wins").constrain(new Integer(5)).greater().and(constr);
result = q.execute();

Salida:

1
Kirk Rueter:0.131, 9
Giants

El último ejemplo muestra cómo combinar condiciones de atibutos de diferentes clases para encontrar "jugadores con promedio de bateo superior a .300 que estén en un equipo con menos de 92 wins". La forma más fácil de hacer esto es empezar con Player, y luego navegar a Team. Descendemos para encontrar el nodo "battingAverage" igual que antes y seleccionamos una Constraint. Luego descendemos para encontrar el atributo "team". Como este atributo es del tipo Team, el nodo representa la clase Team, podemos descender de nuevo al nodo que representa el atributo "won" del Team y configurar una restricción para él. Finalmente, combinamos esto con la primera Constraint.

Query q = db.query();
q.constrain(Player.class);
Constraint constr = q.descend("battingAverage").constrain(new Float(0.3f)).greater();
q.descend("team").descend("won").constrain(new Integer(92)).smaller().and(constr);
result = q.execute();

Salida:

1
Barry Bonds:0.362
Giants

Conclusión

Una base de datos de objetos, pequeña y embebible ofrece una forma de persistencia de objetos simple y compacta. db4o ahora es una base de datos Open Source que ofrece muchas características atractivas y soporta tanto Java como .NET. La simplicidad de instalación y utilización así como la ausencia de la diferencia de impedancia entre los modelos de objetos y de datos hace que db4o sea muy útil en un amplio rango de aplicaciones de negocio y educativas.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP