Репозиторий SpringData Redis со сложным ключом

Мы пытаемся использовать Spring Data CrudRepository в нашем проекте, чтобы обеспечить постоянство для наших объектов домена.
Для начала я выбрал REDIS в качестве бэкэнда, поскольку в первом эксперименте с CrudRepository<ExperimentDomainObject, String> казалось, что запустить его очень просто.

Когда мы пытались поместить его в наш производственный код, все стало еще сложнее, потому что здесь для наших объектов домена не было необходимости использовать простой тип в качестве ключа, поэтому репозиторий был CrudRepository<TestObject, ObjectId>.

Теперь у меня исключение:

Не найдено преобразователя, способного преобразовывать тип [... ObjectId] в тип [byte []]

В поисках этого исключения ​​этот ответ, который привел меня к необразованному экспериментируя с конфигурацией RedisTemplate. (Для своего эксперимента я использую emdedded-redis)

Моя идея заключалась в том, чтобы предоставить RedisTemplate<Object, Object> вместо RedisTemplate<String, Object>, чтобы разрешить использование Jackson2JsonRedisSerializer для работы в качестве keySerializer.

Тем не менее, позвонить testRepository.save(testObject) не удается.

Пожалуйста, посмотрите мой код:

У меня есть общедоступные поля, и я исключил импорт для краткости этого примера. Если они потребуются (чтобы сделать это MVCE), я с радостью их предоставлю. Просто оставьте мне комментарий.

зависимости:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation group: 'redis.clients', name: "jedis", version: '2.9.0'
    implementation group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'
}

RedisConfiguration:

@Configuration
@EnableRedisRepositories
public class RedisConfiguration {
    @Bean
    JedisConnectionFactory jedisConnectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public RedisTemplate<Object, Object> redisTemplate() {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        final RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setDefaultSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setEnableDefaultSerializer(true);

        return template;
    }
}

TestObject

@RedisHash("test")
public class TestObject
{
    @Id public ObjectId testId;
    public String value;

    public TestObject(ObjectId id, String value)
    {
        this.testId = id;
        this.value = value; // In experiment this is: "magic"
    }
}

ObjectId

@EqualsAndHashCode
public class ObjectId {
    public String creator; // In experiment, this is "me"
    public String name;    // In experiment, this is "fool"
}

TestRepository

@Repository
public interface TestRepository extends CrudRepository<TestObject, ObjectId>
{
}

EmbeddedRedisConfiguration

@Configuration
public class EmbeddedRedisConfiguration
{
    private final redis.embedded.RedisServer redisServer;

    EmbeddedRedisConfiguration(RedisProperties redisProperties)
    {
        this.redisServer = new redis.embedded.RedisServer(redisProperties.getPort());
    }

    @PostConstruct
    public void init()
    {
        redisServer.start();
    }

    @PreDestroy
    public void shutdown()
    {
        redisServer.stop();
    }
}

Применение:

@SpringBootApplication
public class ExperimentApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(ExperimentApplication.class, args);
    }
}

Не тот ответ:

Конечно, я мог бы ввести специальный идентификатор, который представляет собой простой тип данных, например JSON-String, который я создаю вручную с помощью jacksons ObjectMapper, а затем использую CrudRepository<TestObject, String>.

Что я тем временем пробовал:

  • RedisTemplate<String, String>
  • RedisTemplate<String, Object>
  • Автоматическое подключение RedisTemplate и установка его сериализатора по умолчанию
  • Registering a Converter<ObjectId, byte[]> to
    • An autowired ConverterRegistry
    • GenericConversionService
      с автоматическим подключением, но, видимо, они ошиблись.

person derM    schedule 29.10.2019    source источник


Ответы (1)


По сути, репозитории Redis используют RedisKeyValueTemplate под капотом для хранения данных в виде пары Key (Id) и Value. Таким образом, ваша конфигурация RedisTemplate не будет работать, если вы не используете ее напрямую.

Таким образом, одним из способов для вас будет использование RedistTemplate напрямую, что-то вроде этого сработает для вас.

@Service
public class TestService {

    @Autowired
    private RedisTemplate redisTemplate;

    public void saveIt(TestObject testObject){
        ValueOperations<ObjectId, TestObject> values = redisTemplate.opsForValue();
        values.set(testObject.testId, testObject);
    }

}

Таким образом, приведенный выше код будет использовать вашу конфигурацию и сгенерировать пару строк в Redis, используя Джексон в качестве сопоставителя как для ключа, так и для значения.

Но если вы хотите использовать репозитории Redis через CrudRepository, вам необходимо создать преобразователи чтения и записи для ObjectId из и в String и byte[] и зарегистрировать их как пользовательские преобразования Redis.

Итак, давайте создадим преобразователи чтения и записи для ObjectId ‹-> String

Читатель

@Component
@ReadingConverter
@Slf4j
public class RedisReadingStringConverter implements Converter<String, ObjectId> {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public ObjectId convert(String source) {
        try {
            return objectMapper.readValue(source, ObjectId.class);
        } catch (IOException e) {
            log.warn("Error while converting to ObjectId.", e);
            throw new IllegalArgumentException("Can not convert to ObjectId");
        }
    }
}

Писатель

@Component
@WritingConverter
@Slf4j
public class RedisWritingStringConverter implements Converter<ObjectId, String> {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convert(ObjectId source) {
        try {
            return objectMapper.writeValueAsString(source);
        } catch (JsonProcessingException e) {
            log.warn("Error while converting ObjectId to String.", e);
            throw new IllegalArgumentException("Can not convert ObjectId to String");
        }
    }
}

И преобразователи чтения и записи для ObjectId ‹-> byte []

Писатель

@Component
@WritingConverter
public class RedisWritingByteConverter implements Converter<ObjectId, byte[]> {

    Jackson2JsonRedisSerializer<ObjectId> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ObjectId.class);

    @Override
    public byte[] convert(ObjectId source) {
        return jackson2JsonRedisSerializer.serialize(source);
    }
}

Читатель

@Component
@ReadingConverter
public class RedisReadingByteConverter implements Converter<byte[], ObjectId> {

     Jackson2JsonRedisSerializer<ObjectId> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ObjectId.class);

    @Override
    public ObjectId convert(byte[] source) {
        return jackson2JsonRedisSerializer.deserialize(source);
    }
}

И наконец, добавьте настраиваемые разговоры Redis. Просто введите код в RedisConfiguration

@Bean
public RedisCustomConversions redisCustomConversions(RedisReadingByteConverter readingConverter,
                                                     RedisWritingByteConverter redisWritingConverter,
                                                     RedisWritingStringConverter redisWritingByteConverter,
                                                     RedisReadingStringConverter redisReadingByteConverter) {
    return new RedisCustomConversions(Arrays.asList(readingConverter, redisWritingConverter, redisWritingByteConverter, redisReadingByteConverter));
}

Итак, теперь, когда преобразователи созданы и зарегистрированы как пользовательские преобразователи Redis, RedisKeyValueTemplate может их использовать, и ваш код должен работать должным образом.

person Babl    schedule 04.11.2019
comment
Спасибо, это хорошее начало для меня. Я сделаю еще несколько оценок, возможно, попрошу разъяснений, и тогда я приму их. Мои требования к награде уже выполнены. - person derM; 04.11.2019
comment
Теперь я использую GenericConverter, который использует ObjectMapper для выполнения всех видов преобразований в и из String и byte[]. Мне просто нужно передать ему список классов, используемых в качестве ключей. Следующим шагом будет настройка этих классов с помощью аннотации. Большое спасибо, @Babl - person derM; 05.11.2019
comment
Ура, рад слышать, что это помогло :) - person Babl; 05.11.2019
comment
привет @derM, не могли бы вы уточнить, мне просто нужно передать ему список классов, используемых в качестве ключей, я хочу написать клиентский преобразователь для List<Int>, может быть, это мне поможет. - person Pavel Polyakov; 13.11.2020