Вступление
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
:
{.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 .