Omet navegació

3.3 - Consultes

DB4O disposa de tres formes de realitzar consultes. Totes elles són de tipus NoSQL.

Nota

Si havíeu fet tots els exemples anteriors, potser siga millor esborrar Empleats.db4o i tornar a executar Exemple1_InserirEmpleat.ktExemple1_1_InserirMesEmpleats.kt per a crear-les de nou.

 

Mètode Query By Example

La primera forma ja s’ha comentat, és la que s’anomena consulta basada en un exemple o “query by example”. Consisteix, com ja hem vist, en trobar totes les instàncies guardades que coincidesquen amb els valors no nuls i diferents de zero (en cas que siguen numèrics) d’un patró o exemple passat per paràmetre.

Si, per exemple, volem traure els empleats del departament 10 que són de Castelló, n’hi hauria prou amb crear el patró que posem a continuació. Copieu el següent codi al fitxer Exemple11_Consulta_QueryByExample.kt

import com.db4o.Db4oEmbedded
import com.db4o.ObjectContainer
import com.db4o.ObjectSet

import classesEmpleat.Adreca
import classesEmpleat.Empleat

fun main() {
	val bd = Db4oEmbedded. openFile("Empleats.db4o")

	val patro =  Empleat()
	patro.departament = 10
	patro.adreca = Adreca (null, null, "Castelló")

	val llista = bd.queryByExample<Empleat>(patro)
	for (e in llista) {
		System.out.println("Nif: " + e.nif + ". Nom: " + e.nom
							+ ". Departament: " + e.departament	+ ". Població: " + e.adreca?.poblacio
		)
	}
	bd.close()
}

cosa que donarà com a resultat el següent, que es pot comprovar que són del departament 10 i de Castelló:

Nif: 11111111a. Nom: Albert. Departament: 10. Població: Castelló
Nif: 22222222b. Nom: Berta. Departament: 10. Població: Castelló

Seguint aquest raonament, per obtenir tots els empleats de l’aplicació caldrà passar un patró empleat sense valors (bd.queryByExample(Empleat() )), i si el que desitgem és obtenir tots els objectes emmagatzemats a la base de dades, el que haurem de passar com a paràmetre és un valor null ( bd.queryByExample(null) ).

Com podeu veure, resulta un sistema molt simple. Ara bé, també té moltes limitacions en consultes més complexes, i fins i tot poden resultar impossibles. Posem alguns exemples en els quals no funciona aquest tipus de consulta:

  • És impossible trobar tots els empleats que no tinguen algun camp assignat encara (és a dir, null) a causa del mecanisme utilitzat: només s’avaluen els camps no nulls.
  • Tampoc podríem trobar aquells empleats que cobren més de 1300€ . En aquest tipus de consulta només podem buscar igualtats.
  • Com es basa en la coincidència, no podrem fer consultes que puguen agafar un de dos o més valors determinats. Per exemple, agafar els empleats que són de Castelló o Borriana.

Mètode Native Queries

DB4O disposa d’un sistema molt més potent anomenat Native Queries. És fàcil deduir que es tracta d’un sistema vinculat directament al mateix llenguatge de programació. De fet, es tracta de construir un procediment en el qual s'avaluen els objectes i es decideix quins objectes acompleixen la condició i quins no.

Per a fer la consulta haurem de crear una classe que implemente una interfície anomenada Predicate. Aquesta interfície consta d'un únic mètode declarat anomenat match. La classe nostra que implementarà Predicate haurà de sobreescriure el mètode match(), i en aquest mètode podrem posar una sèrie de sentències Kotlin i dir si cada objecte de la Base de Dades acompleix o no la condició tornant respectivament true o false.

En el següent exemple creem una classe anomenada EmpleatsPerPoblacio (que implementa Predicate), a la qual se li pot passar en el constructor un vector de cadenes de caràcters amb els noms de les poblacions de les quals volem els empleats. En la implementació del mètode match tornarem cert si l'empleat és d'alguna de les poblacions, i fals en cas contrari. Com que utilitzem el mateix llenguatge de programació, la potència és molt elevada. Copieu el següent codi en un fitxer anomenat Exemple12_Consula_NativeQuery_1.kt

import com.db4o.Db4oEmbedded
import com.db4o.ObjectContainer
import com.db4o.ObjectSet
import com.db4o.query.Predicate

import classesEmpleat.Empleat

class EmpleatsPerPoblacio(pobles: Array<String>) : Predicate<Empleat>() {
	val poblacions = pobles

	override
	fun match(emp: Empleat): Boolean {
		return (emp.adreca?.poblacio in poblacions)
	}
}


fun main() {
	val bd = Db4oEmbedded.openFile("Empleats.db4o")
	val pobl = arrayOf("Castelló", "Borriana")

	val llista = bd.query(EmpleatsPerPoblacio(pobl))

	for (e in llista) {
		println(e.nom + " (" + e.adreca?.poblacio + ")")
	}
	bd.close()
}

Observeu que una vegada definida la classe, podem fer-la servir en una Query per realitzar una consulta específica. En l'exemple, s'obtenen tots els empleats que són de Castelló o de Borriana. En variar la llista de poblacions obtindrem uns objectes empleat o uns altres. En el mètode match, que és qui diu si un element Empleat compleix la condició, es comprova si la població de l'empleat (que està dins d'adreça, i per tant s'accedeix amb emp.adreca?.poblacio ) està dins de l'array de poblacions.

 

Com que  es tracta d’una interfície amb un únic mètode a implementar, no caldrà que implementem sempre noves classes per a cada consulta diferent, sinó que podem fer servir classes anidades anònimes (anonymous nested class), per a fer-lo molt més curt, definint la classe en el oment d'utilitzar-la.

Mirem-ho en un altre exemple, en el qual es buscaran els empleats que tinguen el sou entre dos valors determinats. Construïm la classe Predicate en el mateix lloc on s'utilitza, en el query(), i no abulta molt perquè només té el mètode match(). En el mètode match() és on es comprova la condició. Copieu el següent codi en el fitxer Exemple13_NativeQuery_2.kt:

import com.db4o.Db4oEmbedded
import com.db4o.ObjectContainer
import com.db4o.ObjectSet
import com.db4o.query.Predicate

import classesEmpleat.Empleat

fun main() {
	val bd = Db4oEmbedded.openFile("Empleats.db4o")
	val max = 1500.0
	val min = 1000.0
	val llista = bd.query(object: Predicate<Empleat>() {
		override
		fun match(emp: Empleat): Boolean {
			if (emp.sou.toString().toDouble() <= max && emp.sou.toString().toDouble() >= min)
				return true
			else
				return false
		}
	})

	for (e in llista) {
		System.out.println(e.nom + " (" + e.sou + ")")
	}
	bd.close()
}
En tractar-se d’una interfície amb un únic mètode a implementar, no caldrà que implementem sempre noves classes per a cada consulta diferent, sinó que podem fer servir classes imbricades anònimes (anonimous nested class). Recordeu que les classes imbricades poden treballar directament amb tots els atributs (tinguin l’àmbit que tinguin) de la classe que les contingui i que les classes anònimes es defineixen a l’interior d’un mètode qualsevol.

Mètode SODA

Existeix encara una altra forma de definir consultes. DB4O l’anomena SODA (Simple Object Database Access), i es pot considerar com la forma d’accedir a l’estructura interna de la base de dades a baix nivell per tal de seleccionar els nodes de dades que complesquen uns determinats requisits i que acabaran determinant el resultat de la consulta. De fet, segons indiquen els autors, és la forma de consulta més ràpida de les tres.

La idea fonamental de SODA és construir les consultes com un recorregut d’una xarxa de nodes enllaçats. Els nodes de la consulta s’estructuren de forma semblant a les classes emmagatzemades a la base de dades, de manera que el camí seguit en avaluar la consulta, node a node, es repeteix en les instàncies emmagatzemades, la qual cosa permet accedir als valors per avaluar de forma ràpida.

El camí s’especifica utilitzant el mètode descend() per mitjà del qual seleccionem la branca de l’estructura de classes que vulguem fer referència. Per exemple, si ens trobem en el node de la classe Empleat i volguérem fer referència al nom de la població que en l’estructura de classes es troba a empleat.adreca.poblacio, hauríem de fer 

node.descend("adreca").descend("poblacio")

El resultat de la sentència anterior és un node focalitzat a l’atribut població continguda a l’adreça de l'empleat.

Cada node pot estar afectat per una restricció, per una ordenació i/o per una operació amb una altre node. Les restriccions permeten seleccionar o desestimar les instàncies que es vagen comprovant. Les ordenacions, com és natural, forcen l’ordre de les instàncies seleccionades d’acord amb els valors de l’atribut representat pel node afectat. Finalment, les operacions marquen quin serà el següent node a avaluar, el qual actuarà també com a filtre dels objectes de la selecció.

Les restriccions es veuran afectades per una o més relacions que permetran modificar la comparació i sentenciar en favor o en contra de la selecció d’una instància. Per defecte, la relació avaluada és la d’igualtat. Per exemple, si partim d’un node que representa el NIF d’un empleat, podem definir la relació d’igualtat següent:

 node.constrain("11111111a")

Mirem com quedaria el programa que selecciona únicament l'empleat amb el nif anterior. Guardeu el següent codi al fitxer Exemple14_QuerySoda_1.kt

import com.db4o.Db4oEmbedded
import com.db4o.ObjectContainer
import com.db4o.ObjectSet
import com.db4o.query.Query
import classesEmpleat.Empleat

fun main() {
	val bd = Db4oEmbedded.openFile("Empleats.db4o")
	val q = bd.query ()          //node arrel.

	var node = q.descend ("nif") //arribem a l'altura del nif, que és on posem la restricció
	node.constrain("11111111a")

	val llista = q.execute<Empleat>()
	for (e in llista){
		println("Nif: " + e.nif + ". Nom: " + e.nom + " (" + e.sou + ")")
	}
	bd.close()
}

En realitat intentarà agafar en principi tots els objectes de la Base de Dades, i recordem que tenim objectes Empleat, Adreca i Telefon. Però observeu que no cal especificar que siga únicament objectes de la classe Empleat, perquè és l'única classe que té una propietat anomenada nif, per tant únicament selccionarà empleats

Però si la relació ha de ser una comparació de tipus major que , menor o igual que, ... , caldrà especificar-les expressament. La manera serà especificant un mètode de la restricció. Les possibilitats seran:

  • Major: greater()
    Si suposem que partim d’un node focalitzat al sou d’un empleat i volem la condició que el sou siga major estrictament que 1300. S’indicaria d'aquesta manera:

    node.constrain(1300).greater()

  • Menor: smaller()
     Si volem que el sou siga estrictament menor que 1500:

    node.constrain(1500).smaller()

  • Major o igual, menor o igual: equal() (després del greater o smaller)
    Si ara volem que el sou siga menor o igual que 1500:

    node.constrain(1300).smaller().equal()

  • Que comence per: startsWith(boolean) 
    Si partim d'un node focalitzat al nom de l'empleat i volem els que comencen per A:

    node.constrain("A").startsWith(true)

    Si en el paràmetre booleà posem true, haurà de coincidir exactament el principi. Si posem false, no distingirà entre majúscules i minúscules.

  • Per a unir restriccions: or(restriccio) and(restriccio). Per a negar not()
    Per exemple, si partim d’un node focalitzat al nom de l'empleat, podem seleccionar tots els que comencen per A o per B, fent:

    var constr1 = node.constrain("A").startsWith()
    val constr2 = node.constrain("B").startsWith()
    constr1.or(constr2)

  • Si posem més d'una restricció (més d'un constrain), s'hauran de complir totes, i per tant actua com un and

A banda de les restriccions, si volem ordenar de forma ascendent o descendent, ho indicarem amb els mètode orderAscending() o orderDescending() del node pel mig del qual volem ordenar .

Mirem un parell d'exemples per veure com es posa tot en joc. Anem a construir la sentència que permeta seleccionar tots els empleats amb un sou que oscil·le entre un rang de valors definits (estrictament major que 1000, i menor o igual que 1500, per exemple) ordenats de forma descendent per sou. Guardeu-lo al fitxer Exemple15_QuerySoda_2.kt

import com.db4o.Db4oEmbedded
import com.db4o.ObjectContainer
import com.db4o.ObjectSet
import com.db4o.query.Query
import classesEmpleat.Empleat

fun main() {
	val bd = Db4oEmbedded.openFile("Empleats.db4o")
	val q = bd.query()          //node arrel.

	var node = q.descend("sou") //arribem a l'altura del sou, que és on posem la restricció
	node.constrain(1000).greater().and(node.constrain(1500).smaller().equal())
	node.orderDescending()

	val llista = q.execute<Empleat>()
	for (e in llista) {
		println("Nif: " + e.nif + ". Nom: " + e.nom + " (" + e.sou + ")")
	}
	bd.close()
}

I ara els empleats del departament 10 que són de Castelló. Podem utilitzar el mateix objecte node per anar afegint restriccions, però haurem de cuidar de localitzar-lo al lloc oportú. Guardeu-llo amb el nom Exemple16_QuerySoda_3.kt

import com.db4o.Db4oEmbedded
import com.db4o.ObjectContainer
import com.db4o.ObjectSet
import com.db4o.query.Query
import classesEmpleat.Empleat

fun main() {
	val bd = Db4oEmbedded.openFile("Empleats.db4o")
	val q = bd.query()          //node arrel.

	var node = q.descend("departament") //arribem a l'altura del departament, que és on posem la restricció
	node.constrain(10)

	node = q.descend("adreca").descend("poblacio") //i ara arribem a l'altura de la població de l'adreça
	node.constrain("Castelló")


	val llista = q.execute<Empleat>()
	for (e in llista) {
		println("Nom: " + e.nom + ". Població: " + e.adreca?.poblacio + ". Departament: " + e.departament)
	}
	bd.close()
}

Tot i que cal ens proporciona 3 maneres de fer les consultes, i al final aquestes poden ser potents, no és capaç de tenir tota l’expressivitat d’un llenguatge com OQL. No disposa de funcions d’agregat (SUM, AVG, MAX, MIN, ...), ni es poden expressar relacions entre instàncies. És l’aplicació la que haurà de fer-se responsable que això siga possible.