Tutorial JDBC con aplicaciones de ejemplo

En esta ocasión voy a poner tres ejemplos de como usar JDBC que podrían ser parte de una misma aplicación pero lo he dividido en tres para no perderse. Pero antes vamos a ver como funciona JDBC.

JDBC (Java Database Connectivity) es un API de Java que nos permite conectarnos con bases de datos y realizar operaciones sobre ellas utilizando instrucciones SQL desde una aplicación Java. Con JDBC tenemos una interfaz para conectarnos con una base de datos sin tener que preocuparnos de si es una base de datos MySQl, Oracle, SQLServer o cualquier otro tipo de base de datos. El único cambio que habría que hacer para cambiar el tipo de base de datos de una aplicación sería cambiar el driver especifico de la base de datos en cuestión.

Para usar JDBC hay que seguir los siguientes pasos:

1. Incluir el jar con el Driver de la base de datos

El primer paso es obtener el driver de la base de datos que vamos a utilizar, buscamos en google «MySQL jdbc driver», «Oracle jdbc driver» o el que queramos y descargamos el jar y lo incluimos es nuestro proyecto. Para este ejemplo voy a usar MySQL, puedes descargarlo desde aquí, lo que queremos es el .jar por lo que tienes que elegir Plataform Independent y descargar el zip o el tar.gz, y aunque vienen unos cuantos archivos y carpetas el único que nos interesa es mysql-connector-java-5.1.26-bin.jar que lo tenemos que añadir a nuestro proyecto.

2. Cargar el driver

Ya tenemos el jar con el driver, pero hay que cargarlo para que se pueda hacer uso de el en nuestra aplicación. En nuestro caso como vamos a usar MySQL la instrucción es la siguiente y como puede lanzar una excepción pues se mete dentro de un try-catch.


try {
    Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException ex) {
    log.error("No se encontro el Driver MySQL para JDBC.");
}

3.Establecer una conexión con la base de datos

El siguiente paso es establecer una conexión con la base de datos. Hay dos opciones para obtener una conexión la primera es crear una nueva conexión con la base de datos que será la que veremos en este ejemplo y la otra es obtener la conexión de un pool de conexiones creado previamente que es una opción más eficiente porque no hace falta estar abriendo y cerrando conexiones continuamente ya que simplemente se cogen las conexiones de pool cuando se necesitan y se dejan en el pool cuando ya no se necesitan para que puedan volver a usarse. En un articulo previo, Como crear un pool de conexiones en Tomcat, puedes ver un ejemplo de como usar un pool de conexiones con dicho servidor pero para un ejemplo de una aplicación de escritorio como la que vamos a ver supuestamente vamos a tener pocas llamadas a base de datos por lo que no se va a notar mucha diferencia.


//Connection cn = DriverManager.getConnection("jdbc:mysql://servidor_bd:puerto/nombre_bd", "usuario", "contraseña");
Connection cn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cuentas", "root", "");

Para obtener una conexión tenemos que hacer uso del método getConnection de DriverManager y le tenemos que pasar como parámetros, la base de datos, el usuario y la contraseña. Si en lugar de usar mysql se usa otra base de datos el formato de la url de la base de datos cambia, por ejemplo para oracle el formato es jdbc:oracle:<tipo_driver>:@<<base_datos>.

4. Realizar una consulta

Una vez obtenida la conexión ya podemos hacer consultas a la base de datos. Hay tres métodos para ejecutar una consulta de tipo Statement que es una consulta normal, PreparedStatement con la que se puede crear una consulta que se precompila en la base de datos y como es de esperar se pueden establecer los distintos datos porque aunque la estructura de la consulta será la misma lo lógico es que los datos de la consulta sean distintos y finalmente están las CallableStatements que sirven para ejecutar procedimientos almacenados.

Vamos a ver como hacer una consulta de tipo Statement:


// Creamos el Statement para poder hacer consultas
Statement st = cn.createStatement();

// La consulta es un String con código SQL
String sql1 = "SELECT * FROM cuentas";

// Ejecuta una consulta que devuelve resultados                
ResultSet rs = st.executeQuery(sql1);   
while (rs.next()) {
    System.out.println (rs.getString ("propietario") + " " + rs.getString (2)+ " " + rs.getInt(saldo));
}             

String sql2 = "UPDATE cuentas SET saldo = saldo - "
                    + cantidad + " WHERE codigo = '" 
                    + codigo + "' AND saldo >= "
                    + cantidad;

// Ejecuta una consulta de tipo insert, update o delete
st.executeUpdate(sql2);                

Como se puede ver el primer paso es crear un statement sobre el que luego podemos hacer las consultas que queramos, una vez creado para hacer nuestra consulta tenemos los métodos execute (para hacer cualquier consulta, pero su valor de devolución es boolean y devuelve true si lo que devuelve es un resulset y falso en caso contrario), exeteQuery (para hacer select) y executeUpdate (para hacer insert, update y delate). Y finalmente si se ejecuta una query podemos obtener el resultado mediante ResultSet, para obtener los valores de las columnas resulset tiene un get para cada tipo de datos que se puede llamar pasándole como parámetro el nombre de la columna de base de datos o el numero de la columna según se prefiera.

Como se puede ver el código para hacer una consulta no es muy complicado pero es muy propenso a errores porque hay que estar creando cadenas de texto concatenadas con variables java y hay que recordar que si el dato es de tipo texto hay que anidar comillas y entre unas cosas y otras es fácil cometer algún error. Para facilitar el trabajo con bases de datos hay alternativas a trabajar directamente con JDBC como JPA o Hibernate, pero estamos hablando de JDBC…

Si usamos PreparedStatement además de que son más eficientes nos facilitan la tarea de escribir las consultas SQL porque en lugar de tener que estar concatenando las distintas variables con el código SQL, lo que se hace es sustituir en este las variables por ? y posteriormente se introducen los datos concretos mediante setters. En el siguiente código puedes ver como se crean unas PreparedStatement.


PreparedStatement pstBuscarCodigo;
PreparedStatement pstInsertarCuenta;

String sqlBusqueda = "SELECT codigo FROM cuentas WHERE codigo=?";
pstBuscarCodigo = cn.prepareStatement(sqlBusqueda);
pstBuscarCodigo.setString(1, codigo);
ResultSet rs = pstBuscarCodigo.executeQuery();
if (!rs.next){...}

String sqlNuevaCuenta = "INSERT INTO cuentas VALUES (?,?,?,?)";
pstInsertarCuenta = cn.prepareStatement(sqlNuevaCuenta); 
pstInsertarCuenta.setString(1, codigo);
pstInsertarCuenta.setString(2, nombre);
pstInsertarCuenta.setString(3, email);
pstInsertarCuenta.setDouble(4, saldo);
pstInsertarCuenta.executeUpdate();    

5. Cerrar la conexión

Después de hacer las consultas que se necesite se debe de cerrar la conexión para liberar los recursos, también se pueden cerrar el resulset y el statement de forma manual pero cerrando la conexión se cierran los otros dos porque están creados a partir de la conexión, del mismo modo al cerrar el statement también se liberan los recursos del resulset.


rs.close(); // Cierra el resulset
st.close(); // Cierra el statement
cn.close(); // Cierra la conexión

6. Ejemplos

Una vez visto más o menos como funciona vamos a ver unos ejemplo de como usar JDBC.

La base de datos para los ejemplos será la siguiente:

esquema ER base datos

En este primer ejemplo se crea la conexión y los prepareStatements en el constructor por lo que luego podemos usarlo sin tener que crear una nueva conexión para insertar un nuevo dato, pero no cerramos nunca la conexión porque si la cerrásemos como se esta creando en el constructor ya no se podría volver a usar.

Este primer ejemplo consiste en una aplicación que permite almacenar una nueva cuenta en la base de datos siempre y cuando el código de la cuenta no este ya en la base de datos. En este ejemplo se usan PreparedStatements.

Una de las cosas que podemos ver en el ejemplo y que siempre se deben de hacer es declarar la conexión, los statements y los resultsets fuera de los bloques try catch, porque puede pasar que como en este ejemplo la conexión se cree en un sitio y luego se use en otro (lo normal sería tener una clase especifica para crear la conexión con la base de datos) y aunque no fuese así lo normal es cerrar la conexión en un bloque finally porque aunque en este no cerramos la conexión lo normal y recomendable es hacerlo, sin ir más lejos en los siguientes ejemplos si se cierra la conexión en el finally porque la conexión hay que cerrarla tanto si la ejecución a sido correcta como si se produce alguna excepción.


package ejemplo1;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.*;
import javax.swing.JLabel;
import org.apache.log4j.Logger;

/**
 *
 * @author Ivan Salas Corrales <http://programandoointentandolo.com/>
 */

public class CreaCuenta implements ActionListener {

    private Logger log = Logger.getLogger(CreaCuenta.class);
    private Ventana v;
    private Connection cn;
    private PreparedStatement pstBuscarCodigo;
    private PreparedStatement pstInsertarCuenta;

    public CreaCuenta(Ventana v) {
        this.v = v;
        try {
            // Driver para conectar con MySQL
            Class.forName("com.mysql.jdbc.Driver");
            // Conexion con la base de datos
            cn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cuentas", "root", "");


            String sqlBusqueda = "SELECT codigo FROM cuentas WHERE codigo=?";
            pstBuscarCodigo = cn.prepareStatement(sqlBusqueda);

            String sqlNuevaCuenta = "INSERT INTO cuentas VALUES (?,?,?,?)";
            pstInsertarCuenta = cn.prepareStatement(sqlNuevaCuenta);

        } catch (SQLException ex) {
            log.error("Error " + ex.getErrorCode() + ": " + ex.getMessage());
        } catch (ClassNotFoundException ex) {
            log.error("No se encontro el Driver MySQL para JDBC.");
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        String codigo = v.getTextCodigo().getText();
        String nombre = v.getTextNombre().getText();
        String email = v.getTextEmail().getText();
        double saldo = (double) v.getTextSaldo().getValue();
        JLabel lbMensaje = v.getLbMensaje();

        try {
            pstBuscarCodigo.setString(1, codigo);
            // Si el codigo no esta en la bd se añade la cuenta
            ResultSet rs = pstBuscarCodigo.executeQuery();
            if (!rs.next()) {
                pstInsertarCuenta.setString(1, codigo);
                pstInsertarCuenta.setString(2, nombre);
                pstInsertarCuenta.setString(3, email);
                pstInsertarCuenta.setDouble(4, saldo);
                pstInsertarCuenta.executeUpdate();
                lbMensaje.setText("Cuenta agregada correctamente");

            } else {
                lbMensaje.setText("El codigo indicado ya esta en la base de datos");
            }
        } catch (SQLException ex) {
            log.error("Error " + ex.getErrorCode() + ": " + ex.getMessage());
        }
    }
}

Como supongo que te habrás dado cuenta el código anterior es el de una clase que implementa ActionListener por lo que como te podrás imaginar que es parte de una aplicación Swing, pero como eso no aporta nada al entendimiento de JDBC no lo pongo aunque al final puedes descargarte el proyecto con los ejemplos y ver y probar los ejemplos concretos.

La aplicación del segundo ejemplo sirve para hacer transferencias entre las cuentas. Con este ejemplo vamos a aprovechar para ver como crear transacciones y deshacerlas si falla alguna de las operaciones. En este ejemplo se usan statement para ver lo tedioso que puede ser.

Cuando usamos JDBC por defecto cuando hacemos un executeXXX ya sea con una statement o una preparedstatement la operación se realiza inmediatamente pero hay ocasiones en las que este no será el comportamiento deseado como es el caso de este ejemplo, porque la operación que global que queremos hacer es sacar el dinero de una cuenta y meterlo en otra, pero si usamos el funcionamiento estándar si se extrae el dinero de la primera cuenta y luego hay un error al insertar el dinero en la segunda cuenta el dinero habrá desaparecido y esto no es un comportamiento aceptable por lo que lo lógico sería que si falla la segunda operación se deshaga la primera y todo quede como antes de empezar la operación.


package ejemplo2;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Date;
import org.apache.log4j.Logger;

/**
 *
 * @author Ivan Salas Corrales <http://programandoointentandolo.com/>
 */

public class Transferir implements ActionListener {

    private Logger log = Logger.getLogger(Transferir.class);
    private Transferencias t;

    public Transferir(Transferencias t) {
        this.t = t;
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        Connection cn = null;

        try {
            // Driver para conectar con MySQL
            Class.forName("com.mysql.jdbc.Driver");

            cn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cuentas", "root", "");

            // Obtenemos la fecha actual
            Date date = new Date();
            Date fecha = new java.sql.Date(date.getTime());

            String sqlCuenta1 = "UPDATE cuentas SET saldo = saldo - "
                    + t.getTfCantidad().getValue() + " WHERE codigo = '" + t.getTfCuenta1().getText()
                    + "' AND saldo >= " + t.getTfCantidad().getValue();

            String sqlMovimiento1 = "INSERT INTO movimientos SET codigo = '"
                    + t.getTfCuenta1().getText() + "', tipo = '1', cantidad= 0-"
                    + t.getTfCantidad().getValue() + ", fecha= '" + fecha + "'";

            String sqlCuenta2 = "UPDATE cuentas SET saldo = saldo + "
                    + t.getTfCantidad().getValue() + " WHERE codigo = '" + t.getTfCuenta2().getText() + "'";

            String sqlMovimiento2 = "INSERT INTO movimientos SET codigo = '"
                    + t.getTfCuenta2().getText() + "', tipo = '1', cantidad= "
                    + t.getTfCantidad().getValue() + ", fecha= '" + fecha + "'";

            Statement st = cn.createStatement();

            // Indica que las operaciones no se validan automaticamente
            cn.setAutoCommit(false);

            // Si se hacen ambas actualizaciones se hace la transaccion y sino se deshace
            if ((st.executeUpdate(sqlCuenta1) != 0) && (st.executeUpdate(sqlCuenta2) != 0)
                  &&(st.executeUpdate(sqlMovimiento1) != 0) && (st.executeUpdate(sqlMovimiento2) != 0)) {
               
                // Ejecuta la transaccion
                cn.commit();
                t.setLbResultado("Transeferencia completada.");
            } else {
                // Deshace los cambios hechos dentro de la transaccion 
                cn.rollback();
                t.setLbResultado("Error: La transeferencia no se ha completado.");
            }

        } catch (SQLException ex) {
            log.error("Error " + ex.getErrorCode() + ": " + ex.getMessage());
            try {
                // Deshace los cambios hechos dentro de la transaccion 
                cn.rollback();
                t.setLbResultado("Error: La transeferencia no se ha completado.");
            } catch (SQLException ex1) {
                log.error("Error " + ex1.getErrorCode() + ": " + ex1.getMessage());
            }
        } catch (ClassNotFoundException ex) {
            log.error("No se encontro el Driver MySQL para JDBC.");
        } finally {
            try {
                cn.close();
            } catch (SQLException ex) {
                log.error("Error " + ex.getErrorCode() + ": " + ex.getMessage());
            }
        }
    }
}

Para poder controlar cuando se hacen las transacciones para poder englobar las operaciones que nosotros queramos dentro de la misma transacción lo primero es indicar que no queremos que las operaciones se hagan automáticamente (cn.setAutoCommit(false)) y luego se ejecutan las operaciones como se hace normalmente y cuando se han hecho todas se hace el commit o si ha fallado algo se hace un rollback para deshacer las operaciones. Lo normal es comprobar que se han hecho correctamente las operaciones y entonces hacer el commit o el rollback si han fallado, pero también se debería de hacer un rollback en el catch porque si se produce alguna excepción será que algo a fallado por lo que habrá que deshacerlo por si acaso queda la base de datos en algún estado inconsistente (Que no se realice correctamente un executeXXX no quiere decir que se tenga que lanzar una excepción porque por ejemplo si se intenta transferir más saldo del disponible no se produce ninguna excepción pero el update correspondiente no produce ningún cambio en la base de datos por lo que hay que controlarlo).

Y finalmente en ultimo ejemplo vamos a hacer una sencilla aplicación que muestra la lista de operaciones realizadas sobre una cuenta y para ver algo nuevo los datos de la consulta se obtienen de dos tablas distintas usando un join, aunque esto es cosa del sql y para la parte java no hay nada distinto y vamos coger los datos del resulset obtenidos de la consulta para mostrarlos en una tabla.


package ejemplo3;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.*;
import javax.swing.JTextField;
import javax.swing.table.DefaultTableModel;
import org.apache.log4j.Logger;

/**
 *
 * @author Ivan Salas Corrales <http://programandoointentandolo.com/>
 */

public class MuestraMovimientos implements ActionListener {

    private Logger log = Logger.getLogger(MuestraMovimientos.class);
    private DefaultTableModel dtm;
    private Connection cn;
    private JTextField tfCuenta;

    public MuestraMovimientos(DefaultTableModel dtm, JTextField tfCuenta) {
        this.dtm = dtm;
        this.tfCuenta = tfCuenta;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        String tipo;
        String cantidad;
        String fecha;

        // Se borra el contenido de la tabla
        int i = 0;
        while (dtm.getRowCount() > 0) {
            dtm.removeRow(i);
        }

        try {
            // Driver para conectar con MySQL
            Class.forName("com.mysql.jdbc.Driver");

            cn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cuentas", "root", "");

            String sqlBusqueda = "SELECT tm.tipo,  m.cantidad,  m.fecha FROM movimientos AS m INNER JOIN tipos_movimiento AS tm ON tm.id=m.tipo WHERE m.codigo=?";

            PreparedStatement psBusqueda = cn.prepareStatement(sqlBusqueda);

            psBusqueda.setString(1, tfCuenta.getText());
            ResultSet listaMovimientos = psBusqueda.executeQuery();

            // Se ponene en la tabla los valores obtenidos de la consulta    
            while (listaMovimientos.next()) {
                tipo = listaMovimientos.getString("tipo");
                cantidad = listaMovimientos.getString("cantidad");
                fecha = listaMovimientos.getString("fecha");
                dtm.addRow(new String[]{tipo, cantidad, fecha});
            }

            // Cierro el ResultSet y el Statement aunque al cerrar el Statement ya se cierra el ResulSet
            listaMovimientos.close();
            psBusqueda.close();


        } catch (SQLException ex) {
            log.error("Error " + ex.getErrorCode() + ": " + ex.getMessage());
        } catch (ClassNotFoundException ex) {
            log.error("No se encontro el Driver MySQL para JDBC.");
        } finally {
            try {
                // Cierra la conexion con la base de datos
                cn.close();
            } catch (SQLException ex) {
                log.error("Error " + ex.getErrorCode() + ": " + ex.getMessage());
            }
        }
    }
}

Y esto es todo, si quieres puedes descargarte el código de los ejemplos y la base de datos de ejemplo aquí.

No Responses

Leave a Reply