Clase PersonaDAO actualizada - eliizquierdo/material_programacion_avanzada2026 GitHub Wiki
📦 Proyecto:
mini-crud-sql(Tema 3 — JDBC + MySQL) — ⬇️ Descargar ZIP · ☁️ Abrir en Codespace
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.
- 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
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()ycantidad()de la versión en memoria no se usaban en el controlador, así que en esta versión no los incluimos.
Todos los métodos del PersonaDAO siguen un patrón consistente de 5 fases. Comprender estas fases es fundamental para trabajar con JDBC:
Escribimos la consulta SQL en un String, usando ? como placeholders para los parámetros.
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 deltryy necesita quesqlya exista. Hasta que se prepara y se ejecuta, la sentencia es solo unStringen la memoria de Java: todavía no viaja a la base de datos.
Preparamos la sentencia SQL y asignamos valores a los parámetros usando métodos setXxx().
- Para consultas (SELECT): usamos
executeQuery()que retorna un ResultSet - Para modificaciones (INSERT/UPDATE/DELETE): usamos
executeUpdate()que retorna el número de filas afectadas
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.
| 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()) |
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
}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
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 bloqueJava 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
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;
}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.
try (Connection con = Conexion.obtener();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {-
Conexion.obtener(): Obtiene una nueva conexión leyendo las credenciales desde
db.properties - con.prepareStatement(sql): Prepara la consulta SQL
-
ps.executeQuery(): Ejecuta y devuelve un ResultSet con los resultados
Como
getListano tiene parámetros, podemos incluir elResultSetdirectamente en el encabezado deltry, 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)
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 retornatruesi existe - Cuando no hay más filas, retorna
falsey sale del while Para cada fila:
- rs.getInt("codigo"): Extrae el valor de la columna "codigo" como entero
- rs.getString("nombre"): Extrae el valor de la columna "nombre" como String
- new PersonaVO(...): Crea un objeto PersonaVO con esos datos
- lista.add(persona): Agrega ese objeto al ArrayList
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
PersonaVOel atributo se llamacod, pero se lee conrs.getInt("codigo")porque así se llama la columna. - El método se elige según el tipo de la columna:
getIntparaINT,getStringparaVARCHAR(la tabla completa está más abajo, en el métodoagregar()). - Los valores leídos se pasan al constructor de
PersonaVOen el mismo orden que sus parámetros: primero el código, después el nombre.
💡 También se puede leer por posición:
rs.getInt(1)lee la primera columna delSELECT(empieza en 1, igual que lossetXxx). 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 unaSQLExceptiondel tipo Column 'xxx' not found.
} 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.
return lista;Retornamos el ArrayList. Si hubo éxito, contendrá todas las personas. Si falló, estará vacío (pero nunca será null).
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;
}- No necesitamos ResultSet: Las operaciones INSERT/UPDATE/DELETE no retornan datos
- Usamos parámetros (?): Los valores vendrán del objeto PersonaVO
-
Retornamos un mensaje (String): La variable
mensajeempieza con el mensaje de error y solo cambia si la inserción se hizo. Es el mismo esquema que usaríamos con unboolean exito = false, pero devolviendo un texto, igual que la versión en memoria
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);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.
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 condouble.
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 unaSQLException: pasamos directo alcatchymensajeconserva"Error al agregar persona."
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"); // PRGEs la misma línea que en mini-crud2: el controlador no cambia.
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;
}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 5ps.setString(1, p.getNombre());
ps.setInt(2, p.getCodigo());¿Por qué este orden?
Los parámetros se asignan según aparecen en el SQL:
-
Primer ?:
nombre = ?→ usamos setString(1, ...) -
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 codigoString mensaje = "Persona no encontrada.";
...
int filasAfectadas = ps.executeUpdate();
if (filasAfectadas > 0) mensaje = "Persona actualizada.";executeUpdate() devuelve cuántas filas cambiaron. Tres escenarios posibles:
-
filasAfectadas = 1: Se actualizó la persona →
"Persona actualizada." -
filasAfectadas = 0: No existe ninguna persona con ese código (no es error SQL, simplemente no había coincidencia) →
"Persona no encontrada." -
SQLException: Error de BD (tabla no existe, conexión perdida, etc.) →
"Error al actualizar persona."Por esomensajeempieza en"Persona no encontrada.": solo cambia si de verdad se actualizó una fila, o si hubo un error.
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;
}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 5Sin WHERE, eliminaríamos TODOS los registros. Por eso SIEMPRE debemos incluir WHERE en DELETE (salvo que realmente queramos vaciar la tabla).
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
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)
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;
}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"?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.
String sql = "SELECT * FROM personas WHERE codigo = ?";Similar a getLista(), pero con WHERE para filtrar por código específico.
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".
return persona;Dos casos:
-
rs.next() fue true:
personacontiene un objeto PersonaVO con datos -
rs.next() fue false:
personasigue siendonull¿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
}| 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) |
¿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-crud2amini-crud-sql: el controlador no cambió) - Servlets más limpios y enfocados en controlar flujo
¿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ónRendimiento:
- La BD puede cachear el plan de ejecución de la consulta
- Útil si ejecutamos la misma consulta con diferentes parámetros
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:
- El ResultSet se cierra: Libera memoria en el servidor de BD
- El PreparedStatement se cierra: Libera recursos del driver JDBC
-
La Connection se cierra: Se devuelve al pool o se termina
Comparado con la versión anterior que requería un bloque
finallymanual:
// ❌ 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();
}
}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.
Veamos un ejemplo completo del flujo cuando un usuario agrega una persona:
<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>POST /persona
action=agregar&codigo=123&nombre=Ana García
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
}
}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
}HTTP/1.1 302 Found
Location: /persona
GET /persona
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);
}<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.
PersonaDAO centraliza toda la lógica de acceso a datos:
- getLista(): Obtiene todas las personas (para mostrar tabla)
- agregar(): Inserta nueva persona (desde formulario)
- actualizar(): Modifica persona existente (desde edición)
- eliminar(): Borra persona (desde botón eliminar)
- 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
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
