使用Spring Boot来使用DynamoDB的查询方法

情况

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);  // <- これはダメという話をします
}

这是我为了将DynamoDB用作Spring应用程序的数据库而创建的Repository类。但是,由于这个原因,应用程序无法启动。
因此,我决定单独创建一个新的repository类作为解决方案。

环境信息

    • OSX Catalina

 

    • spring boot: 2.2.7 RELEASE

 

    • spring-data-dynamodb: 5.0.3

 

    JAVA: 12

先请说明

在Spring中,访问数据库是Repository的职责,但是如果我们继承了各种库提供的Repository接口,就可以创建一个类,而不需要自己准备很多方法,通常就足够了。

如果选择DynamoDB作为数据库,那么spring-data-dynamodb非常方便。然而,似乎没有实现通过查询条件获取数据(如非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方法!”

如果完全依赖于Spring制作的库,并且实现了repository,那么应该可以保证支持查询方法。但是这次使用的spring-data-dynamodb似乎没有保证这种支持。

因为缺乏保证,所以首先想到的解决方法是修改查询方法的设置,但实际上这个库似乎真的不支持查询方法。(查看相关的GitHub问题,可以看到它曾经在v5.0.3更新的TODO列表中,但后来被取消了。看起来挺棘手的呢…)

因此,我採取了一種強行的方式來自行實施。(可能還有其他處理方法。)

解决方案

使用DynamoDBMapper来定义自己的repository等效类。

我将使用AWS提供的DynamoDBMapper来实现一个query方法,以模拟DynamoDB的查询搜索。我会参考AWS官方文档上提供的示例(在这个链接的实现中,模型也被实现在了相当于repository的类中,但我会将模型作为一个单独的类实现)。请参考以下链接:
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBMapper.QueryScanExample.html

這是最終完成的結果。(對於這種情況,應該使用怎樣的類別名呢…)
在DynamoDB中,我們將數據定義為Tweet類別。請想像數據庫中集合了各種Tweet的信息。
這次只需要知道Tweet類別有一個名為”類型”的欄位就可以了,沒有問題的。

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类。由于应该已经注入了某种类型的仓库,所以只需简单地添加注入元素即可。

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

广告
将在 10 秒后关闭
bannerAds