Перейти к основному содержимому

Как реализовать Hibernate в лямбда-функции AWS в Java

· 20 мин. чтения

1. Обзор

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

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

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

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

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

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

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

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

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

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

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

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

2.2. Дело о спящем режиме

Хороший способ определить, нужен ли нам 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, чтобы предоставить этому REST API модель бессерверного приложения для его определения. Это означает, что наша единственная функция-обработчик Lambda должна быть в состоянии удовлетворить все вышеуказанные запросы.

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

4. Создание лямбды

Давайте настроим новую 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

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

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.foreach.lambda.shipping . Это означает, что нам нужно обновить свойства CodeUri и Handler в template.yaml , чтобы они указывали на новое местоположение:

Properties:
CodeUri: ShippingFunction
Handler: com.foreach.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 для 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));

Мы можем использовать Джексона для реализации наших функций 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 , чтобы сделать их упорядоченными списками в отдельных таблицах с отношением внешнего ключа к таблице консигнации :

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

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

Сущность Item более проста:

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

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

Точно так же мы определим Checkin :

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

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

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

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

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

7. Жизненный цикл спящего режима

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

private SessionFactory sessionFactory = createSessionFactory();

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

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

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

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

В данном случае это работает, потому что мы указали пул соединений Hikari, который предоставляет softEvictConnections , чтобы мы могли освобождать его соединения.

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

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

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

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

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

7.5. Тестирование спящего режима

На этом этапе, хотя 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. Завершите бизнес-логику

All that remains is to make the ShippingService use the ShippingDao to implement the business logic. Each method will create a session factory in a try-with-resources block to ensure it gets closed.

8.1. Create Consignment

A new consignment hasn't been delivered and should receive a new ID. Then we should save it in the database:

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. View Consignment

To get a consignment, we need to read it from the database by ID. Though a REST API should return Not Found on an unknown request, for this example, we'll just return an empty consignment if none is found:

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

8.3. Add Item

Items will go into our list of items in the order received:

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

Ideally, we'd have better error handling if the consignment did not exist, but for this example, non-existent consignments will be ignored.

8.4. Check-In

The check-ins need to be sorted in order of when they happen, not when the request is received. Also, when the item reaches the final destination, it should be marked as delivered:

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. Testing the App

Let's simulate a package traveling from The White House to the Empire State Building.

An agent creates the journey:

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

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

We now have the ID 3dd0f0e4-fc4a-46b4-8dae-a57d47df5207 for the consignment. Then, someone collects two items for the consignment – a picture and a piano:

$ 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"

Sometime later, there's a check-in:

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

And again later:

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

The customer, at this point, requests the status of the consignment:

$ 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
}%

They see the progress, and it's not yet delivered.

A message should have been sent at 20:12 to say it reached deflection.famed.apple , but it gets delayed, and the message from 21:46 at the destination gets there first:

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

The customer, at this point, requests the status of the consignment:

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

Now it's delivered. So, when the delayed message gets through:

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

The check-in is put in the right place in the timeline.

10. Conclusion

In this article, we discussed the challenges of using a heavyweight framework like Hibernate in a lightweight container such as AWS Lambda.

We built a Lambda and REST API and learned how to test it on our local machine using Docker and AWS SAM CLI. Then, we constructed an entity model for Hibernate to use with our database. We also used Hibernate to initialize our tables.

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

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