Trabajando con PostgreSQL en Java

PostgreSQL es una excelente combinación para Java debido a su naturaleza relacional de objetos. Con el soporte extendido para tipos de datos personalizados y grandes conjuntos de datos, es una opción extremadamente popular para muchos desarrolladores de Java.

Introducción

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

Acceder a una base de datos de Postgres usando Java requiere que confíes en la API de JDBC, como podrías haber 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? {#lo que es postgresql}

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

Las primeras versiones de Postgres estaban dirigidas a las computadoras 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 los sistemas MacOS, Linux y Windows.

Su licencia libre y de código abierto también se ha sumado a su adopción generalizada. A los desarrolladores les encanta, en parte, porque pueden profundizar 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 de CRUD que la acompañe. Estaremos escribiendo una aplicación Java simple que puede crear, leer, actualizar y eliminar información de clientes 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 idea de lo que sucede en la capa de la base de datos, una práctica conocida como [arquitectura en capas](https://dzone.com/articles/layered-architecture-is- bueno). Por lo tanto, optaremos por el patrón Objeto de acceso a datos (DAO) para satisfacer esta necesidad.

Dependencia Maven

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

1
$ mvn archetype:generate -DgroupId=com.wikihtp.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:

1
2
3
4
5
6
7
8
java-postgresql-sample
├── src
|   ├── main
|      ├── java
|         ├── com
|            ├── wikihtp
|               ├── postgresql
└── test

Luego, en su archivo pom.xml, agregue la dependencia de Postgres:

1
2
3
4
5
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>{version}</version>
</dependency>

Modelo de dominio

Hagamos un directorio llamado api en nuestro directorio src en el que definiremos un modelo/entidad - Cliente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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

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

1
2
3
4
5
6
7
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 ay 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.

Crear 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 implementado, usaremos pgAdmin para administrar la instalación.

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

pgAdmin screenshot

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

1
2
3
4
5
6
7
8
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.

Conexión a la base de datos {#conexiónalabasede datos}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 de base de datos. Como no siempre puede devolver un objeto Connection que no sea nulo, la conexión se envuelve 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.

Adición de entidades

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 objeto ‘Cliente’, puede pasarlo al método ‘guardar’ de ‘PostgreSqlDao’ para agregarlo a la base de datos.

El método save usa una cadena SQL para operar:

1
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:

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

Es de interés que la declaración contiene el indicador 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 método save 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, utiliza una función flatMap para garantizar que el valor que devuelve no tenga un envoltorio Opcional.

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

Entidades de lectura

En nuestra implementación, hemos decidido tener un método que devuelva un único Cliente basado en su id, y un método que devuelva todos los clientes persistentes de la base de datos.

Empecemos con el simple método .get() que devuelve un único Cliente con el id correspondiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 objeto Statement y empaquetamos los resultados en un ResultSet. Luego, extraemos la información del ‘ResultSet’ y la empaquetamos en un constructor para un ‘Cliente’, que se devuelve.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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, es bastante sencillo: ejecutamos la consulta SQL adecuada, extraemos la información, creamos una instancia de los objetos Customer y los empaquetamos en una ArrayList.

Actualizar entidades

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 en función de los campos y el id del Cliente pasado al método de actualización.

Eliminación de entidades

Y finalmente, a veces podemos desear eliminar una entidad, y para ello se utiliza el método .delete():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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);
        }
    });
}

Una vez más, en función del id del Cliente, 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 método estático principal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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", "[correo electrónico protegido]");
        Customer secondCustomer =
            new Customer("Joshua", "Daulton", "[correo electrónico protegido]");
        Customer thirdCustomer =
            new Customer("April", "Ellis", "[correo electrónico protegido]");
        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("[correo electrónico protegido]");
        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 se necesite.

Una vez hecho esto, hay otras dos clases de excepción personalizadas que deben implementarse. Estas son NonExistentEntityException:

1
2
3
4
5
6
7
8
public class NonExistentEntityException extends Throwable {

    private static final long serialVersionUID = -3760558819369784286L;

    public NonExistentEntityException(String message) {
        super(message);
    }
}

Y su heredero, NonExistentCustomerException:

1
2
3
4
5
6
7
8
public class NonExistentCustomerException extends NonExistentEntityException {

    private static final long serialVersionUID = 8633588908169766368L;

    public NonExistentCustomerException() {
        super("Customer does not exist");
    }
}

Estas dos clases manejan las excepciones que lanza DAO cuando no existe un “Cliente” 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 separación de capas y ocultación de información.

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