Omet navegació

6.5 - Millora de rendiment

Un altre aspecte important que mesura la qualitat de les aplicacions és l’eficiència amb la qual s’aconsegueix comunicar amb el SGBD. Per optimitzar la connexió és important reconèixer quins processos poden actuar de coll d’ampolla.

En primer lloc, analitzarem la petició de connexió a un SGBD perquè es tracta d’un procés costós però inevitable que cal considerar.

En segon lloc, estudiarem les sentències predefinides, perquè el seu ús facilita la creació de dades clau i índexs temporals de manera que siga possible anticipar-se a la demanda o disposar de les dades de forma molt més ràpida.

Temps de vida d'una connexió

L’establiment d’una connexió és un procediment molt lent, tant a la part client com a la part servidor.

  • A la part client, DriverManager ha de descobrir el controlador correcte d’entre tots els que haja de gestionar. La majoria de vegades les aplicacions treballaran només amb un únic controlador, però cal tenir en compte que DriverManager no coneix a priori quina URL de connexió correspon a cada controlador, i per saber-ho envia una petició de connexió a cada controlador que tinga registrat, el controlador que no li retorna error serà el correcte.
  • A la banda servidor, es crearà un context específic i s’habilitaran un conjunt de recursos per cada client connectat. És a dir, que durant la petició de connexió el SGDB ha de gastar un temps considerable abans de deixar operativa la comunicació client-servidor.

Aquesta elevat cost de temps concentrat en el moment de la petició de connexió fa que ens plantegem si val la pena obrir i tancar la connexió cada vegada que ens toque executar una sentència SQL, o obrir una connexió al principi de l'aplicació que tancaríem en finalitzar. Lamentablement no hi ha una única resposta, sinó que depèn de la freqüència d’ús de la connexió i el número de connexions contra el mateix SGBD.

Com en tot, es tracta de trobar el punt d’equilibri. Si el número de clients, i per tant de connexions, és baix i la freqüència d’ús és alta, serà preferible mantenir les connexions obertes molt de temps. Per contra, si el número de connexions és molt alt i la freqüència d'ús baixa, el que serà preferible serà obrir i tancar la connexió cada vegada que es necessite. I també hi haurà una multitud de casos en què la solució consistirà a mantenir les connexions obertes però no permanentment. Es pot donar un temps de vida a cada connexió, o bé tancar-les després de restar inactiva una quantitat determinada de temps, o es pot fer servir el criteri de mantenir un número màxim de connexions obertes, tancant les més antigues o les més inactives quan se sobrepasse el límit.

Sentències predefinides

JDBC disposa d’un objecte derivat del Statement que s’anomena PreparedStatement., a la qual se li passa la sentència SQL en el moment de crear-lo, no en el moment d'executar la sentència (com passava amb Statement). I a més aquesta sentència pot admetre paràmetres, cosa que ens pot anar molt bé en determinades ocasions.

Siga com siga, PreparedStatement presenta avantatges sobre el seu antecessor Statement quan ens toque treballar amb sentències que s'hagen d'executar diverses vegades. La raó és que qualsevol sentència SQL, quan s’envia al SGBD serà compilada abans de ser executada.

  • Utilitzant un objecte Statement, cada vegada que fem una execució d’una sentència, ja siga via executeUpdate o bé via executeQuery, el SGBD la compilarà, ja que li arribarà en forma de cadena de caràcters.
  • En canvi, al PreparedStament la sentència mai varia i per tant es pot compilar i guardar dins del mateix objecte, de manera que les següents vegades que s’execute no caldrà compilar-la. Això reduirà sensiblement el temps d’execució.

En alguns sistemes gestors, a més, fer servir PreparedStatements pot arribar a suposar més avantatges, ja que utilitzen la seqüència de bytes de la sentència per detectar si es tracta d’una sentència nova o ja s’ha servit amb anterioritat. D’aquesta manera es propicia que el sistema guarde les respostes en la memòria caché, de manera que es puguen lliurar de forma més ràpida.

En el moment de programar, la principal diferència dels objectes PreparedStatement en relació als Statement, és que en els últims la sentència es passa com a paràmetre en el moment d'executar-lo, mentre que en els primers se'ls passa la sentència SQL predefinida en el moment de crear-lo (no d'executar-lo). Com que la sentència queda predefinida, ni els mètodes executeUpdate ni executeQuery requeriran cap paràmetre. És a dir, justet al revès que en el Statement.

Posem un exemple, en el qual demanem per teclat el nom d'una comarca i una altura, per a traure els pobles de la comarca introduïda que estan a una altura superior a l'altura introduïda.

Copieu el següent codi en un fitxer Kotlin anomenat Exemple_4_71_Statement.kt :

package exemples

import java.sql.DriverManager
import java.util.Scanner

fun main(args: Array<String>) {
    val con = DriverManager.getConnection("jdbc:postgresql://89.36.214.106:5432/geo_ad", "geo_ad", "geo_ad")

    println("Introdueix una comarca:")
    val com = Scanner(System.`in`).nextLine()
    println("Introdueix una altura:")
    val alt = Scanner(System.`in`).nextInt()

    val st = con.createStatement()    // La sentència no va en el moment de la creació sinó en el d'execució
    val rs = st.executeQuery("SELECT nom,altura FROM POBLACIO WHERE nom_c='" + com + "' AND altura>" + alt)
    while (rs.next()) {
        println(rs.getString(1) + " (" +rs.getInt(2) + " m.)")
    }
    st.close()
    con.close()
}

Observeu com per al cas del nom de la comarca, que és un String (o millor dit un Varchar), en la sentència SQL hem de posar una cometa simple (') abans i després del valor de la comarca, ja que és així com s'expressen els Varchar. Resulta un poc incòmode.

Si utilitzem PreparedStatement, només haurem de canviar les línies 14 i 15.

Copieu el següent codi en un fitxer Kotlin anomenat Exemple_4_72_PreparedStatement.kt , i observeu com només han canviat les línies 14 i 15:

package exemples

import java.sql.DriverManager
import java.util.Scanner

fun main(args: Array<String>) {
    val con = DriverManager.getConnection("jdbc:postgresql://89.36.214.106:5432/geo_ad", "geo_ad", "geo_ad")

    println("Introdueix una comarca:")
    val com = Scanner(System.`in`).nextLine()
    println("Introdueix una altura:")
    val alt = Scanner(System.`in`).nextInt()

    val st = con.prepareStatement("SELECT nom,altura FROM POBLACIO WHERE nom_c='" + com + "' AND altura>" + alt)
    val rs = st.executeQuery()     // La sentència no va en el moment de l'execució sinó en el de creació
    while (rs.next()) {
        println(rs.getString(1) + " (" +rs.getInt(2) + " m.)")
    }
    st.close()
    con.close()
}

Però anem un pas més enllà i anem a utilitzar paràmetres. La utilització de paràmetres, a banda de la comoditat, en pot anar molt bé per a previndre errors. Observeu com en els dos exemples anteriors teníem un problema si el nom de la comarca conté una cometa simple (') com és en el cas per exemple de la Vall d'Albaida. Si intentem posar aquesta comarca ens saltarà un error, i és perquè interpretarà la cometa com el final de la cadena Varchar, i no sabrà què és Albaida

SELECT nom,altura FROM POBLACIO WHERE nom_c='Vall d'Albaida' AND altura=500

La manera de solucionar-ho seria escapar la cometa, o doblar-la (vull dir 2 cometes simples, no doble cometa). Però també ens pot anar molt bé i molt còmode utilitzar paràmetres, com veurem ara.

Els paràmetres de la sentència es marcaran amb el símbol d’interrogant (?) i s’identificaran per la posició que ocupen a la sentència, començant a comptar des de l’esquerra i a partir del número 1. El valor dels paràmetres s’assignarà fent servir el mètode específic, d’acord amb el tipus de dades a assignar. El nom d'aquestos mètodes començarà per set i continuarà amb el nom del tipus de dades (exemples: setString(), setInt(), setLong(), setBoolean()…). Tots aquestos mètodes segueixen la mateixa sintaxi:

setXXXX(<posicioALaSentenciaSQL>, <valor>)

Veiem com quedaria el nostre exemple.

Copieu el següent codi en un fitxer Kotlin anomenat Exemple_4_73_PreparedStatement_ambParametres.kt :

package exemples

import java.sql.DriverManager
import java.util.Scanner

fun main(args: Array<String>) {
    val con = DriverManager.getConnection("jdbc:postgresql://89.36.214.106:5432/geo_ad", "geo_ad", "geo_ad")

    println("Introdueix una comarca:")
    val com = Scanner(System.`in`).nextLine()
    println("Introdueix una altura:")
    val alt = Scanner(System.`in`).nextInt()

    val st = con.prepareStatement("SELECT nom,altura FROM POBLACIO WHERE nom_c=? AND altura>?")
    st.setString(1,com)				// Abans d'executar-la s'han d'iniciar els paràmetres
    st.setInt(2,alt)
    val rs = st.executeQuery()		// La sentència no va en el moment de l'execució sinó en el de creació
    while (rs.next()) {
        println(rs.getString(1) + " (" +rs.getInt(2) + " m.)")
    }
    st.close()
    con.close()
}

Mireu com és més còmode, perquè en el moment de definir la sentència (quan creem el PreparedStatement) per a el string no ens hem preocupat de posar la cometa. Ni tampoc en el moment de definir el paràmetre en la línia següent.

I a més ara no fallarà quan posem la comarca Vall d'Albaida