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