[Java] Use DynamoDB query method in Spring Boot

4 minute read

Situation

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); // <- This is a bad story
}

This is the Repository class that I created to use DynamoDB as the DB for my Spring application. However, **this will cause the application to fail to start. ** Therefore, I will talk about making a repository class separately as a countermeasure.

Environmental information

  • OSX Catalina
  • spring boot: 2.2.7 RELEASE
  • spring-data-dynamodb: 5.0.3
  • JAVA: 12

Maeoki

In Spring, access to the DB is the responsibility of the Repository, but if you create a class that inherits the Repository interface provided by various libraries, it is often sufficient without preparing the method yourself.

If you also select DynamoDB as DB, spring-data-dynamodbisconvenient.However,Itseemsthatdataacquisitionbyspecifyingaquery(suchasnarrowingdowninfieldsotherthanhashkeyandrangekey) has not been implemented. For this reason, An error is thrown when the Spring application is started and it fails to start.

Actual error

The error when starting the application was as follows.

bottom of stack trace


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

If you look closely, you can see that this error is being emitted by spring. If you translate the error sentence roughly, “I have defined a query method on this repository, but no query lookup strategy is defined. infrastructure doesn’t explicitly support the query method!” Will be.

If it is completed with a library made by Spring, the implementation of repository is naturally made by Spring and it should be guaranteed that the query method is supported, but the spring-data-dynamodb used this time does not support it. The cause is probably not guaranteed.

Since it is not guaranteed, the first solution I can think of is to tamper with the setting of the query method guarantee, but in reality this library does not really support the query method. (GitHub issue corresponding to this issue is confirmed, and once it is included in the TODO of the update to v5.0.3, it is removed. You can see how it was done.

That’s why it’s brute force, but I decided to implement it myself. (Maybe there are other workarounds.)

Countermeasures

Define a class equivalent to repository by using DynamoDBMapper.

Using DynamoDBMapper provided by AWS, implement a query method that reproduces DynamoDB query search. Refer to it as it is shown with an example in the official AWS document. (In this linked implementation, model is also implemented in the repository equivalent class, but I implement model as a separate class.) https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBMapper.QueryScanExample.html

Here is the final product. (What is the class name in such a case…) Data in DynamoDB is defined as Tweet class elsewhere. Imagine that some sort of Tweet information is accumulated in the DB. This time there is no problem if you only know that the Tweet class has a field named 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;

/**
 * In spring-data-dynamodb used to access Dynamo DB
 * Implementation of unsupported methods (such as scan with query)
 *
 * 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" is like DynamoDB reserved word, so
            // Put a placeholder in the query string and replace it later.
            .withFilterExpression("#t = :val1")
            .withExpressionAttributeNames(
                Map.of("#t", "type")
            ).withExpressionAttributeValues(
                Map.of(":val1", new AttributeValue().withS(type))
            );

    return mapper.scan(Tweet.class, scanExpression);
  }
}

About withExpressionAttributeNames

The instance mapper of DynamoDBMapper class is constructor-injected, but this is defined in the Config class prepared separately and registered as a 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 settings
   * @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());
  }
}

For now, it is a Service class that calls the method defined this time. We should have already injected some repository, so we simply add the element to inject.

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 added this time

  private final Twitter twitter;

  public TweetService(TweetRepository tweetRepository, TweetRepositoryPlugin tweetRepositoryPlugin, Twitter twitter) {
    this.tweetRepository = tweetRepository;
    this.tweetRepositoryPlugin = tweetRepositoryPlugin; // <- repository added this time
    this.twitter = twitter;
  }

  // Below, various method definitions.

in conclusion

That’s it. Official explanation explains how to implement with various patterns of queries. If the description in this article isn’t enough, you can refer to it.

Please let me know if you know other measures.

Note: About .withExpressionAttributeNames()

I have included a comment in the code, but I am using .withExpressionAttributeNames() to replace the field names. The reason for doing this seems that the field type included in the search query I wanted to specify this time was a DynamoDB reserved word, and the following error was initially thrown.

Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: type

You can avoid this error by replacing the field name above.

reference: https://note.kiriukun.com/entry/20190212-attribute-name-is-a-reserved-keyword-in-dynamodb