Using 1.3 serialization to store object in Redis


#1

I am playing with the idea of using the new serialization support from 1.3 to store and retrieve object from Redis.
In my generic Redis support Repository I need to receive a KSerializer<T:IEntity> where the IEntity is my entity marker interface

interface IEntity {
  var id: Long?
} 

How can I serialize and deserialize instances of IEntity using the KSerializer ?

Thank you, kindly
Luis Oscar Trigueiros


#2

I assume your work will consist of two parts:

  1. Annotate your DTOs with @Serializable (annotate concrete classes, not interfaces because plugin can’t generate code for interfaces yet)
  2. Write corresponding encoders and decoders. This will require some effort, because it is relatively hard part of framework. You can consult the KEEP to learn the purpose of these interfaces, and then start from learning the existing implementations – e.g. JSON or CBOR encoder/decoder, or probably JsonTreeMapper because it’s rather small. There are some skeleton implementations to help you – TaggedEncoder/Decoder and ElementWiseEncoder/Decoder. Unfortunately, documentation for them is still under development.
    Hope you will get into it, good luck!

#3

Hi @Leonid.Startsev:

This was my solution actualy:

import com.xapp.domain.CarAgent
import com.xapp.domain.Country
import kotlinx.serialization.json.JSON

interface ValueAccessor<T : Any> {
    fun write(value: T): String
    fun read(value: String): T
}

class StringValueAccessor : ValueAccessor<String> {
    override fun write(value: String) = value
    override fun read(value: String) = value
}

class CarAgentValueAccessor : ValueAccessor<CarAgent> {
    override fun write(value: CarAgent) = JSON.stringify(value)
    override fun read(value: String) = JSON.parse<CarAgent>(value)
}

class CountryValueAccessor : ValueAccessor<Country> {
    override fun write(value: Country) = JSON.stringify(value)
    override fun read(value: String) = JSON.parse<Country>(value)
}


enum class Key(val valueAccessor: ValueAccessor<*>) {
    CarAgent(CarAgentValueAccessor()),
    Country(CountryValueAccessor()),
    CountryByCode(StringValueAccessor());

    fun key(id: Any) = prefix() + ":" + id
    fun prefix() = this.name
}

And then the generic redis repository code:

import com.xapp.domain.IEntity
import com.xapp.repository.Repository
import com.xapp.repository.logger
import io.lettuce.core.api.reactive.RedisReactiveCommands
import net.engio.mbassy.bus.MBassador
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

open class RedisRepositorySupport<T : IEntity>(
        val commands: RedisReactiveCommands<String, String>,
        private val eventBus: MBassador<Any>,
        private val key: Key) : Repository<T> {

    companion object {
        private const val NEXT_ID = "XREPO_NEXT_ID"
    }


    private val valueAccessor: ValueAccessor<T> = resolveValueAccessor(key)


    private fun populateNullId(entity: T): Mono<T> {
        return if (entity.id == null) {
            commands.incr(NEXT_ID).map { id ->
                entity.id = id
                entity
            }
        } else {
            Mono.just(entity)
        }
    }

    override fun createOrUpdate(entity: T): Mono<T> = populateNullId(entity).flatMap(::persist)

    private fun persist(entity: T): Mono<T> {
        val entityKey = key.key(entity.id as Long)
        val write = valueAccessor.write(entity)
        return commands.set(entityKey, write)
                .doOnNext { result ->
                    logger().debug("$result [$entityKey]=$entity")
                    eventBus.post(entity).asynchronously()
                }
                .and(commands.persist(entityKey))
                .map { entity }
    }

    override fun find(id: Long): Mono<T> = key.key(id)
            .let { key -> commands.get(key) }
            .map { map -> valueAccessor.read(map) }

    override fun findAll(): Flux<T> {
        val keysToSearch = "${key.prefix()}:*"
        logger().debug("Search for keys=[$keysToSearch]")
        return commands.keys(keysToSearch)
                .flatMap { key -> commands.get(key).map { valueAccessor.read(it) } }
    }

    override fun delete(id: Long): Mono<Long> = key.key(id).let { key -> commands.del(key) }

    @Suppress("UNCHECKED_CAST")
    private fun resolveValueAccessor(key: Key) = key.valueAccessor as ValueAccessor<T>
}

Thank you.


#4

I would add that the KEEP is quite core. It also would help you understand better how things work (it is better structured than the old approach). In particular between struct and non-struct types.