Clase PersonaDAO actualizada - eliizquierdo/material_programacion_avanzada2026 GitHub Wiki

📦 Proyecto: mini-crud-sql (Tema 3 — JDBC + MySQL) — ⬇️ Descargar ZIP · ☁️ Abrir en Codespace

📊 Clase PersonaDAO: Gestión de Datos en la Base de Datos

Esta clase es el corazón de la persistencia de datos en nuestro proyecto MVC. PersonaDAO centraliza todas las operaciones CRUD (Create, Read, Update, Delete) para la entidad Persona, comunicándose directamente con la base de datos MySQL.

✅ ¿Dónde encaja en el patrón MVC?

  • Modelo (M): PersonaDAO pertenece al Modelo, específicamente a la capa de acceso a datos
  • No interactúa directamente con la Vista: El DAO solo se comunica con el Controlador
  • Independiente de la interfaz: Si cambiamos de web a escritorio, esta clase seguiría funcionando igual El flujo típico es:
Vista (JSP) → Controlador (Servlet) → DAO → Base de Datos

🔁 ¿Qué cambia respecto al PersonaDAO en memoria?

En mini-crud2 el PersonaDAO guardaba las personas en un ArrayList. Ahora las guarda en MySQL. Lo importante es que los métodos se siguen llamando igual, reciben los mismos parámetros y devuelven lo mismo:

Método Versión en memoria (mini-crud2) Versión con BD (mini-crud-sql)
ArrayList<PersonaVO> getLista() devuelve el ArrayList hace un SELECT y arma el ArrayList
String agregar(PersonaVO p) lista.add(p) INSERT
String actualizar(PersonaVO p) lista.set(i, p) UPDATE
String eliminar(int codigo) lista.remove(p) DELETE
PersonaVO obtenerXCodigo(int cod) recorre la lista SELECT ... WHERE codigo = ?

Solo cambia el contenido de cada método. Por eso el PersonaControladorServlet y las vistas JSP quedan exactamente iguales que en mini-crud2: el controlador llama a dao.getLista(), dao.agregar(...), etc., sin saber si los datos están en memoria o en una base de datos. Esa es justamente la ventaja de separar el acceso a datos en un DAO.

💡 Los métodos auxiliares estaPersona(), devolver() y cantidad() de la versión en memoria no se usaban en el controlador, así que en esta versión no los incluimos.


🔄 Las 5 Fases del Acceso a Base de Datos

Todos los métodos del PersonaDAO siguen un patrón consistente de 5 fases. Comprender estas fases es fundamental para trabajar con JDBC:

Fase 1: Definir la sentencia SQL

Escribimos la consulta SQL en un String, usando ? como placeholders para los parámetros.

Fase 2: Establecer conexión con la BBDD

Obtenemos el objeto Connection usando Conexion.obtener(). Con try-with-resources, la conexión se cierra automáticamente al finalizar el bloque.

💡 ¿Por qué el SQL va antes que la conexión? Porque con.prepareStatement(sql) está en el encabezado del try y necesita que sql ya exista. Hasta que se prepara y se ejecuta, la sentencia es solo un String en la memoria de Java: todavía no viaja a la base de datos.

Fase 3: Crear un PreparedStatement

Preparamos la sentencia SQL y asignamos valores a los parámetros usando métodos setXxx().

Fase 4: Ejecutar la sentencia SQL

  • Para consultas (SELECT): usamos executeQuery() que retorna un ResultSet
  • Para modificaciones (INSERT/UPDATE/DELETE): usamos executeUpdate() que retorna el número de filas afectadas

Fase 5: Leer el ResultSet (solo para consultas)

Recorremos el ResultSet con rs.next() y extraemos los datos con métodos getXxx().

Nota importante: Las operaciones de modificación (INSERT, UPDATE, DELETE) no necesitan la Fase 5 porque no retornan datos, solo el número de filas afectadas.


Tabla comparativa: ¿Qué fases usa cada método?

Método Fase 1 Fase 2 Fase 3 Fase 4 Fase 5
getLista() ✓ ✓ ✓ ✓ executeQuery() ✓ while(rs.next())
agregar() ✓ ✓ ✓ ✓ executeUpdate() -
actualizar() ✓ ✓ ✓ ✓ executeUpdate() -
eliminar() ✓ ✓ ✓ ✓ executeUpdate() -
obtenerXCodigo() ✓ ✓ ✓ ✓ executeQuery() ✓ if(rs.next())

Analicemos el código completo:

package modelo.dao;
 
import modelo.conexion.Conexion;
import modelo.vo.PersonaVO;
import java.sql.*;
import java.util.ArrayList;
 
public class PersonaDAO {
    // La clase no necesita atributos
    // Cada método manejará su propia conexión
}

1) Declaración del paquete e imports necesarios

package modelo.dao;

Ubicamos la clase en modelo.dao siguiendo la estructura MVC. El paquete dao agrupa todas las clases de acceso a datos.

import modelo.conexion.Conexion;
import modelo.vo.PersonaVO;
  • Conexion: Clase que devuelve conexiones a MySQL mediante Conexion.obtener()
  • PersonaVO: Clase que representa la entidad Persona (Value Object)
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
  • java.sql.*: Incluye Connection, PreparedStatement, ResultSet, SQLException
  • ArrayList y List: Para manejar colecciones de personas cuando listamos datos

2) Estrategia de conexión: ¿Por qué cada método crea su propia conexión?

Usamos try-with-resources con Conexion.obtener() en todos los métodos:

try (Connection con = Conexion.obtener();
     PreparedStatement ps = con.prepareStatement(sql)) {
    // ... operaciones
} // Connection y PreparedStatement se cierran automáticamente al salir del bloque

Java cierra los recursos automáticamente al finalizar el bloque, haya o no excepción. No hace falta un bloque finally manual.

¿Por qué cada método crea su propia conexión?

  • ✅ Independencia: Cada operación es autónoma
  • ✅ Seguridad: No hay riesgo de usar una conexión ya cerrada
  • ✅ Concurrencia: Múltiples usuarios pueden usar el DAO simultáneamente
  • ✅ Robustez: Un error en una operación no afecta a las demás

📋 Método 1: getLista()

Este método obtiene todas las personas de la base de datos y las devuelve como una lista.

public ArrayList<PersonaVO> getLista() {
    ArrayList<PersonaVO> lista = new ArrayList<>();
 
    // FASE 1: Definir la sentencia SQL
    String sql = "SELECT * FROM personas";
 
    // FASES 2, 3 y 4: conexión, PreparedStatement y ejecución en try-with-resources
    try (Connection con = Conexion.obtener();
         PreparedStatement ps = con.prepareStatement(sql);
         ResultSet rs = ps.executeQuery()) {
 
        // FASE 5: Leer el ResultSet
        while (rs.next()) {
            PersonaVO persona = new PersonaVO(
                rs.getInt("codigo"),
                rs.getString("nombre")
            );
            lista.add(persona);
        }
    } catch (SQLException e) {
        System.err.println("Error al listar personas: " + e.getMessage());
        e.printStackTrace();
    }
    // rs, ps y con se cierran automáticamente al salir del try
    return lista;
}

Paso 1: Preparación

ArrayList<PersonaVO> lista = new ArrayList<>();

Creamos un ArrayList vacío donde iremos almacenando cada PersonaVO que recuperemos de la BD.

String sql = "SELECT * FROM personas";

Definimos la consulta SQL. SELECT * recupera todas las columnas (codigo y nombre) de todas las filas de la tabla personas.

Paso 2: Abrir conexión y ejecutar la consulta

try (Connection con = Conexion.obtener();
     PreparedStatement ps = con.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
  1. Conexion.obtener(): Obtiene una nueva conexión leyendo las credenciales desde db.properties
  2. con.prepareStatement(sql): Prepara la consulta SQL
  3. ps.executeQuery(): Ejecuta y devuelve un ResultSet con los resultados Como getLista no tiene parámetros, podemos incluir el ResultSet directamente en el encabezado del try, junto con la conexión y el statement.

¿Por qué PreparedStatement y no Statement?

  • Más seguro contra inyección SQL (aunque aquí no hay parámetros)
  • Más eficiente si ejecutamos la misma consulta varias veces
  • Consistencia en el código (todos los métodos usan PreparedStatement)

Paso 3: Procesar los resultados

        while (rs.next()) {
            PersonaVO persona = new PersonaVO(
                rs.getInt("codigo"),
                rs.getString("nombre")
            );
            lista.add(persona);
        }

¿Cómo funciona rs.next()?

El ResultSet es como un cursor que apunta a las filas:

  • Inicialmente está ANTES de la primera fila
  • rs.next() mueve el cursor a la siguiente fila y retorna true si existe
  • Cuando no hay más filas, retorna false y sale del while Para cada fila:
  1. rs.getInt("codigo"): Extrae el valor de la columna "codigo" como entero
  2. rs.getString("nombre"): Extrae el valor de la columna "nombre" como String
  3. new PersonaVO(...): Crea un objeto PersonaVO con esos datos
  4. lista.add(persona): Agrega ese objeto al ArrayList

¿Cómo se leen las columnas? getXxx por nombre

Dentro del while, en cada vuelta el cursor está parado en una fila. Cada getXxx("columna") lee una columna de esa fila:

  • El texto entre comillas es el nombre de la columna en la tabla de MySQL, no el nombre del atributo del VO. En PersonaVO el atributo se llama cod, pero se lee con rs.getInt("codigo") porque así se llama la columna.
  • El método se elige según el tipo de la columna: getInt para INT, getString para VARCHAR (la tabla completa está más abajo, en el método agregar()).
  • Los valores leídos se pasan al constructor de PersonaVO en el mismo orden que sus parámetros: primero el código, después el nombre.

De la tabla de MySQL a la pantalla: cada columna se lee con getXxx por su nombre y el JSP la muestra con EL

💡 También se puede leer por posición: rs.getInt(1) lee la primera columna del SELECT (empieza en 1, igual que los setXxx). Por nombre es más claro y no depende del orden de las columnas.

⚠️ Si el nombre de la columna está mal escrito, se lanza una SQLException del tipo Column 'xxx' not found.

Paso 4: Manejo de errores y cierre automático

} catch (SQLException e) {
    System.err.println("Error al listar personas: " + e.getMessage());
    e.printStackTrace();
}
// rs, ps y con se cierran automáticamente aquí

Si algo sale mal (BD no disponible, tabla inexistente, columna mal escrita), capturamos la SQLException.

Cierre automático con try-with-resources: Al salir del bloque try (por éxito o por excepción), Java llama automáticamente a rs.close(), ps.close() y con.close() en ese orden. No hace falta escribir un bloque finally.

Nota: El método NO lanza la excepción, la atrapa y continúa. Si falla la consulta, retornará una lista vacía.

Paso 5: Retorno

return lista;

Retornamos el ArrayList. Si hubo éxito, contendrá todas las personas. Si falló, estará vacío (pero nunca será null).


➕ Método 2: agregar(PersonaVO p)

Este método inserta una nueva persona en la base de datos.

public String agregar(PersonaVO p) {
    // FASE 1: Definir la sentencia SQL
    String sql = "INSERT INTO personas(codigo, nombre) VALUES (?, ?)";
    String mensaje = "Error al agregar persona.";

    try (Connection con = Conexion.obtener();              // FASE 2
         PreparedStatement ps = con.prepareStatement(sql)) { // FASE 3

        // FASE 3: Asignar parámetros
        ps.setInt(1, p.getCodigo());
        ps.setString(2, p.getNombre());

        // FASE 4: Ejecutar la sentencia SQL
        int filasAfectadas = ps.executeUpdate();
        if (filasAfectadas > 0) mensaje = "Persona agregada.";
        // NO HAY FASE 5: INSERT no retorna ResultSet
    } catch (SQLException e) {
        System.err.println("Error al agregar persona: " + e.getMessage());
        e.printStackTrace();
    }
    return mensaje;
}

Diferencias clave respecto a getLista()

  1. No necesitamos ResultSet: Las operaciones INSERT/UPDATE/DELETE no retornan datos
  2. Usamos parámetros (?): Los valores vendrán del objeto PersonaVO
  3. Retornamos un mensaje (String): La variable mensaje empieza con el mensaje de error y solo cambia si la inserción se hizo. Es el mismo esquema que usaríamos con un boolean exito = false, pero devolviendo un texto, igual que la versión en memoria

Análisis de la consulta SQL

String sql = "INSERT INTO personas(codigo, nombre) VALUES (?, ?)";
  • INSERT INTO personas: Indica que insertaremos en la tabla personas
  • (codigo, nombre): Especifica las columnas (orden importante)
  • VALUES (?, ?): Los signos de interrogación son PLACEHOLDERS para los valores reales ¿Por qué usar ? en lugar de concatenar valores?

❌ MAL (vulnerable a inyección SQL):

String sql = "INSERT INTO personas VALUES (" + codigo + ", '" + nombre + "')";

✅ BIEN (seguro):

String sql = "INSERT INTO personas(codigo, nombre) VALUES (?, ?)";
ps.setInt(1, codigo);
ps.setString(2, nombre);

Asignación de valores a los parámetros

ps.setInt(1, p.getCodigo());
ps.setString(2, p.getNombre());

ps.setInt(1, p.getCodigo()):

  • 1: Posición del primer ? (las posiciones empiezan en 1, no en 0)
  • getCodigo(): Obtiene el código del objeto PersonaVO
  • setInt: Convierte el valor a formato SQL apropiado para INT ps.setString(2, p.getNombre()):
  • 2: Posición del segundo ?
  • getNombre(): Obtiene el nombre del objeto PersonaVO
  • setString: Maneja correctamente comillas, caracteres especiales, etc.

Del formulario a la tabla: cada setXxx ocupa un ? por su posición

Correspondencia de tipos: setXxx y getXxx

Para escribir (PreparedStatement) se indica la posición del ?; para leer (ResultSet) se indica el nombre de la columna. En los dos casos, el método se elige según el tipo de la columna:

Tipo en MySQL Escribir (PreparedStatement) Leer (ResultSet) Tipo en Java
INT setInt(pos, valor) getInt("columna") int
VARCHAR setString(pos, valor) getString("columna") String
DECIMAL setDouble(pos, valor) getDouble("columna") double
DATE setDate(pos, valor) getDate("columna") java.sql.Date

💡 Para importes de dinero en sistemas reales se usa setBigDecimal / getBigDecimal, que no redondea. En el curso alcanza con double.

Ejecución de la inserción

int filasAfectadas = ps.executeUpdate();
if (filasAfectadas > 0) mensaje = "Persona agregada.";

executeUpdate() vs executeQuery():

  • executeQuery(): Para SELECT (retorna ResultSet)
  • executeUpdate(): Para INSERT/UPDATE/DELETE (retorna un int con la cantidad de filas afectadas) filasAfectadas > 0:
  • Si la inserción fue exitosa, executeUpdate() retorna 1 (una fila insertada) y cambiamos el mensaje a "Persona agregada."
  • Si la inserción falla (por ejemplo, porque el código ya existe), executeUpdate() lanza una SQLException: pasamos directo al catch y mensaje conserva "Error al agregar persona."

Retorno

return mensaje;

Devolvemos el mensaje: "Persona agregada." si se insertó, o "Error al agregar persona." si no. La versión en memoria también devolvía un String.

¿Cómo usa esto el Controlador?

dao.agregar(new PersonaVO(codigo, nombre));
response.sendRedirect(request.getContextPath() + "/persona"); // PRG

Es la misma línea que en mini-crud2: el controlador no cambia.


🔄 Método 3: actualizar(PersonaVO p)

Este método modifica una persona existente en la base de datos.

public String actualizar(PersonaVO p) {
    // FASE 1: Definir la sentencia SQL
    String sql = "UPDATE personas SET nombre = ? WHERE codigo = ?";
    String mensaje = "Persona no encontrada.";

    // FASES 2 y 3: conexión y PreparedStatement en try-with-resources
    try (Connection con = Conexion.obtener();
         PreparedStatement ps = con.prepareStatement(sql)) {

        // FASE 3: Asignar parámetros
        ps.setString(1, p.getNombre());
        ps.setInt(2, p.getCodigo());

        // FASE 4: Ejecutar (devuelve cuántas filas cambiaron)
        int filasAfectadas = ps.executeUpdate();
        if (filasAfectadas > 0) mensaje = "Persona actualizada.";
        // NO HAY FASE 5: UPDATE no retorna ResultSet
    } catch (SQLException e) {
        System.err.println("Error al actualizar persona: " + e.getMessage());
        e.printStackTrace();
        mensaje = "Error al actualizar persona.";
    }
    return mensaje;
}

Análisis de la consulta UPDATE

String sql = "UPDATE personas SET nombre = ? WHERE codigo = ?";
  • UPDATE personas: Tabla a modificar
  • SET nombre = ?: Columna que cambiaremos (primer ?)
  • WHERE codigo = ?: Condición para identificar QUÉ fila modificar (segundo ?) ⚠️ CRÍTICO: La cláusula WHERE

Sin WHERE, actualizaríamos TODAS las filas de la tabla:

UPDATE personas SET nombre = 'Juan' -- ¡CAMBIA TODOS LOS NOMBRES A 'JUAN'!

Con WHERE, solo afectamos la fila que coincida:

UPDATE personas SET nombre = 'Juan' WHERE codigo = 5 -- Solo cambia codigo 5

Asignación de parámetros (orden importante)

ps.setString(1, p.getNombre());
ps.setInt(2, p.getCodigo());

¿Por qué este orden?

Los parámetros se asignan según aparecen en el SQL:

  1. Primer ?: nombre = ? → usamos setString(1, ...)
  2. Segundo ?: codigo = ? → usamos setInt(2, ...) Error común:
// ❌ MAL (orden invertido)
ps.setInt(1, p.getCodigo());     // Intenta poner int en nombre
ps.setString(2, p.getNombre());  // Intenta poner String en codigo

Validación del resultado

String mensaje = "Persona no encontrada.";
...
int filasAfectadas = ps.executeUpdate();
if (filasAfectadas > 0) mensaje = "Persona actualizada.";

executeUpdate() devuelve cuántas filas cambiaron. Tres escenarios posibles:

  1. filasAfectadas = 1: Se actualizó la persona → "Persona actualizada."
  2. filasAfectadas = 0: No existe ninguna persona con ese código (no es error SQL, simplemente no había coincidencia) → "Persona no encontrada."
  3. SQLException: Error de BD (tabla no existe, conexión perdida, etc.) → "Error al actualizar persona." Por eso mensaje empieza en "Persona no encontrada.": solo cambia si de verdad se actualizó una fila, o si hubo un error.

❌ Método 4: eliminar(int codigo)

Este método elimina una persona de la base de datos.

public String eliminar(int codigo) {
    // FASE 1: Definir la sentencia SQL
    String sql = "DELETE FROM personas WHERE codigo = ?";
    String mensaje = "Persona no encontrada.";

    // FASES 2 y 3: conexión y PreparedStatement en try-with-resources
    try (Connection con = Conexion.obtener();
         PreparedStatement ps = con.prepareStatement(sql)) {

        // FASE 3: Asignar parámetros
        ps.setInt(1, codigo);

        // FASE 4: Ejecutar (devuelve cuántas filas se borraron)
        int filasAfectadas = ps.executeUpdate();
        if (filasAfectadas > 0) mensaje = "Persona eliminada.";
        // NO HAY FASE 5: DELETE no retorna ResultSet
    } catch (SQLException e) {
        System.err.println("Error al eliminar persona: " + e.getMessage());
        e.printStackTrace();
        mensaje = "Error al eliminar persona.";
    }
    return mensaje;
}

Análisis de la consulta DELETE

String sql = "DELETE FROM personas WHERE codigo = ?";
  • DELETE FROM personas: Elimina fila(s) de la tabla personas
  • WHERE codigo = ?: Condición para identificar QUÉ fila eliminar ⚠️ SUPER CRÍTICO: La cláusula WHERE
DELETE FROM personas -- ¡BORRA TODA LA TABLA!
DELETE FROM personas WHERE codigo = 5 -- Borra solo la persona con codigo 5

Sin WHERE, eliminaríamos TODOS los registros. Por eso SIEMPRE debemos incluir WHERE en DELETE (salvo que realmente queramos vaciar la tabla).

Simplicidad del método

ps.setInt(1, codigo);
int filasAfectadas = ps.executeUpdate();
if (filasAfectadas > 0) mensaje = "Persona eliminada.";

Este método es el más simple porque:

  • Solo necesita UN parámetro (el código)
  • No necesita un objeto PersonaVO completo
  • La lógica es directa: encontraste y borraste = éxito, no encontraste = fallo

Feedback informativo

El mensaje que devuelve el método ayuda a distinguir:

  • Éxito: La persona existía y se eliminó
  • Fallo lógico: La persona no existía (no es error de BD)
  • Excepción: Problema técnico (se captura en el catch)

🔍 Método 5: obtenerXCodigo(int cod)

Este método busca y retorna una persona específica por su código.

public PersonaVO obtenerXCodigo(int cod) {
    // FASE 1: Definir la sentencia SQL
    String sql = "SELECT * FROM personas WHERE codigo = ?";
    PersonaVO persona = null;
 
    // FASES 2 y 3: conexión y PreparedStatement en try-with-resources
    try (Connection con = Conexion.obtener();
         PreparedStatement ps = con.prepareStatement(sql)) {
 
        // FASE 3: Asignar parámetros
        ps.setInt(1, cod);
 
        // FASE 4 & 5: Ejecutar y leer resultado
        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) {
                persona = new PersonaVO(
                    rs.getInt("codigo"),
                    rs.getString("nombre")
                );
            }
        }
    } catch (SQLException e) {
        System.err.println("Error al obtener persona: " + e.getMessage());
        e.printStackTrace();
    }
    return persona;
}

¿Por qué PersonaVO persona = null?

PersonaVO persona = null;

Inicializamos en null porque:

  • Si encontramos la persona, asignaremos un objeto PersonaVO
  • Si NO la encontramos, retornaremos null
  • El controlador puede verificar: if (persona != null) Alternativa posible pero menos clara:
PersonaVO persona = new PersonaVO(); // Objeto vacío
// Problema: ¿cómo distinguir "no encontrado" de "encontrado vacío"?

¿Por qué el ResultSet va en un try anidado?

try (Connection con = Conexion.obtener();
     PreparedStatement ps = con.prepareStatement(sql)) {
 
    ps.setInt(1, cod);  // ← necesitamos setear el parámetro ANTES de ejecutar
 
    try (ResultSet rs = ps.executeQuery()) { // ← luego ejecutamos
        ...
    }
}

A diferencia de getLista(), aquí necesitamos asignar el parámetro (ps.setInt) antes de ejecutar la consulta. Por eso el ResultSet va en un bloque try anidado dentro del bloque principal.

Consulta con filtro

String sql = "SELECT * FROM personas WHERE codigo = ?";

Similar a getLista(), pero con WHERE para filtrar por código específico.

Procesamiento del resultado (IF en lugar de WHILE)

if (rs.next()) {
    persona = new PersonaVO(
        rs.getInt("codigo"),
        rs.getString("nombre")
    );
}

¿Por qué if y no while?

  • WHERE codigo = ?: Buscamos por PRIMARY KEY, que es ÚNICO
  • Solo puede haber 0 o 1 resultado, nunca más de 1
  • if (rs.next()) verifica si existe esa primera (y única) fila Si usáramos while:
while (rs.next()) { // Funciona igual, pero innecesario
    persona = new PersonaVO(...);
}

Técnicamente funciona, pero if expresa mejor la intención: "si existe, tómala".

Retorno condicional

return persona;

Dos casos:

  1. rs.next() fue true: persona contiene un objeto PersonaVO con datos
  2. rs.next() fue false: persona sigue siendo null ¿Cómo lo usa el Controlador?
PersonaVO p = dao.obtenerXCodigo(5);
if (p != null) {
    // Encontrada: mostrar en formulario de edición
} else {
    // No encontrada: mostrar error 404
}

🧩 Comparación de los 5 métodos

Método SQL Parámetros Retorno ResultSet
getLista() SELECT sin WHERE 0 ArrayList Sí (while)
agregar() INSERT 2 (codigo, nombre) String (mensaje) No
actualizar() UPDATE con WHERE 2 (nombre, codigo) String (mensaje) No
eliminar() DELETE con WHERE 1 (codigo) String (mensaje) No
obtenerXCodigo() SELECT con WHERE 1 (codigo) PersonaVO (o null) Sí (if)

🎯 Patrones y buenas prácticas aplicadas

1. Patrón DAO (Data Access Object)

¿Qué problema resuelve?

Imagina si cada Servlet tuviera este código:

// ❌ Código duplicado en múltiples servlets
Connection con = DriverManager.getConnection(...);
PreparedStatement ps = con.prepareStatement("SELECT * FROM personas");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
    // Procesar...
}

Problemas:

  • Código duplicado en 10 lugares
  • Si cambia la BD, hay que modificar 10 archivos
  • Mezcla lógica de BD con lógica de presentación Solución con DAO:
// ✅ Servlet solo llama al DAO
request.setAttribute("personas", dao.getLista());

Beneficios:

  • Código de BD centralizado en una clase
  • Cambios en BD solo afectan al DAO (lo comprobamos al pasar de mini-crud2 a mini-crud-sql: el controlador no cambió)
  • Servlets más limpios y enfocados en controlar flujo

2. Uso de PreparedStatement (nunca Statement)

¿Por qué PreparedStatement?

Seguridad contra inyección SQL:

// ❌ VULNERABLE
String sql = "SELECT * FROM personas WHERE codigo = " + codigo;
// Si codigo = "1 OR 1=1", devuelve TODA la tabla
 
// ✅ SEGURO
String sql = "SELECT * FROM personas WHERE codigo = ?";
ps.setInt(1, codigo);
// El driver escapa automáticamente cualquier intento de inyección

Rendimiento:

  • La BD puede cachear el plan de ejecución de la consulta
  • Útil si ejecutamos la misma consulta con diferentes parámetros

3. Gestión automática de recursos con try-with-resources

try (Connection con = Conexion.obtener();
     PreparedStatement ps = con.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
    // Código que puede fallar
} catch (SQLException e) {
    // Manejo del error
}
// con, ps y rs se cierran automáticamente al salir del try

¿Por qué try-with-resources?

Java cierra automáticamente todos los recursos declarados en el try(...), en orden inverso al que fueron abiertos, haya o no excepción. Esto garantiza que:

  1. El ResultSet se cierra: Libera memoria en el servidor de BD
  2. El PreparedStatement se cierra: Libera recursos del driver JDBC
  3. La Connection se cierra: Se devuelve al pool o se termina Comparado con la versión anterior que requería un bloque finally manual:
// ❌ Versión anterior: más verbosa y propensa a errores
Connection con = null;
PreparedStatement ps = null;
try {
    con = ...;
    ps = ...;
} finally {
    if (ps != null) {
        ps.close();
    }
    if (con != null) {
        con.close();
    }
}

4. Manejo de null y valores opcionales

PersonaVO vs null:

public PersonaVO obtenerXCodigo(int cod) {
    // Retorna null si no encuentra
}

En Java moderno se podría usar Optional:

public Optional<PersonaVO> obtenerXCodigo(int cod) {
    // Retorna Optional.empty() si no encuentra
}

Ventaja de Optional: Fuerza al programador a manejar el caso "no encontrado" explícitamente.


🚦 Flujo completo: De la Vista a la BD y vuelta

Veamos un ejemplo completo del flujo cuando un usuario agrega una persona:

1. Usuario llena el formulario (persona-form.jsp)

<form action="/persona" method="post">
    <input type="hidden" name="action" value="agregar">
    <input type="number" name="codigo" value="123">
    <input type="text" name="nombre" value="Ana García">
    <button type="submit">Guardar</button>
</form>

2. El navegador envía POST al Servlet

POST /persona
action=agregar&codigo=123&nombre=Ana García

3. El Servlet (PersonaControladorServlet) procesa

protected void doPost(HttpServletRequest request, HttpServletResponse response) {
    String action = request.getParameter("action");
 
    if ("agregar".equals(action)) {
        int codigo = Integer.parseInt(request.getParameter("codigo"));
        String nombre = request.getParameter("nombre");
 
        dao.agregar(new PersonaVO(codigo, nombre));
 
        response.sendRedirect(request.getContextPath() + "/persona"); // PRG
    }
}

4. El DAO ejecuta la inserción

public String agregar(PersonaVO p) {
    // 1. Obtener conexión (Conexion.obtener())
    // 2. Preparar INSERT
    // 3. Asignar valores (123, "Ana García")
    // 4. Ejecutar
    // 5. Cerrar recursos automáticamente (try-with-resources)
    // 6. Retornar el mensaje
}

5. El Servlet redirige al listado

HTTP/1.1 302 Found
Location: /persona

6. El navegador hace GET a /persona

GET /persona

7. El Servlet lista personas

protected void doGet(HttpServletRequest request, HttpServletResponse response) {
    request.setAttribute("personas", dao.getLista());
 
    RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/vista/persona-lista.jsp");
    rd.forward(request, response);
}

8. La Vista muestra la tabla actualizada

<c:forEach var="p" items="${personas}">
    <tr>
        <td>${p.codigo}</td>
        <td>${p.nombre}</td>
    </tr>
</c:forEach>

Resultado final: El usuario ve "Ana García" en la tabla.


🧭 Resumen y conclusiones

PersonaDAO centraliza toda la lógica de acceso a datos:

  1. getLista(): Obtiene todas las personas (para mostrar tabla)
  2. agregar(): Inserta nueva persona (desde formulario)
  3. actualizar(): Modifica persona existente (desde edición)
  4. eliminar(): Borra persona (desde botón eliminar)
  5. obtenerXCodigo(): Busca una persona (para edición) Son los mismos cinco métodos que usaba el controlador con la versión en memoria: solo cambió lo que hacen por dentro.

Ventajas de este diseño:

  • ✅ Separación de responsabilidades: El Servlet no conoce detalles de SQL

  • ✅ Reutilización: Múltiples servlets pueden usar el mismo DAO

  • ✅ Mantenibilidad: Cambios en BD solo afectan al DAO

  • ✅ Testeable: Podemos probar el DAO independientemente

  • ✅ Seguridad: PreparedStatement previene inyección SQL

  • ✅ Robustez: try-with-resources garantiza el cierre de recursos Posibles próximos pasos para mejorar:

  • Implementar Connection Pool (reutilizar conexiones, ej. HikariCP)

  • Agregar validaciones más robustas

  • Implementar logging profesional (en lugar de System.out)

  • Manejar transacciones (cuando múltiples operaciones deben ser atómicas) Veamos un Resumen


📋 Palabras clave

PersonaDAO · patrón DAO · 5 fases JDBC · PreparedStatement · executeQuery · executeUpdate · ResultSet · rs.next() · inyección SQL · CRUD · getLista · agregar · actualizar · eliminar · obtenerXCodigo · try-with-resources


🧠 Autoevaluación — Patrón DAO y JDBC


← Clase Conexion · 🏠 Inicio · Resumen Tema 3 →

⚠️ **GitHub.com Fallback** ⚠️