1. 概要

AWS Lambdaを使用すると、簡単にデプロイおよびスケーリングできる軽量のアプリケーションを作成できます。 Spring Cloud Function のようなフレームワークを使用できますが、パフォーマンス上の理由から、通常は可能な限り少ないフレームワークコードを使用します。

Lambdaからリレーショナルデータベースにアクセスする必要がある場合があります。 これは、HibernateおよびJPAが非常に役立つ場合がある場所です。 しかし、どうすればSpringなしでHibernateをLambdaに追加できますか?

このチュートリアルでは、Lambda内でRDBMSを使用する際の課題と、Hibernateがいつどのように役立つかを見ていきます。 この例では、サーバーレスアプリケーションモデルを使用して、データへのRESTインターフェイスを構築します。

DockerとAWSSAMCLIを使用してローカルマシンですべてをテストする方法を見ていきます。

2. LambdasでRDBMSとHibernateを使用する際の課題

コールドスタートを高速化するには、ラムダコードをできるだけ小さくする必要があります。 また、ラムダはミリ秒でその仕事をすることができるはずです。 ただし、リレーショナルデータベースを使用すると、多くのフレームワークコードが必要になり、実行速度が低下する可能性があります。

クラウドネイティブアプリケーションでは、クラウドネイティブテクノロジーを使用して設計しようとします。 Dynamo DB のようなサーバーレスデータベースは、Lambdasにより適しています。 ただし、リレーショナルデータベースの必要性は、プロジェクト内の他の優先順位から生じる可能性があります。

2.1. ラムダからのRDBMSの使用

ラムダは短時間実行された後、コンテナが一時停止されます。 コンテナは、将来の呼び出しのために再利用される場合があります。または、不要になった場合はAWSランタイムによって破棄される場合があります。 つまり、コンテナが要求するすべてのリソースは、1回の呼び出しの存続期間内に慎重に管理する必要があります。

具体的には、データベースの従来の接続プールに依存することはできません。開いた接続は、安全に破棄せずに開いたままになる可能性があるためです。 呼び出し中に接続プールを使用できますが、毎回接続プールを作成する必要があります。 また、機能が終了したら、すべての接続をシャットダウンし、すべてのリソースを解放する必要があります

これは、データベースでLambdaを使用すると、接続の問題が発生する可能性があることを意味します。 ラムダの突然のアップスケールは、あまりにも多くの接続を消費する可能性があります。 Lambdaは接続をすぐに解放する可能性がありますが、データベースが次のLambda呼び出しに備えて接続を準備できることに依然依存しています。 したがって、リレーショナルデータベースを使用するすべてのLambdaで最大同時実行制限を使用することをお勧めします。

一部のプロジェクトでは、LambdaはRDBMS に接続するための最良の選択ではなく、接続プールを備えた従来のSpring Dataサービスは、おそらくEC2またはECSで実行され、おそらくより良いソリューションです。

2.2. Hibernateの場合

Hibernateが必要かどうかを判断する良い方法は、Hibernateなしでどのような種類のコードを記述しなければならないかを尋ねることです。

Hibernateを使用しないと、フィールドと列の間の複雑な結合または多くの定型マッピングをコーディングする必要がある場合、コーディングの観点から、Hibernateは優れたソリューションです。 アプリケーションで高負荷が発生しない場合、または低レイテンシが必要でない場合は、Hibernateのオーバーヘッドは問題にならない可能性があります。

2.3. Hibernateはヘビー級テクノロジーです

ただし、ラムダでHibernateを使用するコストも考慮する必要があります。

Hibernatejarファイルのサイズは7MBです。 Hibernateは、起動時にアノテーションを検査してORM機能を作成するのに時間がかかります。 これは非常に強力ですが、ラムダにとってはやり過ぎかもしれません。 Lambdaは通常、小さなタスクを実行するように作成されているため、Hibernateのオーバーヘッドはメリットに見合わない場合があります。

JDBCを直接使用する方が簡単な場合があります。 あるいは、 JDBI などの軽量のORMに似たフレームワークは、オーバーヘッドをあまりかけずに、クエリに対して優れた抽象化を提供する場合があります。

3. サンプルアプリケーション

このチュートリアルでは、少量の海運会社向けの追跡アプリケーションを作成します。 彼らが顧客から大きなアイテムを集めて委託販売品を作成すると想像してみてください。 次に、その貨物が移動する場所はどこでも、タイムスタンプを使用してチェックインされるため、顧客はそれを監視できます。 各貨物にはソース宛先があり、ジオロケーションサービスとしてwhat3words.comを使用します。

また、接続や再試行が不適切なモバイルデバイスを使用していると想像してみましょう。 したがって、委託品が作成された後、それに関する残りの情報は任意の順序で到着する可能性があります。 この複雑さは、貨物ごとに2つのリスト(アイテムとチェックイン)が必要であることに加えて、Hibernateを使用する十分な理由です。

3.1. APIデザイン

次のメソッドを使用してRESTAPIを作成します。

  • POST / consignment –新しい委託品を作成し、IDを返し、ソース宛先を提供します。 他の操作の前に実行する必要があります
  • POST / consignment / {id} / item –委託品にアイテムを追加します。 常にリストの最後に追加します
  • POST / consignment / {id} / checkin location とタイムスタンプを指定して、途中の任意の場所で委託品をチェックインします。 タイムスタンプ順にデータベースに常に保持されます
  • GET / consignment / {id} –目的地に到着したかどうかを含め、貨物の完全な履歴を取得します

3.2. ラムダデザイン

単一のLambda関数を使用して、このRESTAPIにサーバーレスアプリケーションモデルを提供して定義します。 これは、単一のLambdaハンドラー関数が上記のすべてのリクエストを満たすことができる必要があることを意味します。

AWSにデプロイするオーバーヘッドなしに、すばやく簡単にテストできるようにするために、開発マシンですべてをテストします。

4. ラムダの作成

APIを満たすために新しいLambdaをセットアップしましょう。ただし、データアクセスレイヤーはまだ実装していません。

4.1. 前提条件

まず、Docker をまだインストールしていない場合は、インストールする必要があります。 テストデータベースをホストするために必要になります。これは、AWSSAMCLIがLambdaランタイムをシミュレートするために使用します。

Dockerがあるかどうかをテストできます。

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

次に、AWS SAM CLI インストールしてから、テストする必要があります。

$ sam --version
SAM CLI, version 1.1.0

これで、ラムダを作成する準備が整いました。

4.2. SAMテンプレートの作成

SAM CLIは、新しいLambda関数を作成する方法を提供します。

$ 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ファイルの内容を見ると、単純なRESTAPIを備えたHelloWorldFunctionという関数が見つかります。

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

デフォルトでは、これは /helloの基本的なGET要求を満たします。 sam を使用してビルドおよびテストすることにより、すべてが機能していることをすばやくテストする必要があります。

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

次に、curlを使用してhelloworldAPIをテストできます。

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

その後、 CTRL + C を使用してプログラムを中止し、samがAPIリスナーの実行を停止しましょう。

空のJava8Lambdaができたので、APIになるようにカスタマイズする必要があります。

4.3. APIの作成

APIを作成するには、template.yamlファイルのEventsセクションに独自のパスを追加する必要があります。

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に変更します。 つまり、template.yamlCodeUriプロパティとHandlerプロパティを更新して、新しい場所を指すようにする必要があります。

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を使用してHTTPGETリクエストをテストしてみましょう。

$ 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

ご覧のとおり、両方のリクエストは正常に終了します。 スタブコードは、 resource (リクエストのパス)を出力します。これは、さまざまなサービスメソッドへのルーティングを設定するときに使用できます。

4.5. ラムダ内にエンドポイントを作成する

単一のLambda関数を使用して4つのエンドポイントを処理しています。同じコードベース内のエンドポイントごとに異なるハンドラークラスを作成するか、エンドポイントごとに個別のアプリケーションを作成することもできますが、関連するAPIは一緒に維持しますラムダの単一のフリートが共通のコードを提供できるようにします。これにより、リソースをより有効に活用できます。

ただし、各リクエストを適切なJava関数にディスパッチするには、RESTコントローラーに相当するものを構築する必要があります。 したがって、スタブ 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の空のクラスも作成します。 これらはまもなく私たちのモデルになります。

サービスができたので、resourceを使用して適切なサービスメソッドにルーティングしましょう。 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. スタッブされた実装

これまで、APIをサポートするAWS Lambdaを作成し、samcurlを使用してテストし、ハンドラー内で基本的なルーティング機能を構築する方法を学びました。 不正な入力に対するエラー処理を追加することができます。

template.yaml 内のマッピングでは、AWSAPIGatewayがAPIの正しいパスではないリクエストをフィルタリングすることをすでに想定していることに注意してください。 したがって、不良パスのエラー処理が少なくて済みます。

次に、データベース、エンティティモデル、およびHibernateを使用してサービスを実装します。

5. データベースの設定

この例では、RDBMSとしてPostgreSQLを使用します。 任意のリレーショナルデータベースが機能する可能性があります。

5.1. DockerでPostgreSQLを起動する

まず、PostgreSQLDockerイメージをプルします。

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

次に、このデータベースを実行するためのDockerネットワークを作成しましょう。 このネットワークにより、Lambdaはデータベースコンテナと通信できるようになります。

$ docker network create shipping

次に、そのネットワーク内でデータベースコンテナを起動する必要があります。

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

–name、を使用して、コンテナーにpostgresという名前を付けました。 –network、を使用して、ShippingDockerネットワークに追加しました。 サーバーのパスワードを設定するには、-eスイッチで設定された環境変数POSTGRES_PASSWORDを使用しました。

また、シェルを拘束するのではなく、-dを使用してコンテナーをバックグラウンドで実行しました。 PostgreSQLは数秒で起動します。

5.2. スキーマの追加

テーブルに新しいスキーマが必要になるので、PostgreSQLコンテナ内の psql クライアントを使用して、shippingスキーマを追加しましょう。

$ 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接続プールを使用して、1回の呼び出しでデータベースに対して複数の接続を実行する必要があるLambdaに対してどのように構成できるかを見てみましょう。

6.1. プロジェクトにHibernateを追加する

HibernateHikari接続プールの両方のpom.xmlに依存関係を追加します。 PostgreSQLJDBCドライバーも追加します。

<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 アノテーションを使用して、consignmentテーブルとの外部キー関係を持つ個別のテーブルの順序付きリストを作成します。

@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 Sessionが渡されることに依存します。 これには、セッションを管理するために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));
}

これを後でShippingServiceに接続します。

7. Hibernateライフサイクル

これまでのところ、エンティティモデルとDAOはLambda以外の実装に匹敵します。 次の課題は、Lambdaのライフサイクル内にHibernate SessionFactoryを作成することです。

7.1. データベースはどこにありますか?

Lambdaからデータベースにアクセスする場合は、構成可能である必要があります。 JDBCURLとデータベースクレデンシャルをtemplate.yaml内の環境変数に入れましょう。

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

これらの環境変数は、Javaランタイムに挿入されます。 postgres ユーザーは、DockerPostgreSQLコンテナーのデフォルトです。 以前にコンテナを起動したときに、パスワードをpasswordとして割り当てました。

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が本番環境でこれを実行できるようにすることは避けてください。 ただし、今のテストには役立ちます。

ご覧のとおり、多くの設定には、Hibernateの AvailableSettings クラスですでに定義されている定数がありますが、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はエンティティオブジェクトの周りでコード生成を実行します。 アプリケーションがこのアクションを複数回実行することは意図されておらず、時間とメモリを使用して実行します。 したがって、ラムダのコールドスタート時にこれを1回実行したいと思います。

したがって、ハンドラオブジェクトはLambdaフレームワークによって作成されるため、SessionFactoryを作成する必要があります。 これは、ハンドラークラスの初期化子リストで実行できます。

private SessionFactory sessionFactory = createSessionFactory();

ただし、 SessionFactory には接続プールがあるため、呼び出し間で接続が開いたままになり、データベースリソースが占有されるリスクがあります。

さらに悪いことに、AWSランタイムによって破棄されている場合にLambdaがリソースをシャットダウンできるライフサイクルイベントはありません。 そのため、この方法で保持されている接続が適切に解放されない可能性があります。

これを解決するには、接続プールの SessionFactory を調べて、接続を明示的に閉じます。

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

この場合、接続を解放できるように softEvictConnections を提供するHikari接続プールを指定したため、これは機能します。

SessionFactorycloseメソッドも接続を閉じますが、SessionFactoryを使用できなくすることに注意してください。

7.4. ハンドラーに追加

ここで、ハンドラーがセッションファクトリを使用し、その接続を解放することを確認する必要があります。 そのことを念頭に置いて、ほとんどのコントローラー機能を routeRequest というメソッドに抽出し、ハンドラーを変更してfinallyブロックのリソースを解放しましょう。

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

また、 Shipping Service を変更して、SessionFactoryShippingDaoをプロパティとして、コンストラクターを介して挿入しましたが、使用していません。それらはまだです。

7.5. Hibernateのテスト

この時点で、 ShippingService は何もしませんが、Lambdaを呼び出すと、Hibernateが起動してDDLが生成されます。

その設定をコメントアウトする前に、生成されるDDLを再確認してみましょう。

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

以前と同じようにアプリケーションをビルドしますが、現在は –docker-networkパラメーターをsamlocalに追加しています。 これにより、データベースと同じネットワーク内でテスト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. ビジネスロジックを完成させる

残っているのは、ShippingServiceShippingDaoを使用してビジネスロジックを実装するようにすることだけです。 各メソッドは、 try-with-resources ブロックにセッションファクトリを作成して、確実に閉じられるようにします。

8.1. 委託販売品を作成する

新しい貨物は配達されておらず、新しいIDを受け取る必要があります。 次に、それをデータベースに保存する必要があります。

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"

これで、委託品のID 3dd0f0e4-fc4a-46b4-8dae-a57d47df5207ができました。 次に、誰かが委託品の2つのアイテム(写真とピアノ)を収集します。

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

彼らは進捗状況を確認しますが、まだ配信されていません。

deflection.famed.apple に到達したことを示すメッセージが20:12に送信されているはずですが、遅延し、宛先の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. 結論

この記事では、AWSLambdaなどの軽量コンテナーでHibernateなどの重量フレームワークを使用する際の課題について説明しました。

LambdaとRESTAPIを構築し、DockerとAWSSAMCLIを使用してローカルマシンでテストする方法を学びました。 次に、データベースで使用するHibernateのエンティティモデルを構築しました。 また、Hibernateを使用してテーブルを初期化しました。

最後に、Hibernate SessionFactory をアプリケーションに統合し、Lambdaが終了する前にアプリケーションを確実に閉じるようにしました。

いつものように、この記事のサンプルコードは、GitHubにあります。