0

Problem

I am trying to save an entity that has a field of type javax.money.MonetaryAmount to Redis using spring-data-redis:

@RedisHash("transaction")
class Transaction(
    @org.springframework.data.annotation.Id
    val id: Id,
    val description: String,
    val amount: MonetaryAmount
) {
    class Id(val value: UUID) {
        companion object {
            fun random(): Id = Id(UUID.randomUUID())
        }
    }
}

The problem is that javax.money.MonetaryAmount has a method called MonetaryAmountFactory<? extends MonetaryAmount> getFactory(); which Spring considers to be a property. When it tries to cache this property, I get the following exception:

org.springframework.data.keyvalue.core.UncategorizedKeyValueException: No getter available for persistent property javax.money.MonetaryAmountFactory.number

Investigation

I've debugged the code and the problem starts from MappingRedisConverter.write(object,sink) on the line:

Iterator var6 = this.indexResolver.resolveIndexesFor(entity.getTypeInformation(), source).iterator();

At this point:

  • Redis has used a custom converter to write the key
  • Redis has used a custom converter to write the value
  • The problem occurs while trying to find properties to index.

I have configured custom serializers but to no avail. This kinda makes sense, because the problem isn't in the serialization, it's in the indexing.

@Configuration
@EnableRedisRepositories
class RedisConfiguration {

    @Bean
    fun redisTemplate(connectionFactory: RedisConnectionFactory?): RedisTemplate<String, Any>? {
        val template = RedisTemplate<String, Any>()
        template.connectionFactory = connectionFactory

        val entitySerializer = GenericJackson2JsonRedisSerializer(objectMapper())
        template.setDefaultSerializer(entitySerializer)
        template.valueSerializer = entitySerializer
        template.hashKeySerializer = entitySerializer
        template.hashValueSerializer = entitySerializer
        return template
    }

    @Bean
    fun objectMapper(): ObjectMapper {
        val objectMapper = ObjectMapper()
        objectMapper.registerModule(Jdk8Module())
        objectMapper.registerModule(JavaTimeModule())
        objectMapper.registerModule(MoneyModule())
        return objectMapper
    }

    @Bean
    fun redisConverter(
        mappingContext: RedisMappingContext?,
        customConversions: RedisCustomConversions?,
        referenceResolver: ReferenceResolver?
    ): MappingRedisConverter? {
        val mappingRedisConverter = MappingRedisConverter(
            mappingContext, null, referenceResolver,
            DefaultRedisTypeMapper()
        )
        mappingRedisConverter.setCustomConversions(redisCustomConversions())
        return mappingRedisConverter
    }


    @Bean
    fun redisCustomConversions(): RedisCustomConversions? {
        return RedisCustomConversions(
            mutableListOf(
                ByteArrayToTransactionIdConverter(), // MappingRedisConverter uses converters to convert Objects to Byte Array.
                TransactionIdToByteArrayConverter(),
                StringToTransactionIdConverter(), // MappingRedisConverter converts Id to type String
                TransactionIdToStringConverter(),
                ByteArrayToMonetaryAmountConverter(), // Custom Converter is required because MonetaryAmount uses java.util module which can't be opneed by Reflection.
                MonetaryAmountToByteArrayConverter(),
            )
        )
    }
}

@Component
@ReadingConverter
class ByteArrayToTransactionIdConverter : Converter<ByteArray, Transaction.Id> {
    override fun convert(source: ByteArray) = source.toString(Charsets.UTF_8)
        .let(UUID::fromString)
        .let(Transaction::Id)
}

@Component
@WritingConverter
class TransactionIdToByteArrayConverter : Converter<Transaction.Id, ByteArray> {
    override fun convert(source: Transaction.Id) = source.value.toString().toByteArray()
}

@Component
@WritingConverter
class TransactionIdToByteArrayConverter : Converter<Transaction.Id, ByteArray> {
    override fun convert(source: Transaction.Id) = source.value.toString().toByteArray()
}

@Component
@ReadingConverter
class StringToTransactionIdConverter : Converter<String, Transaction.Id> {
    override fun convert(source: String) = Transaction.Id(UUID.fromString(source))
}

@Component
@WritingConverter
class TransactionIdToStringConverter : Converter<Transaction.Id, String> {
    override fun convert(source: Transaction.Id) = source.value.toString()
}

@Component
@WritingConverter
class ByteArrayToMonetaryAmountConverter : Converter<ByteArray, MonetaryAmount> {
    override fun convert(source: ByteArray) = MonetaryFormats.getAmountFormat(Locale.US).parse(source.toString(Charsets.UTF_8))
}

@Component
@WritingConverter
class MonetaryAmountToByteArrayConverter : Converter<MonetaryAmount, ByteArray> {
    override fun convert(source: MonetaryAmount) = source.toString().toByteArray(Charsets.UTF_8)
}
  • The ideal solution is to mark MoneyaryAmountFactory.getNumber()as transient but it's an interface method so I can't make it final nor add a cross-dependency to Spring-Data.

Question:

  • Is there a way to prevent spring-data-redis to not index nested properties of a particular entity field?

Disclaimer

  • Forgive me if I've arrived at the wrong conclusions from my investigation. I've described what I could piece together but I might have ar
W.K.S
  • 9,787
  • 15
  • 75
  • 122

1 Answers1

0

Your understanding is correct. Complex object types are considered to be entities by default.

If you want to serialize a complex object, such as MonetaryAmount, then the best would be to implement read and write Converters.

Have a look at Object-to-Hash Mapping in the reference docs.

mp911de
  • 17,546
  • 2
  • 55
  • 95