En conjunto, la aproximación por defecto funciona bien para bajos volúmenes de acceso a la base de datos, pero ¿cómo podemos manejar un gran número de peticiones que actualizan muchas tablas relacionadas a la vez y aún así asegurar la integridad de los datos? Esta sección explica cómo hacerlo con los siguientes tópicos:
Para obtener un idea general de lo que hacer un driver JDBC, podemos examinar el fichero JDCConnectionDriver.java. La clase JDCConnectionDriver implemta la clase java.sql.Driver y actúa como un driver "pass-through" re-enviando peticiones JDBC al driver JDBC real de la base de datos. La clase driver JDBC se carga con un llamada a Class.forName(drivername).
Estas líneas de código muestran cómo cargar tres clases diferentes de drivers JDBC:
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Class.forName("postgresql.Driver");
Class.forName("oracle.jdbc.driver.OracleDriver");
Cada driver JDBC está configurado para entender una URL específica, por eso se pueden cargar varios drivers JDBC a la vez. Cuando especificamos una URL en el momento de la conexión, se selecciona el primer driver JDBC que corresponda.
El puente jdbc-odbc acepta URLS que empiecen con jdbc:odbc: y usa el siguiente campo de esa URL para especificar el nombre de la fuente de los datos. Este nombre identifica el esquema de la base de datos particular a la que queremos acceder. La URL también puede incluir más detalles sobre cómo contactyar con la base de datos e introducir la cuenta.
El siguiente ejemplo contiene la información de Oracle SQL*net sobre una base de datos particular llamada ejbdemo en la máquina dbmachine://access the ejbdemo tables String url = "jdbc:odbc:ejbdemo";
Este siguiente ejemplo usa mysql para conectar con la base de datos ejbdemo en la máquina local. También se incluyen los detalles del nombre de usuario y la password para el login.String url = "jdbc:oracle:thin:user/password@( description=(address_list=( address=(protocol=tcp) (host=dbmachine)(port=1521)))(source_route=yes) (connect_data=(sid=ejbdemo)))";
String url =
"jdbc:mysql://localhost/ejbdemo?user=user;
password=pass";
Los drivers JDBC se dividen en cuatro tipos. También se pueden categorizar como puro java o drivers pequeños para indicar si son usados por aplicaciones clientes (drivers puro java) o por applets (drivers pequeños).
Un driver del tipo 2 podría necesitar código de base de datos en el lado del cliente para conectar a través de la red.
Después de establecer la conexión, se puede ejecutar una sentencia contra la base de datos. Los resultados de la sentencias pueden recuperarse y cerrarse la conexión.Connection con = DriverManager.getConnection(url); Connection con = DriverManager.getConnection(url, "user", "password");
Una características útil de la clase DriverManager es el método setLogStream. Podemos usar este método para generar información de seguimiento para ayudarnos a dignosticar problemas de conexión que normalmente no serían visibles. Para generar la información de seguimiento, sólo tenemos que llamar al método de esta forma:
La sección Connection Pooling en el capítulo 8 muestra cómo podemos mejorar las conexión JDBC sin cerrrar la conexión una vez completada la sentencia. Cada conexión JDBC a una base de datos provoca una sobrecarga al abrir un nuevo socket y usar el nombre de usuario y la password para login en la base de datos. La reutilización de las conexiones reduce la sobrecarga. Las colas de Conexiones mantienen una lista de conexiones abiertas y limpia cualquier conexión que no pueda ser reutilizada.DriverManager.setLogStream(System.out);
El siguiente ejemplo crea un objeto CallableStatement con tres parámetros para almacenar información de la cuenta de login:
CallableStatement cs =
con.prepareCall("{call accountlogin(?,?,?)}");
cs.setString(1,theuser);
cs.setString(2,password);
cs.registerOutParameter(3,Types.DATE);
cs.executeQuery();
Date lastLogin = cs.getDate(3);
Sentencias Query: Este segmento de código crea un objeto Statement y llama al método Statement.executeQuery para seleccionar texto desde la base de datos dba. El resultado de la consulta se devuelve en un objeto ResultSet. Cómo recuperar los resultados desde este objeto ResultSet se explica más abajo en Hoja de Resultados.
Statement stmt = con.createStatement();
ResultSet results = stmt.executeQuery(
"SELECT TEXT FROM dba ");
Sentencias Update: Este segmento de código crea un objeto Statement y llama al método Statement.executeUpdate para añadir una dirección de email a una tabla de la base de datos dba:
String updateString =
"INSERT INTO dba VALUES (some text)";
int count = stmt.executeUpdate(updateString);
Query PreparedStatement: Creamos un objeto PreparedStatement especificando la definición de plantilla y la situación de los parámetros. Los datos de los parámetros se insertan dentro del objeto PreparedStatement llamando a sus métodos setXXX y especificando el parámetro y su dato. Las instrucciones SQL y los parámetros son enviados a la base de datos cuando se llama al método executeXXX.
Este segmento de código crea un objeto PreparedStatement para seleccionar datos de usuarios basados en la dirección email del usuario. El interrogante ("?") indica que este sentencia tiene un parámetro:
PreparedStatement pstmt = con.prepareStatement("
select theuser from
registration where
emailaddress like ?");
//Initialize first parameter with email address
pstmt.setString(1, emailAddress);
ResultSet results = ps.executeQuery();
Una vez que se ha inicializado la plantilla PreparedStatement sólo se insertan los valores modificados para cada llamadas:
pstmt.setString(1, anotherEmailAddress);
Nota: No todos los drivers de bases de datos compilan sentencias preparadas.
Update PreparedStatement: Este segmento de código crea un objeto PreparedStatement para actualizar el registro de un vendedor. La plantilla tiene cinco parámetros, que se seleccionan con cinco llamadas a los métodos PreparedStatement.setXXX apropiados.
PreparedStatement ps = con.prepareStatement(
"insert into registration(theuser, password,
emailaddress, creditcard,
balance) values (
?, ?, ?, ?, ?)");
ps.setString(1, theuser);
ps.setString(2, password);
ps.setString(3, emailaddress);
ps.setString(4, creditcard);
ps.setDouble(5, balance);
ps.executeUpdate();
Almacenar resultados en una tbal hash requiere que la llamada JDBC sea interceptada antes de crear una sentencia real que devuelva los resultados cacheados, y la entrada del caché debe limpiarse si hay una actualización correspondiente con ese item_id.
Se utilizan los métodos getType para recuperar datos desde columnas específicas para cada fila devuelta en la consulta. Este ejemplo recupera la columna TEXT de todas las tablas con una columna TEXT en la base de datosdba. El método results.next mueve hasta la siguiente fila recuperada hasta que se hayan procesado todas las filas devueltas:
Statement stmt = con.createStatement();
ResultSet results = stmt.executeQuery(
"SELECT TEXT FROM dba ");
while(results.next()){
String s = results.getString("TEXT");
displayText.append(s + "\n");
}
stmt.close();
JDBC 2.0 introduce las hojas de resultados scrollables cuyos valores pueden ser leídos y actualizados si así lo permite la base de datos original. Con las hojas de resultados scrollables, cualquier fila puede ser seleccionada de forma aleatorio, y nos podemos mover por la hoja de resultados hacia adelante y hacia atrás.
Una ventaja de la nueva hoja de resultados es que podemos actualizar un conjunto de filas correspondientes son tener que enviar una llamada adicional a executeUpdate. Las actualizaciones se hacen llamando a JDBC y no se necesitan comandos SQL personalinzados. Esto aumenta la portabilidad del código de la base de datos que creemos.
Tanto Statements como PreparedStatements tienen un constructor adicional que acepta un parámetro tipo scroll y otro tipo update. El valor del tipo scroll puede ser uno de los siguientes valores:
Connection con = getConnection();
if(con.getMetaData().supportsResultSetConcurrency(
ResultSet.SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE)) {
PreparedStatement pstmt = con.prepareStatement(
"select password, emailaddress,
creditcard, balance from
registration where theuser = ?",
ResultSet.SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE);
}
El siguiente código actualiza el balance de un usuario desde la hoja de resultados creada anteriormente. La actualización sólo se aplica a la hoja de resultados hasta que se llama a rs.updateRow(), que actualiza la base de datos original. Cerrando la hoja de resultados antes de llamar a updateRow se perderá cualquier edición aplicada en la hoja de resultados.
rs.first();
updateDouble("balance",
rs.getDouble("balance") - 5.00);
Insertar una nueva fila usa los mismos métodos update<type>. La única diferencia es que se llama al método rs.moveToInsertRow de que los datos hayan sido inicializados y después se llama a rs.insertRow(). Podemos borrar la fila actual con una llamada a rs.deleteRow().
El siguiente método muestra cómo usar la sentencia addBatch. Las llamadas a stmt.addBatch añaden sentencias a la Statement original, y la llamada a executeBatch envía la sentencia completa con todos los apéndices a la base de datos.
Statement stmt = con.createStatement();
stmt.addBatch(
"update registration set balance=balance-5.00
where theuser="+theuser);
stmt.addBatch(
"insert into auctionitems(
description, startprice)
values("+description+","+startprice+")");
int[] results = stmt.executeBatch();
La hoja de resultados del método addBatch es un array de cuentas de filas afectadas por cada sentencia ejecutada en el trabajo batch. Si ocurre un problema se lanzará una java.sql.BatchUpdateException. Se puede obteener un array incompleto de contador de fila de
BatchUpdateException llamando a su método getUpdateCounts.
Esta técnica peude usarse para almacenar y recuperar imágenes y objetos Java.
Almacenar y recuperar una imagen: Es muy fácil almacenar un objeto que puede ser serializado o convertido en un array de bytes. Desafortunadamente java.awt.Image no es Serializable. Sin embargo, como se ve en el siguiente ejemplo de código, podemos almacenar los datos de la imagen en un fichero y almacenar la información del fichero como bytes en un campo binario de la base de datos.
int itemnumber=400456;
File file = new File(itemnumber+".jpg");
FileInputStream fis = new FileInputStream(file);
PreparedStatement pstmt = con.prepareStatement(
"update auctionitems
set theimage=? where id= ?");
pstmt.setBinaryStream(1, fis, (int)file.length()):
pstmt.setInt(2, itemnumber);
pstmt.executeUpdate();
pstmt.close();
fis.close();
Para recuperar esta imagen y crear un array de bytes que pueda ser pasado a createImage, hacemos los siguiente:
int itemnumber=400456;
byte[] imageBytes;
PreparedStatement pstmt = con.prepareStatement(
"select theimage from auctionitems where id= ?");
pstmt.setInt(1, itemnumber);
ResultSet rs=pstmt.executeQuery();
if(rs.next()) {
imageBytes = rs.getBytes(1);
}
pstmt.close();
rs.close();
Image auctionimage =
Toolkit.getDefaultToolkit().createImage(
imageBytes);
Almacenar y Recuperar un Objeto: Una clase puede ser serializada a un campo binario de la base de datos de la misma forma que se hizo con la imagen en el ejemplo anterior. En este ejemplo, la clase RegistrationImpl se ha modificado para soportar la serialización por defecto añadiendole implements Serializable a la declaración de la clase.
Luego, se crea un array ByteArrayInputStream para pasarlo como un Stream Binario a JDBC. Para crear el ByteArrayInputStream, RegistrationImpl primero pasa a través de un ObjectOutputStream hacia el ByteArrayInputStream con una llamada a RegistrationImpl.writeObject. Luego el ByteArrayInputStream es convertido a un array de bytes, que puede ser utilizado para crear el ByteArrayInputStream. El método create en RegistrationServer.java se ha modificado de esta forma:
public registration.RegistrationPK create(
String theuser,
String password,
String emailaddress,
String creditcard)
throws registration.CreateException{
double balance=0;
Connection con = null;
PreparedStatement ps = null;;
try {
con=getConnection();
RegistrationImpl reg= new RegistrationImpl();
reg.theuser = theuser;
reg.password = password;
reg.emailaddress = emailaddress;
reg.creditcard = creditcard;
reg.balance = balance;
ByteArrayOutputStream regStore =
new ByteArrayOutputStream();
ObjectOutputStream regObjectStream =
new ObjectOutputStream(regStore);
regObjectStream.writeObject(reg);
byte[] regBytes=regStore.toByteArray();
regObjectStream.close();
regStore.close();
ByteArrayInputStream regArrayStream =
new ByteArrayInputStream(regBytes);
ps=con.prepareStatement(
"insert into registration (
theuser, theclass) values (?, ?)");
ps.setString(1, theuser);
ps.setBinaryStream(2, regArrayStream,
regBytes.length);
if (ps.executeUpdate() != 1) {
throw new CreateException ();
}
RegistrationPK primaryKey =
new RegistrationPKImpl();
primaryKey.theuser(theuser);
return primaryKey;
} catch (IOException ioe) {
throw new CreateException ();
} catch (CreateException ce) {
throw ce;
} catch (SQLException sqe) {
System.out.println("sqe="+sqe);
throw new CreateException ();
} finally {
try {
ps.close();
con.close();
} catch (Exception ignore) {
}
}
}
El objeto es recuperado y resconstruido extrayendo los bytes desde la base de datos, creando un ByteArrayInputStream desde aquellos bytes leídos desde un ObjectInputStream, y llamando a readObject para crear de nuevo el ejemplar.
El siguiente ejemplo muestra los cambios necesarios en el método RegistrationServer.refresh para recuperar el ejemplar Registration desde la base de datos.
private Registration refresh(RegistrationPK pk)
throws FinderException {
if (pk == null) {
throw new FinderException ();
}
Connection con = null;
PreparedStatement ps = null;
try {
con=getConnection();
ps=con.prepareStatement("
select theclass from
registration where theuser = ?");
ps.setString(1, pk.theuser());
ps.executeQuery();
ResultSet rs = ps.getResultSet();
if(rs.next()){
byte[] regBytes = rs.getBytes(1);
ByteArrayInputStream regArrayStream =
new ByteArrayInputStream(regBytes);
ObjectInputStream regObjectStream =
new ObjectInputStream(
regArrayStream);
RegistrationImpl reg=
(RegistrationImpl)
regObjectStream.readObject();
return reg;
}
else {
throw new FinderException ();
}
} catch (Exception sqe) {
System.out.println("exception "+sqe);
throw new FinderException ();
}
finally {
try {
rs.close();
ps.close();
con.close();
}
catch (Exception ignore) {}
}
}
BLOBs y CLOBs: Almacenar grandes campos en un tabla con otros datos no es necesariamente el lugar óptimo especialmente si los datos tienen un tamaño variable. una forma de manejar objetos de tamaño grande y variable es con el tipo "Large Objects" (LOBs). Este tipo usa un localizador, esencialmente un puntero, en el registro de la base de datos que apunta al campo real en la base de datos.
Hay dos tipos de LOBs: "Binary Large Objects" (BLOBs) y "Character Large Objects" (CLOBs). Cuando accedemos a BLOB o CLOB, los datos no se copian en el cliente. Para recuperar los datos reales desde una hoja de resultados, tenemos que recuperar el puntero con una llamada a BLOB blob=getBlob(1) o CLOB clob=getClob(1), y luego recuperar los datos con una llamada a blob.getBinaryStream() o clob.getBinaryStream().
Una descripción de los niveles de aislamiento en la transaciónse cubre con más detalles en el Capítulo 3: Maneja¡o de Datos y Transaciones. Para usar control de transaciones en la plataforma JDBC, primero necesitamos desactivar el moco "full auto-commit" llamando a:
En este punto, podemos enviar cualquier siguiente sentencia JDBC o deshacer cualquier actualización llamando al método Connection.rollback. La llamada rollback se sitúa normalmente en el manejador de excepciones, aunque puede situarse en cualquier lugar en le flujo de la transación.Connection con= getConnection(); con.setAutoCommit(false);
El siguiente ejemplo inserta un ítem en la subasta y decrementa el balance del usuario. Si el balance es menor de cero, se deshace la transación complera y el ítem de susbasta es eliminado.
public int insertItem(String seller,
String password,
String description,
int auctiondays,
double startprice,
String summary) {
Connection con = null;
int count=0;
double balance=0;
java.sql.Date enddate, startdate;
Statement stmt=null;
PreparedStatement ps = null;
try {
con=getConnection();
con.setAutoCommit(false);
stmt= con.createStatement();
stmt.executeQuery(
"select counter from auctionitems");
ResultSet rs = stmt.getResultSet();
if(rs.next()) {
count=rs.getInt(1);
}
Calendar currenttime=Calendar.getInstance();
java.util.Date currentdate=currenttime.getTime();
startdate=new java.sql.Date(
currentdate.getTime());
currenttime.add(Calendar.DATE, auctiondays);
enddate=new java.sql.Date((
currenttime.getTime()).getTime());
ps=con.prepareStatement(
"insert into auctionitems(
id, description, startdate, enddate,
startprice, summary)
values (?,?,?,?,?,?)");
ps.setInt(1, count);
ps.setString(2, description);
ps.setDate(3, startdate);
ps.setDate(4, enddate);
ps.setDouble(5, startprice);
ps.setString(6, summary);
ps.executeUpdate();
ps.close();
ps=con.prepareStatement(
"update registration
set balance=balance -0.50
where theuser= ?");
ps.setString(1, seller);
ps.close();
stmt= con.createStatement();
stmt.executeQuery(
"select balance from registration
where theuser='"+seller+"'");
rs = stmt.getResultSet();
if(rs.next()) {
balance=rs.getDouble(1);
}
stmt.close();
if(balance <0) {
con.rollback();
con.close();
return (-1);
}
stmt= con.createStatement();
stmt.executeUpdate(
"update auctionitems set
counter=counter+1");
stmt.close();
con.commit();
con.close();
return(0);
} catch(SQLException e) {
try {
con.rollback();
con.close();
stmt.close();
ps.close();
}catch (Exception ignore){}
}
return (0);
}
La siguiente sentencia muestra cómo podemos usar la palabra clave escape para buscar por el valor 10%:
stmt.executeQuery(
"select tax from sales where tax like
'10\%' {escape '\'}");
Si nuestro programa almacena nombres y direcciones en la base de datos introducidos desde la línea de comandos o desde un interface de usuario, el símbolo de comilla simple (') podría aparecer en los datos. Pasar una comilla simple directamente a un string SQL causa problemas cuando la sentencia es analizada porque SQL le da a este símbolo otro significado a menos que se le escape.
Para resolver este problem, el siguiente método escapa cualquier símbolo ' encontrado en la línea de entrada. Este método puede ser extendido para escapar cualquier otro caracter como las comas , que la base de datos o su driver podrían interpretar de otra forma:
static public String escapeLine(String s) {
String retvalue = s;
if (s.indexOf ("'") != -1 ) {
StringBuffer hold = new StringBuffer();
char c;
for(int i=0; i < s.length(); i++ ) {
if ((c=s.charAt(i)) == '\'' ) {
hold.append ("''");
}else {
hold.append(c);
}
}
retvalue = hold.toString();
}
return retvalue;
}
Sin embargo, si usamos un PreparedStatement en lugar de una simple Statement, muchos de estos problemas de escape desaparecen. Por ejemplo, en lugar de esta línea con la secuencia de escape:
stmt.executeQuery(
"select tax from sales where tax like
'10\%' {escape '\'}");
Podríamos usar esta línea:
preparedstmt = C.prepareStatement(
"update tax set tax = ?");
Nuestro programa puede determinar los tipos de las columnas de la base de datos desde los datos meta de la base de datos y usar esta información para chequear el valor antes de recuperarlo. Este código chequea que el valor es del tipo INTEGER antes de recuperarlo.
int count=0;
Connection con=getConnection();
Statement stmt= con.createStatement();
stmt.executeQuery(
"select counter from auctionitems");
ResultSet rs = stmt.getResultSet();
if(rs.next()) {
if(rs.getMetaData().getColumnType(1) ==
Types.INTEGER) {
Integer i=(Integer)rs.getObject(1);
count=i.intValue();
}
}
rs.close();
Nota: la clase Timestamp pierde precisión cuando se convierte a java.util.Date porque java.util.Date no contiene un campo de nanosegundos, es mejro no convertir un ejemplar Timestamp si el valor va a ser escrito de vuelta en la base de datos.
Este ejemplo usa la clase java.sql.Date para convertir el valor java.util.Date devuelto por la llamada a Calendar.getTime hacia java.sql.Date.
Calendar currenttime=Calendar.getInstance();
java.sql.Date startdate=
new java.sql.Date((
currenttime.getTime()).getTime());
También podemo usar la clase java.text.SimpleDateFormat para hacer la conversión. Este ejemplo usa la clase java.text.SimpleDateFormat para convertir un objeto java.util.Date a un objeto java.sql.Date:
SimpleDateFormat template =
new SimpleDateFormat("yyyy-MM-dd");
java.util.Date enddate =
new java.util.Date("10/31/99");
java.sql.Date sqlDate =
java.sql.Date.valueOf(
template.format(enddate));
Si encontramos que una representación de fecha de una base de datos no puede ser mapeada a un tipo Java con una llamada a getObject o getDate, recuperamos el valor con una llamada a getString y formateamos el string como un valor Date usando la clase SimpleDateFormat mostrada arriba.
_______
1 Cuando se usan en toda esta site, los términos, "Java virtual machine" o "JVM" significa una máquina virtual de la plataforma Java.