A continuación puedes ver un applet Swing que:
Las primeras opciones de driver y host hacen referencia a recursos de un servidor local. Las segundas opciones se refieren a un servidor remoto. En su caso se encuentra ante un applet remoto, por tanto debe escoger el driver del servidor: org.gjt.mm.mysql.Driver; además debería escoger el host remoto, es decir, jdbc:mysql://proactiva-calidad.com:3306. Con ello hacemos referencia al servidor de la base de datos (no al servidor web), indicando el puerto (3306) asignado al gestor (mysql) de base de datos. Escriba como password la palabra 'nebrija' (sin comillas y en minúsculas).
Si cambia la tabla y pone 'cliente', visualizará los clientes de las operaciones de venta.
Si el applet es local no tiene problemas en acceder a la base de datos local. Por lo mismo, si el applet está en el servidor no tendrá problemas en acceder a la base de datos remota (del servidor). El problema está en las situaciones mixtas:
¿Por que estas situaciones son problematicas a priori? El applet esta limitado por severas restricciones de seguridad. No puede acceder a resursos ajenos a su espacio de trabajo. Vea el capitulo dedicado a la seguridad en la sección de applets, donde se expone una solución a estos problemas.
Se aplica el patrón modelo-vista:
Código fuente:
Para más información sobre las tablas y sus modelos de datos: Tutorial de SUN sobre JTable
Vea que la clase modelo hereda de AbstractTableModel, más abajo explicaremos las razones. Lo que importa para empezar es ver que los atributos son en su mayoría clases JDBC que se instancian durante la conexión a la base de datos y la carga de datos:
class modelo extends AbstractTableModel {
private Statement stat;
private ResultSet rs; // Conjunto de datos de sentencia SELECT
private ResultSetMetaData rsmd; // Necesario para nombres de columnas
private Connection con; // Conexión con base de datos
private String mensaje_error = null;// Registro de mensajes de error
private String sentencia; // Ultima sentencia ejecutada
/*********************************************************************
Conecta a la base de datos y carga los datos. Devuelve true si ha ido bien.
**********************************************************************/
public boolean conexion( String driver, String host_basedatos, String tabla, String login,
String pwd ) {
//// Si no puedo cargar driver de base de datos, muestro error y salgo
if ( !cargarDriver( driver ) ){
System.out.println( obt_mensaje_error() );
return false;
}
//// Si no puedo conectar, muestro error y salgo
if ( !conectar( host_basedatos, login, pwd ) ) {
System.out.println( obt_mensaje_error() );
return false;
}
//// Si puedo conectar, ...
else {
//// Si no puedo cargar datos, muestro error y salgo
if ( !cargarDatos( tabla ) ) {
System.out.println( obt_mensaje_error() );
return false;
}
}
return true;
}
/*************************************************************************
* Carga el driver. Devuelve:
* - true: todo ha ido bien
* - false: hay un error. El mensaje de error se registra en atributo mensaje_error
*************************************************************************/
private boolean cargarDriver( String driver) {
try {
Class.forName( driver );
return true;
}
catch( ClassNotFoundException e ) {
mensaje_error = new String( "No encuentra el driver. " + e.getMessage() );
return false;
}
}
....
Lo primero que debe hacer la clase modelo es cargar el driver del gestor de base de datos y a continuación conectarse a la base de datos para poder realizar la consulta. Esto último implica la ejecución de la sentencia SELECT. Estas funciones no tienen nada especial, ya hemos visto en otros capítulos como realizar la conexión y ejecución. Un ejemplo de código para la conexión:
private boolean conectar( String base, String login, String pwd ) {
try {
con = DriverManager.getConnection( base, login, pwd );
return true;
}
catch (SQLException e) {
mensaje_error = new String( "Error de conexión. " + e.getMessage() );
return false;
}
}
A la hora de la consulta el trabajo es sencillo, aunque en nuestro ejemplo hay algunas particularidades:
/**********************************************
Ejecuta la sentencia SELECT, con lo que carga el ResultSet y el ResultSetMetaData.
Devuelve:
- true: todo ha ido bien
- false: hay un error. El mensaje de error se registra en atributo mensaje_error
************************************************/
private boolean cargarDatos( String tabla ) {
try {
stat = con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE );
boolean resultado = actualizar( tabla );
if ( resultado )
rsmd = rs.getMetaData(); // Obtenemos un ResultSetMetaData
return resultado;
}
catch (SQLException e) {
mensaje_error = new String( e.getMessage() );
return false;
}
}
/**************** actualizar() ************************/
public boolean actualizar( String tabla ) {
try {
sentencia = "SELECT * FROM " + tabla;
if (rs != null)
rs.close();
if ( stat != null ) {
rs = stat.executeQuery(sentencia); // Ejecutar la consulta
fireTableStructureChanged(); // Ordena a la JTable que se actualice
return true;
}
mensaje_error = new String( "No se pueden cargar los datos. Probablemente no hay conexión" );
return false;
}
catch (SQLException e) {
mensaje_error = new String( "No se pueden cargar los datos. " + e.getMessage() );
return false;
}
}
Observe que al crear el objeto de la clase Statement con createStatement() le indicamos a JDBC que queremos un ResultSet que al menos admita desplazamiento, aunque sea insensible a cambios de otros usuarios. Los ResulSet pueden tener o no desplazamento y pueden ser sensibles o no a los cambios en la base de datos que pueden producir otros usuarios o procesos. Veamos los tipos:
| TYPE_FORWARD_ONLY | Sólo se puede recorrer hacia adelante y no es sensible a cambios en base de datos. |
| TYPE_SCROLL_INSENSITIVE | Tiene desplazamiento (adelante/atrás), pero es insensible a cambios en base de datos. |
| TYPE_SCROLL_SENSITIVE | Tiene desplazamiento (adelante/atrás) y es sensible a cambios en base de datos. |
Tipos de ResulSet en función de si pueden hacer cambios en la base de datos:
| CONCUR_READ_ONLY | El ResultSet no puede modificar la base de datos. |
| CONCUR_UPDATABLE | El ResultSet puede modificar la base de datos. |
No lo incluimos en nuestro código, pero puede comprobar que el ResultSet tiene las capacidades deseadas. Con la siguiente función comprobamos que el ResultSet tenga desplazamiento:
/**********************************************************
Si la base de datos admite scroll, devuelve true
Si no: registra el error en atributo mensaje_error y devuelve false
***********************************************************/
private boolean admite_scroll() throws SQLException {
/* Obtener metadatos de base de datos */
DatabaseMetaData dbmd = con.getMetaData();
/***** Si la base de datos no admite ResultSet con Scroll, lo indicamos y salimos ****/
if ( dbmd.supportsResultSetType( ResultSet.TYPE_FORWARD_ONLY ) ) {
mensaje_error = new String( "Esta base de datos no admite cursor con desplazamiento (scroll)");
return false;
}
return true;
}
Una vez que hemos solventado el problema de la conexión a base de datos y de la ejecución de la sentencia SELECT, veamos cómo programar la tabla (JTable).Varios controles Swing (entre ellos JTable) están diseñados para que el programador diferencie dos componentes:
Se puede trabajar con una JTable sin un modelo, al principio parece que incluso puede ahorrarse líneas de código, pero es una falsa impresión, ya que a a la larga la reusabilidad, eficiencia y legibilidad de los JTable mejora si usa un modelo abstracto. Los modelos de datos de JTable heredan de AbstractTableModel. El constructor de la tabla (JTable) nos permite pasar como argumento una clase AbstractTableModel:
public JTable(AbstractTableModel dm)
Por ello nuestra clase modelo hereda de AbstractTableModel:
class modelo extends AbstractTableModel { ....
Como nuestra clase hereda de la clase abstracta AbstractTableModel debemos redefinir al menos una seríe de métodos:
Programando con JDBC tenemos una ventaja que nos ahorra bastante trabajo, el ResultSet y su ResultSetMetaData nos ofrecen funcionalidades para navegar y modificar los datos. Vea que se usa el objeto rsmd (del tipo ResultSetMetaData para determinar el número y título de las columnas:
public String getColumnName( int c ) {
try {
if ( rsmd != null )
return rsmd.getColumnName(c + 1);
return "";
}
catch(SQLException e) { e.printStackTrace(); return ""; }
}
/*************** getColumnCount() ******************/
public int getColumnCount() {
try {
if ( rsmd != null )
return rsmd.getColumnCount();
return 0;
}
catch(SQLException e) { e.printStackTrace(); return 0; }
}
/****************** getRowCount() *******************/
public int getRowCount() {
try {
if ( rs != null ) {
rs.last(); // Nos situamos en la última fila
return rs.getRow(); // Devolvemos el número de la fila
}
return 0;
}
catch(SQLException e) { e.printStackTrace(); return 0; }
}
/******************* getValueAt() *********************/
public Object getValueAt( int fila, int col ) {
try {
if ( rs != null ) {
rs.absolute( fila + 1 );
return rs.getObject( col + 1 );
}
return "";
}
catch(SQLException e) { e.printStackTrace(); return null; }
}
Observe que hay una pequeña asimetría: las filas y columnas de un JTable empiezan a contarse desde cero. Mientras que los ResultSet cuentan las filas desde uno. Por ello, por ejemplo, la fila tres de la tabla es la fila cuatro del ResultSet. Ver a este respecto la implementación de getValueAt(): la posición 'fila' de la JTable equivale a la posición 'fila+1' del ResultSet.
No lo hemos puesto en este ejemplo, pero gracias a los ResultSet podemos modificar de forma sencilla los datos. Un ejemplo de código para borrar un registro podría ser:
boolean borrar( int fila ) {
try {
rs.absolute( fila + 1 );
rs.deleteRow();
return true;
}
catch (SQLException e) { mensaje_error = new String( e.getMessage() ); return false; }
}
Si no lo hubieramos hecho por medio del cursor del ResultSet, tendriamos que haber recurrido a una sentencia "DELETE ..." como agumento de una llamada a ResultSet.executeUpdate().
La vista no tiene nada especial:
public class vista extends JApplet {
JTable tabla;
modelo mod; // modelo de datos
...
//// Crea un modelo vacio (no hay conexión a la base de datos todavía)
mod = new modelo();
tabla = new JTable( mod);
JScrollPane pnlScroll = new JScrollPane( tabla );
En nuestro código usamos un JTabbedPane, un panel con "pestañas". Le asignamos los subpaneles de forma sencilla:
pnlTab.addTab( "Conexión", pnlFormulario );
pnlTab.addTab( "Consulta", pnlConsulta );
En nuestro caso queremos además que si la conexión ha tenido éxito, se cambie de forma automática al panel de consulta:
pnlTab.setSelectedComponent( pnlConsulta );
En cuanto a la gestión de eventos de botón el planteamiento es sencillo. Vamos a recibir los eventos en nuestra clase del tipo JApplet, por tanto hacemos que esta clase implemente el interfaz adecuado:
public class vista extends JApplet implements java.awt.event.ActionListener {
Por el sólo hecho de utilizar este interfaz debemos implementar el método actionPerformed(). Para que este método sea llamado como consecuencia de los eventos debemos indicar que el listener u oyente del evento será nuestro applet (this):
btnConexion.addActionListener( this );