Ya estoy inscrito ¿Todavía no tienes acceso? Nuestros Planes
Ya estoy inscrito ¿Todavía no tienes acceso? Nuestros Planes
4
respuestas

[Duda] Problema con clase DAO, conexión cerrada y buenas prácticas

Bueno decir que me esperé a terminar el curso para ver si este tema era abordado en otro punto del mismo pero no fue así, igualmente ya revisé las respuestas a posts similares al mío por lo que ver otras respuestas tampoco aclaró mi situación, es por eso que realizo esta consulta. Decir que mi consulta va más enfocada hacia una cuestión de buenas prácticas y no a solucionar el error del código.

Aclarando lo anterior comienzo con mí duda, al momento de llegar al punto donde se crea la clase ProductoDAO y se comienzan a migrar los métodos de ProductoController para esta nueva clase se llega a un punto donde se migra el código "listar" y se deja como desafio crear los úlitimos dos métodos restantes, adjunto el código para el método listar. Adjunto el código migrado a la clase ProductoDAO por el profesor:

public List<Producto> listar() {
        List<Producto> resultado = new ArrayList<>();
        try (con) {

            final PreparedStatement statement = con
                    .prepareStatement("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM producto");

            try (statement) {

                boolean result = statement.execute();
                System.out.println(result);

                final ResultSet resultSet = statement.getResultSet();

                try (resultSet) {

                    while (resultSet.next()) {
                        Producto fila = new Producto(resultSet.getInt("ID"),
                                resultSet.getString("NOMBRE"),
                                resultSet.getString("DESCRIPCION"),
                                resultSet.getInt("CANTIDAD"));

                        resultado.add(fila);
                    }
                }
                return resultado;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

Hasta ese punto todo bien pues en el video solo se ve que el instructor abre la aplicación pero no la utiliza lo que genera todo el conflicto, al migrar el código tal como lo hace el profesor el método listar rompe la aplicación ya que cada que carga la base de datos cierra la conexión abierta en productoDAO y los demás métodos no se pueden utilizar mandando error en la consola "no se puede operar sobre una conexión cerrada", esto se debe a que como podemos ver el instructor utiliza el try-with-resouces en la conexión al comienzo del codigo lo que cerrará la conexión automaticamente.

Aquí comienza el problema, desde el inicio del curso así como dentro del curso anterior de base de datos se ha dicho que siempre que una conexión se abre es necesario cerrarla, ya que no hacerlo claramente es un error grave considerado una mala práctica, así que retomando esto la solución parece ser obvia: crear una nueva conexión en cada método lo cual se reafirma con la respuesta de la instructura Génesys Rondón en el siguiente post https://app.aluracursos.com/forum/topico-duda-cierre-de-conexion-para-otras-consultas-175209 .Sin embargo en posteriores videos y hasta el final del curso se ve al instructor ocupar el siguiente código:

public List<Producto> listar(Integer categoriaId) {
        List<Producto> resultado = new ArrayList<>();
        try {

            final PreparedStatement statement = con
                    .prepareStatement("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM producto "
                                    + "WHERE categoria_id = ?");

            try (statement) {

                statement.setInt(1, categoriaId);
                statement.execute();

                final ResultSet resultSet = statement.getResultSet();

                try (resultSet) {

                    while (resultSet.next()) {
                        Producto fila = new Producto(resultSet.getInt("ID"),
                                resultSet.getString("NOMBRE"),
                                resultSet.getString("DESCRIPCION"),
                                resultSet.getInt("CANTIDAD"));

                        resultado.add(fila);
                    }
                }
                return resultado;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

Como se puede ver en lugar de optar por abrir una conexión en cada método decide sin explicar el por qué quitar el try-with-resouces lo cual se repite en la soluciones brindadas dentro del desafio, claro que sí soluciona el problema pero a costa de dejar la conexión abierta, y en ningun video explica el por qué de esta desición ni tampoco se ve que delegue el dato importante de cerrar la conexión a otra instancia de la aplicación lo que genera mis dudas:

Qué es lo mejor: Tal cual como en el curso ¿Abrir la conexión y dejarla abierta? lo que claramente se nos ha indicado que no hagamos.

¿Abrir una conexión nueva en cada método? lo cual evidentemente hace que se repita una linea de código en cada método ¿Esto se consideraría una mala práctica por el DRY?

¿Cuál es el enfoque correcto y por qué?

También aprovecho para sugerir que estaría bien que aclararan esto dentro del mismo curso, si no se quiere grabar un video para esto se puede poner a modo de texto, es una duda recurrente por lo que más de uno lo agradecerá en el futuro, como digo la duda no es por solucionar el código, el problema radica más en los enfoques que se toman sin explicar ya que generan una incongruencia con lo que nos explican al inicio del curso sin darnos el motivo.

De antemano muchas gracias por su tiempo.

4 respuestas

Yo tambien comente esto en el discord pero nadie lo respondio. Yo considero que se debe corregir este error del intuctor dado que nunca se explico por que se realizo el cambio del codigo y esto puede generar algunas confusiones y tambien considero que fue una mala practica del instructor dado que tambien en videos hace pequeñas correciones de codigo de un video a aotro y no se explica por que.

Si alguien tiene duda como corregir el error para poder seguir usando los metodos con el try with resources, la solucion que encontré es que en la clase ControlDeStockFrame en cada uno de los metodos guardar, modificar y eliminar, se debe instanciar un nuevo productoControler con "this.productoController = new ProductoController();" antes de llamar el metodo productoController.guardar por ejemplo, ya con esto va iniciar el constructor y va a recuperar la conexion en cada metodo.

//clase ControlDeStockFrame.java

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 = comboCategoria.getSelectedItem();

        this.productoController = new ProductoController();  //aqui se intancia ProductoController para que llame constructor y se recupere la conexion        
        this.productoController.guardar(producto);        

        JOptionPane.showMessageDialog(this, "Registrado con éxito!");

        this.limpiarFormulario();
    }

Bastante interesante tu solución compañero Angel yo al comienzo pensé en una solución silimar solo que no quería instanciar el nuevo objeto dentro de la clase frame ya que siguiendo el patron de diseño M-V-C las views(frames) debereían hacer relación unicamente al aspecto gráfico propocionado por la interfaz y reducir ahí minimamente la lógica, por lo que despues de pensarlo decidí instanciar el objeto nuevo dentro de la clase controller, que al final es justamente la clase encargada en comunicar las views con el modelo, y también para solucionar el problema de la conexión nunca cerrada o cerrada antes de tiempo, decidí pasar toda la lógica de conexión que antes se iniciaba en ProductoController a la clase ProductoDAO que al final es la clase especifica para ecapsular las conexiones y las instrucciones a la base de datos. Cabe decir que de esta forma logre solucionar algunas de mis dudas aunque aún no tengo claro cual sería el enfoque correcto aunque de esta forma, evito repetir código por lo que no caigo en el DRY, al mismo tiempo que evito dejar la conexión siempre abierta, comparto mi código de la clase ProductoController y de la clase ProductoDAO para que puedas ver mejor en codigo mi solución y brindarme tu opinión, o igual cualquier otro compañero/instructor. Sirve que igualmente comparto mi solución como el compañero para que en esta duda tan recurrente haya más enfoques de como solucionar el conflicto.

Clase ProductoController

public class ProductoController {

    public int modificar(String nombre, String descripcion, Integer id, Integer cantidad) {
        //cada método regresa un nuevo objetoDAO que es creado abriendo y cerrando la conexión ocupada
        return new ProductoDAO().modificar(nombre, descripcion, id, cantidad); 
    }

    public int eliminar(Integer id) {
        return new ProductoDAO().eliminar(id);
    }

    public List<Producto> listar() {
        return new ProductoDAO().listar();
    }

    public List<Producto> listar(Categoria categoria) {
        return new ProductoDAO().listar(categoria.getId());
    }

    public void guardar(Producto producto, Integer categoriaId) {
        producto.setCategoriaId(categoriaId);
        new ProductoDAO().guardar(producto);
    }
}

De esta forma considero que queda una clase más limpia enfocada unicamente en actuar como controlador o lazo entre las dos capaz el frame y la conexión.

Y la clase ProductoDAO conteniendo toda la lógica de conexión e instrucciones quedaría de la siguiente forma:

public class ProductoDAO {
    final private Connection con;

    //se modifica el constructor dado por el instructor para utilizar uno sin parametros y que automaticamente crea una nueva conexión del pool que posteriormente será cerrada automaticamente por los try-with-resources
    public ProductoDAO() {
        con = new ConnectionFactory().recuperaConexion();
    }

    public void guardar(Producto producto) {
        try (con) {
            final PreparedStatement statement = con.prepareStatement("INSERT INTO producto "
                    + "(nombre, descripcion, cantidad, categoria_id)"
                    + "VALUES (?, ?, ?, ?)",
                    Statement.RETURN_GENERATED_KEYS);
            try (statement) {
                ejecutaRegistro(producto, statement);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private void ejecutaRegistro(Producto producto, PreparedStatement statement)
            throws SQLException {
        statement.setString(1, producto.getNombre());
        statement.setString(2, producto.getDescripcion());
        statement.setInt(3, producto.getCantidad());
        statement.setInt(4, producto.getCategoriaId());

        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));
            }

        }
    }

    public List<Producto> listar() {
        List<Producto> resultado = new ArrayList<>();
        try (con) {

            final PreparedStatement statement = con
                    .prepareStatement("SELECT ID, NOMBRE, DESCRIPCION, CANTIDAD FROM producto");

            try (statement) {

                boolean result = statement.execute();
                System.out.println(result);

                final ResultSet resultSet = statement.getResultSet();

                try (resultSet) {

                    while (resultSet.next()) {
                        Producto fila = new Producto(resultSet.getInt("ID"),
                                resultSet.getString("NOMBRE"),
                                resultSet.getString("DESCRIPCION"),
                                resultSet.getInt("CANTIDAD"));

                        resultado.add(fila);
                    }
                }
                return resultado;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

Tuve que eliminar parte del codigo de ProductoDAO por eso del límite de 5000 palabras pero creo con esto se pueden dar una idea clara de como se desarrolla el resto del código que es igual al del profesor solamente dejando los try-with-resources para que cierren la conexión automaticamente.

Excelente solucion la que propone nuestro compañero Manuel, modifque el codigo como el lo explica y funciona, antes de estas modificaciones no me dejaba hacer ninguna operacion pues se cerraban las conexiones antes de hacer alguna operacion.

Muy buenas soluciones, excelente aporte.