[JAVA] Use DynamoDB query method in Spring Boot

Status

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 no good
}

This is the Repository class I created to use DynamoDB as the DB for Spring applications. But ** this causes the application to fail to launch. ** ** So, as a countermeasure, I will talk about making a separate repository class.

Environmental information

Before

In Spring, access to DB is the responsibility of Repository, but if you create a class that inherits the interface of Repository prepared by various libraries, it is often not necessary to prepare the method yourself.

Spring-data-dynamodb is convenient when you also select DynamoDB as the DB. However, it seems that ** data acquisition by query specification (narrowing down by fields other than hash key, range key, etc.) is not implemented **. For this reason, When I start the Spring application, I get an error 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

As you can see from a closer look, this error is being thrown by spring. If you roughly translate the error sentence, "I have a query method defined in this repository, but no query lookup strategy is defined. Infrastructure does not explicitly support the query method!" It will be.

If it is completed with the library made by Spring, the implementation of repository is also 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. It seems that it is not guaranteed.

The first solution that comes to mind is to tweak the query method guarantee settings, as it's because it's not guaranteed, but it seems that this library doesn't really support the query method. (If you check [GitHub issue corresponding to this issue), it will be removed after being included in the TODO of the update to v5.0.3. You can see how it happened. It seems to be difficult ...)

So, although it is forcible, I decided to implement it myself. (Maybe there are other workarounds.)

Countermeasures

Define a class equivalent to repository by yourself using DynamoDBMapper.

Using DynamoDBMapper provided by AWS, implement a query method that reproduces the query search of DynamoDB. It is shown with an example in the official AWS documentation, so please refer to it. (In the implementation of this link, model is also implemented in the class equivalent to repository, 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 elsewhere as a Tweet class. Imagine that the DB has some sort of Tweet information. This time it's okay if you just 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;

/**
 *Spring used to access Dynamo DB-data-In dynamodb
 *Implementation of unsupported methods (such as scan with query specification)
 *
 * 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 a reserved word in DynamoDB, 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](https://qiita.com/ponponpopon/items/9b723d6210a26d512a9f#%E6%B3%A8-withexpressionattributenames%E3%81%AB%E3%81%A4%E3%81%84%E3%81% A6)

The instance mapper of DynamoDBMapper class is injected by constructor, but this is defined in the Config class prepared separately and registered as 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 the time being, it is a Service class that calls the method defined this time. You should have already injected some repository, so simply add the element you want 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; // <-This time added repository

  private final Twitter twitter;

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

  //Below, various method definitions.

in conclusion

That's it. Official Explanation explains how to implement various patterns of queries. If the explanation in this article is not enough, please refer to it.

If anyone knows other solutions, please let me know.

Note: About .withExpressionAttributeNames ()

I've included comments in the code, but I'm using **. WithExpressionAttributeNames () ** to replace the field names. The reason for doing this is that the field ** type ** included in the search query I wanted to specify this time seems to have been a reserved word in DynamoDB, 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 previous field name.

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

Recommended Posts

Use DynamoDB query method in Spring Boot
Use Servlet filter in Spring Boot [Spring Boot 1.x, 2.x compatible]
Set context-param in Spring Boot
Spring Boot 2 multi-project in Gradle
How to use CommandLineRunner in Spring Batch of Spring Boot
Spring Boot + Springfox springfox-boot-starter 3.0.0 Use
Use Spring JDBC with Spring Boot
Major changes in Spring Boot 1.5
NoHttpResponseException in Spring Boot + WireMock
Spring boot controller method memo
How to call and use API in Java (Spring Boot)
Use thymeleaf3 with parent without specifying spring-boot-starter-parent in Spring Boot
Use @ControllerAdvice, @ExceptionHandler, HandlerExceptionResolver in Spring Boot to catch exceptions
How to use Lombok in Spring
Use Basic Authentication with Spring Boot
Spring Boot Hello World in Eclipse
Spring Boot application development in Eclipse
Spring Boot + PostgreSQL error resolution method
Implement REST API in Spring Boot
What is @Autowired in Spring boot?
Use DBUnit for Spring Boot test
Implement Spring Boot application in Gradle
How to use ModelMapper (Spring boot)
Beginning with Spring Boot 0. Use Spring CLI
Thymeleaf usage notes in Spring Boot
Launch (old) Spring Boot project in IntelliJ
Build Spring Boot + Docker image in Gradle
Static file access priority in Spring boot
Output Spring Boot log in json format
Local file download memorandum in Spring Boot
Loosen Thymeleaf syntax checking in Spring Boot
[Practice! ] Display Hello World in Spring Boot
Spring Boot + Thymeleaf BootStrap installation method memo
DI SessionScope Bean in Spring Boot 2 Filter
Use cache with EhCashe 2.x with Spring Boot
Change session timeout time in Spring Boot
SameSite cookie in Spring Boot (Spring Web MVC + Tomcat)
Test controller with Mock MVC in Spring Boot
Asynchronous processing with regular execution in Spring Boot
Use Thymeleaf text template mode from Spring Boot
How to use MyBatis2 (iBatis) with Spring Boot 1.4 (Spring 4)
Run a Spring Boot project in VS Code
How to use built-in h2db with spring boot
Output request and response log in Spring Boot
Challenge Spring Boot
How to add a classpath in Spring Boot
Java tips-Create a Spring Boot project in Gradle
Spring Boot Form
How to bind to property file in Spring Boot
[JAVA] [Spring] [MyBatis] Use IN () with SQL Builder
Spring Boot Memorandum
gae + spring boot
Annotations used in Spring Boot task management tool
View the Gradle task in the Spring Boot project
Check the behavior of getOne, findById, and query methods in Spring Boot + Spring Data JPA
Include external jar in package with Spring boot2 + Maven3
How to create a Spring Boot project in IntelliJ
Encode query parameters with Uri Components Builder in spring
SSO with GitHub OAuth in Spring Boot 1.5.x environment
Test field-injected class in Spring boot test without using Spring container
Until you start development with Spring Boot in eclipse 1