原文はこちら
このシリーズではここまで、KubernetesおよびDocker用のクラウド環境のセットアップ、Autonomous DBの構築と稼働、HelidonとHibernateを使うマイクロサービスの作成を行ってきました。ここまでで、われわれの最初のマイクロサービスである架空のソーシャルメディアアプリケーションについて、ユーザーデータを永続化するためのデータモデルと永続化ロジックの作成に進む準備ができました。もしこれまでのシリーズを読んでいない場合には、これからご説明することについていきやすくなるように、以前のポストを先に読んでおくことをオススメします。注意:このシリーズのすべてのコードは
GitHubに置いてあります。
ユーザーマイクロサービスアプリケーションの設定などはここまででできたため、ユーザーオブジェクトを表すモデルを作成していきましょう。 model
という新しいパッケージを作成して、その中に User.java
クラスを作成します。データベースカラムにマップするためのいくつかのプロパティと、永続化を試みる前にデータが適正であることを確かにしておくためのいくつかのバリデーション制約を追加します。覚えているかもしれませんが、われわれのテーブルは以下のDDLスクリプトで作成されたものでした:
| CREATETABLEusers( |
| "ID"VARCHAR2(32 BYTE) DEFAULT ONNULL 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 ONNULLCURRENT_TIMESTAMP, |
| CONSTRAINT"USER_PK"PRIMARY KEY ("ID") |
| ); |
なので、 User
オブジェクトには5つのプロパティが必要です。id
, firstName
, lastName
, username
と createdOn
です。firstName
, lastName
, username
のプロパティは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) |
| privateString id; |
| |
| @Column(name="first_name") |
| @NotNull |
| @Size(max=50) |
| privateString firstName; |
| |
| @Column(name="last_name") |
| @NotNull |
| @Size(max=50) |
| privateString lastName; |
| |
| @Column(name="username") |
| @NotNull |
| @Size(max=50) |
| privateString username; |
| |
| @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSSXXX") |
| @Column(name="created_on") |
| privateDate createdOn =newDate(); |
次のステップはサービス永続化オペレーションのためのリポジトリ作成です。作成した user
パッケージの中に UserRepository.java
というクラスを作りましょう。
| @RequestScoped |
| publicclassUserRepository { |
| } |
シリーズの前回のポストで作成したUserProviderをインジェクションし設定情報をこのリポジトリに入れ込むとともにコンストラクタでエンディティマネージャを作成しておきます:
| @RequestScoped |
| publicclassUserRepository { |
| @PersistenceContext |
| privatestaticEntityManager entityManager; |
| |
| @Inject |
| publicUserRepository(UserProvideruserProvider) { |
| Map<String, Object> configOverrides =newHashMap<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(); |
| } |
| } |
ここで validate()
メソッドを追加し、Userをセーブする前に適切であることを確かにしておきましょう:
| publicSet<ConstraintViolation<User>> validate(User user) { |
| Validator validator =Validation.buildDefaultValidatorFactory().getValidator(); |
| Set<ConstraintViolation<User>> constraintViolations = validator.validate(user); |
| return constraintViolations; |
| } |
最後に、 save()
, get()
, findAll()
, count()
と deleteById()
メソッドを追加してリポジトリは完成です。これらはよくあるふつうのCRUDメソッドなので、説明は省いてコードを貼りますね:
| publicUser save(User user) { |
| entityManager.getTransaction().begin(); |
| entityManager.persist(user); |
| entityManager.getTransaction().commit(); |
| return user; |
| } |
| |
| publicUser get(String id) { |
| User user = entityManager.find(User.class, id); |
| return user; |
| } |
| |
| publicList<User> findAll() { |
| return entityManager.createQuery("from User").getResultList(); |
| } |
| |
| publicList<User> findAll(int offset, int max) { |
| Query query = entityManager.createQuery("from User"); |
| query.setFirstResult(offset); |
| query.setMaxResults(max); |
| return query.getResultList(); |
| } |
| |
| publiclong count() { |
| Query queryTotal = entityManager.createQuery("Select count(u.id) from User u"); |
| long countResult = (long)queryTotal.getSingleResult(); |
| return countResult; |
| } |
| |
| publicvoid 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(); |
| } |
| } |
| } |
次に、 GreetResource
を修正するか置き換えるかして UserResource
を作成します。このリソースファイルはHelidonでのサービスエンドポイントとして定義したところに格納されています:
| @Path("/user") |
| @RequestScoped |
| publicclassUserResource { |
| } |
これはHelidonに対して /users
のパスでクラスの全てのメソッドをリッスンするように命じています。あとでそれぞれのメソッドに対してパスを定義していきますが、その前に @Inject
を使って UserRepository
を取得するコンストラクタを追加しましょう:
| @Inject |
| public UserResource(UserRepository userRepository) { |
| this.userRepository = userRepository; |
| } |
ではリソースにパスを追加していきます。デフォルトパスを定義するには、 @Path
アノテーションをそのメソッドから省きましょう。 http://localhost:8080/user
が呼ばれると常にこのメソッドが呼ばれることになります:
| @GET |
| @Produces(MediaType.APPLICATION_JSON) |
| publicResponse getDefaultMessage() { |
| returnResponse.ok(Map.of("OK", true)).build(); |
| } |
ということで、各CRUDオペレーションを定義するのはリソースメソッドを作成し、適切なリポジトリメソッドを呼び出すということになります。
UserをIDで取得するには:
| @Path("/{id}") |
| @GET |
| @Produces(MediaType.APPLICATION_JSON) |
| publicResponse getById(@PathParam("id") String id) { |
| User user = userRepository.get(id); |
| if( user !=null ) { |
| returnResponse.ok(user).build(); |
| } |
| else { |
| returnResponse.status(404).build(); |
| } |
| } |
Userをリストするには:
| @Path("/list") |
| @GET |
| @Produces(MediaType.APPLICATION_JSON) |
| publicResponse getAllUsers() { |
| returnResponse.ok(this.userRepository.findAll()).build(); |
| } |
Userのリストをページごとに取り出すには:
| @Path("/list/{offset}/{max}") |
| @GET |
| @Produces(MediaType.APPLICATION_JSON) |
| publicResponse getAllUsersPaginated(@PathParam("offset") int offset, @PathParam("max") int max) { |
| returnResponse.ok(this.userRepository.findAll(offset, max)).build(); |
| } |
ID指定でUserを削除するには:
| @Path("/list/{offset}/{max}") |
| @GET |
| @Produces(MediaType.APPLICATION_JSON) |
| publicResponse getAllUsersPaginated(@PathParam("offset") int offset, @PathParam("max") int max) { |
| returnResponse.ok(this.userRepository.findAll(offset, max)).build(); |
| } |
そして最後に、Userの保存です(保存前に validate()
を呼び出し、エラーがあれば422 Unprocessable Entityステータスを返却していることに留意されたし):
| @Path("/save") |
| @POST |
| @Produces(MediaType.APPLICATION_JSON) |
| publicResponse saveUser(User user) { |
| Set<ConstraintViolation<User>> violations = userRepository.validate(user); |
| |
| if( violations.size() ==0 ) { |
| userRepository.save(user); |
| returnResponse.created( |
| uriInfo.getBaseUriBuilder() |
| .path("/user/{id}") |
| .build(user.getId()) |
| ).build(); |
| } |
| else { |
| List<HashMap<String, String>> errors =newArrayList<>(); |
| |
| violations.stream() |
| .forEach( (violation) -> { |
| Object invalidValue = violation.getInvalidValue(); |
| HashMap<String, String> errorMap =newHashMap<>(); |
| errorMap.put("field", violation.getPropertyPath().toString()); |
| errorMap.put("message", violation.getMessage()); |
| errorMap.put("currentValue", invalidValue ==null?null: invalidValue.toString()); |
| errors.add(errorMap); |
| } |
| ); |
| |
| returnResponse.status(422) |
| .entity(Map.of( "validationErrors", errors )) |
| .build(); |
| } |
| |
| } |
これでエンドポイントのコンパイルとテストの準備が完了です。 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} |
新規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 |
新規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"}]} |
作成した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"} |
すべての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"}] |
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 |
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 |
これであなたは最初のHelidonとHibernateを使ったマイクロサービスを作成できました!次のポストではこのサービスをDockerおよびKubernetes上にデプロイします。