Работа с PostgreSQL на Java

Введение PostgreSQL [https://www.postgresql.org] (известный под названием Postgres) известен своей объектно-реляционной природой. Напротив, другие системы баз данных обычно реляционные. По своей природе это отличное сочетание с Java, которая сильно объектно-ориентирована. Доступ к базе данных Postgres с использованием Java требует, чтобы вы полагались на JDBC API [https://www.oracle.com/technetwork/java/javase/jdbc/index.html], как вы могли догадаться. Из-за этого подпрограммы Postgres и подпрограммы

Вступление

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

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

Что такое PostgreSQL?

PostgreSQL - это производная от ныне несуществующего проекта POSTGRES. POSTGRES нацелен не только на объектную ориентацию, но и на расширяемость. Тем не менее, Калифорнийский университет прекратил разработку ПОСТГРЭС в 1994 году.

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

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

Демо-приложение

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

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

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

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

Мы начнем с maven-archetype-quickstart для простого скелетного проекта 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:

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

Модель домена

Давайте создадим каталог с именем api в нашем src в котором мы определим модель / объект - 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 
 + ']'; 
 } 
 } 

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

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

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

 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); 
 } 
Обратите внимание на два [дженерика уровня
класса](https://docs.oracle.com/javase/tutorial/java/generics/types.html)
T и I T представляет собой фактический объект класса для передачи в базу данных и из нее, тогда как I - это класс первичного ключа сущности.

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

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

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

В нашей localhost мы создадим базу данных с именем sampledb и создадим таблицу для наших Customer :

снимок экранаpgAdmin{.ezlazyload}

Для этого в 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) 
 ) 

Итак, мы создали таблицу для Customer s.

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

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

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

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

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

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

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

 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 
 } 

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

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

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

Затем, используя соединение с базой данных, DAO подготавливает оператор:

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

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

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

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

Чтение сущностей

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

Начнем с простого .get() который возвращает одного Customer с соответствующим 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; 
 }); 
 } 

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

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

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

Опять же, довольно просто - мы выполняем соответствующий 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); 
 } 
 }); 
 } 

Опять же, мы подготовили оператор и выполнили запрос на обновление на основе полей и id Customer переданного методу обновления.

Удаление объектов

И, наконец, иногда мы можем захотеть удалить объект, и для этого используется метод .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); 
 } 
 }); 
 } 

Опять же, на основе id Customer выполняется запрос на удаление для удаления объекта.

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

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

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

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

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

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

И его наследник NonExistentCustomerException :

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

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

Заключение

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

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

comments powered by Disqus