[Microservices]Hibernateを使ったHelidonマイクロサービスの作成とデプロイ パート2/ Building And Deploying A Helidon Microservice With Hibernate Part 2

原文はこちら

このシリーズではここまで、KubernetesおよびDocker用のクラウド環境のセットアップAutonomous DBの構築と稼働HelidonとHibernateを使うマイクロサービスの作成を行ってきました。ここまでで、われわれの最初のマイクロサービスである架空のソーシャルメディアアプリケーションについて、ユーザーデータを永続化するためのデータモデルと永続化ロジックの作成に進む準備ができました。もしこれまでのシリーズを読んでいない場合には、これからご説明することについていきやすくなるように、以前のポストを先に読んでおくことをオススメします。

注意:このシリーズのすべてのコードはGitHubに置いてあります。
ユーザーマイクロサービスアプリケーションの設定などはここまででできたため、ユーザーオブジェクトを表すモデルを作成していきましょう。 model という新しいパッケージを作成して、その中に User.javaクラスを作成します。データベースカラムにマップするためのいくつかのプロパティと、永続化を試みる前にデータが適正であることを確かにしておくためのいくつかのバリデーション制約を追加します。覚えているかもしれませんが、われわれのテーブルは以下のDDLスクリプトで作成されたものでした:
CREATE TABLE users(
"ID" VARCHAR2(32 BYTE) DEFAULT ON NULL SYS_GUID(), 
"FIRST_NAME" VARCHAR2(50 BYTE) COLLATE "USING_NLS_COMP" NOT NULL ENABLE, 
"LAST_NAME" VARCHAR2(50 BYTE) COLLATE "USING_NLS_COMP" NOT NULL ENABLE, 
"USERNAME" VARCHAR2(50 BYTE) COLLATE "USING_NLS_COMP" NOT NULL ENABLE, 
"CREATED_ON" TIMESTAMP (6) DEFAULT ON NULL CURRENT_TIMESTAMP
CONSTRAINT "USER_PK" PRIMARY KEY ("ID")
);
view rawcreate-user-tbl.sql hosted with ❤ by GitHub
なので、 User オブジェクトには5つのプロパティが必要です。idfirstNamelastNameusername と createdOnです。firstNamelastNameusername のプロパティはNull不可のstringで、最大50文字にします。 ID プロパティはGUIDで、 createdOn プロパティはタイムスタンプです。そういうわけで、われわれのUserオブジェクトのプロパティとバリデーションアノテーションは以下のようになります:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "guid")
@Column(name = "id", unique = true, nullable = false)
private String id;
@Column(name = "first_name")
@NotNull
@Size(max=50)
private String firstName;
@Column(name = "last_name")
@NotNull
@Size(max=50)
private String lastName;
@Column(name = "username")
@NotNull
@Size(max=50)
private String username;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
@Column(name = "created_on")
private Date createdOn = new Date();
view rawUser.java hosted with ❤ by GitHub
Userオブジェクトの残りの部分はふつうのテンプレで、なにも難しいところはありません。
次のステップはサービス永続化オペレーションのためのリポジトリ作成です。作成した user パッケージの中に UserRepository.java というクラスを作りましょう。
@RequestScoped
public class UserRepository {
}
view rawUserRepository.java hosted with ❤ by GitHub
シリーズの前回のポストで作成したUserProviderをインジェクションし設定情報をこのリポジトリに入れ込むとともにコンストラクタでエンディティマネージャを作成しておきます:
@RequestScoped
public class UserRepository {
    @PersistenceContext
    private static EntityManager entityManager;
    @Inject
    public UserRepository(UserProvider userProvider) {
        Map<String, Object> configOverrides = new HashMap<String, Object>();
        configOverrides.put("hibernate.connection.url", userProvider.getDbUrl());
        configOverrides.put("hibernate.connection.username", userProvider.getDbUser());
        configOverrides.put("hibernate.connection.password", userProvider.getDbPassword());
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("UserPU", configOverrides);
        entityManager = emf.createEntityManager();
    }
}
view rawUserRepository.java hosted with ❤ by GitHub
ここで validate() メソッドを追加し、Userをセーブする前に適切であることを確かにしておきましょう:
public Set<ConstraintViolation<User>> validate(User user) {
    Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    Set<ConstraintViolation<User>> constraintViolations = validator.validate(user);
    return constraintViolations;
}
view rawUserRepository.java hosted with ❤ by GitHub
最後に、 save()get()findAll()count() と deleteById()メソッドを追加してリポジトリは完成です。これらはよくあるふつうのCRUDメソッドなので、説明は省いてコードを貼りますね:
public User save(User user) {
    entityManager.getTransaction().begin();
    entityManager.persist(user);
    entityManager.getTransaction().commit();
    return user;
}
public User get(String id) {
    User user = entityManager.find(User.class, id);
    return user;
}
public List<User> findAll() {
    return entityManager.createQuery("from User").getResultList();
}
public List<User> findAll(int offset, int max) {
    Query query = entityManager.createQuery("from User");
    query.setFirstResult(offset);
    query.setMaxResults(max);
    return query.getResultList();
}
public long count() {
    Query queryTotal = entityManager.createQuery("Select count(u.id) from User u");
    long countResult = (long)queryTotal.getSingleResult();
    return countResult;
}
public void deleteById(String id) {
    // Retrieve the movie with this ID
    User user = get(id);
    if (user != null) {
        try {
            entityManager.getTransaction().begin();
            entityManager.remove(user);
            entityManager.getTransaction().commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
view rawUserRepository.java hosted with ❤ by GitHub
次に、 GreetResource を修正するか置き換えるかして UserResourceを作成します。このリソースファイルはHelidonでのサービスエンドポイントとして定義したところに格納されています:
@Path("/user")
@RequestScoped
public class UserResource {
}
view rawUserResource.java hosted with ❤ by GitHub
これはHelidonに対して /users のパスでクラスの全てのメソッドをリッスンするように命じています。あとでそれぞれのメソッドに対してパスを定義していきますが、その前に @Inject を使って UserRepositoryを取得するコンストラクタを追加しましょう:
@Inject
public UserResource(UserRepository userRepository) {
    this.userRepository = userRepository;
}
view rawUserResource.java hosted with ❤ by GitHub
ではリソースにパスを追加していきます。デフォルトパスを定義するには、 @Path アノテーションをそのメソッドから省きましょう。 http://localhost:8080/user が呼ばれると常にこのメソッドが呼ばれることになります:
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getDefaultMessage() {
    return Response.ok(Map.of("OK", true)).build();
}
view rawUserResource.java hosted with ❤ by GitHub
ということで、各CRUDオペレーションを定義するのはリソースメソッドを作成し、適切なリポジトリメソッドを呼び出すということになります。
UserをIDで取得するには:
@Path("/{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getById(@PathParam("id") String id) {
    User user = userRepository.get(id);
    if( user != null ) {
        return Response.ok(user).build();
    }
    else {
        return Response.status(404).build();
    }
}
view rawUserResource.java hosted with ❤ by GitHub
Userをリストするには:
@Path("/list")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getAllUsers() {
    return Response.ok(this.userRepository.findAll()).build();
}
view rawUserRepository.java hosted with ❤ by GitHub
Userのリストをページごとに取り出すには:
@Path("/list/{offset}/{max}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getAllUsersPaginated(@PathParam("offset") int offset, @PathParam("max") int max) {
    return Response.ok(this.userRepository.findAll(offset, max)).build();
}
view rawUserRepository.java hosted with ❤ by GitHub
ID指定でUserを削除するには:
@Path("/list/{offset}/{max}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getAllUsersPaginated(@PathParam("offset") int offset, @PathParam("max") int max) {
    return Response.ok(this.userRepository.findAll(offset, max)).build();
}
view rawUserRepository.java hosted with ❤ by GitHub
そして最後に、Userの保存です(保存前に validate() を呼び出し、エラーがあれば422 Unprocessable Entityステータスを返却していることに留意されたし):
@Path("/save")
@POST
@Produces(MediaType.APPLICATION_JSON)
public Response saveUser(User user) {
    Set<ConstraintViolation<User>> violations = userRepository.validate(user);
    if( violations.size() == 0 ) {
        userRepository.save(user);
        return Response.created(
                uriInfo.getBaseUriBuilder()
                        .path("/user/{id}")
                        .build(user.getId())
        ).build();
    }
    else {
        List<HashMap<String, String>> errors = new ArrayList<>();
        violations.stream()
                .forEach( (violation) -> {
                            Object invalidValue = violation.getInvalidValue();
                            HashMap<String, String> errorMap = new HashMap<>();
                            errorMap.put("field", violation.getPropertyPath().toString());
                            errorMap.put("message", violation.getMessage());
                            errorMap.put("currentValue", invalidValue == null ? null : invalidValue.toString());
                            errors.add(errorMap);
                        }
                );
        return Response.status(422)
                .entity(Map.of( "validationErrors", errors ))
                .build();
    }
}
view rawUserResource.java hosted with ❤ by GitHub
これでエンドポイントのコンパイルとテストの準備が完了です。 mvn package でサービスをコンパイルし、以下のコマンドでアプリケーションを実行しましょう。いくつかのプロパティにはパスを指定してやる必要があるので、思い出せなければ以前のポストなどをみてWalletファイルへのパスやスキーマユーザー名とパスワードなどを調べてください。パスとクレデンシャルは適切に置き換えて使ってください:
java 
    -Doracle.net.wallet_location=/path/to/wallet \
    -Doracle.net.authentication_services="(TCPS)" \
    -Doracle.net.tns_admin=/wallet-demodb \
    -Djavax.net.ssl.trustStore=/path/to/wallet/cwallet.sso \
    -Djavax.net.ssl.trustStoreType=SSO \
    -Djavax.net.ssl.keyStore=/path/to/wallet/cwallet.sso \
    -Djavax.net.ssl.keyStoreType=SSO \
    -Doracle.net.ssl_server_dn_match=true \
    -Doracle.net.ssl_version="1.2" \
    -Ddatasource.username=[username] \
    -Ddatasource.password=[password] \
    -Ddatasource.url=jdbc:oracle:thin:@demodb_LOW?TNS_ADMIN=/path/to/wallet \
-jar target/user-svc.jar
アプリケーションが起動し、ローカルホストのポート8080番で稼働しているはずです。この時点でエンドポイントのテストを以下のように行なえます:
ユーザーサービスエンドポイントのGET(200 OKが返却):
curl -iX GET http://localhost:8080/user                                                                                                                                                    
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 20 Jun 2019 10:35:06 -0400
transfer-encoding: chunked
connection: keep-alive
{"OK":true} 
view rawtest-path.sh hosted with ❤ by GitHub
新規Userの保存(LocationヘッダでIDが返却される):
curl -iX POST -H "Content-Type: application/json" -d '{"firstName": "Todd", "lastName": "Sharp", "username": "recursivecodes"}' http://localhost:8080/user/save                            
HTTP/1.1 201 Created
Date: Thu, 20 Jun 2019 10:45:38 -0400
Location: http://[0:0:0:0:0:0:0:1]:8080/user/8BC3669097C9EC53E0532110000A6E11
transfer-encoding: chunked
connection: keep-alive
view rawsave-user.sh hosted with ❤ by GitHub
新規Userを不正なデータで保存(422とバリデーションエラーが返却):
curl -iX POST -H "Content-Type: application/json" -d '{"firstName": "A Really Long First Name That Will Be Longer Than 50 Chars", "lastName": null, "username": null}' http://localhost:8080/user/save
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Date: Mon, 1 Jul 2019 11:21:57 -0400
transfer-encoding: chunked
connection: keep-alive
{"validationErrors":[{"field":"username","message":"may not be null","currentValue":null},{"field":"lastName","message":"may not be null","currentValue":null},{"field":"firstName","message":"size must be between 0 and 50","currentValue":"A Really Long First Name That Will Be Longer Than 50 Chars"}]}
view rawsave-invalid-user.sh hosted with ❤ by GitHub
作成したUserのGET:
curl -iX GET http://localhost:8080/user/8BC3669097C9EC53E0532110000A6E11                                                                                                                   
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 20 Jun 2019 10:46:17 -0400
transfer-encoding: chunked
connection: keep-alive
{"id":"8BC3669097C9EC53E0532110000A6E11","firstName":"Todd","lastName":"Sharp","username":"recursivecodes","createdOn":"2019-06-20T14:45:38.509Z"}
view rawget-user.sh hosted with ❤ by GitHub
すべてのUserのList:
curl -iX GET http://localhost:8080/user/list                                                                                                                                               
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 20 Jun 2019 10:46:51 -0400
transfer-encoding: chunked
connection: keep-alive
[{"id":"8BC3669097C9EC53E0532110000A6E11","firstName":"Todd","lastName":"Sharp","username":"recursivecodes","createdOn":"2019-06-20T14:45:38.509Z"}]
view rawlist-users.sh hosted with ❤ by GitHub
UserのDELETE:
curl -iX DELETE http://localhost:8080/user/8BC3669097C9EC53E0532110000A6E11                                                                                                                
HTTP/1.1 204 No Content
Date: Thu, 20 Jun 2019 10:47:21 -0400
connection: keep-alive
view rawdelete-user.sh hosted with ❤ by GitHub
DELETEの確認(ID指定でGETすると404が返却):
curl -iX GET http://localhost:8080/user/8BC3669097C9EC53E0532110000A6E11                                                                                                                   
HTTP/1.1 404 Not Found
Date: Thu, 20 Jun 2019 10:47:43 -0400
transfer-encoding: chunked
connection: keep-alive
view rawconfirm-delete.sh hosted with ❤ by GitHub

これであなたは最初のHelidonとHibernateを使ったマイクロサービスを作成できました!次のポストではこのサービスをDockerおよびKubernetes上にデプロイします。