kotlinmicronautmicronaut-data

Micronaut JDBC: using kotlin value classes as @Id


Is it possible to use Kotlin value classes as @Id property?

Entity:

import io.micronaut.data.annotation.*

@JvmInline
value class UserId(val value: Int)

@MappedEntity("users")
data class User(
    @field:Id
    @field:GeneratedValue
    val id: Int = UserId(0),
    val name: String
)

Repo:

import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository

// Repo
@JdbcRepository(dialect = Dialect.POSTGRES)
interface UsersRepo : CrudRepository<User, UserId>

Currently, during compilation I am getting the following error:

error: Unable to implement Repository method: UsersRepo.update(Object entity). No identity is present

AFAIK, it is not possible with micronaut-data-jpa, but what about micronaut-data-jdbc?


Solution

  • You can achieve type checking without a value class by simply using a data class. The trick is that you need to write a mapper for each layer so that it 'remains hidden.' Naturally, the efficiency of this approach isn't as good due to the wrapping, but I assume the goal here is safety rather than micro-optimization; otherwise, the specific type would simply be written out.

    package example.micronaut
    
    import io.micronaut.core.convert.ConversionContext
    import io.micronaut.core.convert.TypeConverter
    import io.micronaut.core.type.Argument
    import io.micronaut.data.annotation.Id
    import io.micronaut.data.annotation.MappedEntity
    import io.micronaut.data.annotation.TypeDef
    import io.micronaut.data.model.DataType
    import io.micronaut.data.model.runtime.convert.AttributeConverter
    import io.micronaut.serde.*
    import io.micronaut.serde.annotation.Serdeable
    import jakarta.inject.Singleton
    import java.util.*
    
    @Serdeable
    @MappedEntity
    data class SaasSubscription(
        @Id val id: SaasSubscriptionId,
        val name: String,
        val cents: Int
    )
    
    @TypeDef(type = DataType.LONG, converter = SaasSubscriptionIdAttributeConverter::class)
    class SaasSubscriptionId(
        val value: Long,
    ) {
        override fun toString(): String {
            return value.toString()
        }
    
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is SaasSubscriptionId) return false
            return value == other.value
        }
    
        override fun hashCode(): Int {
            return value.hashCode()
        }
    }
    
    /**
     * Data repository support.
     */
    @Singleton
    class SaasSubscriptionIdAttributeConverter : AttributeConverter<SaasSubscriptionId?, Long?> {
    
        override fun convertToPersistedValue(quantity: SaasSubscriptionId?, context: ConversionContext): Long? {
            return quantity?.value
        }
    
        override fun convertToEntityValue(value: Long?, context: ConversionContext): SaasSubscriptionId? {
            return if (value == null) null else SaasSubscriptionId(value)
        }
    
    }
    
    /**
     * Controller path variable support.
     */
    @Singleton
    class SaasSubscriptionIdConverter : TypeConverter<String, SaasSubscriptionId> {
        override fun convert(value: String, targetType: Class<SaasSubscriptionId>, context: ConversionContext): Optional<SaasSubscriptionId> {
            return Optional.of(SaasSubscriptionId(value.toLong()))
        }
    }
    
    /**
     * JSON request/response support.
     */
    @Singleton
    class SaasSubscriptionIdSerde : Serde<SaasSubscriptionId> {
        override fun deserialize(
            decoder: Decoder,
            context: Deserializer.DecoderContext,
            type: Argument<in SaasSubscriptionId>
        ): SaasSubscriptionId {
            return SaasSubscriptionId(decoder.decodeLong())
        }
    
        override fun serialize(
            encoder: Encoder,
            context: Serializer.EncoderContext,
            type: Argument<out SaasSubscriptionId>,
            value: SaasSubscriptionId
        ) {
            encoder.encodeLong(value.value)
        }
    }