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 *