原文はこちら
このシリーズではここまで、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") |
| ); |
なので、 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) |
| 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(); |
次のステップはサービス永続化オペレーションのためのリポジトリ作成です。作成した user
パッケージの中に UserRepository.java
というクラスを作りましょう。
| @RequestScoped |
| public class UserRepository { |
| } |
シリーズの前回のポストで作成した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(); |
| } |
| } |
ここで validate()
メソッドを追加し、Userをセーブする前に適切であることを確かにしておきましょう:
| public Set<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メソッドなので、説明は省いてコードを貼りますね:
| 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(); |
| } |
| } |
| } |
次に、 GreetResource
を修正するか置き換えるかして UserResource
を作成します。このリソースファイルはHelidonでのサービスエンドポイントとして定義したところに格納されています:
| @Path("/user") |
| @RequestScoped |
| public class UserResource { |
| } |
これはHelidonに対して /users
のパスでクラスの全てのメソッドをリッスンするように命じています。あとでそれぞれのメソッドに対してパスを定義していきますが、その前に @Inject
を使って UserRepository
を取得するコンストラクタを追加しましょう:
| @Inject |
| public UserResource(UserRepository userRepository) { |
| this.userRepository = userRepository; |
| } |
ではリソースにパスを追加していきます。デフォルトパスを定義するには、 @Path
アノテーションをそのメソッドから省きましょう。 http://localhost:8080/user
が呼ばれると常にこのメソッドが呼ばれることになります:
| @GET |
| @Produces(MediaType.APPLICATION_JSON) |
| public Response getDefaultMessage() { |
| return Response.ok(Map.of("OK", true)).build(); |
| } |
ということで、各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(); |
| } |
| } |
Userをリストするには:
| @Path("/list") |
| @GET |
| @Produces(MediaType.APPLICATION_JSON) |
| public Response getAllUsers() { |
| return Response.ok(this.userRepository.findAll()).build(); |
| } |
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(); |
| } |
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(); |
| } |
そして最後に、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(); |
| } |
|
|
| } |
これでエンドポイントのコンパイルとテストの準備が完了です。 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上にデプロイします。