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
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
Use thymeleaf3 with parent without specifying spring-boot-starter-parent in Spring Boot
Use @ControllerAdvice, @ExceptionHandler, HandlerExceptionResolver in Spring Boot to catch exceptions
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)
Thymeleaf usage notes 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
Use Thymeleaf text template mode from Spring Boot
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
Test field-injected class in Spring boot test without using Spring container
Until you start development with Spring Boot in eclipse 1
Try gRPC in Spring Boot & Spring Cloud project (Mac OS)
Introduce swagger-ui to REST API implemented in Spring Boot
[JAVA] [Spring] [MyBatis] Use GROUP BY in SQL Builder
Until you start development with Spring Boot in eclipse 2
How to use In-Memory Job repository in Spring Batch
Specify spring.profiles.active via context-param of web.xml in Spring Boot
SerializationException in Spring Boot (1 series) + spring-security-oauth2 + Redis Session + Heroku
Database environment construction with Docker in Spring boot (IntellJ)
File upload with Spring Boot (do not use Multipart File)
Call your own method with PreAuthorize in Spring Security
Spring Boot learning procedure
Microservices in Spring Cloud
Spring boot memo writing (2)
Spring Boot 2.2 Document Summary
Use MouseListener in Processing
Use images in Rails
[Spring Boot] DataSourceProperties $ DataSourceBeanCreationException
Use PostgreSQL in Scala
Spring Boot 2.3 Application Availability
Spring boot tutorials Topics
Get cookies in Spring
Use PreparedStatement in Java
Download with Spring Boot
spring data dynamodb trap
Get error information using DefaultErrorAttributes and ErrorAttributeOptions in Spring Boot 2.3
Use Spring Test + Mockito + JUnit 4 for Spring Boot + Spring Retry unit tests
How to control transactions in Spring Boot without using @Transactional
I want to know the Method of the Controller where the Exception was thrown in the ExceptionHandler of Spring Boot
[JAVA] [Spring] [MyBatis] Use IN () with SQL Builder
How to bind request parameters in list format with bean property in Spring
Use DynamoDB query method in Spring Boot
Return custom object with @Query in spring-data-jpa