lunes, 15 de octubre de 2012

Transacciones con Java y JDBC

Hay veces que uno no quiere que una sentencia sql tome efecto a menos que otras se completen. A este le llamamos transacciones. Las transacciones son un conjunto de órdenes que representan la unidad de trabajo mínima e indivisible de un gestor de base de datos. Para esto, los gestores de base de datos proveen los mecanismos necesarios para constituir una transacción, pero cuando queremos o debemos manejar este tipo acciones a nivel de aplicación Java nos provee de las herramientas para hacerlo.

El ejemplo mostrado ha sido tomado de la página oficial de tutoriales de Oracle y se han añadido algunas clases para poder hacerlo más comprensible y propenso al error para de ese modo poder apreciar a plenitud el manejo de transacciones.
El caso es el siguiente: supongamos que necesitamos que el administrador de una cafetería actualize los precios de las ventas por semana. Esto implicará que se tenga que actualizar la venta total al mismo tiempo que la venta por semana pues de no darse el caso los datos quedarían inconsistentes. Este es un típico caso de transacciones en donde dos consultas sql distintas convergen en un solo punto y la depencia hace que se vean como una sola cumpliéndose de este modo el principio de atomicidad.

Para solucionar este caso nuestro método encargado de la actualización deberá implementar transacciones y frente a cualquier error deberá dejar la base de datos en un estado consistente. El código mostrado es el encargado de esta tarea:
package com.blogspot.rolandopalermo.dao;

import com.blogspot.rolandopalermo.bean.Coffees;
import com.blogspot.rolandopalermo.util.Conexion;
import com.blogspot.rolandopalermo.util.Constantes;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
 * @author Rolando
 */
public class CoffeesDAO {

    public List<Coffees> obtenerTodos() {
        List<Coffees> coffees = new ArrayList<Coffees>();
        String sql = "SELECT * FROM " + Constantes.dbName + ".COFFEES";
        PreparedStatement getaAll = null;
        Connection con = Conexion.obtenerConexion();
        try {
            getaAll = con.prepareStatement(sql); // create a statement
            ResultSet rs = getaAll.executeQuery();
            while (rs.next()) {
                Long id = rs.getLong(1);
                String name = rs.getString(2);
                Double sales = rs.getDouble(3);
                Double total = rs.getDouble(4);
                Coffees cf = new Coffees();
                cf.setId(id);
                cf.setCof_name(name);
                cf.setSales(sales);
                cf.setTotal(total);
                coffees.add(cf);
            }
        } catch (SQLException e) {
            e.printStackTrace();
            if (con != null) {
                try {
                    con.close();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        }
        return coffees;
    }

    public void updateCoffeeSales(HashMap<String, String> salesForWeek)
            throws SQLException {
        PreparedStatement updateSales = null;
        PreparedStatement updateTotal = null;
        String updateString =
                "update " + Constantes.dbName + ".COFFEES "
                + "set SALES = ? where COF_NAME = ?";
        String updateStatement =
                "update " + Constantes.dbName + ".COFFEES "
                + "set TOTAL = TOTAL + ? "
                + "where COF_NAME = ?";
        Connection con = Conexion.obtenerConexion();
        try {
            con.setAutoCommit(false);
            updateSales = con.prepareStatement(updateString);
            updateTotal = con.prepareStatement(updateStatement);

            for (Map.Entry<String, String> e : salesForWeek.entrySet()) {
                Double value = Double.parseDouble(e.getValue());
                updateSales.setDouble(1, value);
                updateSales.setString(2, e.getKey());
                updateSales.executeUpdate();
                updateTotal.setDouble(1, value);
                updateTotal.setString(2, e.getKey());
                updateTotal.executeUpdate();
                con.commit();
            }
        } catch (SQLException e) {
            e.printStackTrace();
            if (con != null) {
                try {
                    System.err.print("La transacción se deshace.");
                    con.rollback();
                } catch (SQLException excep) {
                    e.printStackTrace();
                }
            }
        } finally {
            if (updateSales != null) {
                updateSales.close();
            }
            if (updateTotal != null) {
                updateTotal.close();
            }
            con.setAutoCommit(true);
        }

    }
}
Cuando registramos los datos de ventas de manera correcta, estos se actualizarán en la base de datos.



Si registramos algún dato incorrecto, entonces lanzará una excepción y regresará todos los datos al último estado que mantuvo consistencia.


Y si volvemos a cargar los datos en la tabla obtendremos esto:

Y eso no es todo, pues si vamos a la base de datos podemos apreciar que los datos relacionados a una cafetería permanecen consistentes. Si en una fila ingresamos una cantidad por semana que no representa un número, el valor total en base de datos no se verá afectada y el valor  de ventas por semana regresará a su estado anterior. De esa manera nos aseguramos que el valor total para una cafetería se actualize solo cuando se ingresa un valor correcto para las ventas por semana. Para mayor detalle adjunto el código fuente del proyecto y les dejo el script de la base de datos.
-- MySQL Administrator dump 1.4
--
-- ------------------------------------------------------
-- Server version 5.5.16


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;


--
-- Create schema coffees
--

CREATE DATABASE IF NOT EXISTS coffees;
USE coffees;

--
-- Definition of table `coffees`
--

DROP TABLE IF EXISTS `coffees`;
CREATE TABLE `coffees` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `cof_name` varchar(45) NOT NULL,
  `sales` double NOT NULL DEFAULT '0',
  `total` double NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

--
-- Dumping data for table `coffees`
--

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
Todos los archivos del proyecto los pueden descargar aquí:

 
Espero que les sea de utilidad y no se olviden de seguir este blog a través de su página de facebook.

6 comentarios:

  1. Una pregunta como hago para que en "VENTAS POR SEMANA" pueda ingresar un dato tipo varchar porque cuando ingreso en la base de datos un dato varchar no me deja hacerlo y por ende no puedo probar lo del error

    ResponderEliminar
  2. GRACIAS YA LO DESCUBRI SOLO ERA DE SELECCIONA EL ELEMENTO DE LA TABLA

    ResponderEliminar
  3. Muy buen aporte, pero al momento de correr me abre el frame pero nose como llenar los datos, ayuda porfa

    ResponderEliminar
  4. Hola, si tengo un .txt con unos datos, necesito un programa que lo lea u me extraiga algunos datos del txt

    ResponderEliminar
  5. Francisco Cifuentes19 de mayo de 2016, 08:54

    Un detalle del código es que el commit que realizas en la línea 79 lo deberías hacer fuera del for para que sea realmente transaccional

    ResponderEliminar