DAM-M3-UF6. Persistència a bases de dades OO
torna M3 - Programació
Contingut
Introducció
Hi ha múltiples maneres de gestionar la persistència des de Java, alguns exemples:
BBDD Relacionals | SQL incrustat (Embedded) | Per exemple SQLJ | Utilitza directament SQL
Molt eficient. Poc usat i amb poc suport (Per exemple Eclipse no) |
|
Intermediari (Driver) | Exemples:
JDBC ODBC |
Les interfícies són lleugeres, aporten mètodes d'utilitat per facilitar la gestió de les tasques més comuns |
|
Eines específiques | Eines d'alt nivell:
Hibernate API de Persistència Java |
Afegeixen gran abstracció respecte la base de dades, i per tant independència de la tecnologia.
En general requereixen d'un procés previ de configuració |
BBDD OO | Llibreries específiques per a cada SGBD OO |
En general depèn del tipus de base de dades, les més comuns les BBDD Relacionals.
Bases de dades OO
Aquests tipus de BBDD emmagatzemen objectes (atributs i mètodes). La persistència és doncs molt més directa, no existeixen les limitacions de les BBDD Relacionals. Des del punt de vista del programador és molt més senzill treballar amb aquest tipus de base de dades (Si es programa amb objectes).
Per contra existeixen poques BBDD OO al mercat, en general no s'utilitzen i conceptualment són difícils d'entendre (com s'emmagatzemen les dades? i els mètodes?...)
Cada objecte guardat s'identifica per un OID (Object Identifier), independent de les dades que conté.
ODBMS : Object Database Management System
http://en.wikipedia.org/wiki/Object_database
http://en.wikipedia.org/wiki/Comparison_of_object_database_management_systems
db4o - Database 4(for) Objects
db4o és un exemple de les múltiples BBDD OO del mercat
Algunes de les seves característiques són:
- De codi obert sota llicència GPL, orientat a Java
- Senzill d'utilitzar i amb bon rendiment,
- La base de dades és un únic fitxer
- Només cal importar una llibreria "db4o-8.0.....-all-java5.jar"
OME (Object Manager Enterprise)
El propi gestor incorpora una utilitat (Plugin per Eclipse) que permet explorar els objectes de la base de dades
Vista OME:
- db4o browser: mostra les classes i els atributs (Botó dret: View All Objects)
- Property View: Propietats de la BBDD, de les classes i dels objecte (OID), permet crear índexs
- Build Query: Crear consultes, arrossegant des del Browser
- Query Results: Mostrar resultat de les consultes i modificar valors
Menú OME
- Connectar i desconnectar de la Base de dades
Els exemples que es treballen a continuació són extrets de la documentació de db4o
Connectar i desconnectar
La connexió es molt simple, només cal indicar el fitxer.
Això genera un objecte ObjectContainer que representa la BBDD.
Per tancar la connexió es crida el mètode close().
private static final String BBDDFITXER = "DB4Oexemple"; public static void main(String[] args) { new File(BBDDFITXER).delete(); // Obliga a crear nova BBDD cada vegada (Opcional) ObjectContainer db = Db4oEmbedded.openFile(Db4oEmbedded.newConfiguration(), BBDDFITXER); try { // Treballar amb la BBDD } finally { db.close(); } }
Classe Exemple Inicial
public class Pilot { private String name; private int points; public Pilot(String name,int points) { this.name=name; this.points=points; } public int getPoints() { return points; } public void addPoints(int points) { this.points+=points; } public String getName() { return name; } public String toString() { return name+"/"+points; } }
Operacions ABM
La inserció i la modificació es realitzen a través del mètode store(object).
// Desar objectes Pilot pilot1 = new Pilot("Michael Schumacher", 100); db.store(pilot1); Pilot pilot2 = new Pilot("Rubens Barrichello", 99); db.store(pilot2);
Per modificar un objecte només cal cridar a store(object) després de fer els canvis
pilot1.addPoints(20); db.store(pilot1);
La eliminació amb el mètode delete(object), més endavant veurem que aquí hi ha certes consideracions a tenir en compte.
db.delete(pilot2);
Consultes
El gestor db4o permet tres tipus de consultes
- Query By Example (QBE).
- Native Queries (NQ). Opció recomanada, interface principal de consulta.
- SODA Query API.
Query By Example (QBE)
Es basa en crear un objecte propotip que s'utilitza d'exemple per a la consulta.
A partir del prototip s'obtenen els objectes que tenen els mateixos valors, per indicar "qualsevol" s'utilitzen els valors per defecte del tipus de l'atribut: 0 - enters, null - objectes, etc..
El resultat es retorna en un ObjectSet (Conjunt) que implementa les interfaces java.util.Collection<T>, java.lang.Iterable<T>, java.util.List<T>.
Es recomanable utilitzar alguna implementació com List
Pilot proto = new Pilot(null, 0); // Tots List<Pilot> result = db.queryByExample(proto); System.out.println(result.size()); for (Pilot p : result) { System.out.println(p); }
proto = new Pilot("Michael Schumacher", 0); // Pilots amb nom "Michael Schumacher" result = db.queryByExample(proto); System.out.println(result.size()); Iterator<Pilot> it = result.iterator(); while (it.hasNext()) { System.out.println(it.next()); }
Native Queries (NQ)
Aquesta és la opció recomanada a la documentació.
Els Inconvenients de QBE són:
- No es poden utilitzar expressions complexes AND, OR, NOT, >, <, == etc...
- No es poden posar condicions sobre els valors 0, null, etc... perquè s'interpreten com "qualsevol"
- Es necessari un constructor per a indicar les condicions sobre els atributs
Les consultes NQ:
- Permeten expressions complexes
- S'escriuen en el propi llenguatge (Java)
- Si es possible utilitzen els índexs definits
Les restriccions es creen a partir d'un predicat : Classe Abstracte Predicate
Aquesta classe només inclou el mètode abstracte match(candidat) que retorna l'expressió a avaluar sobre un objecte candidat
A l'exemple següent es mostra com fer una consulta amb anonymous inner classes, en general això no és una bona pràctica perquè no permet reutilitzar codi i és poc clar. Tot i això hi ha situacions on és útil.
List <Pilot> pilots1 = db.query(new Predicate<Pilot>() { public boolean match(Pilot pilot) { return pilot.getPoints() == 100; } });
També es pot extendre la classe Predicate i sobreescriure el mètode match(candidate)
class PilotHundredPoints extends Predicate<Pilot> { public boolean match(Pilot pilot) { return pilot.getPoints() == 100; } } List <Pilot> pilots2 = db.query(new PilotHundredPoints());
A partir d'aquí es poden fer les consultes tant complexes com calgui
List <Pilot> pilots3 = db.query(new Predicate<Pilot>() { public boolean match(Pilot pilot) { return pilot.getPoints() > 99 && pilot.getPoints() < 199 || pilot.getName().equals("Rubens Barrichello"); } });
Pas de paràmetres
Per passar paràmetres a les consultes utilitzant classes anònimes cal defini variables "final"
final int pts = 100; // final: només es pot inicialitzar una vegada final int pts2; pts2 = 101; //pts2 = 102; List <Pilot> pilots1 = db.query(new Predicate<Pilot>() { public boolean match(Pilot pilot) { return pilot.getPoints() == pts; } });
Per a classes normals es pot afegir atributs i crear un contructor
class PilotPuntsVariable extends Predicate<Pilot> { private int pts; public PilotPuntsVariable(int pts) { //super(); this.pts = pts; } public boolean match(Pilot pilot) { return pilot.getPoints() == pts; } } List <Pilot> pilots2 = db.query(new PilotPuntsVariable(100));
SODA Query API
SODA és l'API de baix nivell per a consultes. Per exemple internament les consultes NQ es transformen en SODA per optimitzar-les abans d'executar-les.
Utilitza strings per identificar els noms dels camps sobre els que posar restriccions
El gestor construeix un graf (arbre) de la consulta amb els nodes i les restriccions
- node --> Múltiples classes, una classe o un atribut d'una classe
- constrain --> Afegeix una restricció al node
- descend --> retorna un node descendent en el graf
// Tots els objectes Query query=db.query(); // Crea SODA query query.constrain(Pilot.class); // Afegeix restricció List<Pilot> res = query.execute(); // Tots els objectes amb un valor d'atribut concret Query query2=db.query(); query2.constrain(Pilot.class); // Afegeix restricció sobre el node descendent (en aquest cas un atribut de tipus primitiu) query2.descend("name").constrain("Michael Schumacher"); List<Pilot> res2=query2.execute();
També permet afegir tot tipus de restriccions: AND, OR, NOT, etc...
// Negació query.descend("name").constrain("Michael Schumacher").not(); // AND query.constrain(Pilot.class); Constraint constr=query.descend("name").constrain("Michael Schumacher"); query.descend("points").constrain(99).and(constr); // OR query.descend("points").constrain(99).or(constr); // Més greater, equal, endsWith ... query.descend("points").constrain(99).greater();
Alhora que ordenar el resultat
// Ordre dels resultats query.descend("name").orderAscending(); query.descend("name").orderDescending();
Associacions
Què passa quan tenim objectes associats? com es gestiona?
Afegim la següent classe als exemples
public class Car { private String model; private Pilot pilot; private List history; public Car(String model) { this(model,new ArrayList()); } public Car(String model,List history) { this.model=model; this.pilot=null; this.history=history; } public Pilot getPilot() { return pilot; } public void setPilot(Pilot pilot) { this.pilot=pilot; } public String getModel() { return model; } public List getHistory() { return history; } public void snapshot() { history.add(new SensorReadout(poll(),new Date(),this)); } protected double[] poll() { int factor=history.size()+1; return new double[]{0.1d*factor,0.2d*factor,0.3d*factor}; } public String toString() { return model+"["+pilot+"]/"+history.size()+" -> " + history.toString(); } }
Les insercions funcionen igual.
Si es desen objectes, implícitament també es desen els objectes associats.
Car car1 = new Car("Ferrari"); Pilot pilot3 = new Pilot("Michael Schumacher", 100); car1.setPilot(pilot3); db.store(car1); // Desa també el pilot
Les consultes també es treballen de la mateixa forma
final String pilotName = "Rubens Barrichello"; List<Car> results = db.query(new Predicate<Car>() { public boolean match(Car car) { return car.getPilot().getName().equals(pilotName); } });
És interessant veure que es poden posar restriccions navegant d'un objecte a un altre associat, p.e. del cat --> pilot
Què passa si un cotxe no té pilot?
Amb el següent exemple es pot veure que que dins del mètode es genera una excepció si un cotxe no té pilot, però aquesta excepció la controla internament el gestor i no arriba a l'usuari.
El resultat simplement és que el candidat en qüestió no s'inclou en el resultat de la consulta.
Car car1 = new Car("Ferrari"); //car1.setPilot(pilot); db.store(car1); List<Car> cars = db.query(new Predicate<Car>() { public boolean match(Car car) { boolean res = false; try { res = car.getPilot().getName().equals("Rubens Barrichello"); } catch (Exception e) { System.out.println("-Excepcio-"); } return res; } });
Modificacions. Profunditat
Per gestionar la persistència d'objectes existents a la base de dades, cal indicar a db4o un paràmetre anomenat profunditat (depth).
Per defecte la profunditat és 1, això vol dir que es desa l'objecte i les seves dades primitives, però no els objectes associats.
Per contra és pot establir la profunditat fins a tot el sistema indicat el paràmetre cascadeOnUpdate(true). Evidentment penalitzant en el rendiment del programa.
EmbeddedConfiguration config = Db4oEmbedded.newConfiguration(); config.common().objectClass(Car.class).cascadeOnUpdate(true); ObjectContainer db = Db4oEmbedded.openFile(config, BBDDFITXER);
Eliminacions. Recursivitat
De manera similar a les modificacions, les eliminacions no afectes als objectes associats en la configuració per defecte (p.e. Esborrar un cotxe no esborra el Pilot)
Però es pot establir que per una classe s'esborrin tots els objectes associats indicat el paràmetre cascadeOnDelete(true)
EmbeddedConfiguration config = Db4oEmbedded.newConfiguration(); config.common().objectClass(Car.class).cascadeOnDelete(true); ObjectContainer db = Db4oEmbedded.openFile(config, BBDDFITXER);
Cal vigilar en tot cas perquè db4o no valida si els objectes que s'esborren estan referenciats per altres objectes del sistema,
EmbeddedConfiguration config = Db4oEmbedded.newConfiguration(); config.common().objectClass(Car.class).cascadeOnDelete(true); ObjectContainer db = Db4oEmbedded.openFile(config, BBDDFITXER); // Esborra tot List result=db.queryByExample(new Object()); while(result.hasNext()) { db.delete(result.next()); } Pilot pilot = new Pilot("Michael Schumacher", 100); Car car1 = new Car("Ferrari"); Car car2 = new Car("BMW"); car1.setPilot(pilot); car2.setPilot(pilot); db.store(car1); db.store(car2); db.delete(car2); List<Car> cars = db.query(new Predicate<Car>() { public boolean match(Car car) { return true; } }); for (Car c : cars) { System.out.println(c); // Mostra el cotxe car1 } // Mostra Ferrari[Michael Schumacher/100] pq encara està a memòria db.close(); db = Db4oEmbedded.openFile(Db4oEmbedded.newConfiguration(), BBDDFITXER); cars = db.query(new Predicate<Car>() { public boolean match(Car car) { return true; } }); for (Car c : cars) { System.out.println(c); } // Mostra Ferrari[null] el pilot s'havia esborrat db.close();
Col·leccions
Per a les classes d'exemple utilitzarem un sensor genèric que afegirem al cotxe, i 3 sensors concrets: de temperatura, aigua i pressió.
Aquests sensor representa que prenen mesures en un moment concret. El cotxe té un historial de mesures, i té un mètode snapshot() que pren algunes mesures en un moment donat a través d'aquests sensors.
public class SensorReadout { private double[] values; private Date time; private Car car; public SensorReadout(double[] values,Date time,Car car) { this.values=values; this.time=time; this.car=car; } public Car getCar() { return car; } public Date getTime() { return time; } public int getNumValues() { return values.length; } public double[] getValues(){ return values; } public double getValue(int idx) { return values[idx]; } public String toString() { StringBuffer str=new StringBuffer(); str.append(car.getModel()) .append(" : ") .append(time.getTime()) .append(" : "); for(int idx=0;idx<values.length;idx++) { if(idx>0) { str.append(','); } str.append(values[idx]); } return str.toString(); }}
Emmagatzematge i eliminació
Segueix sent molt simple. La herència no aporta res a destacar.
Per exemple per emmagatzemar un objecte que conté una llista d'objectes que pertanyen a una jerarquia d'herència,
// Un cotxe amb vàries medicions de diferents tipus Pilot pilot2=new Pilot("Rubens Barrichello",99); Car car2=new Car("BMW"); car2.setPilot(pilot2); car2.snapshot(); car2.snapshot(); db.store(car2);
Consulta Col·lecció QBE
Tots els sensors amb qualsevol mesures
SensorReadout proto=new SensorReadout(null,null,null); List<SensorReadout> sensors=db.queryByExample(proto); for (SensorReadout c : sensors) { System.out.println(c); } /*Resultats 2 BMW : 1393582541679 : 0.1,0.2,0.3 BMW : 1393582541679 : 0.2,0.4,0.6 */
Consulta amb prototip, valors desordenats no afecten, compara cada element del prototip amb tots els elements dels objectes cercats
SensorReadout proto = new SensorReadout(new double[] { 0.3, 0.1, 0.2 }, null, null); List<SensorReadout> results = db.queryByExample(proto); listResult(results);
Sensors amb unes mesures concretes. Tot i que el vector de mesures té mida 3 i el prototip 2, cerca que existeixin TOTS (AND) els valors individuals del prototip
SensorReadout proto = new SensorReadout(new double[] { 0.3, 0.1 }, null, null); List<SensorReadout> results = db.queryByExample(proto); listResult(results);
La següent consulta no troba res
SensorReadout proto = new SensorReadout(new double[] { 0.4, 0.1 }, null, null); List<SensorReadout> results = db.queryByExample(proto); listResult(results);
En canvi aquesta si
SensorReadout proto = new SensorReadout(new double[] { 0.3, 0.3, 0.1, 0.2 }, null, null); List<SensorReadout> results = db.queryByExample(proto); listResult(results);
Cotxes amb un historial de mesures concret
SensorReadout protoreadout = new SensorReadout(new double[] { 0.6, 0.4 }, null, null); List<SensorReadout> protohistory = new ArrayList<SensorReadout>(); protohistory.add(protoreadout); Car protocar = new Car(null, protohistory); List<Car> result = db.queryByExample(protocar); listResult(result);
Consulta Col·lecció NQ
Sensors amb alguna mesura concreta
List<SensorReadout> results = db.query(new Predicate<SensorReadout>() { public boolean match(SensorReadout candidate) { /* Per usar Arrays.binarySearch el vector ha d'estar ordenat */ return Arrays.binarySearch(candidate.getValues(), 0.3) >= 0 && Arrays.binarySearch(candidate.getValues(), 0.1) >= 0; } }); listResult(results);
http://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html#binarySearch%28double[],%20double%29
Cotxes amb sensors que tinguin alguna mesura concreta
List<Car> results = db.query(new Predicate<Car>() { public boolean match(Car candidate) { List history = candidate.getHistory(); for (Object aHistory : history) { SensorReadout readout = (SensorReadout) aHistory; if (Arrays.binarySearch(readout.getValues(), 0.6) >= 0 || Arrays.binarySearch(readout.getValues(), 0.2) >= 0) return true; } return false; } }); listResult(results);
Consulta Col·lecció Soda
Sensors amb uns valors concrets
Query query = db.query(); query.constrain(SensorReadout.class); Query valuequery = query.descend("values"); // valuequery és la col·lecció valuequery.constrain(0.3); valuequery.constrain(0.1); ObjectSet result = query.execute(); listResult(result);
Cotxe amb un historial concret
Query query = db.query(); query.constrain(Car.class); Query historyquery = query.descend("history"); historyquery.constrain(SensorReadout.class); Query valuequery = historyquery.descend("values"); valuequery.constrain(0.3); valuequery.constrain(0.1); ObjectSet result = query.execute(); listResult(result);