LoginSignup
0
1

More than 3 years have passed since last update.

Spring BootでDynamoDBのquery methodを使う

Last updated at Posted at 2020-06-06

状況

TweetRepository.java
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という名前のフィールドをもつことだけ把握していれば問題ありません。

TweetRepositoryPlugin.java
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登録しています。

DynamoDBConfig.java
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をインジェクションしていたはずなので、単純にインジェクションする要素を追加するだけです。

TweetService.java
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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1