«1. Обзор

AWS Lambda позволяет нам создавать легкие приложения, которые можно легко развертывать и масштабировать. Хотя мы можем использовать такие фреймворки, как Spring Cloud Function, из соображений производительности мы обычно используем как можно меньше кода фреймворка.

Иногда нам нужно получить доступ к реляционной базе данных из Lambda. Вот где Hibernate и JPA могут быть очень полезны. Но как добавить Hibernate в нашу Lambda без Spring?

В этом руководстве мы рассмотрим проблемы использования любой СУБД в Lambda, а также то, как и когда может быть полезен Hibernate. В нашем примере будет использоваться модель бессерверного приложения для создания интерфейса REST для наших данных.

Мы рассмотрим, как протестировать все на нашем локальном компьютере с помощью Docker и интерфейса командной строки AWS SAM.

2. Проблемы, связанные с использованием RDBMS и Hibernate в Lambdas

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

В облачных приложениях мы пытаемся проектировать с использованием облачных технологий. Бессерверные базы данных, такие как Dynamo DB, могут лучше подходить для Lambdas. Однако потребность в реляционной базе данных может исходить из какого-то другого приоритета в рамках нашего проекта.

2.1. Использование СУРБД из лямбды

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

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

Это означает, что использование Lambda с базой данных может вызвать проблемы с подключением. Внезапный апскейлинг нашей Lambda может потреблять слишком много соединений. Хотя Lambda может немедленно освободить соединения, мы по-прежнему полагаемся на то, что база данных сможет подготовить их к следующему вызову Lambda. Поэтому часто рекомендуется использовать максимальное ограничение параллелизма для любой Lambda, использующей реляционную базу данных.

В некоторых проектах Lambda — не лучший выбор для подключения к СУБД, а традиционный сервис Spring Data с пулом подключений, возможно, работающий в EC2 или ECS, может быть лучшим решением.

2.2. Пример использования Hibernate

Хороший способ определить, нужен ли нам Hibernate, — спросить, какой код нам пришлось бы писать без него.

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

2.3. Hibernate — тяжеловесная технология

Однако нам также необходимо учитывать стоимость использования Hibernate в Lambda.

JAR-файл Hibernate имеет размер 7 МБ. Hibernate требует времени при запуске для проверки аннотаций и создания возможностей ORM. Это чрезвычайно мощно, но для Lambda это может быть излишним. Поскольку Lambdas обычно пишутся для выполнения небольших задач, накладные расходы Hibernate могут не стоить преимуществ.

Может быть проще использовать JDBC напрямую. В качестве альтернативы, легкая инфраструктура, подобная ORM, такая как JDBI, может обеспечить хорошую абстракцию над запросами без слишком больших накладных расходов.

3. Пример приложения

«В этом руководстве мы создадим приложение для отслеживания для транспортной компании с небольшими объемами. Давайте представим, что они собирают у клиентов крупные предметы для создания Консигнации. Затем, куда бы ни направлялся этот груз, он регистрируется с отметкой времени, чтобы клиент мог отслеживать его. У каждой партии есть источник и пункт назначения, для которых мы будем использовать what3words.com в качестве службы геолокации.

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

3.1. Дизайн API

Мы создадим REST API со следующими методами:

    POST /consignment — создайте новую партию, возвращая идентификатор и указывая источник и пункт назначения; необходимо выполнить перед любыми другими операциями POST /consignment/{id}/item — добавить товар в посылку; всегда добавляет в конец списка POST /consignment/{id}/checkin — регистрирует посылку в любом месте по пути, указывая место и временную метку; всегда будет храниться в базе данных в порядке отметок времени GET /consignment/{id} — получить полную историю отправления, в том числе о том, достигло ли оно пункта назначения

3.2. Lambda Design

Мы будем использовать одну функцию Lambda, чтобы предоставить этому REST API модель бессерверного приложения для его определения. Это означает, что наша единственная функция-обработчик Lambda должна быть в состоянии удовлетворить все вышеуказанные запросы.

Чтобы тестирование было быстрым и простым, без затрат на развертывание в AWS, мы будем тестировать все на наших машинах для разработки.

4. Создание Lambda

Давайте настроим новую Lambda, чтобы она соответствовала нашему API, но еще не реализовав уровень доступа к данным.

4.1. Предварительные условия

Во-первых, нам нужно установить Docker, если у нас его еще нет. Он понадобится нам для размещения нашей тестовой базы данных, и он используется интерфейсом командной строки AWS SAM для моделирования среды выполнения Lambda.

Мы можем проверить, есть ли у нас Docker:

$ docker --version
Docker version 19.03.12, build 48a66213fe

Далее нам нужно установить интерфейс командной строки AWS SAM, а затем протестировать его:

$ sam --version
SAM CLI, version 1.1.0

Теперь мы готовы создать нашу Lambda.

4.2. Создание шаблона SAM

Интерфейс командной строки SAM предоставляет нам способ создания новой лямбда-функции:

$ sam init

Это подскажет нам настройки нового проекта. Давайте выберем следующие параметры:

1 - AWS Quick Start Templates
13 - Java 8
1 - maven
Project name - shipping-tracker
1 - Hello World Example: Maven

Следует отметить, что эти номера параметров могут отличаться в более поздних версиях инструментария SAM.

Теперь должен быть новый каталог с именем shipping-tracker, в котором есть приложение-заглушка. Если мы посмотрим на содержимое его файла template.yaml, то обнаружим функцию HelloWorldFunction с простым REST API:

Events:
  HelloWorld:
    Type: Api 
    Properties:
      Path: /hello
      Method: get

По умолчанию это удовлетворяет базовому GET-запросу к /hello. Мы должны быстро проверить, что все работает, используя sam для сборки и тестирования:

$ sam build
... lots of maven output
$ sam start-api

Затем мы можем протестировать API hello world с помощью curl:

$ curl localhost:3000/hello
{ "message": "hello world", "location": "192.168.1.1" }

После этого давайте остановим sam, запускающий его API. слушателя с помощью CTRL+C, чтобы прервать программу.

Теперь, когда у нас есть пустой Java 8 Lambda, нам нужно настроить его, чтобы он стал нашим API.

4.3. Создание нашего API

Чтобы создать наш API, нам нужно добавить наши собственные пути в раздел Events файла template.yaml:

CreateConsignment:
  Type: Api 
  Properties:
    Path: /consignment
    Method: post
AddItem:
  Type: Api
  Properties:
    Path: /consignment/{id}/item
    Method: post
CheckIn:
  Type: Api
  Properties:
    Path: /consignment/{id}/checkin
    Method: post
ViewConsignment:
  Type: Api
  Properties:
    Path: /consignment/{id}
    Method: get

Давайте также переименуем функцию, которую мы вызываем, из HelloWorldFunction в ShippingFunction: ~ ~~

Resources:
  ShippingFunction:
    Type: AWS::Serverless::Function 

Далее мы переименуем каталог в ShippingFunction и изменим пакет Java с helloworld на com.baeldung.lambda.shipping. Это означает, что нам нужно обновить свойства CodeUri и Handler в template.yaml, чтобы они указывали на новое местоположение:

Properties:
  CodeUri: ShippingFunction
  Handler: com.baeldung.lambda.shipping.App::handleRequest

Наконец, чтобы освободить место для нашей собственной реализации, давайте заменим тело обработчика: ~~ ~

public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
    Map<String, String> headers = new HashMap<>();
    headers.put("Content-Type", "application/json");
    headers.put("X-Custom-Header", "application/json");

    return new APIGatewayProxyResponseEvent()
      .withHeaders(headers)
      .withStatusCode(200)
      .withBody(input.getResource());
}

Хотя модульные тесты — хорошая идея, для этого примера мы также удалим предоставленные модульные тесты, удалив каталог src/test.

4.4. Тестирование пустого API

«Теперь мы переместили все вокруг и создали наш API и базовый обработчик, давайте еще раз проверим, все ли работает:

$ sam build
... maven output
$ sam start-api

Давайте используем curl для проверки HTTP-запроса GET:

$ curl localhost:3000/consignment/123
/consignment/{id}

Мы также можем использовать curl -d to POST:

$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/
/consignment

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

4.5. Создание конечных точек в Lambda

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

Однако нам нужно создать эквивалент контроллера REST для отправки каждого запроса в подходящую функцию Java. Итак, мы создадим класс-заглушку ShippingService и направим к нему маршрут из обработчика:

public class ShippingService {
    public String createConsignment(Consignment consignment) {
        return UUID.randomUUID().toString();
    }

    public void addItem(String consignmentId, Item item) {
    }

    public void checkIn(String consignmentId, Checkin checkin) {
    }

    public Consignment view(String consignmentId) {
        return new Consignment();
    }
}

Мы также создадим пустые классы для Consignment, Item и Checkin. Они скоро станут нашей моделью.

Теперь, когда у нас есть сервис, давайте воспользуемся ресурсом для маршрутизации к соответствующим методам сервиса. Мы добавим оператор switch в наш обработчик для маршрутизации запросов к сервису:

Object result = "OK";
ShippingService service = new ShippingService();

switch (input.getResource()) {
    case "/consignment":
        result = service.createConsignment(
          fromJson(input.getBody(), Consignment.class));
        break;
    case "/consignment/{id}":
        result = service.view(input.getPathParameters().get("id"));
        break;
    case "/consignment/{id}/item":
        service.addItem(input.getPathParameters().get("id"),
          fromJson(input.getBody(), Item.class));
        break;
    case "/consignment/{id}/checkin":
        service.checkIn(input.getPathParameters().get("id"),
          fromJson(input.getBody(), Checkin.class));
        break;
}

return new APIGatewayProxyResponseEvent()
  .withHeaders(headers)
  .withStatusCode(200)
  .withBody(toJson(result));

Мы можем использовать Jackson для реализации наших функций fromJson и toJson.

4.6. Заглушенная реализация

До сих пор мы научились создавать AWS Lambda для поддержки API, тестировать его с помощью sam и curl и создавать базовые функции маршрутизации в нашем обработчике. Мы могли бы добавить больше обработки ошибок при неверных входных данных.

Следует отметить, что сопоставления в файле template.yaml уже предполагают, что AWS API Gateway будет фильтровать запросы, которые не относятся к правильным путям в нашем API. Таким образом, нам нужно меньше обработки ошибок для плохих путей.

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

5. Настройка базы данных

В этом примере мы будем использовать PostgreSQL в качестве СУБД. Подойдет любая реляционная база данных.

5.1. Запуск PostgreSQL в Docker

Сначала мы вытащим образ докера PostgreSQL:

$ docker pull postgres:latest
... docker output
Status: Downloaded newer image for postgres:latest
docker.io/library/postgres:latest

Давайте теперь создадим сеть докеров для запуска этой базы данных. Эта сеть позволит нашей Lambda взаимодействовать с контейнером базы данных:

$ docker network create shipping

Далее нам нужно запустить контейнер базы данных в этой сети:

docker run --name postgres \
  --network shipping \
  -e POSTGRES_PASSWORD=password \
  -d postgres:latest

С помощью –name мы дали контейнеру имя postgres. С помощью -network мы добавили его в нашу сеть докеров для доставки. Чтобы установить пароль для сервера, мы использовали переменную среды POSTGRES_PASSWORD, установленную с помощью переключателя -e.

Мы также использовали -d для запуска контейнера в фоновом режиме, а не привязывать нашу оболочку. PostgreSQL запустится через несколько секунд.

5.2. Добавление схемы

Нам понадобится новая схема для наших таблиц, поэтому давайте воспользуемся клиентом psql внутри нашего контейнера PostgreSQL, чтобы добавить схему доставки:

$ docker exec -it postgres psql -U postgres
psql (12.4 (Debian 12.4-1.pgdg100+1))
Type "help" for help.

postgres=#

В этой оболочке мы создадим схему: ~~ ~

postgres=# create schema shipping;
CREATE SCHEMA

Затем мы используем CTRL+D для выхода из оболочки.

Теперь у нас есть запущенный PostgreSQL, готовый для использования нашей Lambda.

6. Добавление нашей модели сущности и DAO

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

6.1. Добавление Hibernate в проект

Мы добавим зависимости в наш pom.xml как для Hibernate, так и для пула соединений Hikari. Мы также добавим драйвер PostgreSQL JDBC:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.21.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-hikaricp</artifactId>
    <version>5.4.21.Final</version>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.16</version>
</dependency>

6.2. Модель сущностей

Давайте конкретизируем объекты сущностей. Груз имеет список товаров и проверок, а также его источник, пункт назначения и информацию о том, был ли он уже доставлен (то есть зарегистрирован ли он в своем конечном пункте назначения):

@Entity(name = "consignment")
@Table(name = "consignment")
public class Consignment {
    private String id;
    private String source;
    private String destination;
    private boolean isDelivered;
    private List items = new ArrayList<>();
    private List checkins = new ArrayList<>();
    
    // getters and setters
}

Мы аннотировал класс как сущность и имя таблицы. Мы также предоставим геттеры и сеттеры. Пометим геттеры именами столбцов:

@Id
@Column(name = "consignment_id")
public String getId() {
    return id;
}

@Column(name = "source")
public String getSource() {
    return source;
}

@Column(name = "destination")
public String getDestination() {
    return destination;
}

@Column(name = "delivered", columnDefinition = "boolean")
public boolean isDelivered() {
    return isDelivered;
}

«

@ElementCollection(fetch = EAGER)
@CollectionTable(name = "consignment_item", joinColumns = @JoinColumn(name = "consignment_id"))
@OrderColumn(name = "item_index")
public List getItems() {
    return items;
}

@ElementCollection(fetch = EAGER)
@CollectionTable(name = "consignment_checkin", joinColumns = @JoinColumn(name = "consignment_id"))
@OrderColumn(name = "checkin_index")
public List getCheckins() {
    return checkins;
}

«Для наших списков мы будем использовать аннотацию @ElementCollection, чтобы сделать их упорядоченными списками в отдельных таблицах с отношением внешнего ключа к консигнационной таблице:

Здесь Hibernate начинает окупаться, выполняя работу по управлению коллекции довольно легко.

@Embeddable
public class Item {
    private String location;
    private String description;
    private String timeStamp;

    @Column(name = "location")
    public String getLocation() {
        return location;
    }

    @Column(name = "description")
    public String getDescription() {
        return description;
    }

    @Column(name = "timestamp")
    public String getTimeStamp() {
        return timeStamp;
    }

    // ... setters omitted
}

Объект Item более прямолинеен:

Он помечен как @Embeddable, чтобы позволить ему быть частью определения списка в родительском объекте.

@Embeddable
public class Checkin {
    private String timeStamp;
    private String location;

    @Column(name = "timestamp")
    public String getTimeStamp() {
        return timeStamp;
    }

    @Column(name = "location")
    public String getLocation() {
        return location;
    }

    // ... setters omitted
}

Аналогично определим Checkin:

6.3. Создание DAO доставки

public void save(Session session, Consignment consignment) {
    Transaction transaction = session.beginTransaction();
    session.save(consignment);
    transaction.commit();
}

public Optional<Consignment> find(Session session, String id) {
    return Optional.ofNullable(session.get(Consignment.class, id));
}

Наш класс ShippingDao будет полагаться на передачу открытой сессии Hibernate. Для этого потребуется служба доставки для управления сеансом:

Позже мы соединим это с нашей службой доставки.

7. Жизненный цикл Hibernate

На данный момент наша модель сущностей и DAO сопоставимы с реализациями, отличными от Lambda. Следующая задача — создать Hibernate SessionFactory в жизненном цикле Lambda.

7.1. Где находится база данных?

Environment: 
  Variables:
    DB_URL: jdbc:postgresql://postgres/postgres
    DB_USER: postgres
    DB_PASSWORD: password

Если мы собираемся получить доступ к базе данных из нашей Lambda, она должна быть настраиваемой. Давайте поместим URL-адрес JDBC и учетные данные базы данных в переменные среды в нашем файле template.yaml:

Эти переменные среды будут внедрены в среду выполнения Java. Пользователь postgres используется по умолчанию для нашего контейнера Docker PostgreSQL. Мы назначили пароль в качестве пароля, когда запускали контейнер ранее.

Внутри DB_URL у нас есть имя сервера — //postgres — это имя, которое мы дали нашему контейнеру — и имя базы данных postgres — это база данных по умолчанию.

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

7.2. Создание фабрики сеансов

Map<String, String> settings = new HashMap<>();
settings.put(URL, System.getenv("DB_URL"));
settings.put(DIALECT, "org.hibernate.dialect.PostgreSQLDialect");
settings.put(DEFAULT_SCHEMA, "shipping");
settings.put(DRIVER, "org.postgresql.Driver");
settings.put(USER, System.getenv("DB_USER"));
settings.put(PASS, System.getenv("DB_PASSWORD"));
settings.put("hibernate.hikari.connectionTimeout", "20000");
settings.put("hibernate.hikari.minimumIdle", "1");
settings.put("hibernate.hikari.maximumPoolSize", "2");
settings.put("hibernate.hikari.idleTimeout", "30000");
settings.put(HBM2DDL_AUTO, "create-only");
settings.put(HBM2DDL_DATABASE_ACTION, "create");

Нам нужно настроить Hibernate и пул соединений Hikari. Чтобы предоставить настройки для Hibernate, мы добавляем их в карту:

Здесь мы используем System.getenv для извлечения настроек среды выполнения из среды. Мы добавили настройки HBM2DDL_, чтобы наше приложение генерировало таблицы базы данных. Однако мы должны закомментировать или удалить эти строки после создания схемы базы данных и не позволять нашей Lambda делать это в рабочей среде. Однако сейчас это полезно для нашего тестирования.

Как мы видим, многие настройки имеют константы, уже определенные в классе AvailableSettings в Hibernate, хотя специфичные для Hikari константы отсутствуют.

StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
  .applySettings(settings)
  .build();

return new MetadataSources(registry)
  .addAnnotatedClass(Consignment.class)
  .addAnnotatedClass(Item.class)
  .addAnnotatedClass(Checkin.class)
  .buildMetadata()
  .buildSessionFactory();

Теперь, когда у нас есть настройки, нам нужно построить SessionFactory. Мы по отдельности добавим в него наши классы сущностей:

7.3. Управление ресурсами

При запуске Hibernate выполняет генерацию кода вокруг объектов сущностей. Это действие не предназначено для того, чтобы приложение выполняло это действие более одного раза, и оно использует для этого время и память. Итак, мы хотим сделать это один раз при холодном запуске нашей Lambda.

private SessionFactory sessionFactory = createSessionFactory();

Поэтому мы должны создать SessionFactory, так как наш объект-обработчик создается фреймворком Lambda. Мы можем сделать это в списке инициализаторов класса обработчика:

Однако, поскольку наша SessionFactory имеет пул соединений, есть риск, что он будет удерживать соединения открытыми между вызовами, связывая ресурсы базы данных.

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

private void flushConnectionPool() {
    ConnectionProvider connectionProvider = sessionFactory.getSessionFactoryOptions()
      .getServiceRegistry()
      .getService(ConnectionProvider.class);
    HikariDataSource hikariDataSource = connectionProvider.unwrap(HikariDataSource.class);
    hikariDataSource.getHikariPoolMXBean().softEvictConnections();
}

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

Следует отметить, что метод закрытия SessionFactory также закроет соединения, но также сделает SessionFactory непригодным для использования.

7.4. Добавить в обработчик

«Теперь нам нужно убедиться, что обработчик использует фабрику сеансов и освобождает свои соединения. Имея это в виду, давайте извлечем большую часть функциональности контроллера в метод с именем routeRequest и изменим наш обработчик, чтобы освободить ресурсы в блоке finally:

try {
    ShippingService service = new ShippingService(sessionFactory, new ShippingDao());
    return routeRequest(input, service);
} finally {
    flushConnectionPool();
}

Мы также изменили наш ShippingService, чтобы иметь SessionFactory и ShippingDao как свойства, введенные через конструктор, но он их еще не использует.

7.5. Тестирование Hibernate

На этом этапе, хотя ShippingService ничего не делает, вызов Lambda должен вызвать запуск Hibernate и создание DDL.

Давайте дважды проверим сгенерированный DDL, прежде чем комментировать настройки для этого:

$ sam build
$ sam local start-api --docker-network shipping

Мы создаем приложение, как и раньше, но теперь мы добавляем параметр –docker-network в sam local . Это запустит тестовую Lambda в той же сети, что и наша база данных, чтобы Lambda могла получить доступ к контейнеру базы данных, используя имя своего контейнера.

Когда мы впервые попали в конечную точку с помощью curl, наши таблицы должны быть созданы:

$ curl localhost:3000/consignment/123
{"id":null,"source":null,"destination":null,"items":[],"checkins":[],"delivered":false}

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

$ docker exec -it postgres pg_dump -s -U postgres
... DDL output
CREATE TABLE shipping.consignment_item (
    consignment_id character varying(255) NOT NULL,
...

Когда мы будем довольны, что наша установка Hibernate работает, мы можем закомментировать настройки HBM2DDL_.

8. Завершение бизнес-логики

Осталось только заставить службу доставки использовать ShippingDao для реализации бизнес-логики. Каждый метод создаст фабрику сеансов в блоке try-with-resources, чтобы обеспечить его закрытие.

8.1. Создать партию

Новая партия не доставлена ​​и должна получить новый идентификатор. Затем мы должны сохранить его в базе данных:

public String createConsignment(Consignment consignment) {
    try (Session session = sessionFactory.openSession()) {
        consignment.setDelivered(false);
        consignment.setId(UUID.randomUUID().toString());
        shippingDao.save(session, consignment);
        return consignment.getId();
    }
}

8.2. Просмотр партии

Чтобы получить партию, нам нужно прочитать ее из базы данных по ID. Хотя REST API должен возвращать Not Found по неизвестному запросу, в этом примере мы просто вернем пустую партию, если ничего не найдено:

public Consignment view(String consignmentId) {
    try (Session session = sessionFactory.openSession()) {
        return shippingDao.find(session, consignmentId)
          .orElseGet(Consignment::new);
    }
}

8.3. Добавить товар

Товары будут добавлены в наш список товаров в том порядке, в котором они были получены:

public void addItem(String consignmentId, Item item) {
    try (Session session = sessionFactory.openSession()) {
        shippingDao.find(session, consignmentId)
          .ifPresent(consignment -> addItem(session, consignment, item));
    }
}

private void addItem(Session session, Consignment consignment, Item item) {
    consignment.getItems()
      .add(item);
    shippingDao.save(session, consignment);
}

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

8.4. Регистрация

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

public void checkIn(String consignmentId, Checkin checkin) {
    try (Session session = sessionFactory.openSession()) {
        shippingDao.find(session, consignmentId)
          .ifPresent(consignment -> checkIn(session, consignment, checkin));
    }
}

private void checkIn(Session session, Consignment consignment, Checkin checkin) {
    consignment.getCheckins().add(checkin);
    consignment.getCheckins().sort(Comparator.comparing(Checkin::getTimeStamp));
    if (checkin.getLocation().equals(consignment.getDestination())) {
        consignment.setDelivered(true);
    }
    shippingDao.save(session, consignment);
}

9. Тестирование приложения

Давайте смоделируем посылку, путешествующую из Белого дома в Эмпайр-стейт-билдинг.

Агент создает маршрут:

$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/

"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207"

Теперь у нас есть идентификатор 3dd0f0e4-fc4a-46b4-8dae-a57d47df5207 для груза. Затем кто-то собирает два предмета для отправки — картину и пианино:

$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120000", "description":"picture"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"

$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120001", "description":"piano"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"

Чуть позже идет регистрация:

$ curl -d '{"location":"united.alarm.raves", "timeStamp":"20200101T173301"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

И снова позже:

$ curl -d '{"location":"wink.sour.chasing", "timeStamp":"20200101T191202"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

клиент, в этот момент, запрашивает статус груза:

$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
  "id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
  "source":"data.orange.brings",
  "destination":"heave.wipes.clay",
  "items":[
    {"location":"data.orange.brings","description":"picture","timeStamp":"20200101T120000"},
    {"location":"data.orange.brings","description":"piano","timeStamp":"20200101T120001"}
  ],
  "checkins":[
    {"timeStamp":"20200101T173301","location":"united.alarm.raves"},
    {"timeStamp":"20200101T191202","location":"wink.sour.chasing"}
  ],
  "delivered":false
}%

Они видят прогресс, и он еще не доставлен.

Сообщение должно было быть отправлено в 20:12, чтобы сказать, что оно достигло deflection.famed.apple, но оно задерживается, и сообщение от 21:46 в пункте назначения доходит до него первым:

$ curl -d '{"location":"heave.wipes.clay", "timeStamp":"20200101T214622"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

Клиент , в этот момент запрашивает статус груза:

$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
  "id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
...
    {"timeStamp":"20200101T191202","location":"wink.sour.chasing"},
    {"timeStamp":"20200101T214622","location":"heave.wipes.clay"}
  ],
  "delivered":true
}

Теперь он доставлен. Таким образом, когда задержанное сообщение доходит:

$ curl -d '{"location":"deflection.famed.apple", "timeStamp":"20200101T201254"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"

$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
"id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
...
{"timeStamp":"20200101T191202","location":"wink.sour.chasing"},
{"timeStamp":"20200101T201254","location":"deflection.famed.apple"},
{"timeStamp":"20200101T214622","location":"heave.wipes.clay"}
],
"delivered":true
}

Регистрация помещается в нужное место на временной шкале.

10. Заключение

В этой статье мы обсудили проблемы использования тяжеловесной инфраструктуры, такой как Hibernate, в легком контейнере, таком как AWS Lambda.

Мы создали Lambda и REST API и научились тестировать их на нашем локальном компьютере с помощью Docker и AWS SAM CLI. Затем мы построили модель объекта для Hibernate для использования с нашей базой данных. Мы также использовали Hibernate для инициализации наших таблиц.

Наконец, мы интегрировали Hibernate SessionFactory в наше приложение, обеспечив его закрытие перед выходом из Lambda.

Как обычно, пример кода для этой статьи можно найти на GitHub.