Java y JDBC: Trabajando con una Base de Datos - Notas
PostgreSQL SQL Server ORACLE database - Mysql Desde Consola: C:\Program Files\MySQL\MySQL Server 8.0\bin>mysql -u root -p Ingresamos la contraseña: Solocuestavida1! create database control_de_stock; use control_de_stock; Creamos la tabla: mysql> create table producto( -> id INT AUTO_INCREMENT, -> nombre VARCHAR(50) NOT NULL, -> descripcion VARCHAR(255), -> cantidad INT NOT NULL DEFAULT 0, -> PRIMARY KEY(id) -> )Engine=InnoDB; // Data mysql> select * from producto; // Ingresamos data mysql> insert into producto(nombre, descripcion, cantidad) values("Mesa", "Mesa de4 lugares", 10); mysql> insert into producto(nombre, descripcion, cantidad) values("Celular", "Samsung", 40); mysql> select * from producto; +----+---------+------------------+----------+ | id | nombre | descripcion | cantidad | +----+---------+------------------+----------+ | 1 | Mesa | Mesa de4 lugares | 10 | | 2 | Celular | Samsung | 40 | +----+---------+------------------+----------+
Usamos la librería.. Para el puente entre Java y Mysql Librería: Conocida como Driver, en este caso driver de Mysql. Java provee una capa de abstracción para la conexión.. Hacia cualquier driver de conexión que necesitemos. Se llama JDBC de Java Database Connectivity. El JDBC es una lib que contiene las interfaces de comunicación y operación con la base de datos. El driver de cualquier otra base de datos que tenga una librería para Java, todas sus clases implementan las interfaces de JDBC. Solo con la abstracción de JDBC vamos a poder conectarnos a cualquier base de datos sin tener que cambiar ninguna línea de código Con el JDBC, si queremos conectarnos a una base de datos vamos a estar utilizando la clase DriverManager y el método getConnection. En este método vamos a estar enviando los parámetros que son la URL de conexión con la base de datos, el usuario y la contraseña. jdbc:mysql://localhost:3306/base_de_pruebas jdbc:mysql://localhost:3306/base_de_pruebas?(Podemos agregar parámetros opcionales)
¿Cuál es la ventaja de utilizar una API como la JDBC, basada en interfaces, para realizar la comunicación entre el código y una base de datos relacional? Seleccione una alternativa: Transparencia a la hora de elegir la base de datos o cambiar de una para otra, con muy pocos cambios de código. ¡Alternativa correcta! Lo único que va a ser necesario hacer es cambiar la dependencia del driver de base datos y el JDBC se encarga de todo.
En el Eclipse vamos a crear un proyecto Maven, entonces vamos a hacer un file new project.
Aquí voy a escribir el Maven, Maven Project.
Create simple project.
El artifact va a ser control-de-stock y hago un finish. Listo, ya creamos el proyecto.
Ahora vamos a hacer un clic derecho aquí en el proyecto, y vamos a properties.
Y aquí, cuando abrimos las propiedades del proyecto, vamos aquí a la opción de Java build path. ¿Por qué?
Siempre que estoy creando un proyecto en eclipse, me está creando con la versión 1.5 de Java y aquí lo vamos a hacer un cambio para que sigamos con la versión 11 como habíamos combinado.
Ahora lo que vamos a hacer es expandir acá el proyecto y vamos a abrir el archivo pom.xml.
Aquí en este archivo tenemos todas las configuraciones del proyecto..
Tenemos 2 formas:
O podemos utilizar el archivo .jar que bajamos desde internet para agregar aquí manualmente el proyecto
o nosotros podemos utilizar herramientas de control de dependencias, de manejo de dependencias, como es el caso de Maven. Aquí vamos a hacer unas configuraciones iniciales que van a ser las siguientes.
Ahora aquí en el proyecto, vamos a crear un paquete.
Va a hacer un clic acá, "Ctrl + N" package.
Vamos a crear un paquete llamado com.alura.tests en donde vamos a estar creando nuestras clases de pruebas para hacer las operaciones con la base de datos.
Aquí, en este paquete, vamos a estar creando una clase.
Esta clase va a llamarse pruebaConexion..
package com.alura.tests;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class PruebaConexion {
// En caso de error arrojamos la excepción o podemos usar un try y catch
public static void main(String[] args) throws SQLException {
// Con getConnection manejamos la conexión, usuario y también conexión.
Connection con = DriverManager.getConnection(
"jdbc:mysql://localhost/control_de_stock?useTimeZone=true&serverTimeZone=UTC",
"root",
"Solocuestavida1!");
System.out.println("Cerrando la conexión");
con.close();
}
}
José ha desarrollado el siguiente código para abrir y cerrar una conexión con la base de datos:
public class PruebaConexionConBaseDeDatos {
public static void main(String[] args) throws SQLException {
Connection con = DriverManager
.getConnection("jdbc:mysql://localhost/control_de_stock?useTimezone=true&serverTimezone=UTC");
con.close();
}
}
Pero cuando va a ejecutar el código le aparece la siguiente excepción: Access denied for user.
¿Qué falta hacer para lograr conectarse con éxito a la base de datos?
Seleccione una alternativa
Se necesita agregar los parámetros de usuario y contraseña en el método de conexión.
DriverManager
.getConnection("jdbc:mysql://localhost/control_de_stock?useTimezone=true&serverTimezone=UTC", "root", "root1234");
Lo que aprendimos en esta aula: Para acceder a una base de datos necesitamos del driver de conexión; Un driver es simplemente una librería .jar. JDBC significa Java DataBase Connectivity; El JDBC define una capa de abstracción entre la aplicación y el driver de la base de datos. Esta capa es compuesta de interfaces que el driver implementa. Para abrir una conexión con la base de datos debemos utilizar el método getConnection de la clase DriverManager; El método getConnection recibe tres parámetros. Son ellos la URL de conexión JDBC, el usuario y la contraseña.
Cómo importar un proyecto nuevo en el Maven: En la opción import project vamos a buscar existing Maven projects. Esta aplicación está utilizando el Java swing para crear las pantallas. Este es un recurso propio del Java para el desarrollo de aplicaciones desktop. Por ejemplo, para una aplicación web estaríamos utilizando el HTML con CSS y Javascript. Así que con el Swing vamos a aprender un poco este flujo y estructura del proyecto en donde tenemos una view acá, esta view, que es el front-end del proyecto, como una página HTML.
Ahora dentro del ProductoController, creamos la clase correspondiente.. Las sentencias sql en Java son consideradas como Statement
package com.alura.jdbc.controller;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.alura.jdbc.factory.ConnectionFactory;
public class ProductoController {
public List
public class ProductoController {
public List> listar() throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
Connection con = factory.recuperaConexion();
// STATEMENT, con el execute con el query correspondiente.
Statement statement = con.createStatement();
statement.execute("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM PRODUCTO");
// Hacemos el getResultSet
ResultSet resultSet = statement.getResultSet();
List> resultado = new ArrayList<>();
// Usamos el .next.. del Set
while (resultSet.next()) {
// Armamos una fila con el map
Map fila = new HashMap<>();
// getInt, es el número del id
// Y a l fila le vamos agregando con put..
fila.put("ID", String.valueOf(resultSet.getInt("ID")));
// Para el string es .getString
fila.put("NOMBRE", resultSet.getString("NOMBRE"));
fila.put("DESCRIPCION", resultSet.getString("DESCRIPCION"));
// cantidad como el int
fila.put("CANTIDAD", String.valueOf(resultSet.getInt("CANTIDAD")));
// Le agregamos la fila al resultado, que es el map
resultado.add(fila);
}
con.close();
return resultado;
}
}
Ahora en .. ControlDeStockFrame
public class ControlDeStockFrame extends JFrame {
private void cargarTabla() {
List> productos = new ArrayList>();
try {
productos = this.productoController.listar();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
productos.forEach(producto -> modelo.addRow(
new Object[] {
producto.get("ID"),
producto.get("NOMBRE"),
producto.get("DESCRIPCION"),
producto.get("CANTIDAD") }));
}
}
Y dentro de .. ControlDeStockFrame
private void configurarTablaDeContenido(Container container) {
tabla = new JTable();
// Armamos el modelo de la tabla
modelo = (DefaultTableModel) tabla.getModel();
modelo.addColumn("Identificador del Producto");
modelo.addColumn("Nombre del Producto");
modelo.addColumn("Descripción del Producto");
modelo.addColumn("Cantidad");
cargarTabla();
tabla.setBounds(10, 205, 760, 280);
botonEliminar = new JButton("Eliminar");
botonModificar = new JButton("Modificar");
botonReporte = new JButton("Ver Reporte");
botonEliminar.setBounds(10, 500, 80, 20);
botonModificar.setBounds(100, 500, 80, 20);
botonReporte.setBounds(190, 500, 80, 20);
container.add(tabla);
container.add(botonEliminar);
container.add(botonModificar);
container.add(botonReporte);
setSize(800, 600);
setVisible(true);
setLocationRelativeTo(null);
}
¿Que hay en común entre las clases java.sql.Connection, java.sql.Statement y java.sql.ResultSet? Seleccione una alternativa Todas son interfaces. ¡Alternativa correcta! Connection, Statement y ResultSet son algunas de las interfaces del paquete java.sql.
Esto es para evitar repetir duplicar código.
// Esto se mete dentro de un paquete llamado factory
package com.alura.jdbc.factory;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
// Patrón el cual concentra la lógica en un punto, el Factory
public class ConnectionFactory {
public Connection recuperaConexion() throws SQLException {
// Hacemos el return
return DriverManager.getConnection(
"jdbc:mysql://localhost/control_de_stock?useTimeZone=true&serverTimeZone=UTC",
"root",
"Solocuestavida1!");
}
}
¿Cuál es la ventaja de utilizar una ConnectionFactory en nuestra aplicación? Seleccione una alternativa Proveer una forma más sencilla de crear un objeto. ¡Alternativa correcta! Los objetos son creados sin exponer la lógica o las configuraciones de creación al cliente. Además, es posible referirnos al objeto recién creado utilizando una interfaz (una abstracción), desacoplando la implementación.
Hasta acá..
Ya estamos recuperando la conexión y listando los productos de la base de datos en la pantalla.
private void guardar() {
if (textoNombre.getText().isBlank() || textoDescripcion.getText().isBlank()) {
JOptionPane.showMessageDialog(this, "Los campos Nombre y Descripción son requeridos.");
return;
}
Integer cantidadInt;
try {
cantidadInt = Integer.parseInt(textoCantidad.getText());
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(this, String
.format("El campo cantidad debe ser numérico dentro del rango %d y %d.", 0, Integer.MAX_VALUE));
return;
}
// Hacemos un map con el producto..
var producto = new HashMap();
producto.put("NOMBRE", textoNombre.getText());
producto.put("DESCRIPCION", textoDescripcion.getText());
producto.put("CANTIDAD", String.valueOf(cantidadInt));
var categoria = comboCategoria.getSelectedItem();
// Ahora el producto lo enviamos al metodo guardar..
try {
this.productoController.guardar(producto);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
JOptionPane.showMessageDialog(this, "Registrado con éxito!");
this.limpiarFormulario();
}
El método guardar se encuentra dentro de la clase ProductoController
//Recibimos un map.. Lo anterior
public void guardar(Map producto) throws SQLException {
// Instanciamos la conexion.
ConnectionFactory factory = new ConnectionFactory();
Connection con = factory.recuperaConexion();
// Ahora le damos lugar al statement, la query..
// A partir del producto que nos envía el método guardar desde el ControlDeStockFrame
Statement statement = con.createStatement();
statement.execute(
"INSERT INTO PRODUCTO (nombre, descripcion, cantidad)"
+ " VALUES ('" + producto.get("NOMBRE") + "', '"
+ producto.get("DESCRIPCION") + "', '"
+ producto.get("CANTIDAD") + "')",
Statement.RETURN_GENERATED_KEYS);
// Con este valor, tomamos el id..
ResultSet resultSet = statement.getGeneratedKeys();
while(resultSet.next()) {
System.out.println(String.format(
"Fue insertado el producto de ID: %d",
resultSet.getInt(1)));
}
}
Ahora desde el ControlDeStockFrame
private void eliminar() {
if (tieneFilaElegida()) {
JOptionPane.showMessageDialog(this, "Por favor, elije un item");
return;
}
Optional.ofNullable(modelo.getValueAt(tabla.getSelectedRow(), tabla.getSelectedColumn()))
.ifPresentOrElse(fila -> {
Integer id = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 0).toString());
// Aca vemos la cantidad que eliminamos.
int filasModificadas;
try { // Debemos armar el try catch
// Este metodo es el que armamos ahora..
filasModificadas = this.productoController.eliminar(id);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
modelo.removeRow(tabla.getSelectedRow());
// Lo corroboramos desde acá..
JOptionPane.showMessageDialog(this, String.format("%d item eliminado con éxito!", filasModificadas));
}, () -> JOptionPane.showMessageDialog(this, "Por favor, elije un item"));
}
Ahora en el productoController armamos el método correspondiente..
// Arrojamos la excepeción
public int eliminar(Integer id) throws SQLException {
// Instanciamos la conexion
ConnectionFactory factory = new ConnectionFactory();
Connection con = factory.recuperaConexion();
Statement statement = con.createStatement();
// Creamos el execute, con la query necesaria..
statement.execute("DELETE FROM PRODUCTO WHERE ID = " + id);
// Esto nos sirve para corroborar el registro eliminado.. En vez de hacer print.
int updateCount = statement.getUpdateCount();
con.close();
return updateCount; // Devolvemos la cantidad eliminada.
}
Ahora armamos una prueba dentro del paquete de pruebas..
package com.alura.jdbc.pruebas;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import com.alura.jdbc.factory.ConnectionFactory;
public class PruebaDelete {
public static void main(String[] args) throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
Connection con = factory.recuperaConexion();
Statement statement = con.createStatement();
statement.execute("DELETE FROM PRODUCTO WHERE ID = 10"); // Si el id no existe, la app devuelve 0.
int updateCount = statement.getUpdateCount();
System.out.println(String.format("%d registros eliminados", updateCount));
con.close();
}
}
¿Para qué sirve el retorno del método execute de la interfaz java.sql.Statement? Seleccione una alternativa El método devuelve true cuando el resultado devuelve un java.sql.ResultSet (resultado de un SELECT) y false cuando el resultado no devuelve contenido (resultado de un DELETE, UPDATE o DELETE). ¡Alternativa correcta!
En el vídeo anterior agregamos un registro con un error de escritura. En lugar de escribir "cuchara" fue escrito "cucaracha". Para modificar el valor de este registro es posible hacer doble click en la columna que queremos actualizar, escribir el nuevo valor y hacer click en el botón Modificar.
Ahora es tu turno de poner en práctica lo que aprendimos en las clases anteriores para completar la funcionalidad de modificar un registro del depósito.
Lo primero que tenemos que hacer aquí es arreglar el problema de java.lang.ClassCastException que vimos en la clase anterior cambiando el código con problemas por:
Integer id = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 0).toString());
También tenemos que agregar el campo de cantidad, que no está presente en la lógica pero es importante que también pueda ser modificado.
Integer cantidad = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 3).toString());
Luego, tenemos que ver cómo está configurado el evento del botonModificar en el método configurarAccionesDelFormulario(). Vemos dos métodos conocidos: limpiarTabla() y cargarTabla(). Vamos a ver el detalle del método modificar().
Aquí estamos recibiendo los valores de la fila elegida y enviando como parámetro en el método productoController.modificar(nombre, descripcion, id). Este es el método que debemos completar.
La lógica para hacer un UPDATE es muy similar a la lógica del DELETE, empezamos tomando la conexión de la ConnectionFactory y creando un Statement statement = con.createStatement().
Luego ejecutamos el método execute con el código SQL de UPDATE, cerramos la conexión y retornamos la cantidad de líneas modificadas. No olvidemos de las comillas simples para modificar valores del tipo String.
statement.execute("UPDATE PRODUCTO SET "
+ " NOMBRE = '" + nombre + "'"
+ ", DESCRIPCION = '" + descripcion + "'"
+ ", CANTIDAD = " + cantidad
+ "WHERE ID = " + id);
No podemos dejar pasar el detalle de la SQLException y, por fin, vamos a mostrar un cartel informando cuántos registros fueron modificados con éxito. Parecido con el cartel de eliminar registros.
Así quedan los códigos de resultado:
// Clase ProductoController
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
Connection con = factory.recuperaConexion();
Statement statement = con.createStatement();
statement.execute("UPDATE PRODUCTO SET "
+ " NOMBRE = '" + nombre + "'"
+ ", DESCRIPCION = '" + descripcion + "'"
+ ", CANTIDAD = " + cantidad
+ " WHERE ID = " + id);
int updateCount = statement.getUpdateCount();
con.close();
return updateCount;
}
// Clase ControlDeStockFrame
private void modificar() {
if (tieneFilaElegida()) {
JOptionPane.showMessageDialog(this, "Por favor, elije un item");
return;
}
Optional.ofNullable(modelo.getValueAt(tabla.getSelectedRow(), tabla.getSelectedColumn()))
.ifPresentOrElse(fila -> {
Integer id = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 0).toString());
String nombre = (String) modelo.getValueAt(tabla.getSelectedRow(), 1);
String descripcion = (String) modelo.getValueAt(tabla.getSelectedRow(), 2);
Integer cantidad = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 3).toString());
int filasModificadas;
try {
filasModificadas = this.productoController.modificar(nombre, descripcion, cantidad, id);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
JOptionPane.showMessageDialog(this, String.format("%d item modificado con éxito!", filasModificadas));
}, () -> JOptionPane.showMessageDialog(this, "Por favor, elije un item"));
}
Para simplificar y encapsular la creación de la conexión debemos utilizar una clase ConnectionFactory; Esta clase sigue el estándar de creación Factory Method, que encapsula la creación de un objeto. Podemos utilizar la interfaz java.sql.Statement para ejecutar un comando SQL en la aplicación; El método execute envía el comando para la base de datos. A depender del comando SQL, podemos recuperar la clave primaria o los registros buscados.
SQL Injection, esto es el hecho de intentar inyectar scripts SQL en un campo de formulario o URL para intentar romper una aplicación o buscar informaciones que son críticas y que son sensibles.
¿Entonces, qué podemos hacer para protegernos?
De esta manera evitamos pasar los parámetros al usar el prepareStatement y le dejamos la responsabilidad al Jdbc
- Evitamos el sql Injection
- Todo String desde el formulario, es tratado como Strin no como una query
- Mejor legibilidad del código.
Bueno, nosotros podríamos crear una validación para detectar caracteres especiales y comandos de SQL y agregarla en todos los locales que se conecten a la base de datos.
public void guardar(Map producto) throws SQLException {
var nombre = producto.get("NOMBRE");
var descripcion = producto.get("DESCRIPCION");
var cantidad = Integer.valueOf(producto.get("CANTIDAD"));
final var maximoCantidad = 50;
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion();
try(con) {
con.setAutoCommit(false);
// Preparamos el statement
final PreparedStatement statement = con.prepareStatement(
"INSERT INTO PRODUCTO "
+ "(nombre, descripcion, cantidad)"
+ " VALUES (?, ?, ?)", // Le indicamos, pero no le precisamos como lo haciamos antes.
Statement.RETURN_GENERATED_KEYS);
try(statement) {
do {
int cantidadParaGuardar = Math.min(cantidad, maximoCantidad);
ejecutaRegistro(nombre, descripcion, cantidadParaGuardar, statement);
cantidad -= maximoCantidad;
} while (cantidad > 0);
con.commit();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("ROLLBACK de la transacción");
con.rollback();
}
}
Y luego agregamos el método..
private void ejecutaRegistro(String nombre, String descripcion, Integer cantidad, PreparedStatement statement) throws SQLException {
statement.setString(1, nombre); // Estos values los seteamos desde acá, sin precisarlos..
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
if (cantidad < 50) {
throw new RuntimeException("Ocurrió un error");
}
statement.execute();
final ResultSet resultSet = statement.getGeneratedKeys();
try(resultSet) {
while (resultSet.next()) {
System.out.println(String.format("Fue insertado el producto de ID: %d", resultSet.getInt(1)));
}
}
}
// Ahora hacemos lo mismo para el método de listar(), usando el prepareStatement
public List> listar() throws SQLException {
List> resultado = new ArrayList<>();
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion();
try(con) {
final PreparedStatement statement = con
.prepareStatement("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM PRODUCTO");
try(statement) {
statement.execute();
final ResultSet resultSet = statement.getResultSet();
try(resultSet) {
while (resultSet.next()) {
Map fila = new HashMap<>();
fila.put("ID", String.valueOf(resultSet.getInt("ID")));
fila.put("NOMBRE", resultSet.getString("NOMBRE"));
fila.put("DESCRIPCION", resultSet.getString("DESCRIPCION"));
fila.put("CANTIDAD", String.valueOf(resultSet.getInt("CANTIDAD")));
resultado.add(fila);
}
}
}
return resultado;
}
}
// También para el método de eliminar(), usando el prepareStatement
public int eliminar(Integer id) throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion();
try(con) {
final PreparedStatement statement = con
.prepareStatement("DELETE FROM PRODUCTO WHERE ID = ?");
try(statement) {
statement.setInt(1, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
}
}
// También para el método de modificar(), usando el prepareStatement
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion();
try(con) {
final PreparedStatement statement = con
.prepareStatement("UPDATE PRODUCTO SET "
+ " NOMBRE = ?, "
+ " DESCRIPCION = ?,"
+ " CANTIDAD = ?"
+ " WHERE ID = ?");
try(statement) {
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
statement.setInt(4, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
}
}
¿Cuál es el riesgo de utilizar un Statement en lugar del PreparedStatement? Seleccione una alternativa El Statement no mantiene una versión de la query compilada en la base de datos. ¡Alternativa correcta! El PreparedStatement mantiene la query compilada en la base de datos, de forma parametrizada. Así el usuario puede ejecutar la misma consulta diversas veces con parámetros distintos.
Ya vimos cómo dejar nuestra aplicación más segura y legible utilizando el PreparedStatement.
Con eso eliminamos la vulnerabilidad de sufrir ataques de SQL Injection.
Ahora vamos a replicar esta solución para las demás operaciones de modificar, eliminar y listar productos.
Podemos seguir el ejemplo visto en la clase anterior para realizar los cambios necesarios en los demás métodos de la clase ProductoController:
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
Connection con = factory.recuperaConexion();
PreparedStatement statement = con
.prepareStatement("UPDATE PRODUCTO SET "
+ " NOMBRE = ?, "
+ " DESCRIPCION = ?,"
+ " CANTIDAD = ?"
+ " WHERE ID = ?");
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
statement.setInt(4, id);
statement.execute();
int updateCount = statement.getUpdateCount();
con.close();
return updateCount;
}
public int eliminar(Integer id) throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
Connection con = factory.recuperaConexion();
PreparedStatement statement = con
.prepareStatement("DELETE FROM PRODUCTO WHERE ID = ?");
statement.setInt(1, id);
statement.execute();
int updateCount = statement.getUpdateCount();
con.close();
return updateCount;
}
public List> listar() throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
Connection con = factory.recuperaConexion();
PreparedStatement statement = con
.prepareStatement("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM PRODUCTO");
statement.execute();
ResultSet resultSet = statement.getResultSet();
List> resultado = new ArrayList<>();
while (resultSet.next()) {
Map fila = new HashMap<>();
fila.put("ID", String.valueOf(resultSet.getInt("ID")));
fila.put("NOMBRE", resultSet.getString("NOMBRE"));
fila.put("DESCRIPCION", resultSet.getString("DESCRIPCION"));
fila.put("CANTIDAD", String.valueOf(resultSet.getInt("CANTIDAD")));
resultado.add(fila);
}
con.close();
return resultado;
}
José estaba actualizando su aplicación para utilizar el PreparedStatement y aprovechar los beneficios que esta interfaz provee. Su código quedó de esta forma: Statement stm = con.prepareStatement("DELETE FROM CLIENTE WHERE NOMBRE = ? AND DNI = ?"); stm.setString(1, "Jose"); stm.setString(2, "12345678"); Pero su código no compila correctamente. El Eclipse dice que hay un error en las líneas que setean valores a los atributos de la query. ¿Qué sería necesario arreglar para que el código compile? Seleccione una alternativa José necesita actualizar la interfaz del objeto de Statement para PreparedStatement. ¡Alternativa correcta! La interfaz Statement no conoce el método setString. Este método pertenece a la interfaz PreparedStatement.
Ahora vamos a modificar la cantidad de stock para cada producto, 50.. AL momento de insertar.
public void guardar(Map producto) throws SQLException {
var nombre = producto.get("NOMBRE");
var descripcion = producto.get("DESCRIPCION");
var cantidad = Integer.valueOf(producto.get("CANTIDAD"));
final var maximoCantidad = 50;
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion();
try(con) {
con.setAutoCommit(false);
final PreparedStatement statement = con.prepareStatement(
"INSERT INTO PRODUCTO "
+ "(nombre, descripcion, cantidad)"
+ " VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS);
try(statement) {
do {
// En este caso usamos el valor minimo, siempre usando como referencia el 50
int cantidadParaGuardar = Math.min(cantidad, maximoCantidad);
ejecutaRegistro(nombre, descripcion, cantidadParaGuardar, statement);
cantidad -= maximoCantidad;
} while (cantidad > 0);
con.commit();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("ROLLBACK de la transacción");
con.rollback();
}
}
Del metodo de arriba, desglosamos a un nuevo método..
ejecutaRegistro(nombre, descripcion, cantidadParaGuardar, statement);
private void ejecutaRegistro(String nombre, String descripcion, Integer cantidad, PreparedStatement statement)
throws SQLException {
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
if (cantidad < 50) {
throw new RuntimeException("Ocurrió un error");
}
statement.execute();
final ResultSet resultSet = statement.getGeneratedKeys();
try(resultSet) {
while (resultSet.next()) {
System.out.println(String.format("Fue insertado el producto de ID: %d", resultSet.getInt(1)));
}
}
}
//Con esta lógica guardamos 50 productos como máximo y el resto irá con otro Id.. Como productos restantes.
En este punto simulamos un error.. Para luego tomar el control de la transacción, la cual ejecutará la query
- con.setAutoCommit(false);
Ahora, nosotros sacamos la responsabilidad del JDBC para concluir la transacción pero ahora nosotros no podemos registrar ningún producto.
public void guardar(Map producto) throws SQLException {
var nombre = producto.get("NOMBRE");
var descripcion = producto.get("DESCRIPCION");
var cantidad = Integer.valueOf(producto.get("CANTIDAD"));
final var maximoCantidad = 50;
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion();
try(con) {
con.setAutoCommit(false); // El setAutoCommit lo dejamos en false..
final PreparedStatement statement = con.prepareStatement(
"INSERT INTO PRODUCTO "
+ "(nombre, descripcion, cantidad)"
+ " VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS);
try(statement) {
do {
int cantidadParaGuardar = Math.min(cantidad, maximoCantidad);
ejecutaRegistro(nombre, descripcion, cantidadParaGuardar, statement);
cantidad -= maximoCantidad;
} while (cantidad > 0);
con.commit();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("ROLLBACK de la transacción");
con.rollback();
}
}
¿Cuál es el estándar de JDBC (del driver) para manejar transacciones de base de datos? Seleccione una alternativa Auto-Commit. ¡Alternativa correcta! Este es el estándar, que puede ser modificado por el método setAutoCommit, de la interfaz Connection.
Considerando lo siguiente, si hay un error en el medio del proceso, todas las operaciones de la base de datos deben ser revertidas.
con.commit(); // De esta manera garantizamos que todos los comandos del loop hayan sido garantizados correctamente.
con.rollback(); // Volvemos todo hacia atrás.
public void guardar(Map producto) throws SQLException {
var nombre = producto.get("NOMBRE");
var descripcion = producto.get("DESCRIPCION");
var cantidad = Integer.valueOf(producto.get("CANTIDAD"));
final var maximoCantidad = 50;
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion();
try(con) {
con.setAutoCommit(false); // El setAutoCommit lo dejamos en false..
final PreparedStatement statement = con.prepareStatement(
"INSERT INTO PRODUCTO "
+ "(nombre, descripcion, cantidad)"
+ " VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS);
try(statement) {
do {
int cantidadParaGuardar = Math.min(cantidad, maximoCantidad);
ejecutaRegistro(nombre, descripcion, cantidadParaGuardar, statement);
cantidad -= maximoCantidad;
} while (cantidad > 0);
con.commit();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("ROLLBACK de la transacción");
con.rollback(); // Acá agregamos el rollback
}
}
// En el caso de haber un error en la ejecución, nada es guardado y se hace un rollback de la transacción.
Ahora garantizamos que la transacción o guarda todo o no guarda nada.
José ha decidido que iría manejar las transacciones de la aplicación en lugar de JDBC y realizó el seteo del Auto-Commit para false ¿Que más es necesario para que Jose tenga el control total de las transacciones? Seleccione una alternativa Hay que explicitar el commit y el rollback. ¡Alternativa correcta! Si la transacción es exitosa, José necesita realizar el commit explícito. Pero si hay un error en la transacción, José también necesita realizar el rollback explícito.
Debemos cerrar cada una de las aperturas hacia la base de datos..
Por suerte, porque desde la versión 7 de Java hay un recurso llamado Try With Resources.
El Try With Resources nos permite declarar recursos que van a ser utilizados en un bloque de try catch con la certeza de que estos recursos van a ser cerrados o finalizados automáticamente después de la ejecución del bloque.
Versión 7
ejecutaRegistro(nombre, descripcion, cantidadParaGuardar, statement);
private void ejecutaRegistro(String nombre, String descripcion, Integer cantidad, PreparedStatement statement)
throws SQLException {
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
if (cantidad < 50) {
throw new RuntimeException("Ocurrió un error");
}
statement.execute();
// De esta manera la propia JVM se ocupa de estar cerrando estos recursos.
try(ResulSet resultSet = statement.getGeneratedKeys();) {
while (resultSet.next()) {
System.out.println(String.format("Fue insertado el producto de ID: %d", resultSet.getInt(1)));
}
}
}
Verisón 9
private void ejecutaRegistro(String nombre, String descripcion, Integer cantidad, PreparedStatement statement)
throws SQLException {
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
if (cantidad < 50) {
throw new RuntimeException("Ocurrió un error");
}
statement.execute();
final ResultSet resultSet = statement.getGeneratedKeys(); // Declaramos el final
try(resultSet) { // Más legible.
while (resultSet.next()) {
System.out.println(String.format("Fue insertado el producto de ID: %d", resultSet.getInt(1)));
}
}
}
Y ahora que ya hicimos eso con el resultSet, vamos a hacer lo mismo con la conexión y el statement acá del método guardar
public void guardar(Map producto) throws SQLException {
var nombre = producto.get("NOMBRE");
var descripcion = producto.get("DESCRIPCION");
var cantidad = Integer.valueOf(producto.get("CANTIDAD"));
final var maximoCantidad = 50;
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion(); // Agregamos el final
try(con) {
con.setAutoCommit(false);
final PreparedStatement statement = con.prepareStatement( // Agregamos el final
"INSERT INTO PRODUCTO "
+ "(nombre, descripcion, cantidad)"
+ " VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS);
try(statement) { // Intentamos el statement
do {
int cantidadParaGuardar = Math.min(cantidad, maximoCantidad);
ejecutaRegistro(nombre, descripcion, cantidadParaGuardar, statement);
cantidad -= maximoCantidad;
} while (cantidad > 0);
con.commit();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("ROLLBACK de la transacción");
con.rollback();
}
}
Ahora nosotros podemos hacer lo mismo para las otras operaciones de listado.
public List> listar() throws SQLException {
List> resultado = new ArrayList<>();
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion(); // Agregamos el final
try(con) { // Le pasamos la instancia de con
final PreparedStatement statement = con // Agregamos el final
.prepareStatement("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM PRODUCTO");
try(statement) {
statement.execute();
final ResultSet resultSet = statement.getResultSet(); // Agregamos el final
try(resultSet) {
while (resultSet.next()) {
Map fila = new HashMap<>();
fila.put("ID", String.valueOf(resultSet.getInt("ID")));
fila.put("NOMBRE", resultSet.getString("NOMBRE"));
fila.put("DESCRIPCION", resultSet.getString("DESCRIPCION"));
fila.put("CANTIDAD", String.valueOf(resultSet.getInt("CANTIDAD")));
resultado.add(fila);
}
}
}
return resultado;
}
}
Siguiendo acá tenemos el de eliminar
public int eliminar(Integer id) throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion();
try(con) {
final PreparedStatement statement = con
.prepareStatement("DELETE FROM PRODUCTO WHERE ID = ?");
try(statement) {
statement.setInt(1, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
}
}
Tambien para el de modificación..
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) throws SQLException {
ConnectionFactory factory = new ConnectionFactory();
final Connection con = factory.recuperaConexion(); // Final
try(con) { // El try psando el con
final PreparedStatement statement = con // Final
.prepareStatement("UPDATE PRODUCTO SET "
+ " NOMBRE = ?, "
+ " DESCRIPCION = ?,"
+ " CANTIDAD = ?"
+ " WHERE ID = ?");
try(statement) {
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
statement.setInt(4, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
}
}
De esta forma nosotros ya no tenemos más que preocuparnos con estos comandos de close que ya fueron todos eliminados y no tenemos que estar preocupándonos más con abrí un recurso, lo tengo que cerrarr.
¿Por qué cuando utilizamos el try-with-resources no hay más la necesidad de explicitar el comando close para cerrar los recursos (PreparedStatement, Connection, PreparedStatement)? Seleccione una alternativa Por el hecho de que estos recursos extienden la interfaz AutoCloseable. ¡Alternativa correcta! Como estas interfaces extienden la interfaz AutoCloseable, el try-with-resources ejecuta el comando close implícitamente.
Cuando ejecutamos una query SQL como Statement tenemos un riesgo de seguridad llamado SQL Injection; SQL Injection es el hecho de enviar comandos SQL como parámetro de las solicitudes en una aplicación. Para evitar el fallo por SQL Injection debemos utilizar la interfaz PreparedStatement; Diferente del Statement, el PreparedStatement trata los parámetros del comando SQL para que caracteres y comandos especiales sean tratados como strings. Las bases de datos ofrecen un recurso llamado transacción, que junta muchas operaciones SQL como un conjunto de ejecución; Si el conjunto falla no es aplicada ninguna modificación y ocurre el rollback de la transacción. Todos los comandos del conjunto necesitan funcionar para que la transacción sea finalizada con un commit. Para garantizar el cierre de los recursos abiertos en el código, Java provee un recurso llamado try-with-resources para ayudarnos; Para utilizar este recurso es necesario que la clase utilizada (como la Connection) implemente la interfaz Autocloseable.
Nosotros tenemos una interfaz llamada JDBC - Con esta nos conectamos a la base de datos que deseemos.. Nosotros usamos mysql. App Java -> JDBC -> Driver Mysql -> Mysql - Con la ConnectionFactory, aplicamos la lógica para conectar. App Java -> ConnectionFactory -> JDBC -> Driver Mysql -> Mysql No podemos dejar que la aplicación siga abriendo conexiones sin límite, entonces debemos poder configurar una cantidad mínima, una cantidad máxima de conexiones. - Pool de Conexiones, se conecta con la JDBC App Java -> ConnectionFactory -> Pool de Conexiones -> JDBC -> Driver Mysql -> Mysql C3P0 Es decir.. tenemos un pool de conexiones que va a comunicarse con el JDBC.. para mantener una cantidad mínima y una cantidad máxima de conexiones abiertas en la aplicación para que pueda atender a las requisiciones sin que tenga una cola muy grande y que no ahogue la aplicación. El C3P0. Para utilizarlo bien vamos a aprovechar las ventajas del JDBC para utilizar una nueva interfaz llamada datasource. Esta interfaz va abstraer la implementación del pool de conexiones para nosotros y con esta nueva estructura. Mientras que, la connectionFactory que tenemos no va más a tener la responsabilidad de crear una conexión para nosotros. Lo que ella va a hacer ahora es solicitar una conexión abierta para el data source DataSoource: App Java -> ConnectionFactory -> - Pool de Conexiones -> JDBC -> Driver Mysql -> Mysql - C3P0 De esta forma, nosotros no tenemos nuestra aplicación con una configuración sostenible de conexiones abiertas, así no tenemos ningún problema de performance ni de agotamiento de recursos, tal como las aplicaciones reales del mercado, y por ahora es eso.
Imagina una aplicación que tiene un único cliente y él puede ejecutar una sola tarea por turno. Mientras una tarea termina no es posible ejecutar la siguiente. ¿Qué harías en esta aplicación para atender este escenario? Selecciona 2 alternativas Abrir una nueva conexión siempre que haya una nueva tarea para hacer. ¡Alternativa correcta! Con la forma que funciona la aplicación este abordaje es válido ya que solamente necesitaríamos una sola conexión. Abrir una sola conexión y mantenerla siempre abierta. ¡Alternativa correcta! Es un abordaje válido ya que la aplicación tiene un único cliente.
Modificamos el Pom.. Para el c3p0 y com.change
- C3P0: os va a dar la posibilidad de configurar un pool de conexiones
- mchange-commons-java: nos va a dar la posibilidad de agregar más detalles del datasource vía el log de la aplicación en la consola.
mysql
mysql-connector-java
8.0.26
com.mchange
c3p0
0.9.5.5
com.mchange
mchange-commons-java
0.2.20
Y ahora el connectionFactory lo modificamos.. Para evitar la conexion pura y..
La idea acá es que la connectionFactory utilice las conexiones desde el pool de conexiones.
package com.alura.jdbc.factory;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class ConnectionFactory {
private DataSource dataSource;
public ConnectionFactory() { // Creamos un constructor
var comboPooledDataSource = new ComboPooledDataSource(); // Creamos el ComboPooledDataSource
comboPooledDataSource.setJdbcUrl("jdbc:mysql://localhost/control_de_stock?useTimeZone=true&serverTimeZone=UTC");
comboPooledDataSource.setUser("root");
comboPooledDataSource.setPassword("root1234");
comboPooledDataSource.setMaxPoolSize(10);
this.dataSource = comboPooledDataSource; // Le asignamos el dataSource
}
public Connection recuperaConexion() throws SQLException {
return this.dataSource.getConnection();
}
}
Entonces ahora, la única diferencia es que en lugar de estar creando una conexión por el driver manager..
Nosotros estamos tomando la conexión desde el pool para realizar las operaciones en la base de datos.
En un escenario donde múltiples clientes pueden acceder a una misma aplicación simultáneamente. ¿Cuál sería el mejor abordaje? Seleccione una alternativa Reutilizar un conjunto de conexiones de tamaño fijo o dinámico. ¡Alternativa correcta! Esta es la estratégia de mantener un pool de conexiones. Vamos a abrir una cantidad específica de conexiones y reutilizarlas.
Ahora seteamos la máxima cantidad de conexiones..
package com.alura.jdbc.factory;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class ConnectionFactory {
private DataSource dataSource;
public ConnectionFactory() {
var comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setJdbcUrl("jdbc:mysql://localhost/control_de_stock?useTimeZone=true&serverTimeZone=UTC");
comboPooledDataSource.setUser("root");
comboPooledDataSource.setPassword("root1234");
comboPooledDataSource.setMaxPoolSize(10); // Un máximo de 10 conexiones abiertas..
this.dataSource = comboPooledDataSource;
}
public Connection recuperaConexion() throws SQLException {
return this.dataSource.getConnection();
}
}
Ahora creamos la clase PruebaPoolDeConexiones.. Para testear el máximo.
package com.alura.jdbc.pruebas;
import java.sql.SQLException;
import com.alura.jdbc.factory.ConnectionFactory;
public class PruebaPoolDeConexiones {
public static void main(String[] args) throws SQLException {
// Instanciamos una conexión..
ConnectionFactory factory = new ConnectionFactory();
for (int i = 0; i < 20; i++) { // Realizamos un 20 de conexiones..
factory.recuperaConexion(); //
System.out.println("Abriendo conexión #" + i);
}
}
}
Qué pasó con eso?
Nosotros solicitamos 20 conexiones, pero fueron abiertas las 10 conexiones del pool.
Está bien, el loop está parado, esperando que una conexión se quede disponible para utilizarla.
¿Eso también es así en el mundo real?
Sí, es así que funciona.
Si llegamos al tope de conexiones de pool, el próximo cliente va a tener que esperar un ratito hasta que el procesamiento de un cliente termine y él va a poder utilizar la próxima conexión libre.
De esta forma, nosotros limitamos la apertura descontrolada de conexiones y no saturamos la base de datos.
Y esta cola de espera para una próxima conexión es muy rara de suceder en el mundo real, porque los procesamientos son muy rápidos.
Desde consola luego de ejecutar podemos pasar el comando:
mysql> show processlist;
Para observar el listado de los procesos realizados..
En un pool de conexiones con 9 conexiones disponibles. Si todas las 9 conexiones están ocupadas y entra una décima requisición, ¿el usuario logra conectarse? Seleccione una alternativa El décimo usuario va a esperar que una de las 9 conexiones se libere. ¡Alternativa correcta! En el momento que una conexión quede disponible, el décimo cliente va a tener su requisición procesada.
Lo que aprendimos en esta aula: Utilizar el pool de conexiones es una buena práctica; El pool de conexiones controla la cantidad de conexiones abiertas entre la aplicación y la base de datos; Es normal que haya un mínimo y un máximo de conexiones. De la misma forma que hay, en JDBC, una interfaz para representar la conexión (java.sql.Connection), también hay una interfaz que representa el pool de conexiones (javax.sql.DataSource); C3P0 es una implementación Java de un pool de conexiones.
Hasta ahora venimos tratando los atributos de las tablas como variables sueltas, sin ninguna representatividad del producto en el código, tal como tenemos en la tabla de la base de datos.
La idea ahora es, en lugar de estar asignando variables sueltas, como estamos haciendo acá, recibidas como parámetros, vamos a crear una forma de representar el producto directamente en el proyecto en Java.
O sea, vamos a crear un modelo y para eso, nosotros vamos a crear una clase en un nuevo paquete y esta clase va a ser llamada de producto, para representar justamente la tabla de producto.
Creamos el paquete.. com.alura.jdbc.modelo
Y creamos la clase para el Producto, debe tener los mismos campos de la tabla.
package com.alura.jdbc.modelo;
public class Producto {
private Integer id;
private String nombre;
private String descripcion;
private Integer cantidad;
public Producto(String nombre, String descripcion, Integer cantidad) {
this.nombre = nombre;
this.descripcion = descripcion;
this.cantidad = cantidad;
}
public Producto(Integer id, String nombre, String descripcion, Integer cantidad) {
this.id = id;
this.nombre = nombre;
this.descripcion = descripcion;
this.cantidad = cantidad;
}
public Integer getId() {
return id;
}
public String getNombre() {
return nombre;
}
public String getDescripcion() {
return descripcion;
}
public Integer getCantidad() {
return cantidad;
}
public void setId(Integer id) {
this.id = id;
}
@Override // Sobreescribimos el método para el toString
public String toString() {
return String.format(
"{ id: %d, nombre: %s, descripcion: %s, cantidad: %d }",
this.id, this.nombre, this.descripcion, this.cantidad);
}
}
Ahora en el ProductoController
package com.alura.jdbc.controller;
import java.util.List;
import com.alura.jdbc.dao.ProductoDAO;
import com.alura.jdbc.factory.ConnectionFactory;
import com.alura.jdbc.modelo.Producto;
public class ProductoController {
private ProductoDAO productoDao;
public ProductoController() {
var factory = new ConnectionFactory();
this.productoDao = new ProductoDAO(factory.recuperaConexion());
}
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) {
return productoDao.modificar(nombre, descripcion, cantidad, id);
}
public int eliminar(Integer id) {
return productoDao.eliminar(id);
}
public List listar() {
return productoDao.listar();
}
public void guardar(Producto producto) {
productoDao.guardar(producto);
}
}
Creamos del método de arriba..
public void guardar(Producto producto) {
productoDao.guardar(producto);
}
Nosotros podríamos crear una clase que mantenga todos lo que es necesario, como la conexión de la base de datos para persistir el producto y solamente tengamos que basar el objeto como parámetro.
- Evitando código repetido.
Sería una clase más específica para realizar operaciones en la tabla de productos.
DAO = Data Access Object
Entonces creamos un package como ProductoDAO..
En este archivo tendremos todas las operaciones centralizadas que se refieren al acceso de la tabla de producto.
package com.alura.jdbc.persistencia;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import com.alura.jdbc.modelo.Producto;
public class ProductoDAO {
private Connection con;
public ProductoDAO(Connection con) {
this.con = con;
}
public void guardar(Producto producto) {
try {
PreparedStatement statement;
statement = con.prepareStatement(
"INSERT INTO PRODUCTO "
+ "(nombre, descripcion, cantidad)"
+ " VALUES (?, ?, ?)", Statement.RETURN_GENERATED_KEYS);
try (statement) {
statement.setString(1, producto.getNombre());
statement.setString(2, producto.getDescripcion());
statement.setInt(3, producto.getCantidad());
statement.execute();
final ResultSet resultSet = statement.getGeneratedKeys();
try (resultSet) {
while (resultSet.next()) {
producto.setId(resultSet.getInt(1));
System.out.println(String.format("Fue insertado el producto: %s", producto));
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public List listar() {
List resultado = new ArrayList<>();
try {
final PreparedStatement statement = con
.prepareStatement("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM PRODUCTO");
try (statement) {
statement.execute();
final ResultSet resultSet = statement.getResultSet();
try (resultSet) {
while (resultSet.next()) {
resultado.add(new Producto(
resultSet.getInt("ID"),
resultSet.getString("NOMBRE"),
resultSet.getString("DESCRIPCION"),
resultSet.getInt("CANTIDAD")));
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return resultado;
}
public int eliminar(Integer id) {
try {
final PreparedStatement statement = con.prepareStatement("DELETE FROM PRODUCTO WHERE ID = ?");
try (statement) {
statement.setInt(1, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) {
try {
final PreparedStatement statement = con.prepareStatement(
"UPDATE PRODUCTO SET "
+ " NOMBRE = ?, "
+ " DESCRIPCION = ?,"
+ " CANTIDAD = ?"
+ " WHERE ID = ?");
try (statement) {
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
statement.setInt(4, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
Aprendimos un nuevo patrón de diseño que es el DAO.
La finalidad de este patrón de diseño es tener un objeto que tiene como responsabilidad acceder a la base de datos y realizar las operaciones necesarias sobre la entidad.
¿Entonces este patrón sirve solamente para acceder a bases de datos?
No necesariamente.
Podemos acceder a cualquier fuente de datos con este patrón.
La idea es justamente centralizar las operaciones en él para evitar la replicación de código.
Agregamos una capa de persistencia a nuestra aplicación y ella va quedando cada momento más ordenada y completa, muy similar a las operaciones del mundo real.
¿Cuáles son las ventajas de utilizar clases con el estándar DAO? Seleccione una alternativa Tiene que ver con la capacidad de aislar en un lugar centralizado, toda la lógica de acceso al repositorio de datos de la entidad. ¡Alternativa correcta! Así estaremos evitando duplicación de código y centralización de la lógica.
* No es una buena práctica el hacer eso, delegar las excepciones de una capa para otras. Lo ideal es que las tratemos en el mismo lugar en donde el error ocurre.
Ahora en el mismo archivo de ConnectionFactory.. Tratamos la excepción.
package com.alura.jdbc.factory;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class ConnectionFactory {
private DataSource dataSource;
public ConnectionFactory() {
var comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setJdbcUrl("jdbc:mysql://localhost/control_de_stock?useTimeZone=true&serverTimeZone=UTC");
comboPooledDataSource.setUser("root");
comboPooledDataSource.setPassword("root1234");
comboPooledDataSource.setMaxPoolSize(10);
this.dataSource = comboPooledDataSource;
}
public Connection recuperaConexion() {
try { // Acá tratamos la excepción general.
return this.dataSource.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
La idea es que con eso, de a poco vayamos sacando el throws SQLException de los otros métodos también.
De esta manera no tenemos que estar propagando esa declaración de que este método lanza una excepción y eso va subiendo hasta el primer lugar que lo llama.
package com.alura.jdbc.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import com.alura.jdbc.modelo.Producto;
public class ProductoDAO {
private Connection con;
public ProductoDAO(Connection con) {
this.con = con;
}
public void guardar(Producto producto) {
try {
PreparedStatement statement;
statement = con.prepareStatement(
"INSERT INTO PRODUCTO "
+ "(nombre, descripcion, cantidad)"
+ " VALUES (?, ?, ?)", Statement.RETURN_GENERATED_KEYS);
try (statement) {
statement.setString(1, producto.getNombre());
statement.setString(2, producto.getDescripcion());
statement.setInt(3, producto.getCantidad());
statement.execute();
final ResultSet resultSet = statement.getGeneratedKeys();
try (resultSet) {
while (resultSet.next()) {
producto.setId(resultSet.getInt(1));
System.out.println(String.format("Fue insertado el producto: %s", producto));
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public List listar() {
List resultado = new ArrayList<>(); // Trtamos el objeto como tal
try {
final PreparedStatement statement = con
.prepareStatement("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM PRODUCTO");
try (statement) {
statement.execute();
final ResultSet resultSet = statement.getResultSet();
try (resultSet) {
while (resultSet.next()) {
resultado.add(new Producto(
resultSet.getInt("ID"),
resultSet.getString("NOMBRE"),
resultSet.getString("DESCRIPCION"),
resultSet.getInt("CANTIDAD")));
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return resultado;
}
public int eliminar(Integer id) {
try {
final PreparedStatement statement = con.prepareStatement("DELETE FROM PRODUCTO WHERE ID = ?");
try (statement) {
statement.setInt(1, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) {
try {
final PreparedStatement statement = con.prepareStatement(
"UPDATE PRODUCTO SET "
+ " NOMBRE = ?, "
+ " DESCRIPCION = ?,"
+ " CANTIDAD = ?"
+ " WHERE ID = ?");
try (statement) {
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
statement.setInt(4, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
Y creamos el constructor de la clase Producto..
public Producto(Integer id, String nombre, String descripcion, Integer cantidad) {
this.id = id;
this.nombre = nombre;
this.descripcion = descripcion;
this.cantidad = cantidad;
}
Ahora cada capa de aplicación tiene su responsabilidad bien definida.
La capa DAO acá se encarga de mantener toda la lógica de acceso a la base de datos, las queries y traducción de result set para la clase de producto que es nuestro model, y la capa de controller se encarga de llamar las demás clases para completar la operación solicitada por la view.
Y por último, la view, que es la que hace las requisiciones para el controller, para hacer todo esto que acá es nuestra pantallita del listado de productos y con el formulario que es la representación de nuestra clase ControlDeStockFrame.
Todo esto que acabo de comentar es un patrón de diseño conocido como MVC, Model View Controller.
Las clases DAO que creamos reciben la conexión en el constructor. Imaginemos que, en lugar de hacer así, tomemos una nueva conexión automáticamente en un constructor sin argumentos, como en el ejemplo: public class ProductoDAO { private final Connection con; ProductoDAO() { con = Database.getConnection(); } // restante de la lógica del DAO aquí } ¿Qué pasa si una tarea necesita acceder dos datos, como el ProductoDAO y el CategoriaDAO? ¿Cuál desventaja tenemos en este abordaje? Seleccione una alternativa No será posible utilizar la transacción. ¡Alternativa correcta! Como cada uno de los DAOs tiene una conexión distinta, ellos van a estar en distintas transacciones. Y por eso no será posible utilizar este recurso. Tendríamos que abrir una conexión únicamente para ejecutar esta tarea. ¡Alternativa incorrecta! Sería abierta una gran cantidad de conexiones ya que si tenemos dos DAOs van a ser dos conexiones. No hay ninguna desventaja ya que el resultado sería el mismo que pasar la conexión como un parámetro del constructor. ¡Alternativa incorrecta! Con esta estratégia perdemos el poder de utilizar transacciones y serían abiertas muchas conexiones sin necesidad.
Utilizando todo lo que aprendimos en la refactorización de la lógica de acceso a la capa de datos del ProductoController para la clase ProductoDAO es el momento de poner en práctica lo que aprendimos para refactorizar las operaciones de modificación y exclusión de productos siguiendo las buenas prácticas que aprendimos.
Empecemos por el método de eliminar de la clase ProductoController. Lo que vamos hacer primero es crear una llamada a productoDao.eliminar(id);. Este método aún no existe, entonces vamos a crearlo en el ProductoDAO y mover la lógica de exclusión del producto para allá. Vamos a dejarla más sencilla también, removiendo pedazos de código que ya no son más necesarios, como la de tomar una conexión.
Recordemos que no debemos más estar lanzando la SQLException para las demás capas. Los errores que una clase puede lanzar deben ser tratados en ella de forma que podamos lanzar, como máximo, una excepción del tipo unchecked.
Por último vamos a ajustar el código referente a la exclusión de un producto en la clase ControlDeStockFrame y listo. Podemos hacer lo mismo con el método de modificación.
Así va a quedar el resultado:
// ControlDeStockFrame
private void modificar() {
if (tieneFilaElegida()) {
JOptionPane.showMessageDialog(this, "Por favor, elije un item");
return;
}
Optional.ofNullable(modelo.getValueAt(tabla.getSelectedRow(), tabla.getSelectedColumn()))
.ifPresentOrElse(fila -> {
Integer id = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 0).toString());
String nombre = (String) modelo.getValueAt(tabla.getSelectedRow(), 1);
String descripcion = (String) modelo.getValueAt(tabla.getSelectedRow(), 2);
Integer cantidad = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 3).toString());
var filasModificadas = this.productoController.modificar(nombre, descripcion, cantidad, id);
JOptionPane.showMessageDialog(this, String.format("%d item modificado con éxito!", filasModificadas));
}, () -> JOptionPane.showMessageDialog(this, "Por favor, elije un item"));
}
private void eliminar() {
if (tieneFilaElegida()) {
JOptionPane.showMessageDialog(this, "Por favor, elije un item");
return;
}
Optional.ofNullable(modelo.getValueAt(tabla.getSelectedRow(), tabla.getSelectedColumn()))
.ifPresentOrElse(fila -> {
Integer id = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 0).toString());
var filasModificadas = this.productoController.eliminar(id);
modelo.removeRow(tabla.getSelectedRow());
JOptionPane.showMessageDialog(this,
String.format("%d item eliminado con éxito!", filasModificadas));
}, () -> JOptionPane.showMessageDialog(this, "Por favor, elije un item"));
}
// ProductoController
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) {
return productoDao.modificar(nombre, descripcion, cantidad, id);
}
public int eliminar(Integer id) {
return productoDao.eliminar(id);
}
// ProductoDAO
public int eliminar(Integer id) {
try {
final PreparedStatement statement = con.prepareStatement("DELETE FROM PRODUCTO WHERE ID = ?");
try (statement) {
statement.setInt(1, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) {
try {
final PreparedStatement statement = con.prepareStatement(
"UPDATE PRODUCTO SET "
+ " NOMBRE = ?, "
+ " DESCRIPCION = ?,"
+ " CANTIDAD = ?"
+ " WHERE ID = ?");
try (statement) {
statement.setString(1, nombre);
statement.setString(2, descripcion);
statement.setInt(3, cantidad);
statement.setInt(4, id);
statement.execute();
int updateCount = statement.getUpdateCount();
return updateCount;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
Este es un proyecto desarrollado en Java Swing, es la forma de desarrollar vistas de aplicación, así como el HTML para aplicaciones web. Pero la diferencia aquí es que ella no corre en un servidor de aplicaciones, sino en nuestra propia máquina, un ejecutable. Es una aplicación embebida. Esa pantalla acá tiene el formulario y el listado de productos que es construida por el ControlDeStockFrame, es responsable por presentar al usuario las informaciones buscadas en la base de datos de una forma ordenada. Esto aquí compone nuestra capa de view, que es la vista de la aplicación. El productoController pertenece a la capa de controller.. que es la capa que hace la conexión de la vista con la capa de datos y contiene las lógicas de negocio para manipular los datos antes de guardar en la base de datos o para devolver a la pantalla. Por último tenemos aquí la clase de productoDAO.. que es la que contiene toda la lógica relacionada a operaciones de la base de datos con la conexión, con la creación de queries, con la conversión de un objeto para hacer la query para insert, para update o delete o también para tomar el resultado y convertir en result set en un objeto del tipo producto para devolver a la pantalla. La clase productoDAO tiene la finalidad de realizar las operaciones directas en la tabla de producto. Entonces ella tiene una conexión directa con el modelo de producto. Si nosotros llegamos a tener nuevas tablas en la aplicación, nosotros vamos a crear nuevas clases DAO y nuevas clases de modelo también para representar a estas tablas en la aplicación y para realizar las operaciones sobre ellas. El conjunto de clases de modelo, producto y de la clase productoDAO, forman nuestra capa de modelo, la model, que representa las entidades del negocio y realiza las operaciones sobre sus informaciones. Para este conjunto de capas que revisamos ahora, le damos en nombre de modelo MVC, de Model View Controller. Este modelo es un estándar de arquitectura de aplicación que ayuda a dividir las responsabilidades de una aplicación. Este modelo tiene como ventajas, más allá de la división de las responsabilidades, la facilidad de mantenimiento, claridad y reutilización del código. El modelo MVC es sencillo y aún sigue siendo muy adoptado por empresas para desarrollar aplicaciones del mundo real. Hay otros modelos de arquitectura y variaciones de cada uno que pueden ser utilizados. Cada uno con sus ventajas y objetivos.
Para cada tabla del modelo tenemos una clase de dominio; Para la tabla de producto tenemos una clase Producto asociada. Los objetos del tipo Producto representan un registro de la tabla. Para acceder a la tabla vamos a utilizar el estándar llamado Data Access Object (DAO); Para cada clase de dominio hay un DAO asociado. Por ejemplo, la clase Producto posee la clase ProductoDAO. Todos los métodos JDBC relacionados al producto deben estar encapsulados en ProductoDAO. Una aplicación es escrita en capas; Las capas más conocidas son las de view, controller, modelo y persistencia, que componen el estándar MVC. El flujo de una requisición entre las capas es el siguiente; view <--> controller <--> persistencia En este curso utilizamos una aplicación con las views y los controllers ya creados y enfocamos en la capa de persistencia y modelo; No es una buena práctica dejar los detalles de implementación de una capa en otras que no tienen esta responsabilidad (por ejemplo la capa de controller lanzar una SQLException); Aquí estamos aprendiendo con una aplicación desktop embebida, pero hay otros tipos de aplicaciones con otros tipos de view, como html para aplicaciones web.
Tenemos que hacer una mejora en el modelo de producto para que cada uno pertenezca a una categoría y además será necesario abrir un reporte con el listado de todos los productos por categoría. Desde Consola: C:\Program Files\MySQL\MySQL Server 8.0\bin>mysql -u root -p Ingresamos la contraseña: Solocuestavida1! create database control_de_stock; use control_de_stock; select * from producto; Como podemos toparnos con palabras ambiguas.. Un campo de texto libre no es una buena solución para elegir la categoria. Podríamos crear una nueva tabla solo para registrar las categorías disponibles en la aplicación.. Así los usuarios tiene opciones precargadas para elegir, y evitamos posibles confusiones para referirnos a una misma categoría de productos. Ahora desde consola.. Creamos la tabal Categoria.. Mysql> CREATE TABLE CATEGORIA( -> id INT AUTO_INCREMENT, -> nombre VARCHAR(50) NOT NULL, -> PRIMARY KEY(id) -> )Engine=InnoDB; Agregamos las categorías.. mysql> INSERT INTO CATEGORIA(nombre) VALUES( -> 'Muebles'), ('Tecnología'), ('Cocina'), ('Zapatillas'); Para ver las categorías.. mysql> select * from categoria; Ahora nosotros podemos asignar las para cada producto que está en la tabla del producto.. Entonces para eso podremos decir que acá en la tabla producto, el producto mesa puede referirse al id de la categoría mueble. Pero para eso, para que tengamos esta referencia, nosotros tenemos que crear una columna nueva en la tabla de productos. mysql> ALTER TABLE PRODUCTO -> ADD COLUMN categoria_id INT; mysql> select * from producto; // Vemos la columna agregada. Ahora lo que tenemos que hacer es vincular las categorías a los productos que tenemos acá registrados. Clave Foránea: sta clave es la que va a vincular las dos tablas de forma que sea creada una relación entre ellas y para agregar esta clave foránea acá, nosotros tenemos que hacer un nuevo cambio en la estructura de la tabla de producto, para decir que la categoría id es referencia a la columna id de la tabla de categoría. VENTAJA DE LA LLAVE FORANEA, SETEADA ** Ahí está la ventaja de tener la clave foránea configurada porque evitamos de asignar valores inválidos de nuestras referencias.** mysql> ALTER TABLE PRODUCTO ADD FOREIGN KEY (CATEGORIA_ID) REFERENCES CATEGORIA(ID); Ahora sí podremos asignar el valor de las categorías a los productos.. mysql> UPDATE PRODUCTO SET CATEGORIA_ID = 1 WHERE ID = 1; SELECT * from producto; // Vemoa ahora sí, la mesa con la categoría asignada. El celular tendrá ahora la categoría de tecnología.. mysql> UPDATE PRODUCTO SET CATEGORIA_ID = 2 WHERE ID = 2; Por lo tanto: mysql> SELECT * from producto; +----+-----------------------+----------------------+----------+--------------+ | id | nombre | descripcion | cantidad | categoria_id | +----+-----------------------+----------------------+----------+--------------+ | 1 | Mesa | Mesa de 2 Lugares | 10 | 4 | | 2 | Celular SmarthPhone | Samsung AirForce | 40 | 2 | | 3 | Vaso | Vaso de Cristal | 2 | 3 | | 4 | Sillon de 4 cuerpos | Sillon de Terciopelo | 10 | 1 | | 6 | Linternas Recargables | Tipo Leds | 50 | 2 | +----+-----------------------+----------------------+----------+--------------+ 5 rows in set (0.00 sec) Con... mysql> SELECT * from CATEGORIA; +----+------------+ | id | nombre | +----+------------+ | 1 | Muebles | | 2 | Tecnología | | 3 | Cocina | | 4 | Zapatillas | +----+------------+ 4 rows in set (0.00 sec)
José quería agregar una categoría a los productos y resolvió crear en la tabla una nueva columna del tipo string, llamada categoria. Con eso fue posible informar la categoría del producto en el momento de su creación. ¿Por qué esta estratégia no es la mejor? Seleccione una alternativa Puede haber un problema para estandarizar la inserción de la categoría porque el campo sería libre para que los usuarios puedan escribir como quieran, generando muchas variaciones para una misma categoría. Eso perjudica la utilización de filtros por categoría. ¡Alternativa correcta! Como cada usuario iba a poder escribir libremente podrían haber diferentes formas en el nombre de las categorías. Podrían haber errores de tipeo o caracteres adicionales. Y eso dificulta el filtrado de los productos por ser casi imposible cubrir todos los escenarios.
Ahora modificamos el ControlDeStockFrame..
package com.alura.jdbc.view;
import java.awt.Color;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Optional;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.DefaultTableModel;
import com.alura.jdbc.controller.CategoriaController;
import com.alura.jdbc.controller.ProductoController;
import com.alura.jdbc.modelo.Categoria;
import com.alura.jdbc.modelo.Producto;
public class ControlDeStockFrame extends JFrame {
private static final long serialVersionUID = 1L;
private JLabel labelNombre, labelDescripcion, labelCantidad, labelCategoria;
private JTextField textoNombre, textoDescripcion, textoCantidad;
private JComboBox comboCategoria; // Instaciamos una categoria, que ahora crearemos.
private JButton botonGuardar, botonModificar, botonLimpiar, botonEliminar, botonReporte;
private JTable tabla;
private DefaultTableModel modelo;
private ProductoController productoController;
private CategoriaController categoriaController;
public ControlDeStockFrame() {
super("Productos");
this.categoriaController = new CategoriaController();
this.productoController = new ProductoController();
Container container = getContentPane();
setLayout(null);
configurarCamposDelFormulario(container);
configurarTablaDeContenido(container);
configurarAccionesDelFormulario();
}
private void configurarTablaDeContenido(Container container) {
tabla = new JTable();
modelo = (DefaultTableModel) tabla.getModel();
modelo.addColumn("Identificador del Producto");
modelo.addColumn("Nombre del Producto");
modelo.addColumn("Descripción del Producto");
modelo.addColumn("Cantidad");
cargarTabla();
tabla.setBounds(10, 205, 760, 280);
botonEliminar = new JButton("Eliminar");
botonModificar = new JButton("Modificar");
botonReporte = new JButton("Ver Reporte");
botonEliminar.setBounds(10, 500, 80, 20);
botonModificar.setBounds(100, 500, 80, 20);
botonReporte.setBounds(190, 500, 80, 20);
container.add(tabla);
container.add(botonEliminar);
container.add(botonModificar);
container.add(botonReporte);
setSize(800, 600);
setVisible(true);
setLocationRelativeTo(null);
}
private void configurarCamposDelFormulario(Container container) {
labelNombre = new JLabel("Nombre del Producto");
labelDescripcion = new JLabel("Descripción del Producto");
labelCantidad = new JLabel("Cantidad");
labelCategoria = new JLabel("Categoría del Producto");
labelNombre.setBounds(10, 10, 240, 15);
labelDescripcion.setBounds(10, 50, 240, 15);
labelCantidad.setBounds(10, 90, 240, 15);
labelCategoria.setBounds(10, 130, 240, 15);
labelNombre.setForeground(Color.BLACK);
labelDescripcion.setForeground(Color.BLACK);
labelCategoria.setForeground(Color.BLACK);
textoNombre = new JTextField();
textoDescripcion = new JTextField();
textoCantidad = new JTextField();
comboCategoria = new JComboBox<>();
comboCategoria.addItem(new Categoria(0, "Elige una Categoría")); // Ahora creamos en la categoría el constructor..
var categorias = this.categoriaController.listar();
categorias.forEach(categoria -> comboCategoria.addItem(categoria)); // Reccoremos las categorias
textoNombre.setBounds(10, 25, 265, 20);
textoDescripcion.setBounds(10, 65, 265, 20);
textoCantidad.setBounds(10, 105, 265, 20);
comboCategoria.setBounds(10, 145, 265, 20);
botonGuardar = new JButton("Guardar");
botonLimpiar = new JButton("Limpiar");
botonGuardar.setBounds(10, 175, 80, 20);
botonLimpiar.setBounds(100, 175, 80, 20);
container.add(labelNombre);
container.add(labelDescripcion);
container.add(labelCantidad);
container.add(labelCategoria);
container.add(textoNombre);
container.add(textoDescripcion);
container.add(textoCantidad);
container.add(comboCategoria);
container.add(botonGuardar);
container.add(botonLimpiar);
}
private void configurarAccionesDelFormulario() {
botonGuardar.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
guardar();
limpiarTabla();
cargarTabla();
}
});
botonLimpiar.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
limpiarFormulario();
}
});
botonEliminar.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
eliminar();
limpiarTabla();
cargarTabla();
}
});
botonModificar.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
modificar();
limpiarTabla();
cargarTabla();
}
});
botonReporte.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
abrirReporte();
}
});
}
private void abrirReporte() {
new ReporteFrame(this);
}
private void limpiarTabla() {
modelo.getDataVector().clear();
}
private boolean tieneFilaElegida() {
return tabla.getSelectedRowCount() == 0 || tabla.getSelectedColumnCount() == 0;
}
private void modificar() {
if (tieneFilaElegida()) {
JOptionPane.showMessageDialog(this, "Por favor, elije un item");
return;
}
Optional.ofNullable(modelo.getValueAt(tabla.getSelectedRow(), tabla.getSelectedColumn()))
.ifPresentOrElse(fila -> {
Integer id = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 0).toString());
String nombre = (String) modelo.getValueAt(tabla.getSelectedRow(), 1);
String descripcion = (String) modelo.getValueAt(tabla.getSelectedRow(), 2);
Integer cantidad = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 3).toString());
var filasModificadas = this.productoController.modificar(nombre, descripcion, cantidad, id);
JOptionPane.showMessageDialog(this, String.format("%d item modificado con éxito!", filasModificadas));
}, () -> JOptionPane.showMessageDialog(this, "Por favor, elije un item"));
}
private void eliminar() {
if (tieneFilaElegida()) {
JOptionPane.showMessageDialog(this, "Por favor, elije un item");
return;
}
Optional.ofNullable(modelo.getValueAt(tabla.getSelectedRow(), tabla.getSelectedColumn()))
.ifPresentOrElse(fila -> {
Integer id = Integer.valueOf(modelo.getValueAt(tabla.getSelectedRow(), 0).toString());
var filasModificadas = this.productoController.eliminar(id);
modelo.removeRow(tabla.getSelectedRow());
JOptionPane.showMessageDialog(this,
String.format("%d item eliminado con éxito!", filasModificadas));
}, () -> JOptionPane.showMessageDialog(this, "Por favor, elije un item"));
}
private void cargarTabla() {
var productos = this.productoController.listar();
productos.forEach(producto -> modelo.addRow(
new Object[] {
producto.getId(),
producto.getNombre(),
producto.getDescripcion(),
producto.getCantidad() }));
}
private void guardar() {
if (textoNombre.getText().isBlank() || textoDescripcion.getText().isBlank()) {
JOptionPane.showMessageDialog(this, "Los campos Nombre y Descripción son requeridos.");
return;
}
Integer cantidadInt;
try {
cantidadInt = Integer.parseInt(textoCantidad.getText());
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(this, String
.format("El campo cantidad debe ser numérico dentro del rango %d y %d.", 0, Integer.MAX_VALUE));
return;
}
var producto = new Producto(
textoNombre.getText(),
textoDescripcion.getText(),
cantidadInt);
var categoria = (Categoria) comboCategoria.getSelectedItem();
this.productoController.guardar(producto, categoria.getId());
JOptionPane.showMessageDialog(this, "Registrado con éxito!");
this.limpiarFormulario();
}
private void limpiarFormulario() {
this.textoNombre.setText("");
this.textoDescripcion.setText("");
this.textoCantidad.setText("");
this.comboCategoria.setSelectedIndex(0);
}
}
Ahora desde el modelo creamos la categoría en relación a ella..
package com.alura.jdbc.modelo;
import java.util.ArrayList;
import java.util.List;
public class Categoria {
private Integer id; // Los campos de la tabla de la base de datos
private String nombre; // Los campos de la tabla de la base de datos
private List productos = new ArrayList<>();
public Categoria(Integer id, String nombre) { // Definimos el constructor.
this.id = id;
this.nombre = nombre;
}
public Integer getId() {
return this.id;
}
@Override
public String toString() {
return this.nombre;
}
public void agregar(Producto producto) {
this.productos.add(producto);
}
public List getProductos() {
return this.productos;
}
}
Ahora en el categoriaController..
package com.alura.jdbc.controller;
import java.util.List;
import com.alura.jdbc.dao.CategoriaDAO;
import com.alura.jdbc.factory.ConnectionFactory;
import com.alura.jdbc.modelo.Categoria;
public class CategoriaController {
private CategoriaDAO categoriaDAO;
public CategoriaController() {
var factory = new ConnectionFactory();
this.categoriaDAO = new CategoriaDAO(factory.recuperaConexion());
}
public List listar() { // Tendremos la lista de categoria..
return this.categoriaDAO.listar();
}
public List cargaReporte() {
return this.categoriaDAO.listarConProductos();
}
}
Y creamos el CategoriaDAO..
package com.alura.jdbc.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.alura.jdbc.modelo.Categoria;
import com.alura.jdbc.modelo.Producto;
public class CategoriaDAO {
private Connection con; // Nos traemos el paquete de sql, para hacer la conexión..
public CategoriaDAO(Connection con) {
this.con = con;
}
public List listar() { // Para listar
List resultado = new ArrayList<>();
try {
String sql = "SELECT ID, NOMBRE FROM CATEGORIA";
System.out.println(sql);
final PreparedStatement statement = con // Hacemos el prepareStatement
.prepareStatement(sql);
try (statement) {
final ResultSet resultSet = statement.executeQuery(); // Es más directo el executeQuery()
try (resultSet) {
while (resultSet.next()) {
resultado.add(new Categoria(
resultSet.getInt("ID"),
resultSet.getString("NOMBRE")));
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return resultado;
}
public List listarConProductos() {
List resultado = new ArrayList<>();
try {
String sql = "SELECT C.ID, C.NOMBRE, P.ID, P.NOMBRE, P.CANTIDAD "
+ " FROM CATEGORIA C INNER JOIN PRODUCTO P "
+ " ON C.ID = P.CATEGORIA_ID";
System.out.println(sql);
final PreparedStatement statement = con
.prepareStatement(sql);
try (statement) {
final ResultSet resultSet = statement.executeQuery();
try (resultSet) {
while (resultSet.next()) {
int categoriaId = resultSet.getInt("C.ID");
String categoriaNombre = resultSet.getString("C.NOMBRE");
Categoria categoria = resultado
.stream()
.filter(cat -> cat.getId().equals(categoriaId))
.findAny().orElseGet(() -> {
Categoria cat = new Categoria(
categoriaId, categoriaNombre);
resultado.add(cat);
return cat;
});
Producto producto = new Producto(
resultSet.getInt("P.ID"),
resultSet.getString("P.NOMBRE"),
resultSet.getInt("P.CANTIDAD"));
categoria.agregar(producto);
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return resultado;
}
}
Soluciones - Para corroboar porque no me levantaba el jar.. Ir a la carpeta del proyecto, click derecho, properties, y buscar la opcion de java build path.. En esa pestaña nos indica las rutas de los archivos necesario para compilar. Buscamos la ruta y guardamos el archivo jar.,
Ahora nos encargaremos de la lógica para generar el Reporte..
Ahora en el productoController..
package com.alura.jdbc.controller;
import java.util.List;
import com.alura.jdbc.dao.ProductoDAO;
import com.alura.jdbc.factory.ConnectionFactory;
import com.alura.jdbc.modelo.Categoria;
import com.alura.jdbc.modelo.Producto;
public class ProductoController {
private ProductoDAO productoDao;
public ProductoController() {
var factory = new ConnectionFactory();
this.productoDao = new ProductoDAO(factory.recuperaConexion());
}
public int modificar(String nombre, String descripcion, Integer cantidad, Integer id) {
return productoDao.modificar(nombre, descripcion, cantidad, id);
}
public int eliminar(Integer id) {
return productoDao.eliminar(id);
}
public List listar() {
return productoDao.listar();
}
public void guardar(Producto producto, Integer categoriaId) {
producto.setCategoriaId(categoriaId);
productoDao.guardar(producto);
}
public List listar(Categoria categoria) { // Armamos este método para listar la categoría
return productoDao.listar(categoria);
}
}
Ahora desde el ProductoDao..
public List listar(Categoria categoria) {
List resultado = new ArrayList<>();
try {
String sql = "SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD "
+ " FROM PRODUCTO WHERE CATEGORIA_ID = ?";
System.out.println(sql);
final PreparedStatement statement = con.prepareStatement(
sql);
try (statement) {
statement.setInt(1, categoria.getId());
statement.execute();
final ResultSet resultSet = statement.getResultSet();
try (resultSet) {
while (resultSet.next()) {
resultado.add(new Producto(
resultSet.getInt("ID"),
resultSet.getString("NOMBRE"),
resultSet.getString("DESCRIPCION"),
resultSet.getInt("CANTIDAD")));
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return resultado;
}
Ahora en el ReporteFrame..
package com.alura.jdbc.view;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
import com.alura.jdbc.controller.CategoriaController;
public class ReporteFrame extends JFrame {
private void cargaReporte() { // Armamos este méotodo
var categorias = categoriaController.cargaReporte();
categorias.forEach(categoria -> {
modelo.addRow(new Object[] { categoria });
var productos = categoria.getProductos();
productos.forEach(producto -> modelo.addRow(new Object[] {
"", // Columna vacía..
producto.getNombre(),
producto.getCantidad()
}));
});
}
}
Esta solución que desarrollamos no está siguiendo las buenas prácticas de programación porque resolvimos un problema, pero creamos otros.
Esta situación es conocida como queries N + 1..
que es cuando para ejecutar una cierta funcionalidad, estamos yendo a la base de datos más de lo que es necesario cuando hay la posibilidad de ir una sola vez.
¿Cuál es el problema de la aplicación tener queries N + 1? Seleccione una alternativa Porque son utilizadas múltiples queries, aumentando la cantidad de acceso a la base de datos y, por consecuencia, empeorando la performance de la aplicación y del propio sistema de base de datos. ¡Alternativa correcta! Cuando las consultas son sencillas no hay problemas. Pero cuanto más complejidad van teniendo nuestras consultas hay la necesidad de buscar más informaciones de múltiples tablas, aumentando el acceso exponencialmente. Eso impacta gravemente la performance de la aplicación y del sistema de base de datos.
Para crear el reporte, nosotros utilizamos una solución que no es muy buena..
Ya que genera n queries en la base de datos, porque nosotros aquí en el cargaReporte para cada categoría estamos yendo a la base de datos para listar a los productos de esta categoría, eso genera muchas conexiones con la base de datos.
Y con el aumento del volumen de información y de utilización de la base de datos en la aplicación, eso puede generar problemas de performance más adelante.
package com.alura.jdbc.controller;
import java.util.List;
import com.alura.jdbc.dao.CategoriaDAO;
import com.alura.jdbc.factory.ConnectionFactory;
import com.alura.jdbc.modelo.Categoria;
public class CategoriaController {
private CategoriaDAO categoriaDAO;
public CategoriaController() {
var factory = new ConnectionFactory();
this.categoriaDAO = new CategoriaDAO(factory.recuperaConexion());
}
public List listar() {
return this.categoriaDAO.listar();
}
public List cargaReporte() {
return this.categoriaDAO.listarConProductos(); // Creamos este método..
}
}
Y ahora dentro del.. CategoriaController
public List listarConProductos() {
List resultado = new ArrayList<>();
try {
String sql = "SELECT C.ID, C.NOMBRE, P.ID, P.NOMBRE, P.CANTIDAD "
+ " FROM CATEGORIA C INNER JOIN PRODUCTO P " // El objetivo aquí ahora es cargar las informaciones de productos y las categorías punto en la misma query.
+ " ON C.ID = P.CATEGORIA_ID"; // INNER JOIN posibilita unificar dos tablas que tienen columnas en común y para nuestro caso la tabla producto tiene la columna categoría id, que hace referencia a la columna de id, a la de la tabla categoría.
// ON es la condición.. el id y la categoria_id, de acuerdo a la llave foránea.
System.out.println(sql);
final PreparedStatement statement = con
.prepareStatement(sql);
try (statement) {
final ResultSet resultSet = statement.executeQuery();
try (resultSet) {
while (resultSet.next()) {
int categoriaId = resultSet.getInt("C.ID");
String categoriaNombre = resultSet.getString("C.NOMBRE");
Categoria categoria = resultado
.stream() // Ahora filtramos en funcion del id..
.filter(cat -> cat.getId().equals(categoriaId))
.findAny().orElseGet(() -> {
Categoria cat = new Categoria(
categoriaId, categoriaNombre);
resultado.add(cat);
return cat;
});
Producto producto = new Producto( // Armamos el producto.
resultSet.getInt("P.ID"),
resultSet.getString("P.NOMBRE"),
resultSet.getInt("P.CANTIDAD"));
categoria.agregar(producto);
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return resultado;
}
Ahora creamos el constructor de Producto.. public Producto(String nombre, String descripcion, Integer cantidad) { this.nombre = nombre; this.descripcion = descripcion; this.cantidad = cantidad; }
José creó una relación entre dos tablas utilizando la clave foránea. Pero ahora tiene una duda de como hacer para buscar las informaciones relacionadas de las dos tablas en su aplicación. ¿Qué podemos decir a José que haga para relacionar las dos tablas en una sola búsqueda? Seleccione una alternativa José debe utilizar el INNER JOIN. ¡Alternativa correcta! Con el INNER JOIN José va a lograr buscar las informaciones que están relacionadas entre las dos tablas. Cuando tenemos una relación entre tables debemos tener cuidado para no crear el problema de queries N + 1; N + 1 quiere decir que, para buscar los datos de una relación, es ejecutada una query y luego una más por cada relación. Este tipo de problema puede generar problemas de performance en la aplicación y en la base de datos. Este tipo de problema puede ser evitado utilizando join en la query SQL.