前回に引き続き、Spring Data DynamoDBを使ってデータベースアクセスするアプリケーションを実装していきます。
アプリケーションコンポーネントの実装
では早速、アプリケーションコンポーネントを実装していきましょう。データ追加/更新処理では、画面から受け取るリクエストパラメータのクラスを以下のように作成しています。
package org.debugroom.mynavi.sample.spring.data.dynamodb.app.model;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class SampleModel implements Serializable {
private String samplePartitionKey;
private String sampleSortKey;
private String sampleText;
}
Controllerでは、以下5種類のリクエストを受け取り、ロジックを実行して結果をテンプレートに渡す処理を実装します。
- データを全件取得するServiceを実行し、実行結果をfindAll.htmlのテンプレートへ渡す処理
- 指定したパーティションキー/ソートキーをもつデータを取得するServiceを実行し、実行結果をfindOne.htmlのテンプレートへ渡す処理
- データ追加するServiceを実行し、実行結果をadd.htmlのテンプレートに渡す処理
- 指定したパーティションキー/ソートキーのデータを更新するServiceを実行し、実行結果update.htmlのテンプレートへ渡す処理
- 指定したパーティションキー/ソートキーのデータを削除するServiceを実行し、実行結果delete.htmlのテンプレートへ渡す処理
package org.debugroom.mynavi.sample.spring.data.dynamodb.app.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class SampleController {
@Autowired
SampleService sampleService;
@RequestMapping(method = RequestMethod.GET, value="findAll")
public String findAll(Model model){
model.addAttribute("mynaviSampleTables", sampleService.getMynaviSampleTables());
return "findAll";
}
@RequestMapping(method = RequestMethod.GET, value="findOne")
public String findOne(SampleModel sampleModel, Model model){
model.addAttribute("mynaviSampleTable", sampleService.getMynaviSampleTable(
SampleModelMapper.mapToMynaviSampleTableKey(sampleModel)));
return "findOne";
}
@RequestMapping(method = RequestMethod.POST, value="add")
public String add(SampleModel sampleModel, Model model){
model.addAttribute("mynaviSampleTable",
sampleService.addMynaviSampleTable(SampleModelMapper.map(sampleModel)));
return "add";
}
@RequestMapping(method = RequestMethod.POST, value = "update")
public String update(SampleModel sampleModel, Model model){
model.addAttribute("mynaviSampleTable",
sampleService.updateMynaviSampleTable(SampleModelMapper.map(sampleModel)));
return "update";
}
@RequestMapping(method = RequestMethod.POST, value = "delete")
public String delete(SampleModel sampleModel, Model model){
model.addAttribute("mynaviSampleTable",
sampleService.deleteMynaviSampleTable(
SampleModelMapper.mapToMynaviSampleTableKey(sampleModel)));
return "delete";
}
}
また、Serviceを呼び出す際には、リクエストパラメータオブジェクトをServiceのインプットオブジェクトであるDynamoDBのテーブルクラスやプライマリキークラスへ変換するのですが、そこで必要になるマッパークラスをインタフェースで実装しておきます※。
※ Java8から、staticメソッドであればインタフェースでも実装できるようになっています。
package org.debugroom.mynavi.sample.spring.data.dynamodb.app.model;
import org.debugroom.mynavi.sample.spring.data.dynamodb.domain.model.MynaviSampleTable;
public interface SampleModelMapper {
public static MynaviSampleTable map(SampleModel sampleModel){
return MynaviSampleTable.builder()
.samplePartitionKey(sampleModel.getSamplePartitionKey())
.sampleSortKey(sampleModel.getSampleSortKey())
.sampleText(sampleModel.getSampleText())
.build();
}
public static MynaviSampleTableKey mapToMynaviSampleTableKey(SampleModel sampleModel){
return MynaviSampleTableKey.builder()
.samplePartitionKey(sampleModel.getSamplePartitionKey())
.sampleSortKey(sampleModel.getSampleSortKey())
.build();
}
}
Serviceクラスでは、以下の通り、Repositoryを通してCRUD処理を実行する処理を実装します。
- 【findOne】SampleRepository.findById():指定したパーティションキー/ソートキーでデータを取得する処理
- 【findAll】SampleRepository.findAll:作成したDynamoDBのテーブルの全件データをList型で受け取る処理
- 【add】SampleRepository.save():パーティションキーにランダムなUUID文字列を、ソートキーには”1”を設定し、リクエストから受け取ったテキスト文字列を設定して保存するロジック
- 【update】SampleRepository.save():指定したパーティションキー/ソートキーのテキストデータを更新する処理
- 【delete】SampleRepository.deleteById():指定したパーティションキー/ソートキーのデータを削除する処理
package org.debugroom.mynavi.sample.spring.data.dynamodb.domain.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SampleServiceImpl implements SampleService{
@Autowired
SampleRepository sampleRepository;
@Override
public MynaviSampleTable getMynaviSampleTable(MynaviSampleTableKey mynaviSampleTableKey) {
return sampleRepository.findById(mynaviSampleTableKey).get();
}
@Override
public List getMynaviSampleTables() {
List sampleTables = new ArrayList<>();
sampleRepository.findAll().iterator().forEachRemaining(sampleTables::add);
return sampleTables;
}
@Override
public MynaviSampleTable addMynaviSampleTable(MynaviSampleTable mynaviSampleTable) {
mynaviSampleTable.setSamplePartitionKey(UUID.randomUUID().toString());
mynaviSampleTable.setSampleSortKey("1");
return sampleRepository.save(mynaviSampleTable);
}
@Override
public MynaviSampleTable updateMynaviSampleTable(MynaviSampleTable mynaviSampleTable) {
return sampleRepository.save(mynaviSampleTable);
}
@Override
public MynaviSampleTable deleteMynaviSampleTable(MynaviSampleTableKey mynaviSampleTableKey) {
MynaviSampleTable mynaviSampleTable = sampleRepository.findById(mynaviSampleTableKey).get();
sampleRepository.deleteById(mynaviSampleTableKey);
return mynaviSampleTable;
}
}
ここで、DynamoDBへアクセスするコンポーネントであるSampleRepositoryインタフェースは、以下のような要領で実装しておく必要があります。
- org.socialsignin.spring.data.dynamodb.repository.EnableScanアノテーションを付与する
- org.springframework.data.repository.CrudRepositoryを継承する
- テーブルをモデル化したクラスとキーをCrudRepositoryの型パラメータに設定する
package org.debugroom.mynavi.sample.spring.data.dynamodb.domain.repository;
import org.debugroom.mynavi.sample.spring.data.dynamodb.domain.model.MynaviSampleTableKey;
import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
import org.springframework.data.repository.CrudRepository;
import org.debugroom.mynavi.sample.spring.data.dynamodb.domain.model.MynaviSampleTable;
@EnableScan
public interface SampleRepository extends CrudRepository {
}
●なぜインタフェースだけでメソッドを実行できるのか?
特に実装クラスを作成することもなく、インタフェースだけでfindAllやsaveメソッドが実行できる理由は、Spring Data DynamoDBが、GenericDAOパターンに基づく実装クラスを提供しているためです。
GenericDAOパターンはJavaの型パラメータを利用した実装で、CRUD処理をJavaのジェネリクス機構を使って実装しておき、テーブルの種類に応じた型パラメータクラスを設定します。こうすることにより、汎用的なCRUD共通処理を実装したDAO(DatabaseAccessObject)を作成しておくわけです。
Spring Data Dynamoに限らず、Spring Data JPAなど類似する一連のプロダクトでは、同様にインタフェースを作成するだけで、基本的なCRUDはほぼ実装せずにデータベースアクセス処理を行うことが可能です。型パラメータとして指定するDynamoDBのテーブルクラスは、以下の要領で作成します。
- テーブルクラスに@DynamoDBTableアノテーションを付与し、前回作成したDynamoDBテーブルのテーブル名を指定します
- テーブルクラスにコンストラクタやGetter、Setterメソッドを付与する実装を省略する目的でLombokアノテーションを付与します
- パーティションキーには@DynamoDBHashKey、ソートキーには@DynamoDBRangeKeyアノテーションを付与します
- その他、追加したい属性には@DynamoDBAttributeアノテーションを付与します
また、プライマリキーがパーティションキーとソートキーで構成される場合、プライマリキークラスを作成し、テーブルクラスに定義する必要があります。プライマリキークラスには、org.springframework.data.annotation.Idアノテーションを付与しますが、Getter/Setterメソッドは付与してはいけません。
一方、パーティションキーのみで構成される場合は、テーブルクラスに@DynamoDBHashKeyのみを設定し、Repositoryの型パラメータにはキーのプリミティブな型を設定してください。
package org.debugroom.mynavi.sample.spring.data.dynamodb.domain.model;
import java.io.Serializable;
import lombok.*;
import org.springframework.data.annotation.Id;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@DynamoDBTable(tableName = "mynavi-sample-table")
public class MynaviSampleTable implements Serializable {
@Id
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private MynaviSampleTableKey mynaviSampleTableKey;
@DynamoDBHashKey
private String samplePartitionKey;
@DynamoDBRangeKey
private String sampleSortKey;
@DynamoDBAttribute
private String sampleText;
}
package org.debugroom.mynavi.sample.spring.data.dynamodb.domain.model;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey;
import java.io.Serializable;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class MynaviSampleTableKey implements Serializable {
@DynamoDBHashKey
private String samplePartitionKey;
@DynamoDBRangeKey
private String sampleSortKey;
}
上記のテーブルクラスでは、一律で@DataアノテーションによりSetter/Getterメソッドを付与していますが、プライマリキークラスには「AccessLevel.NONE」を設定して除外しています。Spring Data DynamoDBの公式Wikiにもありますが、プライマリキークラスにGetter/Setterメソッドを付けていると、例外(DynamoDBMappingException: not supported; requires @DynamoDBTyped or @DynamoDBTypeConverted)が発生します。
アプリケーションを実行してみる
これでアプリケーションが完成しました。SpringBoot起動クラスを実行し、アプリケーションを実行しましょう。ブラウザから「http://localhost:8080/index.html」にアクセスすると以下のような画面が表示され、5つのサービスを実行できます。
任意のテキストデータを入力し、「add Data」ボタンを押下すると、データが追加されます。パーティションキーは「UUID」、ソートキーは「1」で固定です。
「find All Data」ボタンを押下すると、全てのデータが取得されます。
また、パーティションキーとソートキーを指定して「find One Data」ボタンを押下すると、該当のデータが取得されます。
パーティションキーとソートキーを指定して、テキストデータを入力し、「Update Data」ボタンを押下すると、該当のデータが更新されます。
パーティションキーとソートキーを指定して「Delete Data」ボタンを押下すると、該当のデータが削除されます。
このように、DynamoDBへCRUD処理するアプリケーションをSpring Data DynamoDBを用いて簡単に実装することができます。実際のアプリケーションで、さまざまなユースケースに応じてデータモデルを検討しておく必要がありますが、その辺りについては応用編などでおいおい触れたいと思います。
次回以降はApache Cassandraについて解説を進める予定でしたが、そちらについては別の機会に引き継ぐこととし、CP型データベースの代表的プロダクトである「Redis」とAWSのマネージドサービス「Amazon ElastiCache」へアクセスするアプリケーションを「Spring Session」、「Spring Data Redis」を用いて実装する例をご紹介します。
著者紹介
川畑 光平(KAWABATA Kohei) - NTTデータ 課長代理
金融機関システム業務アプリケーション開発・システム基盤担当を経て、現在はソフトウェア開発自動化関連の研究開発・推進に従事。
Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなどさまざまな開発プロジェクト支援にも携わる。2019 APN AWS Top Engineers & Ambassadors選出。