Programación Eficiente con SQL y JDBC
Programación Eficiente con SQL y JDBC
mediante SQL
PID_00275642
Ninguna parte de esta publicación, incluido el diseño general y la cubierta, puede ser copiada,
reproducida, almacenada o transmitida de ninguna forma, ni por ningún medio, sea este eléctrico,
mecánico, óptico, grabación, fotocopia, o cualquier otro, sin la previa autorización escrita
del titular de los derechos.
© FUOC • PID_00275642 Programación mediante SQL
Índice
Introducción............................................................................................... 5
Objetivos....................................................................................................... 6
Resumen....................................................................................................... 54
Actividades.................................................................................................. 55
Glosario........................................................................................................ 56
Bibliografía................................................................................................. 57
© FUOC • PID_00275642 5 Programación mediante SQL
Introducción
En este módulo estudiaremos diversas técnicas para operar con bases de datos
(BD) relacionales desde nuestras aplicaciones. Es lo que se denomina SQL pro-
gramado o SQL inmerso.
Objetivos
2. Comprender las diferencias más importantes que hay entre las diversas
técnicas de SQL programado y las ventajas y los inconvenientes principales
de cada una.
4. Combinar correctamente los mecanismos que nos ofrece JDBC para desa-
rrollar aplicaciones que operen eficientemente con una BD.
© FUOC • PID_00275642 7 Programación mediante SQL
Cuando enlazamos una aplicación con una BD, podemos aprovechar cual-
quiera de las funcionalidades propias de los SGBD que hemos ido estudiando
a lo largo del curso.
Todas las soluciones tendrán como elemento común la utilización de una bi-
blioteca encargada de enlazar las aplicaciones con las BD. SQL hospedado es
la solución que más esconde la utilización de una biblioteca, pero en último
término, hace llamadas a funciones de una biblioteca utilizando una sintaxis
diferente.
© FUOC • PID_00275642 8 Programación mediante SQL
1.2. BD de referencia
d)�Lecturas. Contendrá un registro de las lecturas que los usuarios hacen de los
mensajes (columnas: código mensaje, nombre de usuario –estas dos primeras
columnas definirán la clave–, fecha y hora).
1)�Conexión�y�desconexión
Partiendo de la base de que las aplicaciones acceden a una BD para hacer unas
cuantas operaciones y no una sola, todas las soluciones optan por separar la
conexión o la desconexión de las operaciones. La razón es amortizar el tiempo
que se tarda en ponerse en contacto con la BD entre las diversas operaciones.
Hemos de tener en cuenta que este tiempo puede ser significativo, especial-
mente si la BD está en una máquina diferente de la de la aplicación.
De las dos, la segunda es más simple pero menos eficiente a causa de la len-
titud de la conexión. En este sentido, en los últimos años han salido biblio-
tecas que permiten hacer conexiones más ligeras y rápidas, optimizadas para
aplicaciones web.
Sea como fuere, durante la conexión a una BD hay que especificar unos cuan-
tos parámetros, entre los cuales los más importantes son:
2)�Sentencias�SQL�estáticas�o�dinámicas
Una vez conectados a la BD, podemos interaccionar con ella mediante senten-
cias SQL. Antes de la ejecución de una sentencia SQL, el SGBD ha de analizar
su texto, verificar que es sintácticamente correcto, transformarlo, optimizarlo,
etc. Según el momento en el que se realiza este proceso, distinguimos entre:
© FUOC • PID_00275642 10 Programación mediante SQL
3)�Sentencias�SQL,�parámetros�y�variables
Incluso en SQL estático, la mayor parte de las sentencias tienen una parte va-
riable que se define en tiempo de ejecución. Nos referimos a valores de colum-
nas, utilizados en sentencias de modificación o de consulta, que no impiden
que el análisis de la sentencia se haga en tiempo de compilación. Por ejemplo,
en nuestra BD de referencia:
• Para añadir un nuevo mensaje, hemos de hacer INSERT e indicar los valo-
res de las columnas. Una aplicación pediría los datos al usuario, los alma-
cenaría temporalmente en variables y, de alguna manera, estas variables
se vincularían con la sentencia SQL.
4)�Sentencias�SQL�con�resultados,�iteraciones�y�variables
5)�Transacciones
© FUOC • PID_00275642 11 Programación mediante SQL
6)�Tratamiento�de�errores
Todas las soluciones ofrecen alguna vía para capturar los errores que se pueden
producir al ejecutar una sentencia SQL. La manera de hacerlo, sin embargo,
cambia mucho dependiendo de la solución que escojamos.
Ejemplo
int main() {
EXEC SQL CONNECT TO bdMail USER jmarti/1234;
EXEC SQL DISCONNECT;
}
#line 1 "[Link]"
int main() {
{
ECPGconnect(__LINE__, 0, "bdMail" , "jMarti" ,
"1234" , NULL, 0);
}
#line 3 "[Link]"
{ ECPGdisconnect(__LINE__, "CURRENT");}
#line 7 "[Link]"
}
Ejemplo
Nota
Veamos un ejemplo que muestra los mensajes de un usuario. Está escrito en SQL hospe-
dado en C y, de entrada, podemos apreciar cómo se combinan las líneas de C y de SQL. El código del ejemplo es-
Para recorrer los mensajes de un usuario utilizamos un cursor que nos permite movernos tá disponible en el fiche-
ro ex002SelectMensajes
a la fila siguiente y acceder a los valores de sus columnas (lo que conseguimos con la
[Link].
operación FETCH...INTO ). El vínculo entre las sentencias SQL y el código C son las
variables puente, declaradas en la sección DECLARE SECTION y, en el ejemplo, asignadas
cada vez que hacemos un FETCH...INTO (fijémonos en los dos puntos ":" que preceden a
las variables puente). Finalmente, para detectar el final del recorrido, hemos de consultar
© FUOC • PID_00275642 13 Programación mediante SQL
si hay algún error ([Link] ). Cuando el error nos indica que no hay más filas
por leer (error con código "02000"), acabamos el recorrido.
while (!final) {
//Mostramos las variables puente
printf("%d -- %s -- %s\n",codigo_mensaje,titulo,texto);
1.4.2. SQL/CLI
El grupo formado por X/Open y SQL Access Group (SAG) desarrolló la especi-
ficación de una interfaz para permitir SQL programado denominada Call Le-
vel Interface (CLI). Tenían como objetivo incrementar la portabilidad de apli-
caciones gracias a la definición de una interfaz a partir de la que se pudiera
acceder a cualquier SGBD. La mayor parte de la especificación de la interfaz
fue añadida al estándar SQL:1992 en el año 1995.
© FUOC • PID_00275642 14 Programación mediante SQL
1.4.3. ODBC
La aportación de ODBC fue la definición del entorno que permite cargar di-
námicamente los drivers de un SGBD concreto a partir del nombre de la BD.
1.4.4. OLE DB
La segunda biblioteca fue RDO (Remote Data Objects), que permitía acceder
a prácticamente todas las funciones de ODBC, de manera que aumentaba el
potencial de DAO. Iba orientada a aplicaciones grandes.
1.4.6. El JDBC
1.5. Comparativa
Ante todas estas alternativas, hay que hacer un punto y aparte para reflexionar
sobre las ventajas y los inconvenientes de cada solución y, sobre todo, para
sacar conclusiones que nos guíen en la elección de la solución óptima en cada
situación. Enfocaremos la comparativa desde perspectivas diferentes:
• Nivel de programación.
• Flexibilidad y eficiencia de sentencias SQL.
• Portabilidad.
• Lenguajes de programación soportados.
SQL hospedado es la única solución que permite SQL estático. El resto de solu-
ciones permiten SQL dinámico, pero pueden simular el SQL estático mediante
procedimientos almacenados.
© FUOC • PID_00275642 16 Programación mediante SQL
Aunque todas las soluciones que hemos visto tienen por objeto la portabilidad,
ésta no siempre se consigue de la misma manera.
Tanto SQL hospedado como SQL/CLI han de ser compilados con la biblioteca
de acceso al SGBD. A pesar de que podemos encontrar bibliotecas para la gran
mayoría de SGBD, debemos hacer la elección en el momento de compilar la
aplicación y, por lo tanto, antes de distribuirla. Cambiar de SGBD comportaría
la recompilación de la aplicación.
Entre otras, algunas de las combinaciones más habituales son las siguientes:
Para acceder a BD, JDBC sigue el mismo patrón utilizado en todas las solucio-
nes vistas en el apartado anterior:
Veamos ahora un resumen de las versiones de JDBC y los cambios que han
ido introduciendo:
4)�JDBC�4.0�(JDK�1.6). Simplifica la carga de drivers, mejora los ResultSets JDBC 3.0 y JDBC 4.0
(RowSet, WebRowSet ), incorpora novedades SQL:2003 (NCHAR, NVARCHAR,
A modo de ejemplo, mientras
LONGNVARCHAR, NCLOB ) para poder trabajar con textos de cualquier alfabeto, se elabora este material, el dri-
da apoyo para excepciones encadenadas y facilita el trabajo con tipos de datos ver de PostgreSQL ofrece una
implementación razonable del
XML. JDBC 3.0 y alguna extensión
propia. La versión JDBC 4.0
también se ofrece, pero no es
completa.
2.1. Drivers JDBC
El API JDBC está formado por dos packages, [Link] y [Link]. Estos
packages contienen las clases y las interfaces necesarias para acceder a diferen-
tes orígenes de datos, pero no son completos. Faltan las clases que implemen-
tan las interfaces JDBC para acceder a cada uno de los SGBD con los que que-
ramos trabajar. Es lo que denominamos drivers JDBC. Entre otras, en el package
[Link] se define la interfaz Driver, que es la interfaz principal que un
driver ha de implementar.
Los drivers se deberán instalar en todos los equipos en los que queramos eje-
cutar aplicaciones que utilicen JDBC. Como los drivers son implementaciones
hechas en Java, como tales, serán portables a cualquier plataforma que acepte
Java y, por lo tanto, no tendríamos que tener ningún problema. Más adelante
ya veremos que, desgraciadamente, esto no siempre es así.
© FUOC • PID_00275642 20 Programación mediante SQL
Por ejemplo, para ejecutar una aplicación que use PostgreSQL, tendremos que
instalar los drivers JDBC de PostgreSQL. Sin estos drivers podríamos compilar la
aplicación, pero se generaría un error en tiempo de ejecución (recordemos que,
siguiendo el principio de ODBC, los drivers se cargan en tiempo de ejecución).
Para instalar manualmente un driver en un equipo, hay que descargar los fi-
cheros necesarios (normalmente un fichero .jar). A continuación, hay que
modificar el CLASSPATH para que incluya los ficheros que hemos descargado.
Podemos hallar drivers JDBC para acceder a un amplio abanico de SGBD. Nos
puede sorprender encontrar más de un driver para acceder a un mismo SGBD,
especialmente porque todos permiten hacer lo mismo. ¿Qué sentido puede
tener eso?
Los diferentes drivers que permiten acceder a un mismo SGBD ofrecen las mis-
mas clases y métodos, pero utilizan caminos diferentes para comunicarse con
la BD. Podemos encontrar cuatro tipos de acceso diferentes:
2.3. Comparativa
1)�Portabilidad
Los drivers de tipo 1 y 2 aparecieron en los orígenes del JDBC con la intención
de aprovechar los drivers ODBC y las bibliotecas de acceso al SGBD que ya
había implementadas. Eso garantizaba la introducción del JDBC sin ningún
esfuerzo por parte de los fabricantes de SGBD, pero limitaba su portabilidad.
Los drivers de tipo 3 y 4 también aparecieron en los orígenes del JDBC, aun-
que inicialmente había pocos SGBD que los ofrecieran. De forma gradual, los
fabricantes de SGBD han ido publicando drivers de tipo 3 y 4 y, actualmente,
son las opciones recomendadas.
© FUOC • PID_00275642 24 Programación mediante SQL
2)�Tipos�y�versiones�de�JDBC�implementados
No todos los fabricantes de SGBD ofrecen drivers de todos los tipos, y los que
se ofrecen no siempre implementan la última versión de JDBC.
Para el resto de casos, hay que averiguar qué tipos de driver ofrece el fabricante
de SGBD que nos interesa y qué versión JDBC implementa. Además, podemos
encontrar drivers que implementen parcialmente alguna versión.
3)�Nivel�de�seguridad
Así, con respecto a la seguridad, basta con utilizar drivers de tipo 4 en escenarios
de red local (o de tipo 1 o 2 si no hay alternativa). En escenarios de Internet
se recomienda el uso de drivers de tipo 3.
© FUOC • PID_00275642 25 Programación mediante SQL
Ahora nos centraremos en la vertiente más práctica del módulo didáctico. Su-
pongamos que queremos desarrollar una aplicación en Java y que ya hemos
decidido qué SGBD y qué driver utilizaremos. Veamos cómo se usa la API JDBC,
es decir, cómo se programa mediante las clases de esta biblioteca.
El primer paso para trabajar con una BD es la conexión. Para poder conectar- Nota
nos, hemos de seguir una serie de pasos:
Tenéis disponible el códi-
go del ejemplo en el fichero
1)�Importar�los�paquetes�informáticos�necesarios. Tal como se ha comen- [Link].
tado, API JDBC está formada por dos paquetes informáticos, [Link] y
[Link]. El primero contiene las clases y las interfaces esenciales (incluye
las clases Driver, Connection, Statement, ResultSet, PreparedState-
ment y CallableStatement, principalmente) y lo tendremos que importar
siempre. El segundo es la extensión estándar y contiene clases más especiali-
zadas que se escapan de los objetivos de este módulo didáctico (RowSet, Da-
taSource y PooledConnection, entre otras).
[Link]( "[Link]" );
Cargar una clase a partir de su nombre puede fallar si el classloader (objeto res-
ponsable de cargar las clases necesarias para la ejecución de un programa) no
es capaz de encontrar ninguna clase con este nombre. Por lo tanto, nos tene-
© FUOC • PID_00275642 27 Programación mediante SQL
String dbURL="jdbc:postgresql:bdMail";
Connection conn = [Link]( dbURL,
"usuario","contraseña");
jdbc:<subprotocol>:<subname>
En los casos más explícitos, identifica el servidor (en el que está el SGBD),
el puerto en el que escucha el SGBD y el nombre de la BD. Si no indicamos
servidor, se entiende que es el mismo ordenador (localhost ) y, si tampoco
indicamos puerto, se entiende que es el puerto por defecto del SGBD.
4)� Cerrar� la� conexión. Sin duda, es la operación más sencilla de las vistas
hasta ahora. Simplemente hay que llamar al método close de la conexión
que queramos cerrar.
[Link]();
Llegados en este punto, nos podemos plantear si los pasos que vamos siguien-
do son elegantes. Hemos indicado el nombre del driver a la hora de cargarlo,
y lo tenemos que volver a indicar a la hora de conectarnos. ¿Es necesaria esta
redundancia? En la versión JDBC 4.0 se delega la carga de drivers al Driver-
Manager, de manera que sólo hay que indicar el driver en el momento de crear
la conexión.
Clase Statement
El proceso para hacer una consulta o una modificación arranca de ahí mismo.
Hemos de crear un objeto de tipo Statement que contendrá la sentencia SQL
de consulta (SELECT), modificación de datos (INSERT, UPDATE y DELETE) o
modificación de la estructura de BD (CREATE TABLE, DROP TABLE, ALTER
TABLE, etc.).
Statement st = [Link]();
Una vez que tenemos el objeto de tipo Statement creado, utilizaremos el mé-
todo executeQuery para las consultas y el método executeUpdate para las
modificaciones. En el caso de las consultas, el método executeQuery devuel-
ve un objeto ResultSet que nos permitirá acceder a los datos consultados.
Este aspecto lo trataremos en el subapartado siguiente.
ResultSet rs;
rs = [Link]("SELECT * FROM Usuarios");
Las tres sentencias SQL de este ejemplo siempre hacen lo mismo. En algunos
casos, con esto ya hay bastante (por ejemplo, cuando creamos la tabla Usua-
rios ), pero normalmente no será así.
Para conseguir ejecutar una sentencia SQL con valores cambiantes, concate- Nota
naremos las partes fijas de la sentencia con las partes cambiantes (que se sus-
Tenéis disponible el códi-
tituirán por variables). El código queda un poco ilegible, pero con el tiempo go del ejemplo en el fichero
nos acabaremos acostumbrando. [Link].
String nombreUsuario,contrasena,nombre,apellidos;
...
[Link]("INSERT INTO Usuarios "+
"(nombre_usuario,contrasena,nombre,apellidos) VALUES "+
"('"+nombreUsuario+"','"+contrasena+"','"+nombre+"','"+
apellidos+"')");
Podemos aplicar este mismo patrón para hacer consultas SQL utilizando la Nota
cláusula WHERE. Por ejemplo, nos puede interesar consultar los mensajes del
¡Fijémonos en que las comillas
fórum que el usuario de la aplicación seleccione. Fijémonos en que la parte simples para definir textos en
cambiante corresponde a una columna numérica y que, por lo tanto, no está SQL se mantienen como parte
fija de la instrucción!
rodeada de comillas simples.
int codigoForum;
...
ResultSet rs = [Link]("SELECT * "+
"FROM Mensajes WHERE codigo_forum="+codigoForum);
Para definir sentencias SQL dinámicas con parámetros de tipo tiempo, debe-
remos tener en cuenta el formato de las fechas en Java. Por ejemplo, el método
toString de la clase Timestamp devuelve una fecha en formato YYYY-MM-
DD hh:mm::ss. El código necesario sería el siguiente:
Como ya hemos dicho, la clase ResultSet nos permitirá acceder a los resul-
tados de las consultas. Este acceso, sin embargo, no es libre, por lo que nos
tendremos que ceñir a las restricciones siguientes:
• Simultáneamente sólo podemos acceder a una sola fila. Para poder acceder
a todas las filas, deberemos hacer un recorrido y, en cada iteración, acceder
a una fila.
Por lo tanto, la clase ResultSet nos ofrecerá métodos para poder hacer un
recorrido por las filas de la consulta y, en cada iteración, consultar el valor de
las columnas de la fila actual teniendo en cuenta que:
Tipos�estándar�SQL Método�getTipus
CHAR getString
VARCHAR getString
SMALLINT getShort
INTEGER getInt
DOUBLE getDouble
DECIMAL getBigDecimal
DATE getDate
ESTAFI getTime
MONEY getDouble
© FUOC • PID_00275642 32 Programación mediante SQL
Ejemplo
Nota
En el ejemplo siguiente, consultamos los datos de los usuarios (almacenados en la tabla
Usuarios de nuestra BD de referencia), hacemos un recorrido y, en cada iteración, mos- Tenéis disponible el códi-
tramos la columna número 1 (que corresponde a la columna nombre_usuario ) y los go del ejemplo en el fichero
[Link].
apellidos.
Cada vez que JDBC pide datos a la BD (lo que se conoce como fetch), no recibe
una fila y ya está. Por cuestiones de rendimiento, la BD envía unas cuantas
filas. Además, el número de filas que se envían depende de cada driver.
En el caso del driver JDBC de PostgreSQL, por defecto, se envían todos los datos Consultas ineficientes
de la consulta de golpe. Mientras no son tratados, estos datos se guardan en
Para mejorar la legibilidad de
el equipo cliente, en una memoria caché. Hemos de vigilar que el volumen los ejemplos, en este material
de datos de la consulta no sea demasiado grande; si no, podemos agotar la hemos optado por utilizar con-
sultas que utilizan *. Debéis te-
memoria del equipo cliente. ner presente que en un caso
práctico lo tenéis que evitar,
porque, en general, pueden
ser ineficientes.
En relación con este tema, hemos de tener cuidado de hacer consultas de los
datos que nos sean estrictamente necesarios. En el caso anterior, tenemos un
ejemplo claro de consulta ineficiente. No tiene ningún sentido hacer una con-
sulta de todas las columnas de la tabla Usuarios si después sólo mostramos
dos de ellas. El mismo código refinado sería el siguiente:
Para permitir estas nuevas funcionalidades, hay que crear el objeto State-
ment con el mismo método createStatement, pero con dos parámetros que
determinan el tipo de movimiento y el tipo de operaciones permitidas (ved
la tabla siguiente):
Tipo�de�movimiento TYPE_FORWARD_ONLY. Es el tipo de movimiento asignado por defecto. Sólo permite hacer un
recorrido hacia delante.
TYPE_SCROLL_SENSITIVE. Es como el anterior, pero refleja los cambios que se van produ-
ciendo en la BD mientras está activo. Se entiende que estos cambios normalmente son genera-
dos por la ejecución simultánea de otras aplicaciones en otros equipos.
Para trabajar con ResultSet libres de movimientos, hay que escoger la op-
ción TYPE_SCROLL_INSENSITIVE o la opción TYPE_SCROLL_SENSITIVE (si
el driver lo permite). Así, podremos hacer recorridos:
st=[Link](
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
Ejemplo
A continuación, presentamos un ejemplo para ver cómo se utilizan los diversos métodos
de posicionamiento.
st=[Link](
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
rs = [Link]("SELECT * FROM Usuarios");
[Link]();
...
[Link](-1);
...
[Link]();
...
[Link](3);
...
Una vez superado este problema, los cambios posibles son los siguientes.
Modificación
Después de modificar las columnas de una fila, hay que enviar los cambios Nota
a la BD con el método updateRow(). Si nos movemos de fila sin ejecutar
Tenéis disponible el códi-
este método, los cambios normalmente se perderán (depende del driver que go del ejemplo en el fichero
utilicemos). Al contrario, si queremos descartar los cambios hechos, tenemos [Link].
el método cancelRowUpdates().
Ejemplo
st = [Link](ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_UPDATABLE);
rs = [Link]("SELECT * FROM Usuarios");
while ([Link]())
{
[Link]("nom",[Link]("nombre")+".");
[Link]();
}
[Link]();
st = [Link](
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE);
rs = [Link]("SELECT * FROM Usuarios");
[Link]();
int nFilas=[Link]();
int pos=(int)([Link]()*nFilas)+1;
[Link](pos);
[Link]("nombre",[Link]("nombre")+".");
[Link]();
Inserción
Para poder insertar filas, primero nos hemos de mover a una nueva fila me-
diante el método moveToInsertRow(). A partir de aquí, asignamos el valor
de las diversas columnas con updateXXX y, para añadir filas, llamamos a in-
sertRow().
[Link]();
[Link]("nombre_usuario","root");
[Link]("contrasena","super");
[Link]("nombre","administrador");
[Link]("apellidos","");
[Link]();
Borrados
Los borrados son el caso más simple. Sólo hace falta situarse en la fila adecuada
y llamar al método deleteRow().
Ejemplo
Si una sentencia sólo se ha de ejecutar una vez, utilizar PreparedStatement Ved también
deja de ser tan beneficioso y el rendimiento se equipara a la utilización de
Más adelante, en el punto
clases Statement independientes. 3.5.6, veremos la inyección
SQL (SQL injection) y otros be-
neficios derivados de utilizar
Para asignar los valores de un PreparedStatement tenemos una serie de mé- PreparedStatement.
Ejemplo
Nota
Veamos un ejemplo de cómo podemos añadir unos cuantos fórums a la BD de referencia.
Para este ejemplo, supongamos que tenemos una clase Forum implementada con los Tenéis disponible el códi-
métodos suficientes. Fijémonos también en el método clearParameters, que borra el go del ejemplo en el fiche-
ro ex010PreparedStatement
valor de los parámetros y nos prepara para la ejecución siguiente.
[Link].
Forum[] datos={ new Forum(3,"PostgreSQL"),
new Forum(4,"ODBC"), new Forum(5,"Oracle") };
Cada vez que hacemos una consulta a la BD, nuestra aplicación ha de vigilar
de no recibir valores nulos y ha de protegerse si esto sucede.
En la mayoría de casos, no hay que usar ningún método especial para detec-
tar si un valor es nulo. Los métodos getString, getBigDecimal, getBy-
tes, getDate, getTime, getTimestamp, getAsciiStream, getCharacte-
rStream, getUnicodeStream, getBinaryStream, getObject, getArray,
getBlob, getClob y getRef devuelven un objeto. Si el valor consultado en
la BD es nulo, el objeto Java también lo será.
Hay que poner una atención especial en los métodos que no devuelven Nota
objetos. En caso de valor nulo, los métodos getByte, getShort, getInt,
Tenéis disponible el códi-
getLong, getFloat y getDouble devuelven un 0. Y el método getBoolean go del ejemplo en el fichero
[Link].
© FUOC • PID_00275642 38 Programación mediante SQL
Ejemplo
El ejemplo siguiente obtiene los mensajes de los fórums que inician nuevos hilos, es decir,
de los que tienen el hilo nulo.
Este código es correcto, pero no permite tratar valores nulos. Por ejemplo: Ved también
• No permite dejar el texto nulo. Incluso haciendo que el texto valiera "NULL
", este NULL quedaría cerrado entre comillas simples y, por lo tanto, la BD
lo interpretaría como un texto que contiene literalmente NULL.
La solución al problema de los tipos básicos pasa o bien por trabajar con sus
objetos equivalentes, o bien por determinar un valor que represente el nulo.
Por ejemplo, el hilo ha de identificar un mensaje y, por lo tanto, será un valor
entero > 0. Podríamos reservar el –1 para identificar los valores nulos.
Para solucionar el segundo problema, tendríamos que sacar las comillas sim- Nota
ples de la parte estática de la sentencia SQL y añadirla cuando el objeto fuera
Tenéis disponible el códi-
necesario. go del ejemplo en el fichero
[Link].
[Link]("INSERT INTO Mensajes VALUES("+
codigoMensaje+","+codigoForum+",'"+autor+"','"+
titulo+"',"+
(texto==null?"NULL":"'"+texto+"'")+","+
(hilo==-1?"NULL":hilo)+")");
© FUOC • PID_00275642 39 Programación mediante SQL
Los nulos que aparecen en las condiciones WHERE tienen una sintaxis diferen-
te, xxx IS NULL, en vez de xxx=valor. Este cambio de sintaxis impide tener
un PreparedStatement que sea válido para condiciones con valores nulos
y valores no nulos.
Para el resto de casos, a la hora de definir los valores es cuando habrá que Nota
tener en cuenta los valores nulos. Cuando los valores sean objetos, podemos
Tenéis disponible el códi-
continuar utilizando el método setXXX, pero en los tipos básicos será impres- go del ejemplo en el fichero
cindible la utilización del método setNull. ex013NullsPreparedStatement.
java.
...
[Link]();
[Link](1,codigoMensaje);
[Link](2,codigoForum);
[Link](3,autor);
[Link](4,titulo);
[Link](5,texto);
if (hilo==-1) [Link](6,[Link]);
else [Link](6,hilo);
[Link]();
En este último caso, vuelve a pasar lo mismo. Tenemos un método especial Nota
para asignar nulos que será imprescindible para los tipos básicos, y opcional
Tenéis disponible el códi-
para el resto. go del ejemplo en el fichero
ex014NullsUpdatableResultSet.
[Link]();
[Link](1,codigoMensaje);
[Link](2,codigoForum);
[Link](3,autor);
[Link](4,titulo);
[Link](5,texto);
if (hilo==-1) [Link](6);
else [Link](6,hilo);
[Link]();
[Link]();
Hasta ahora hemos visto ejemplos con consultas simples basadas en una sola
tabla; sin embargo, esta situación no es la más habitual. Una gran parte de las
consultas que una aplicación hace a una BD están basadas en combinaciones
(en inglés, joins) de más de una tabla.
© FUOC • PID_00275642 40 Programación mediante SQL
Podemos encontrar un caso en la BD del ejemplo con las columnas "nombre" Nota
de las tablas Forums y Usuarios. Para solucionar esta ambigüedad, hay que
Tenéis disponible el códi-
hacer la consulta con alias. go del ejemplo en el fichero
[Link].
Ejemplo
Nota
El ejemplo siguiente muestra a cada usuario y, para cada usuario, los mensajes correspon-
dientes. Para ilustrar la situación sin utilizar un ejemplo excesivamente complicado, se Tenéis disponible el códi-
ha resuelto emplear una consulta anidada, pero la solución también podría ser una sola go del ejemplo en el fichero
ex016ConsultaAnidadaStatement.
consulta que combinara los datos de las tablas Usuarios y Mensajes. En una situación
java.
real, sin embargo, tendríamos que escoger la solución con combinaciones, ya que ofrece
un mejor rendimiento.
© FUOC • PID_00275642 41 Programación mediante SQL
st1 = [Link]();
st2 = [Link]();
rsUsuarios = [Link]("SELECT * FROM Usuarios");
while ([Link]())
{
String nombreUsuario=[Link]("nombre_usuario");
rsMensajes=[Link]("SELECT * FROM "+
"Mensajes WHERE autor='"+nombreUsuario+"'");
while ([Link]())
{
[Link](
[Link]("nombre")+" "+
[Link]("apellidos")+"--"+
[Link]("titulo"));
}
[Link]();
}
[Link]();
[Link]();
[Link]();
Aunque el ejemplo es correcto, las consultas anidadas son una buena ocasión
para poner en práctica los PreparedStatement. Efectivamente, la consulta
interior siempre tiene el mismo patrón, y se ejecuta repetidamente.
Nota
st = [Link]();
pst = [Link]("SELECT * FROM "+
"Mensajes WHERE autor=?");
rsUsuarios = [Link]("SELECT * FROM Usuarios");
while ([Link]())
{
String nombreUsuario=[Link]("nombre_usuario");
[Link](1,nombreUsuario);
ResultSet rsMensajes=[Link]();
while ([Link]())
{
[Link](
[Link]("nombre")+" "+
[Link]("apellidos")+"--"+
[Link]("título"));
}
[Link]();
}
[Link]();
[Link]();
[Link]();
• Tal y como planteamos la consulta, hemos de visitar las respuestas del pri-
mer hilo antes de finalizar el recorrido de los hilos iniciales. Esto significa
que necesitamos tantos ResultSet simultáneos como profundidad ten-
ga la recursividad. Y recordemos que un Statement, y por extensión un
PreparedStatement, sólo permite un ResultSet simultáneo.
En resumen, la solución pasa por crear uno nuevo Statement en cada llamada Nota
recursiva. En el ejemplo, consideramos que hilo=-1 significa que el hilo es
Tenéis disponible el códi-
nulo. Hemos añadido un parámetro tab para tabular la salida por pantalla. go del ejemplo en el fichero
[Link].
Ejemplo
Nota
En el ejemplo siguiente, creamos una tabla Grupos con una clave primaria entera auto-
generada. Es importante tener presente que la sintaxis para definir columnas autogene- Tenéis disponible el códi-
radas depende del SGBD. En el ejemplo, hemos utilizado la sintaxis de PostgreSQL. Hay go del ejemplo en el fichero
[Link].
que destacar también que en cuanto a los valores autogenerados, sólo habrá uno, y que,
por eso, podemos recorrer el ResultSet sin iterar.
Ejemplo
El ejemplo siguiente muestra el código necesario para obtener el valor de la clave primaria
autogenerada de la tabla Grupos en el caso de PostgreSQL.
Es muy habitual que las aplicaciones identifiquen a los usuarios antes de em-
pezar a trabajar. Gracias a eso, pueden ofrecer más o menos operaciones a los
usuarios, según el rol que tengan.
BD de referencia).
Hay muchas maneras de hacer esta validación; por ejemplo, haciendo una
consulta como la siguiente:
String nombreUsuario,contrasena;
//leemos nombreUsuario y contrasena
...
ResultSet rs = [Link]("SELECT * "+
"FROM Usuarios WHERE nombre_usuario='"+nombreUsuario+
"' AND contrasena='"+contrasena+"'");
if ([Link]()) [Link]("CORRECTO");
else [Link]("ERROR");
Como quien no quiere la cosa, con un código como éste, tenemos un agujero
de seguridad importante. Fijémonos en lo que pasa si un usuario introduce el
nombre siguiente y cualquier contraseña:
Esto no es el nombre de ningún usuario y, a pesar de todo, nuestro sistema Seguridad de las
da como resultado CORRECTO. El problema radica en la construcción de la contraseñas
sentencia SQL, que concatena partes estáticas con el contenido de variables. El ejemplo de la contraseña ex-
Si alguna variable, en vez de contener un valor, tiene el código SQL adecuado, hibe una mala práctica de se-
guridad muy habitual: almace-
permite modificar la sentencia SQL que se está ejecutando. nar contraseñas planas. Es pre-
ferible almacenar la contraseña
en la BD utilizando alguna téc-
nica de encriptación de datos.
Este problema tiene solución, tanto si se refuerza el código como si se utiliza
PreparedStatement.
Nota
• Las soluciones en código se basan en escapar los apóstrofos, es decir, en
Tenéis disponible el códi-
marcar los apóstrofos de los valores de tipo texto para que el SGBD no go del ejemplo en el fichero
los confunda con los apóstrofos de la sentencia SQL. Cada SGBD ofrece [Link].
alguna alternativa para escapar apóstrofos, como por ejemplo, \' o ''.
© FUOC • PID_00275642 45 Programación mediante SQL
Ejemplo
CallableStatement cst =
[Link]("{?=call conIva(?)}");
Para pasar parámetros de entrada utilizaremos métodos setXXX, una vez crea-
do el CallableStatement. El funcionamiento de estos métodos es idéntico
al de los métodos setXXX de los ResultSet modificables.
Los parámetros de salida se han de registrar antes de hacer la llamada al pro- Nota
cedimiento almacenado. Durante el registro, indicamos el tipo de parámetro
Tenéis disponible el códi-
que recibiremos. Después de ejecutar el procedimiento almacenado, se pueden go del ejemplo en el fichero
recoger los resultados con los métodos getXXX. ex023StoredProcedureIN_OUT.
CallableStatement cst =
[Link]("{?=call conIva(?)}");
[Link](1, [Link]);
[Link](2,10.0f);
[Link]();
[Link]([Link](1));
getXXX.
Todas las aplicaciones están expuestas a errores durante la ejecución. Sin em-
bargo, si además están conectadas a una BD, las probabilidades aumentan.
La razón la podemos encontrar en el hecho de que cooperan con un sistema
complejo (el SGBD), al que nos conectamos por red y que, simultáneamente,
da servicio a otras aplicaciones. Por si no fuera bastante, las sentencias que
enviamos a menudo son dinámicas y, por lo tanto, susceptibles de errores sólo
detectables en tiempo de ejecución.
© FUOC • PID_00275642 47 Programación mediante SQL
JDBC expone los errores mediante excepciones, lo que facilita de forma con-
siderable el desarrollo de aplicaciones. Como es habitual en Java, se define
una jerarquía de excepciones que derivan de Exception. En lo alto de esta
jerarquía está la clase SQLException.
Una gran parte de las excepciones que genera JDBC son de la clase SQLExcep-
tion o derivan de ella, aunque podremos explotar poco la jerarquía. Las ex-
cepciones que nos interesará capturar son sobre todo SQLException y, pun-
tualmente, alguna otra derivada. Así pues, raramente podremos tratar las ex-
cepciones en bloques try-catch, y lo tendremos que hacer a partir de la in-
formación que nos facilitarán los objetos SQLException.
• getMessage(). Devuelve una cadena con información textual del error. Nota
Es el método estándar heredado de la clase Exception.
Tenéis disponible el códi-
go del ejemplo en el fichero
• getSQLState(). Devuelve una cadena de cinco caracteres con un código [Link].
int codigoMensaje;
String nombreUsuario;
...
try
{
[Link]("INSERT INTO Lecturas "+
"VALUES("+codigoMensaje+",'"+nombreUsuario+"',"+
"to_timestamp('"+ts+"','YYYY-MM-DD HH24:MI'))");
}
catch (SQLException e)
{
[Link]("Mensaje:"+[Link]());
[Link]("SQLState:"+[Link]());
[Link]("ErrorCode:"+[Link]());
}
© FUOC • PID_00275642 48 Programación mediante SQL
SQLState:23503
ErrorCode:0
SQLState:23503
ErrorCode:0
SQLState:23505
ErrorCode:0
Podemos comprobar que el driver de PostgreSQL no devuelve ErrorCode, só- Enlace de interés
lo SQLState. Esto no presenta ningún problema, ya que desde el estándar
Podéis encontrar la lista
SQL:1992 se propuso reportar los errores por medio de códigos SQLState. Du- de códigos SQLState de
rante unos cuantos años, se han mantenido las dos versiones de códigos, pero PostgreSQL en el anexo
A de la documentación:
la versión actual del driver JDBC de PostgreSQL ha prescindido del ErrorCode. [Link]
docs/8.1/inter active/errco-
[Link]
3.7.3. Gestión del error desde nuestros programas
Una vez detectada la causa exacta del error, la aplicación tendrá que decidir
entre reintentar la operación, abandonarla silenciosamente (es decir, ignorar-
la) o detenerla con una excepción. Exceptuando el reintento, en las otras situa-
ciones es muy importante cerrar todos los objetos (como por ejemplo, Resul-
tSetStatement ) que podamos tener abiertos. Así permitimos que el SGBD
libere recursos rápidamente y que pueda atender a otros usuarios.
Ejemplo
[Link]( "[Link]" );
String dbURL="jdbc:postgresql:bdMail";
Connection conn = [Link](
dbURL, "usuario","usuario");
Statement st = [Link]();
[Link]("INSERT INTO Lecturas VALUES("+
códigoMensaje+",'"+nombreUsuario+"',"+
"to_timestamp('"+ts+"','YYYY-MM-DD HH24:MI'))");
Nota
Las diversas líneas de este código pueden generar diferentes tipos de excepcio-
nes:
c)�[Link]() :
© FUOC • PID_00275642 50 Programación mediante SQL
d)�[Link](...) :
Ejemplo
Nota
En este ejemplo mostramos una posible estructuración del código para garantizar que,
tanto en caso de éxito como de error, todos los objetos abiertos quedarán cerrados. La Tenéis disponible el códi-
utilización del bloque finally simplifica la codificación. Asimismo, dentro del bloque go del ejemplo en el fichero
[Link].
finally, todos los métodos están protegidos con un bloque try-catch para garantizar
que, aunque se produzca un error, el bloque finally se continúa ejecutando hasta el
final.
try
{
[Link]( "[Link]" );
String dbURL="jdbc:postgresql:bdMail";
conn = [Link]( dbURL,
"usuario","usuario");
st = [Link]();
//...
}
catch (Exception e)
{
throw e;
}
finally
{
//cerramos lo que haga falta
if (st!=null)
{
try [Link]();
catch(SQLException e)
{
//...tratamos el error
© FUOC • PID_00275642 51 Programación mediante SQL
}
}
if (conn!=null)
{
try [Link]();
catch(SQLException e)
{
//...tratamos el error
}
}
}
Los errores provocados por la ejecución de sentencias SQL sí que los tratare-
mos. Si el problema es provocado por datos erróneos que el usuario ha intro-
ducido, es muy interesante dar un mensaje de error fácil de entender por parte
del usuario final. Y, sobre todo, hay que dar una segunda oportunidad.
Ejemplo
Nota
En este ejemplo leemos el código de un mensaje y el nombre de un usuario, y añadimos
una fila en la tabla Lecturas para reflejar que el usuario en cuestión ha leído el mensaje Tenéis disponible el códi-
en la fecha y la hora actuales. go del ejemplo en el fichero
[Link].
boolean correct=false;
while (!correct)
{
try
{
//leemos los datos
[Link]("CódigoMensaje:");
codigoMensaje=[Link]([Link]());
[Link]("NombreUsuario:");
nombreUsuario=[Link]();
ts = new Timestamp([Link]());
correcto=true;
}
catch (SQLException e)
{
if([Link]().equals(FK_ERROR))
if([Link]().indexOf(FK_MENSAJES)!=-1)
[Link]("codigo de Mensaje no existe");
else if([Link]().indexOf(FK_USUARIOS)!=-1)
[Link]("nombre del usuario no existe");
else [Link]("Inesperado:"+[Link]());
else if ([Link]().equals(PK_ERROR))
[Link]("Error: mensaje ya leído");
else [Link]("Inesperado:"+[Link]());
}
catch (Exception e)
{
[Link]("Error leyendo los datos");
}
}
© FUOC • PID_00275642 52 Programación mediante SQL
Cada conexión JDBC puede mantener una sola transacción activa. Cuando
creamos una conexión, por defecto, se trabaja en modo autocommit. Esto sig-
nifica que cada sentencia SQL que ejecutemos constituye una transacción que
se inicia antes de llevar a cabo la sentencia y que, si no se genera ningún error,
acaba en commit al finalizar (de ahí el nombre autocommit).
[Link](false);
...
try
{
... sentencias SQL que forman la transacción
[Link]();
}
catch (SQLException e)
{
[Link]();
}
En nuestra BD de referencia, tenemos una tabla para registrar los mensajes que
han leído los usuarios. Cada vez que un usuario lea un mensaje, deberemos
actualizar la tabla Lecturas con un código parecido al siguiente:
Ejemplo
Nota
En este ejemplo definimos una transacción que comprende las operaciones de registrar
la lectura del mensaje y de leer el mensaje en sí. Si conseguimos registrar la lectura, leer Tenéis disponible el códi-
el mensaje y mostrarlo por pantalla finalizamos la transacción con commit. En caso con- go del ejemplo en el fiche-
ro ex027MensajesLeidos
trario, deshacemos la transacción y volvemos al estado inicial.
[Link].
[Link](false);
...
try
{
[Link]("INSERT INTO Lecturas "+
"VALUES("+codigoMensaje+",'"+nombreUsuario+"',"+
"to_timestamp('"+ts+"','YYYY-MM-DD HH24:MI'))");
rs=[Link]("SELECT * FROM Mensajes "+
"WHERE codigo_mensaje="+codigoMensaje);
if ([Link]()) [Link](
[Link]("titulo")+ "--"+[Link]("texto"));
else throw new SQLException();
[Link]();
[Link]();
}
catch (SQLException e)
{
[Link]();
}
Las sentencias SQL que se ejecutan durante una transacción pueden afectar a
diversas filas de diferentes tablas de la BD. Dependiendo de la gestión interna
que haga el SGBD, estas filas o tablas pueden quedar bloqueadas. Hasta el final
de la transacción el SGBD no libera las filas o las tablas del bloqueo. Mientras
tanto, sentencias SQL que se estén ejecutando de forma concurrente y que
afecten a alguna fila o tabla bloqueadas pueden quedar en pausa y, hasta que
no se acabe la transacción, no continúan la ejecución.
Es decir, cuanto más larga sea la ejecución de una transacción, más probabili-
dades habrá de que se detenga temporalmente la ejecución de otras aplicacio-
nes que accedan simultáneamente a la BD. Por esto es tan importante que las
transacciones duren estrictamente el tiempo necesario.
© FUOC • PID_00275642 54 Programación mediante SQL
Resumen
• SQL hospedado, que permite introducir sentencias SQL en medio del có-
digo y requiere una precompilación antes de ser compilado.
• SQL/CLI, interfaz estándar que permite acceder a la BD a partir de funcio-
nes.
• ODBC, evolución de SQL/CLI que permite cargar el driver en tiempo de
ejecución.
• OLE DB, evolución de ODBC que permite acceder a otros orígenes de datos,
no sólo a SGBD.
• DAO, RDO, ADO y [Link], evolución de OLE DB orientada a objetos
para sistemas operativos Microsoft.
• JDBC, evolución de OLE DB orientada a objetos y abierta a cualquier pla-
taforma en el entorno de programación Java.
Nos hemos centrado en JDBC y hemos visto sus diferentes versiones y sus
diversos tipos de drivers (implementaciones de la API JDBC de los diferentes
SGBD). También hemos estudiado la estructura básica de una aplicación: co-
nexión, ejecución de sentencias SQL, tratamiento de los resultados y desco-
nexión.
Actividades
1.�Os proponemos una serie de actividades basadas en los ejemplos de Java del módulo. Veréis
que se ha utilizado el SGBD PostgreSQL en todos los ejemplos. Recomendamos que utilicéis
los drivers de tipo 4 que podéis encontrar en [Link]
Glosario
API f Sigla correspondiente a application program interface ('interfaz de programación de
aplicaciones').
cursor m Estructura de SQL que permite que una aplicación sea capaz de recorrer las filas
del resultado de una consulta de SQL.
JDBC m (Java Database Connectivity). Marca comercial de Sun Microsystems que define
una API estándar para manipulación de SQL desde Java.
JDK m (Java Development Kit). Intérprete y entorno para desarrollos Java realizado por Sun
Microsystems.
SQL/CLI m Técnica de SQL programado que sigue un enfoque interpretado y que permite
a la aplicación el acceso a las BD gestionadas por el SGBD mediante llamadas a diferentes
subrutinas que están disponibles en bibliotecas.
SQL hospedado m Técnica de SQL programado que sigue un enfoque compilado y que
permite incorporar directamente las sentencias del lenguaje SQL (denominadas sentencias de
SQL hospedado) dentro de la aplicación, mezcladas con las sentencias propias del lenguaje de
programación (llamado lenguaje anfitrión).
variable puente con SQL f Mecanismo que ofrece el SQL hospedado para poder referen-
ciar las variables de la aplicación en las sentencias de SQL. Sirve tanto para pasar datos a una
sentencia de SQL hospedado como para recoger los datos que son resultado de una sentencia
de SQL hospedado.
© FUOC • PID_00275642 57 Programación mediante SQL
Bibliografía
Date, C. J.; Darwen, H. (1996). A guide to the SQL standard (4.ª ed.). Massachusetts: Addi-
son-Wesley.