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?
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)
}
}