状況
package com.pontsuyo.anyiine.domain.repository;
import com.pontsuyo.anyiine.domain.model.Tweet;
import java.util.List;
import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
import org.springframework.data.repository.CrudRepository;
@EnableScan
public interface TweetRepository extends CrudRepository<Tweet, Long> {
@Override
List<Tweet> findAll();
List<Tweet> findAllByType(String type); // <- これはダメという話をします
}
これはSpringアプリケーションのDBとしてDynamoDBを利用するために私が作成したRepositoryクラスです。 が、これのせいでアプリケーションの起動に失敗します。
そこで対処としてrepositoryクラスを別に自作した話をします。
環境情報
- OSX Catalina
- spring boot: 2.2.7 RELEASE
- spring-data-dynamodb: 5.0.3
- JAVA: 12
まえおき
Springでは、DBへのアクセスはRepositoryの責務としますが、各種ライブラリが用意しているRepositoryのinterfaceを継承したクラスを作れば、メソッドを自分で用意しなくても事足りることが多いです。
DBとしてDynamoDBも選択する場合にはspring-data-dynamodbが便利です。しかし、query指定でのデータ取得(hash key, range key以外のフィールドでの絞り込みなど)は実装されていないようでした。このため、
Springアプリケーションの起動時にエラーが吐かれ、起動に失敗します。
実際に出るエラー
アプリケーション起動時のエラーは以下でした。
Caused by: java.lang.IllegalStateException: You have defined query method in the repository but you don't have any query lookup strategy defined. The infrastructure apparently does not support query methods!
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.<init>(RepositoryFactorySupport.java:553) ~[spring-data-commons-2.2.7.RELEASE.jar:2.2.7.RELEASE]
at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:332) ~[spring-data-commons-2.2.7.RELEASE.jar:2.2.7.RELEASE]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.lambda$afterPropertiesSet$5(RepositoryFactoryBeanSupport.java:297) ~[spring-data-commons-2.2.7.RELEASE.jar:2.2.7.RELEASE]
at org.springframework.data.util.Lazy.getNullable(Lazy.java:212) ~[spring-data-commons-2.2.7.RELEASE.jar:2.2.7.RELEASE]
at org.springframework.data.util.Lazy.get(Lazy.java:94) ~[spring-data-commons-2.2.7.RELEASE.jar:2.2.7.RELEASE]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:300) ~[spring-data-commons-2.2.7.RELEASE.jar:2.2.7.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1855) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1792) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
... 44 common frames omitted
よく見るとわかりますが、このエラーはspringが吐いていますね。
エラー文をざっくり訳すと、
「このrepositoryにqueryメソッドを定義してあるのに、query lookup strategy が何も定義されていない。infrastructureは明示的にqueryメソッドをサポートしていない!」
となります。
Spring謹製のライブラリで完結する場合はrepositoryの実装も当然Spring製であり、queryメソッドがサポートされていることを保証されているはずですが、今回利用しているspring-data-dynamodbはそのサポートが保証されていないことが原因と思われます。
保証されていないことが原因なので、解決方法としてまず思いつくのは、queryメソッド保証の設定をいじることですが、実はこのライブラリは本当にqueryメソッドをサポートしていないようです。(本件に対応するGitHub issueを確認すると、v5.0.3へのアップデートのTODOに一旦含めたあとで外された経緯が見えます。大変そうですね…。 )
というわけで強引ではありますが、自分で実装する方針を取りました。(多分他にも対処法は存在します。)
対策
DynamoDBMapperを利用して自分でrepository相当のクラスを定義する。
AWSが提供しているDynamoDBMapperを利用して、DynamoDBのクエリ検索を再現するようなqueryメソッドを実装します。
AWSの公式ドキュメントで例と共に示されているので、参考にします。(このリンク先の実装ではrepository相当のクラスにmodelも実装されていますが、私はmodelを別クラスとして実装しています。)
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBMapper.QueryScanExample.html
最終的に出来上がったのはこちらです。(こういう場合のクラス名ってどういうのがいいのでしょう…)
DynamoDBでのデータは他所でTweetクラスとして定義しています。DBに何かしらのTweetの情報が集積していると思ってください。
今回はTweetクラスがtypeという名前のフィールドをもつことだけ把握していれば問題ありません。
package com.pontsuyo.anyiine.domain.repository;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.pontsuyo.anyiine.domain.model.Tweet;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Repository;
/**
* Dynamo DB へのアクセスに使用しているspring-data-dynamodbの中で
* サポートされていないメソッド(query指定でのscanなど)の実装
*
* issue: https://github.com/derjust/spring-data-dynamodb/issues/114
*/
@Repository
public class TweetRepositoryPlugin {
private final DynamoDBMapper mapper;
public TweetRepositoryPlugin(DynamoDBMapper mapper) {
this.mapper = mapper;
}
public List<Tweet> findAllByType(String type) {
DynamoDBScanExpression scanExpression =
new DynamoDBScanExpression()
// "type"はDynamoDBの予約語のようなので、
// クエリ文字列では一旦プレースホルダを入れておき、後で置き換える。
.withFilterExpression("#t = :val1")
.withExpressionAttributeNames(
Map.of("#t", "type")
)
.withExpressionAttributeValues(
Map.of(":val1", new AttributeValue().withS(type))
);
return mapper.scan(Tweet.class, scanExpression);
}
}
withExpressionAttributeNamesについて
DynamoDBMapperクラスのインスタンスmapperをコンストラクタインジェクションしていますが、これは別に用意してあるConfigクラスに定義しBean登録しています。
package com.pontsuyo.anyiine.config;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.pontsuyo.anyiine.domain.repository")
public class DynamoDBConfig {
/**
* AWS DynamoDBの設定
* @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain
* @return
*/
@Bean
public AmazonDynamoDB amazonDynamoDB() {
return AmazonDynamoDBClientBuilder.standard()
.withCredentials(DefaultAWSCredentialsProviderChain.getInstance())
.withRegion(Regions.AP_NORTHEAST_1)
.build();
}
@Bean
public DynamoDBMapper dynamoDBMapper(){
return new DynamoDBMapper(amazonDynamoDB());
}
}
一応、今回定義したメソッドを呼び出すServiceクラスです。既に何かしらのrepositoryをインジェクションしていたはずなので、単純にインジェクションする要素を追加するだけです。
package com.pontsuyo.anyiine.domain.service;
import com.pontsuyo.anyiine.controller.model.DestroyRequestParameter;
import com.pontsuyo.anyiine.controller.model.UpdateRequestParameter;
import com.pontsuyo.anyiine.domain.model.Tweet;
import com.pontsuyo.anyiine.domain.repository.TweetRepository;
import com.pontsuyo.anyiine.domain.repository.TweetRepositoryPlugin;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
@Slf4j
@Service
public class TweetService {
private final TweetRepository tweetRepository;
private final TweetRepositoryPlugin tweetRepositoryPlugin; // <- 今回追加したrepository
private final Twitter twitter;
public TweetService(TweetRepository tweetRepository, TweetRepositoryPlugin tweetRepositoryPlugin, Twitter twitter) {
this.tweetRepository = tweetRepository;
this.tweetRepositoryPlugin = tweetRepositoryPlugin; // <- 今回追加したrepository
this.twitter = twitter;
}
// 以下、メソッド定義色々。
おわりに
以上になります。
公式の解説では様々なパターンのクエリでの実装方法を説明していくれています。この記事の説明で不十分だった場合は参考にしてください。
他の対処法をご存知の方がいらしたらお教え願います。
注: .withExpressionAttributeNames()について
コード中にコメントを含めていますが、.withExpressionAttributeNames()を使ってフィールド名を置き換えています。
このようにしている理由は、実は今回私が指定したかった検索クエリに含まれるtypeというフィールドがDynamoDBの予約語だったようで、当初以下のエラーが吐かれました。
Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: type
先のフィールド名の置換によりこのエラーを回避することができます。
参考:
https://note.kiriukun.com/entry/20190212-attribute-name-is-a-reserved-keyword-in-dynamodb