Trabajar con PostgreSQL en Java

    Introducción

    PostgreSQL (que se conoce con el apodo de Postgres) es famoso por su naturaleza relacional de objeto. Por el contrario, otros sistemas de bases de datos suelen ser relacionales. Debido a su naturaleza, es una gran combinación con Java, que está muy orientado a objetos.

    Acceder a una base de datos de Postgres usando Java requiere que confíe en el API de JDBC, como habrás sospechado. Debido a esto, las rutinas de Postgres y las de otros sistemas de bases de datos son similares. Aún así, eso no oculta el hecho de que Postgres ofrece capacidades adicionales, como un soporte extendido para tipos de datos personalizados y grandes conjuntos de datos.

    ¿Qué es PostgreSQL?

    PostgreSQL es un derivado del ahora desaparecido POSTGRES proyecto. POSTGRES tenía como objetivo lograr no solo la orientación a objetos, sino también la extensibilidad. No obstante, la Universidad de California detuvo el desarrollo de POSTGRES en 1994.

    Las primeras versiones de Postgres estaban dirigidas a equipos UNIX. Sin embargo, a lo largo de los años, la base de datos se ha vuelto portátil. Por lo tanto, puede encontrarlo en sistemas MacOS, Linux y Windows.

    Su licencia de código abierto y gratuita también se ha sumado a su adopción generalizada. A los desarrolladores les encanta, en parte, porque pueden indagar en las fuentes para descubrir cómo funciona exactamente.

    Aplicación de demostración

    Una guía de Postgres está incompleta sin una implementación CRUD adjunta. Escribiremos una aplicación Java simple que puede crear, leer, actualizar y eliminar información del cliente de una base de datos de Postgres.

    Por supuesto, comenzaremos definiendo las entidades y luego usándolas para generar el esquema de la base de datos para asegurarnos de que las tablas estén mapeadas correctamente.

    Y como exige la API adecuada, la capa de lógica empresarial no debe tener una idea de lo que sucede en la capa de la base de datos, una práctica conocida como arquitectura en capas. Por lo tanto, optaremos por el patrón Data Access Object (DAO) para satisfacer esta necesidad.

    Dependencia de Maven

    Empezaremos con un maven-archetype-quickstart para un proyecto esqueleto simple de Maven a través de su terminal:

    $ mvn archetype:generate -DgroupId=com.Pharos.sh.postgresql -DartifactId=java-postgresql-sample -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
    

    Después de ejecutar el comando, debería terminar con una estructura como esta:

    java-postgresql-sample
    ├── src
    |   ├── main
    |      ├── java
    |         ├── com
    |            ├── Pharos.sh
    |               ├── postgresql
    └── test
    

    Entonces, en tu pom.xml archivo, agregue la dependencia de Postgres:

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>{version}</version>
    </dependency>
    

    Modelo de dominio

    Hagamos un directorio llamado api en nuestro src directorio en el que definiremos un modelo / entidad – Customer:

    public class Customer {
        private Integer id;
        private String firstName;
        private String lastName;
        private String email;
    
        // Constructor, getters and setters...
    
        @Override
        public String toString() {
            return "Customer["
                    + "id=" + id
                    + ", firstName=" + firstName
                    + ", lastName=" + lastName
                    + ", email=" + email
                    + ']';
        }
    }
    

    Esta entidad se mapeará en nuestra base de datos de Postgres con sus respectivos campos un poco más adelante.

    Funcionalidad CRUD

    Como estamos trabajando de acuerdo con el patrón DAO, comencemos a implementar nuestra funcionalidad CRUD a través de un Dao interfaz en el spi directorio, que albergará todas nuestras interfaces y clases de servicio:

    public interface Dao<T, I> {
        Optional<T> get(int id);
        Collection<T> getAll();
        Optional<I> save(T t);
        void update(T t);
        void delete(T t);
    }
    

    Tenga en cuenta los dos genéricos de nivel de clase: T y I. T representa el objeto de clase real para pasar hacia y desde la base de datos, mientras que I es la clase de la clave principal de la entidad.

    Ahora tenemos el esqueleto CRUD y el objeto de dominio en su lugar. Con esos dos hechos, podemos seguir adelante y crear nuestra base de datos.

    Creación de una base de datos PosgreSQL

    Siga la guía de instalación de PostgreSQL para la plataforma que está utilizando; la instalación es bastante sencilla. Con Postgres en su lugar, usaremos pgAdmin para gestionar la instalación.

    En nuestro localhost sistema, crearemos una base de datos llamada sampledb y crear una mesa para nuestro Customers:

    Para hacer esto, en pgAdmin ejecutaremos la entrada en el editor de consultas:

    CREATE TABLE public.customer
    (
        customer_id integer NOT NULL GENERATED ALWAYS AS IDENTITY (START 1 INCREMENT 1 ),
        first_name character varying(45) NOT NULL,
        last_name character varying(45) NOT NULL,
        email character varying(50),
        CONSTRAINT customer_pkey PRIMARY KEY (customer_id)
    )
    

    Y así, hemos generado la tabla para Customers.

    Conectarse a la base de datos

    Antes de que podamos ejecutar cualquier declaración en la base de datos desde nuestro código, primero necesitaremos configurar una conexión a la base de datos. Haremos esto a través de un JdcbConnection clase:

    public class JdbcConnection {
    
        private static final Logger LOGGER =
            Logger.getLogger(JdbcConnection.class.getName());
        private static Optional<Connection> connection = Optional.empty();
    
        public static Optional<Connection> getConnection() {
            if (connection.isEmpty()) {
                String url = "jdbc:postgresql://localhost:5432/sampledb";
                String user = "postgres";
                String password = "postgres";
    
                try {
                    connection = Optional.ofNullable(
                        DriverManager.getConnection(url, user, password));
                } catch (SQLException ex) {
                    LOGGER.log(Level.SEVERE, null, ex);
                }
            }
    
            return connection;
        }
    }
    

    La tarea principal de la clase anterior es recuperar una conexión a la base de datos. Como puede que no siempre devuelva un valor no nulo Connection objeto, la conexión está envuelta en un Optional.

    La otra cosa notable es que la conexión es una variable estática. Por lo tanto, la clase devuelve la primera instancia de conexión no nula que obtuvo en su ejecución inicial.

    Agregar entidades

    Dado que ahora podemos conectarnos a la base de datos, sigamos adelante e intentemos crear una entidad en la base de datos. Para hacerlo, definiremos un PostgreSqlDao clase que implementa el mencionado Dao interfaz:

    public class PostgreSqlDao implements Dao<Customer, Integer> {
    
        private static final Logger LOGGER =
            Logger.getLogger(PostgreSqlDao.class.getName());
        private final Optional<Connection> connection;
    
        public PostgreSqlDao() {
            this.connection = JdbcConnection.getConnection();
        }
    
        @Override
        public Optional<Integer> save(Customer customer) {
            String message = "The customer to be added should not be null";
            Customer nonNullCustomer = Objects.requireNonNull(customer, message);
            String sql = "INSERT INTO "
                    + "customer(first_name, last_name, email) "
                    + "VALUES(?, ?, ?)";
    
            return connection.flatMap(conn -> {
                Optional<Integer> generatedId = Optional.empty();
    
                try (PreparedStatement statement =
                     conn.prepareStatement(
                        sql,
                        Statement.RETURN_GENERATED_KEYS)) {
    
                    statement.setString(1, nonNullCustomer.getFirstName());
                    statement.setString(2, nonNullCustomer.getLastName());
                    statement.setString(3, nonNullCustomer.getEmail());
    
                    int numberOfInsertedRows = statement.executeUpdate();
    
                    // Retrieve the auto-generated id
                    if (numberOfInsertedRows > 0) {
                        try (ResultSet resultSet = statement.getGeneratedKeys()) {
                            if (resultSet.next()) {
                                generatedId = Optional.of(resultSet.getInt(1));
                            }
                        }
                    }
    
                    LOGGER.log(
                        Level.INFO,
                        "{0} created successfully? {1}",
                         new Object[]{nonNullCustomer,
                                (numberOfInsertedRows > 0)});
                } catch (SQLException ex) {
                    LOGGER.log(Level.SEVERE, null, ex);
                }
    
                return generatedId;
            });
        }
    
        // Other methods of the interface which currently aren't implemented yet
    }
    

    Después de crear un Customer objeto, puede pasarlo al save método de PostgreSqlDao para agregarlo a la base de datos.

    los save El método utiliza una cadena SQL para operar:

    INSERT INTO customer(first_name, last_name, email) VALUES(?, ?, ?)
    

    Usando la conexión de la base de datos, el DAO luego prepara la declaración:

    PreparedStatement statement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)
    

    De interés es que la declaración contiene la bandera Statement.RETURN_GENERATED_KEYS. Esto asegura que la base de datos también informe la clave principal que creó para la nueva fila.

    También vale la pena señalar que el save El método utiliza la función de mapeo de Java. Transforma la conexión de la base de datos en el tipo de retorno que requiere el método. Y más aún, usa un flatMap función para asegurarse de que el valor que devuelve no tiene un Optional envase.

    Los métodos CRUD restantes de PostgreSqlDao debe seguir la misma premisa. Deben asignar la conexión a una devolución, cuando sea necesario, y verificar si la conexión existe primero antes de operar con ella.

    Entidades de lectura

    En nuestra implementación, hemos decidido tener un método que devuelva un solo Customer basado en su idy un método que devuelve todos los clientes persistentes de la base de datos.

    Empecemos por lo simple .get() método que devuelve un solo Customer con el correspondiente id:

    public Optional<Customer> get(int id) {
        return connection.flatMap(conn -> {
            Optional<Customer> customer = Optional.empty();
            String sql = "SELECT * FROM customer WHERE customer_id = " + id;
    
            try (Statement statement = conn.createStatement();
                    ResultSet resultSet = statement.executeQuery(sql)) {
    
                if (resultSet.next()) {
                    String firstName = resultSet.getString("first_name");
                    String lastName = resultSet.getString("last_name");
                    String email = resultSet.getString("email");
    
                    customer = Optional.of(
                        new Customer(id, firstName, lastName, email));
    
                    LOGGER.log(Level.INFO, "Found {0} in database", customer.get());
                }
            } catch (SQLException ex) {
                LOGGER.log(Level.SEVERE, null, ex);
            }
    
            return customer;
        });
    }
    

    El código es bastante sencillo. Ejecutamos la consulta a través de nuestro Statement objeto y empaquetar los resultados en un ResultSet. Luego, extraemos la información del ResultSet y empaquetarlo en un constructor para un Customer, que se devuelve.

    Ahora, implementemos el .getAll() método:

    public Collection<Customer> getAll() {
        Collection<Customer> customers = new ArrayList<>();
        String sql = "SELECT * FROM customer";
    
        connection.ifPresent(conn -> {
            try (Statement statement = conn.createStatement();
                    ResultSet resultSet = statement.executeQuery(sql)) {
    
                while (resultSet.next()) {
                    int id = resultSet.getInt("customer_id");
                    String firstName = resultSet.getString("first_name");
                    String lastName = resultSet.getString("last_name");
                    String email = resultSet.getString("email");
    
                    Customer customer = new Customer(id, firstName, lastName, email);
    
                    customers.add(customer);
    
                    LOGGER.log(Level.INFO, "Found {0} in database", customer);
                }
    
            } catch (SQLException ex) {
                LOGGER.log(Level.SEVERE, null, ex);
            }
        });
    
        return customers;
    }
    

    Una vez más, bastante sencillo: ejecutamos la consulta SQL adecuada, extraemos la información, creamos Customer objetos y empáquelos en un ArrayList.

    Actualización de entidades

    A continuación, si alguna vez deseamos actualizar una entidad después de crearla, necesitamos tener un .update() método:

    public void update(Customer customer) {
        String message = "The customer to be updated should not be null";
        Customer nonNullCustomer = Objects.requireNonNull(customer, message);
        String sql = "UPDATE customer "
                + "SET "
                + "first_name = ?, "
                + "last_name = ?, "
                + "email = ? "
                + "WHERE "
                + "customer_id = ?";
    
        connection.ifPresent(conn -> {
            try (PreparedStatement statement = conn.prepareStatement(sql)) {
    
                statement.setString(1, nonNullCustomer.getFirstName());
                statement.setString(2, nonNullCustomer.getLastName());
                statement.setString(3, nonNullCustomer.getEmail());
                statement.setInt(4, nonNullCustomer.getId());
    
                int numberOfUpdatedRows = statement.executeUpdate();
    
                LOGGER.log(Level.INFO, "Was the customer updated successfully? {0}",
                        numberOfUpdatedRows > 0);
    
            } catch (SQLException ex) {
                LOGGER.log(Level.SEVERE, null, ex);
            }
        });
    }
    

    Nuevamente, preparamos una declaración y ejecutamos la consulta de actualización basada en los campos y id del Customer pasado al método de actualización.

    Eliminar entidades

    Y finalmente, a veces es posible que deseemos eliminar una entidad, y para ese propósito, el .delete() se utiliza el método:

    public void delete(Customer customer) {
        String message = "The customer to be deleted should not be null";
        Customer nonNullCustomer = Objects.requireNonNull(customer, message);
        String sql = "DELETE FROM customer WHERE customer_id = ?";
    
        connection.ifPresent(conn -> {
            try (PreparedStatement statement = conn.prepareStatement(sql)) {
    
                statement.setInt(1, nonNullCustomer.getId());
    
                int numberOfDeletedRows = statement.executeUpdate();
    
                LOGGER.log(Level.INFO, "Was the customer deleted successfully? {0}",
                        numberOfDeletedRows > 0);
    
            } catch (SQLException ex) {
                LOGGER.log(Level.SEVERE, null, ex);
            }
        });
    }
    

    Nuevamente, basado en el Customeres id, la consulta de eliminación se ejecuta para eliminar la entidad.

    Ejecutando la Aplicación

    Después de desarrollar la implementación de DAO, el proyecto ahora necesita un punto de entrada. El mejor lugar para esto sería en el main método estático:

    public class CustomerApplication {
    
        private static final Logger LOGGER =
            Logger.getLogger(CustomerApplication.class.getName());
        private static final Dao<Customer, Integer> CUSTOMER_DAO = new PostgreSqlDao();
    
        public static void main(String[] args) {
            // Test whether an exception is thrown when
            // the database is queried for a non-existent customer.
            // But, if the customer does exist, the details will be printed
            // on the console
            try {
                Customer customer = getCustomer(1);
            } catch (NonExistentEntityException ex) {
                LOGGER.log(Level.WARNING, ex.getMessage());
            }
    
            // Test whether a customer can be added to the database
            Customer firstCustomer =
                new Customer("Manuel", "Kelley", "[email protected]");
            Customer secondCustomer =
                new Customer("Joshua", "Daulton", "[email protected]");
            Customer thirdCustomer =
                new Customer("April", "Ellis", "[email protected]");
            addCustomer(firstCustomer).ifPresent(firstCustomer::setId);
            addCustomer(secondCustomer).ifPresent(secondCustomer::setId);
            addCustomer(thirdCustomer).ifPresent(thirdCustomer::setId);
    
            // Test whether the new customer's details can be edited
            firstCustomer.setFirstName("Franklin");
            firstCustomer.setLastName("Hudson");
            firstCustomer.setEmail("[email protected]");
            updateCustomer(firstCustomer);
    
            // Test whether all customers can be read from database
            getAllCustomers().forEach(System.out::println);
    
            // Test whether a customer can be deleted
            deleteCustomer(secondCustomer);
        }
    
        // Static helper methods referenced above
        public static Customer getCustomer(int id) throws NonExistentEntityException {
            Optional<Customer> customer = CUSTOMER_DAO.get(id);
            return customer.orElseThrow(NonExistentCustomerException::new);
        }
    
        public static Collection<Customer> getAllCustomers() {
            return CUSTOMER_DAO.getAll();
        }
    
        public static void updateCustomer(Customer customer) {
            CUSTOMER_DAO.update(customer);
        }
    
        public static Optional<Integer> addCustomer(Customer customer) {
            return CUSTOMER_DAO.save(customer);
        }
    
        public static void deleteCustomer(Customer customer) {
            CUSTOMER_DAO.delete(customer);
        }
    }
    

    Dado que los métodos CRUD de PostgreSqlDao son públicos, lo ajustaremos para evitar la exposición de la capa de la base de datos al resto del código cuando no sea necesario.

    Una vez hecho esto, hay otras dos clases de excepciones personalizadas que deben implementarse. Estos son NonExistentEntityException:

    public class NonExistentEntityException extends Throwable {
    
        private static final long serialVersionUID = -3760558819369784286L;
    
        public NonExistentEntityException(String message) {
            super(message);
        }
    }
    

    Y su heredero, NonExistentCustomerException:

    public class NonExistentCustomerException extends NonExistentEntityException {
    
        private static final long serialVersionUID = 8633588908169766368L;
    
        public NonExistentCustomerException() {
            super("Customer does not exist");
        }
    }
    

    Estas dos clases manejan excepciones que DAO lanza cuando un Customer no existe para hacer que el manejo de excepciones sea un poco más amigable.

    Conclusión

    Hemos visto cómo crear una aplicación CRUD basada en Postgres. Los pasos muestran que en realidad es un asunto trivial configurar el back-end de Postgres. Vincular un modelo de dominio de Java a una conexión de base de datos de Postgres requiere un poco más de trabajo. Esto se debe a que las mejores prácticas exigen la separación de capas y ocultación de información.

    Puede encontrar el código completo del proyecto en GitHub.

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *