Exposed: Proper way to serialize/deserialize DAOs entities.

Created on 18 Feb 2019  路  13Comments  路  Source: JetBrains/Exposed

My use case is Ktor + Exposed

enhancement

Most helpful comment

Maybe you can try Ktorm, another ORM framework for Kotlin. Entity classes in Ktorm are designed serializable (both JDK serialization and Jackson are supported).

https://github.com/vincentlauvlwj/Ktorm

All 13 comments

What I like to do for that scenario is do define a data class that represents the domain model you wish to send/receive via REST or whatnot, and then you transform your DAO to and from it.

Bare-bones example:

data class MyAppUser(val email: String, val realName:String)

internal object MyAppUserTable : LongIdTable("my_app_user_table") {
    val email = varchar("user_email", 255).uniqueIndex()
    val realName = varchar("real_name", 255)
}

internal class MyAppUserDAO(id: EntityID<Long>) : LongEntity(id) {
    companion object : LongEntityClass<MyAppUserDAO>(MyAppUserTable)

    val email by MyAppUserTable.email
    val realName by MyAppUserTable.realName

    fun toModel():MyAppUser{
        return MyAppUser(email, realName)
    }
}

Although it adds a bit of extra code (the data class), the control you get over what gets serialized/deserialized is worth the cost IMHO. Speaking of serialization, once you have a data class, its trivial to get it working with ktor.

That's the approach I'm going at the moment. I was looking for a solution that could allow me to expose the entire entity, id as well, and not to care on future modifications of the entity.

Well I guess exposing the id is pretty easy:

data class MyAppUser(val id: Long, val email: String, val realName:String)

(...)
fun toModel():MyAppUser{
    return MyAppUser(id.value, email, realName)
}

@felix19350 Is it possible to get an example of how this would work? Specifically in a query? I attempted to do something similar to what you posted and I get the following error in my toModel() method

Property klass should be initialized before get

@tylerbobella I put together a small gist, hope it helps:
https://gist.github.com/felix19350/bcb39e50820dcc6872f624d2e925dd9a

@felix19350 you are a saint! thank you so much for this. I was able to figure it out a couple hours after I made that post and my solution is pretty similar to yours :) thanks again and i hope this helps other people who have been confused with this!!

Do you think this would be helpful to add to the docs somewhere? I do not mind documenting this if the repo author believes if would be beneficial.

@felix19350 Did you try to replace data class with an interface? Will it work with ktor?

interface MyAppUserModel { val email: String, val realName: String }

internal class MyAppUserDAO(id: EntityID<Long>) : LongEntity(id), MyAppUserModel {
    companion object : LongEntityClass<MyAppUserDAO>(MyAppUserTable)

    override var email by MyAppUserTable.email
    override var realName by MyAppUserTable.realName   
}

I understand that it covers only serialization case, but if you just have to return entity to a client it might work and you don't need to call/define toModel() functions for every entity class.

@Tapac I tried to solve problem this way, here is the code https://gist.github.com/DisPony/efeacfcff8833e679a6b8141d6764e4f
Serialization gives me this result:
{"id":7,"email":"t","realName":"f","field":"ddd"}
Which mean Gson.toJson() serialize fields of actual class, not of interface. Without calling of .toModel() serialization fails with Exception in thread "main" java.lang.StackOverflowError.

I think the problem is more general then finding a serialization solution. Supposing that you find a proper way to serialize a DAO entity, what about deserialization?
What if the JSON you deserialize is missing a field? It means you want to set it null or that you just don't care?

I came to the conclusion that serialization/deserialization of an entity is not a concern of this library due to the model it uses (which is amazing!). Instead what I think is needed is a framework aware library that allows to integrate Exposed in the proper way into the framework itself. Have a look how Rest Repositories works for Spring!
I need something like that for Ktor, where you create the DAOs and expose some REST endpoints with rules about the integrity of writes and so on. Unfortunately for my use-case, at the moment Exposed relies on JDBC drivers so no real coroutines implementation due to the synchronous nature of the drivers themselves.

I Hope one day JetBrains adds official Exposed support for Ktor with super-easy Rest repositories-like features. Until then, I think a DTO is the proper way!

One trick I am using right now for serialization only is to make the companion object of an entity class extend JsonSerializer and register it during installation of the serialization feature in Ktor. It works pretty well! I am handling deserialization and updates manully tho.

Hope it help!

Maybe you can try Ktorm, another ORM framework for Kotlin. Entity classes in Ktorm are designed serializable (both JDK serialization and Jackson are supported).

https://github.com/vincentlauvlwj/Ktorm

Edited: I found quite a clean way to parse DAO entties to data classes for example OfferItemDTO, OfferCategoryDTO and serialize them to JSON using kotlinx.serialization.

class OfferItem (id: EntityID<Long>) : Entity<Long>(id), DTO<OfferItemDTO> {
    companion object : EntityClass<Long, OfferItem>(OfferItems)

    var name        by OfferItems.name
    var price       by OfferItems.price
    var vat         by OfferItems.vat
    var categoryID  by OfferItems.category_id

    var category:OfferCategory by OfferCategory referencedOn OfferItems.category_id

    override fun dto(rel:List<String>): OfferItemDTO {
        val category = if(rel.contains(::category.toString())) category.dto(rel) else null
        return OfferItemDTO(id.value ,name, price.toString(), vat, category)
    }
}

class OfferCategory(id: EntityID<Long>) : Entity<Long>(id), DTO<OfferCategoryDTO> {
    var name        by OfferCategories.name
    var position    by OfferCategories.position
    var display     by OfferCategories.display
    var color       by OfferCategories.color
    var customer_id by OfferCategories.customer_id

    var customer by Customer referencedOn OfferCategories.customer_id
    val items by OfferItem referrersOn OfferItems.category_id

    companion object : EntityClass<Long, OfferCategory>(OfferCategories)

    override fun dto(rel:List<String>): OfferCategoryDTO {
        val items = if(rel.contains(::items.toString())) items.dto(rel) else null
        return OfferCategoryDTO(name, position, display, color, customer_id.value, items)
    }
}

Extension functions for collections:

interface DTO<T>{
    private fun dto(vararg rel: KProperty<*>): T = dto(rel.map { it.toString() })
    fun dto(rel:List<String>): T
}

fun  <T> Iterable<DTO<T>>.dto(rel:List<String>) : List<T>{
    return  this.toList().map { it.dto(rel) }
}

fun  <T> Iterable<DTO<T>>.dto(vararg rel: KProperty<*>) : List<T>{
    return  this.toList().map { it.dto(rel.map { it.toString() }) }
}

Now you can do something like this:

val dto:List<OfferCategoryDTO> = transaction {
    OfferCategory.all().with(OfferCategory::items).dto(OfferCategory::items)
}
val json = Json(JsonConfiguration.Default).stringify(OfferCategoryDTO.serializer().list, dto)

The only problem I have is how to compare two KPropery. Only working solution I found was using toString().

If there is somene who knows how well it perform. I would really appreciate it. I heven't tested yet.

I haved a simple test with fastjson, and passed.

first

add dependencies
compile 'com.alibaba:fastjson:1.2.59'

custom code

val paramFilter = object : PropertyPreFilter {
        val ignorePs = arrayOf(
                "db",
                "klass",
                "readValues",
                "writeValues"
        )
        override fun apply(serializer: JSONSerializer?, `object`: Any?, name: String?): Boolean {
            return name !in ignorePs
        }
    }
    val idFilter = ValueFilter { obj, name, value ->
        if (obj is Entity<*> && name == "id" && value is EntityID<*>) {
            value.value
        } else value
    }

then use:

val log = RequestLog.findById(200L)
println(JSON.toJSONString(log, arrayOf(paramFilter, idFilter)))

output:
{"id":200,"ip":"171.119.56.165"}

model:

object RlTable : LongIdTable("request_log") {
    val ip = varchar("ip", 50)
}

class RequestLog(id: EntityID<Long>) : LongEntity(id) {
    companion object : LongEntityClass<RequestLog>(RlTable)
    var ip by RlTable.ip
}

Yeah but for every new entity you have to create a DTO and a serializer. That sucks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

supertote picture supertote  路  3Comments

BugsBunnyBR picture BugsBunnyBR  路  3Comments

brabo-hi picture brabo-hi  路  4Comments

hannesstruss picture hannesstruss  路  5Comments

michele-grifa picture michele-grifa  路  4Comments