Рубрики
Без рубрики

Работа с PostgreSQL на Java

PostgreSQL отлично подходит для Java из-за его объектно-реляционной природы. Благодаря расширенной поддержке пользовательских типов данных и больших наборов данных, это чрезвычайно популярный выбор для многих разработчиков Java.

Автор оригинала: Hiram Kamau.

Вступление

PostgreSQL (который носит прозвище Postgres) известен своей объектно-реляционной природой. В отличие от этого, другие системы баз данных обычно являются реляционными . В силу своей природы он отлично сочетается с Java, которая в значительной степени ориентирована на объекты.

Доступ к базе данных Postgres с использованием Java требует , чтобы вы полагались на JDBC API , как вы, возможно, подозревали. Из-за этого процедуры Postgres и другие системы баз данных похожи. Тем не менее, это не скрывает того факта, что Postgres предлагает дополнительные возможности, такие как расширенная поддержка пользовательских типов данных и больших наборов данных.

Что такое PostgreSQL?

PostgreSQL является производной от ныне несуществующего проекта POSTGRES . POSTGRES нацелен на достижение не только объектной ориентации, но и расширяемости. Тем не менее, Калифорнийский университет прекратил разработку POSTGRES в 1994 году.

Ранние выпуски Postgres были нацелены на компьютеры UNIX. Тем не менее, с годами база данных стала переносимой. Таким образом, вы можете найти его в системах macOS, Linux и Windows.

Его открытый исходный код и бесплатное лицензирование также способствовали его широкому внедрению. Разработчикам это нравится отчасти потому, что они могут покопаться в источниках, чтобы узнать, как именно это работает.

Демонстрационное приложение

Руководство по Postgres является неполным без сопутствующей реализации CRUD. Мы напишем простое Java-приложение, которое может создавать, считывать, обновлять и удалять информацию о клиентах из базы данных Postgres.

Конечно, мы начнем с определения сущностей, а затем используем их для создания схемы базы данных, чтобы убедиться, что таблицы сопоставлены правильно.

И, как того требует надлежащий API, уровень бизнес – логики не должен иметь представления о том, что происходит на уровне базы данных-практика, известная как многоуровневая архитектура . Таким образом, мы выберем шаблон Объект доступа к данным (DAO) для удовлетворения этой потребности.

Зависимость от Maven

Мы начнем с maven-архетип-быстрый запуск для простого скелетного проекта Maven через ваш терминал:

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

После выполнения команды вы должны получить структуру, подобную этой:

java-postgresql-sample
├── src
|   ├── main
|      ├── java
|         ├── com
|            ├── stackabuse
|               ├── postgresql
└── test

Затем, в вашем pom.xml файл, добавьте зависимость Postgres:


    org.postgresql
    postgresql
    {version}

Модель предметной области

Давайте создадим каталог с именем api в нашем каталоге src , в котором мы определим модель/сущность – Клиент :

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["
                + ", firstName=" + firstName
                + ", lastName=" + lastName
                + ", email=" + email
                + ']';
    }
}

Эта сущность будет отображена в нашей базе данных Postgres с соответствующими полями немного позже.

Функциональность CRUD

Поскольку мы работаем в соответствии с шаблоном DAO, давайте начнем реализовывать нашу функциональность CRUD через интерфейс Dao в каталоге spi , в котором будут размещены все наши интерфейсы и классы обслуживания:

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

Обратите внимание на два обобщения уровня класса : T и I . T представляет фактический объект класса для передачи в базу данных и из нее, тогда как I является классом первичного ключа сущности.

Теперь у нас есть скелет CRUD и объект домена на месте. Когда эти два дела будут выполнены, мы действительно сможем приступить к созданию нашей базы данных.

Создание базы данных Postgresql

Следуйте руководству по установке PostgreSQL для используемой вами платформы – установка довольно проста. С помощью Postgres мы будем использовать pgAdmin для управления установкой.

В вашей локальной системе мы создадим базу данных с именем образец базы данных и создадим таблицу для наших Клиентов :

Для этого в pgAdmin мы запустим ввод в редакторе запросов:

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

И, таким образом, мы создали таблицу для Клиента s.

Подключение к базе данных

Прежде чем мы сможем выполнить какие-либо инструкции в базе данных из нашего кода, нам сначала нужно будет настроить подключение к базе данных. Мы сделаем это через Jdcconnection класс:

public class JdbcConnection {

    private static final Logger LOGGER =
        Logger.getLogger(JdbcConnection.class.getName());
    private static Optional connection = Optional.empty();

    public static Optional 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;
    }
}

Основная задача вышеприведенного класса-восстановить соединение с базой данных. Поскольку он не всегда может возвращать ненулевой объект Connection , соединение завернуто в Необязательный .

Другая примечательная вещь заключается в том, что соединение является статической переменной . Следовательно, класс возвращает первый ненулевой экземпляр соединения, полученный при первом запуске.

Добавление Сущностей

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

Поскольку теперь мы действительно можем подключиться к базе данных, давайте продолжим и попробуем создать объект в базе данных. Для этого мы определим класс PostgreSQL Dao , который реализует вышеупомянутый интерфейс Dao :

public class PostgreSqlDao implements Dao {

    private static final Logger LOGGER =
        Logger.getLogger(PostgreSqlDao.class.getName());
    private final Optional connection;

    public PostgreSqlDao() {
        this.connection = JdbcConnection.getConnection();
    }

    @Override
    public Optional 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 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
}

После создания объекта Customer вы можете передать его в сохранить метод PostgreSqlDao , чтобы добавить его в базу данных.

Метод save использует строку SQL для работы:

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

Используя подключение к базе данных, DAO затем подготавливает инструкцию:

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

Интерес представляет то, что оператор содержит флаг Оператор.RETURN_GENERATED_KEYS . Это гарантирует, что база данных также сообщит о первичном ключе, созданном ею для новой строки.

Стоит также отметить, что метод save использует средство отображения Java. Он преобразует соединение с базой данных в тип возвращаемого значения, требуемый методом. И более того, он использует функцию flatMap , чтобы гарантировать, что возвращаемое значение не имеет необязательного переноса.

Остальные методы CRUD PostgreSqlDao должны следовать той же предпосылке. Они должны сопоставить соединение с возвратом, где это необходимо, и сначала проверить, существует ли соединение, прежде чем работать с ним в противном случае.

Считывание Сущностей

В нашей реализации мы решили использовать метод, который возвращает одного Клиента на основе их идентификатора , и метод, который возвращает всех постоянных клиентов из базы данных.

Давайте начнем с простого .get() метода, который возвращает одного Клиента с соответствующим идентификатором :

public Optional get(int id) {
    return connection.flatMap(conn -> {
        Optional 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;
    });
}

Код довольно прост. Мы выполняем запрос через наш Оператор объект и упаковываем результаты в Набор результатов . Затем мы извлекаем информацию из набора результатов и упаковываем ее в конструктор для Клиента , который возвращается.

Теперь давайте реализуем метод .GetAll() :

public Collection getAll() {
    Collection 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;
}

Опять же, довольно просто – мы выполняем соответствующий SQL-запрос, извлекаем информацию, создаем объекты Customer и упаковываем их в ArrayList .

Обновление Сущностей

Далее, если мы когда-либо захотим обновить объект после его создания, нам понадобится метод .update() :

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

Опять же, мы подготовили инструкцию и выполнили запрос на обновление на основе полей и идентификатора клиента , переданных методу обновления.

Удаление Сущностей

И, наконец, иногда мы можем пожелать удалить объект, и для этой цели используется метод .delete() :

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

Опять же, на основе Клиента ‘ s id выполняется запрос на удаление для удаления сущности.

Запуск приложения

После завершения реализации DAO проекту теперь нужна точка входа. Лучшее место для этого было бы в main статическом методе:

public class CustomerApplication {

    private static final Logger LOGGER =
        Logger.getLogger(CustomerApplication.class.getName());
    private static final Dao 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_DAO.get(id);
        return customer.orElseThrow(NonExistentCustomerException::new);
    }

    public static Collection getAllCustomers() {
        return CUSTOMER_DAO.getAll();
    }

    public static void updateCustomer(Customer customer) {
        CUSTOMER_DAO.update(customer);
    }

    public static Optional addCustomer(Customer customer) {
        return CUSTOMER_DAO.save(customer);
    }

    public static void deleteCustomer(Customer customer) {
        CUSTOMER_DAO.delete(customer);
    }
}

Поскольку методы CRUD из Данных PostgreSQL являются общедоступными, мы завернем их, чтобы предотвратить доступ уровня базы данных к остальной части кода, когда это не требуется.

После этого необходимо создать два других пользовательских класса исключений, которые необходимо создать. Это Несуществующее исключение :

public class NonExistentEntityException extends Throwable {

    private static final long serialVersionUID = -3760558819369784286L;

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

И его наследник, Несуществующее Исключение Клиента :

public class NonExistentCustomerException extends NonExistentEntityException {

    private static final long serialVersionUID = 8633588908169766368L;

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

Эти два класса обрабатывают исключения, которые DAO выдает, когда Клиент не существует, чтобы сделать обработку исключений немного более удобной.

Вывод

Мы видели, как создать CRUD-приложение на основе Postgres. Шаги показывают, что на самом деле настройка серверной части Postgres-это тривиальное дело. Привязка модели домена Java к подключению к базе данных Postgres требует немного больше работы. Это связано с тем, что наилучшая практика требует разделения слоев и сокрытия информации .

Вы можете найти весь код проекта на GitHub .