Profe Macón
Programación Avanzada
2.º BT · Tecnologías de la Información
PM
Tienda de Cómics · Unidad de persistencia · Material 03

De objetos a registros:
PreparedStatement y ResultSet

Ya sabemos conectarnos a MySQL. Ahora aprenderemos a insertar un Producto, consultar las filas almacenadas y construir nuevos objetos Java con sus datos, sin perder de vista las responsabilidades de Controller y Service.

CET3 · JDBC y SQLCET1 · Objetos ProductoJava 17 + MySQLPreparedStatementResultSet
1 · Tienda de Cómics

Nos conectamos… ¿y ahora cómo guardamos los productos?

En el material 02 aprendimos a solicitar una conexión a MySQL mediante DriverManager.getConnection(). Conseguimos comunicarnos con la base de datos, pero todavía no hemos guardado ningún producto.

En nuestra tienda seguimos creando objetos Java que almacenamos en un ArrayList<Producto>. Por ejemplo:

Producto producto = new Producto(101, "Batman: Año Uno", 790.0, 8);

El objeto contiene cuatro datos: código, nombre, precio y stock. En MySQL, estos mismos datos se organizarán en columnas de una tabla.

Java

Objeto Producto con cuatro atributos.

→

MySQL

Una fila de la tabla producto.

Problema: disponer de una conexión no alcanza. ¿Cómo escribimos una operación SQL y le enviamos los valores de nuestro objeto?
2 · Tienda de Cómics

Antes de escribir Java: ¿dónde se guardarán los datos?

Para los ejemplos supondremos que ya existe una base de datos llamada tiendacomics. Dentro de ella utilizaremos una tabla denominada producto.

Estructura SQL para nuestras pruebas
CREATE TABLE producto (
    codigo BIGINT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    precio DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL
);

Observa la relación entre los tipos:

Producto en JavaColumna en SQL
long codigoBIGINT
String nombreVARCHAR(100)
double precioDECIMAL(10,2)
int stockINT

Para mantenernos dentro del modelo actual de la tienda utilizaremos double para el precio. En una aplicación comercial, BigDecimal resulta más apropiado para dinero porque evita algunas imprecisiones de los números decimales binarios.

Fíjate: no se guarda el objeto Java completo en esta tabla. Se guardan los valores de sus atributos, organizados en columnas.
3 · Tienda de Cómics

La primera instrucción SQL: INSERT

Para guardar un producto utilizaremos la instrucción SQL INSERT.

Un INSERT escrito directamente en SQL
INSERT INTO producto (codigo, nombre, precio, stock)
VALUES (101, 'Batman: Año Uno', 790.00, 8);

¿Qué significa cada parte?

Ese ejemplo sirve para un producto. Sin embargo, necesitamos que la misma operación funcione para cualquier objeto que el usuario agregue.

Pregunta: ¿sería buena idea construir el SQL pegando el nombre y el precio que escribe el usuario dentro de un String?
4 · Tienda de Cómics

PreparedStatement: una sentencia con lugares reservados

Podríamos unir fragmentos de texto con los valores del producto. Pero aparecerían problemas si el nombre contiene una comilla y, además, estaríamos exponiendo el programa a riesgos de inyección SQL.

JDBC ofrece una herramienta adecuada para esta tarea: PreparedStatement.

Primero definimos el SQL con parámetros
String sql = "INSERT INTO producto (codigo, nombre, precio, stock) "
           + "VALUES (?, ?, ?, ?)";

Los signos ? indican lugares reservados. Todavía no contienen los datos de ningún producto.

Después preparamos la sentencia usando la conexión
PreparedStatement sentencia = conexion.prepareStatement(sql);

Analicemos esta instrucción:

Importante: una sentencia preparada no es el resultado de una consulta ni equivale al contenido de una tabla. Representa una instrucción SQL que podemos parametrizar y ejecutar.
5 · Tienda de Cómics

¿Cómo colocamos los valores del objeto en cada parámetro?

Para completar los signos ? utilizamos métodos que empiezan con set. Cada uno recibe la posición del parámetro y el valor correspondiente.

sentencia.setLong(1, producto.getCodigo());
sentencia.setString(2, producto.getNombre());
sentencia.setDouble(3, producto.getPrecio());
sentencia.setInt(4, producto.getStock());
PosiciónMétodoValor del objeto
1setLongcodigo
2setStringnombre
3setDoubleprecio
4setIntstock

Los parámetros JDBC se cuentan desde 1. No debemos confundirlos con las posiciones de los arreglos y listas de Java, que empiezan en 0.

Revisa el orden: si intercambiamos el precio con el stock, ¿estaríamos almacenando los valores en la columna adecuada? ¿Por qué importa la posición?
6 · Tienda de Cómics

executeUpdate(): ahora sí, ejecutamos el INSERT

Preparar la sentencia y asignar los parámetros no significa que el producto ya se haya guardado.

Tenemos que ejecutar la instrucción:

int filasAfectadas = sentencia.executeUpdate();

executeUpdate() se utiliza con instrucciones como INSERT, UPDATE y DELETE. Devuelve un número entero: la cantidad de filas afectadas por la operación.

if (filasAfectadas == 1) {
    System.out.println("Se guardó un producto");
}

El número 1 indica, en este caso, que se insertó una fila.

Distinción esencial: prepareStatement() prepara la instrucción; los métodos set...() asignan sus valores; executeUpdate() solicita ejecutarla.
7 · Tienda de Cómics

Unimos las piezas: guardar un producto

Reunamos lo aprendido. Este código es una demostración de JDBC; todavía no lo colocaremos en Controller ni en Service. Más adelante lo trasladaremos al DAO.

Método didáctico: insertar un Producto
public static void insertarProducto(Producto producto)
        throws SQLException {

    String url = "jdbc:mysql://localhost:3306/tiendacomics";
    String usuario = "usuario_prueba";
    String clave = "clave_prueba";
    String sql = "INSERT INTO producto (codigo, nombre, precio, stock) "
               + "VALUES (?, ?, ?, ?)";

    try (Connection conexion = DriverManager.getConnection(url, usuario, clave);
         PreparedStatement sentencia = conexion.prepareStatement(sql)) {

        sentencia.setLong(1, producto.getCodigo());
        sentencia.setString(2, producto.getNombre());
        sentencia.setDouble(3, producto.getPrecio());
        sentencia.setInt(4, producto.getStock());

        int filasAfectadas = sentencia.executeUpdate();
        System.out.println("Filas insertadas: " + filasAfectadas);
    }
}

Por ahora repetimos la llamada a DriverManager.getConnection(...) que aprendimos en el SCORM 02. Las credenciales son ficticias. En el próximo material veremos por qué nos conviene centralizar esa configuración, en lugar de copiarla en cada operación.

La estructura try-with-resources cierra la sentencia y la conexión al salir del bloque, incluso si ocurre una excepción. El método declara throws SQLException porque deja que esa excepción se propague a quien lo invoque.

Conexión con la unidad anterior: el código que llama a este método deberá manejar la posible SQLException o declarar que también puede propagarla.
8 · Tienda de Cómics

La operación contraria: SELECT para leer los productos

Supongamos ahora que volvemos a abrir nuestra aplicación. Necesitamos recuperar los productos almacenados.

En MySQL podemos consultar los datos con SELECT:

SELECT codigo, nombre, precio, stock
FROM producto
ORDER BY nombre;

Desde Java preparamos la sentencia como antes, pero ahora ejecutamos una consulta que devuelve filas:

String sql = "SELECT codigo, nombre, precio, stock "
           + "FROM producto ORDER BY nombre";

PreparedStatement sentencia = conexion.prepareStatement(sql);
ResultSet resultado = sentencia.executeQuery();

Observa la diferencia:

executeUpdate()

Se utiliza al modificar datos. Devuelve la cantidad de filas afectadas.

executeQuery()

Ejecuta un SELECT y devuelve un objeto ResultSet.

9 · Tienda de Cómics

ResultSet: recorrer las filas recibidas

ResultSet representa los resultados de nuestra consulta. Contiene un cursor que permite recorrer las filas disponibles.

Al principio, ese cursor se encuentra antes de la primera fila. Para avanzar utilizamos next().

while (resultado.next()) {
    String nombre = resultado.getString("nombre");
    System.out.println(nombre);
}

Cuando next() consigue ubicarse en una fila, devuelve true. Cuando ya no hay más filas, devuelve false y termina el while.

Los métodos getLong(), getString(), getDouble() y getInt() permiten recuperar valores de columnas de distintos tipos.

Recuerda: ResultSet no es un ArrayList<Producto>. Es un resultado JDBC que debemos procesar para obtener objetos de nuestro modelo.
10 · Tienda de Cómics

Convertimos cada fila en un objeto Producto

Ahora queremos que la lista de resultados pueda ser utilizada por nuestra aplicación, que trabaja con objetos Producto.

Crearemos un nuevo Producto por cada fila y lo agregaremos a una lista:

List<Producto> productos = new ArrayList<Producto>();

while (resultado.next()) {
    long codigo = resultado.getLong("codigo");
    String nombre = resultado.getString("nombre");
    double precio = resultado.getDouble("precio");
    int stock = resultado.getInt("stock");

    Producto producto = new Producto(codigo, nombre, precio, stock);
    productos.add(producto);
}

El código hace algo muy parecido a lo que ya conocemos: construye objetos e incorpora cada uno a un ArrayList. La diferencia es el origen de los datos: ahora provienen de una consulta SQL.

Al guardar

Objeto Java → atributos → parámetros → INSERT → fila de MySQL.

↔

Al recuperar

SELECT → ResultSet → atributos → nuevo objeto Java.

Pregunta importante: ¿es exactamente el mismo objeto que teníamos en memoria antes de cerrar el programa? No: es un nuevo objeto construido con los valores persistidos.
11 · Tienda de Cómics

El ejemplo completo de consulta y cierre de recursos

Veamos cómo se relacionan todas las instrucciones. Esta es todavía una demostración previa a la creación de nuestro ProductoDAO.

Listar productos desde MySQL
public static List<Producto> listarProductos()
        throws SQLException {

    List<Producto> productos = new ArrayList<Producto>();
    String url = "jdbc:mysql://localhost:3306/tiendacomics";
    String usuario = "usuario_prueba";
    String clave = "clave_prueba";
    String sql = "SELECT codigo, nombre, precio, stock FROM producto";

    try (Connection conexion = DriverManager.getConnection(url, usuario, clave);
         PreparedStatement sentencia = conexion.prepareStatement(sql);
         ResultSet resultado = sentencia.executeQuery()) {

        while (resultado.next()) {
            Producto producto = new Producto(
                resultado.getLong("codigo"),
                resultado.getString("nombre"),
                resultado.getDouble("precio"),
                resultado.getInt("stock")
            );
            productos.add(producto);
        }
    }
    return productos;
}

try-with-resources declara tres recursos: Connection, PreparedStatement y ResultSet. Java los cerrará en orden inverso al de su declaración.

Observa también que devolvemos la lista después del bloque de recursos. Ya no necesitamos mantener una conexión abierta para utilizar los objetos que construimos.

Importante: el código respeta nuestra preferencia de trabajar con un único return al final del método.
12 · Tienda de Cómics

¿Dónde deberíamos colocar todo este código?

Tenemos dos operaciones: insertar productos y recuperar una lista. Ambas necesitan SQL, conexión, sentencias y gestión de resultados.

¿Debería ProductoController encargarse de todas estas instrucciones? ¿Y ProductoService, cuya responsabilidad es comprobar las reglas de negocio?

Producto_Vista
Interfaz: botones y tabla
↓
ProductoController
Coordina eventos
↓
ProductoService
Aplica reglas de negocio
↓
¿Nuevo componente?
Se encargaría del acceso a datos
↓
MySQL

En la unidad anterior aprendimos a reconocer cuando una clase acumula responsabilidades. Aquí está ocurriendo algo semejante: necesitamos un componente especializado para las operaciones de persistencia.

Ese componente será un DAO, pero lo construiremos paso a paso en el próximo material.

13 · Tienda de Cómics

Comprobamos lo aprendido

Selecciona una respuesta y revisa la devolución.

1. ¿Para qué utilizamos PreparedStatement?

2. ¿En qué posición se encuentra el primer signo ? de la sentencia preparada?

3. ¿Cuál de estos métodos devuelve un ResultSet?

4. ¿Qué ocurre cuando resultado.next() devuelve false?

5. Después de cerrar y abrir el programa, ¿qué hacemos con los registros recuperados?

14 · Tienda de Cómics

Documentamos y dejamos preparada la siguiente clase

Microentrega de documentación

  1. Explica la diferencia entre prepareStatement(), executeUpdate() y executeQuery().
  2. Indica a qué atributo de Producto corresponde cada signo ? de nuestro INSERT.
  3. Describe qué sucede durante una iteración de while(resultado.next()).
  4. Realiza un pequeño diagrama: Producto → INSERT → tabla → SELECT → nuevo Producto.
  5. Justifica por qué el Controller no debería contener todo este código JDBC.
Tres niveles de trabajo: mínimo: reconocer SQL y métodos JDBC; estándar: explicar insertar y listar productos; desafío: modificar la consulta para ordenar por precio e investigar qué cambiaría si el nombre del producto contuviera un apóstrofo.

Próximo material: construiremos ProductoDAO y una clase de conexión reutilizable, integrando el acceso a datos con ProductoService sin mezclar las responsabilidades.

Material en curso
← Índice del curso