1

Recently I try to learn Kotlin by writing a Kotlin + Exposed demo.

Common steps using Java + MyBatis would be: creating Java Bean class, mapper class, service class and service implementation class

@Data class UserBean { String username; String password; }
@Mapper interface UserMapper extends BaseMapper<UserBean> {}
interface UserService extends BaseService<UserBean> {}
@Service UserSerivceImpl extends BaseServiceImpl<UserBean> implements UserService {}

Then those Java Bean classes are used in any other parts of system for database IO and JSON serialization.

// create instance and convert to JSON
var user = new UserBean();
user.setUsername("username");
user.setPassword("password");
var om = new ObjectMapper();
var json = om.valueToTree(user);

Following official doc of Exposed DAO API, I create those classes:

class User(id : EntityID<Int>) : IntEntity(id)
{
    companion object : IntEntityClass<User>(Users)
    var username by Users.username
    var password by Users.password
}
object Users : IntIdTable()
{
    val username = varchar("username", 64)
    val password = varchar("password", 64)
}

When performing database IO, User.all() and User.new { } api work well. But creating instance directly would throw an exception:

// get data from JSON or somewhere else
val username = ...
val password = ...
// trying to create instance
val id = EntityID(-1, User.table) // trying to create an empty ID. I don't know if this is allowed
val user = User(id)
user.username = username // this would throw exception
user.password = password
Caused by: java.lang.IllegalStateException: Property klass should be initialized before get.
    at kotlin.properties.NotNullVar.getValue(Delegates.kt:62)
    at org.jetbrains.exposed.dao.Entity.getKlass(Entity.kt:34)
    at org.jetbrains.exposed.dao.Entity.setValue(Entity.kt:198)

Post here says Exposed does not allow creating DAO objects by yourself. So is there a easy way to re-use those DAO classes for JSON serialization or transfering data between parts of program? Should I create a DTO class with identical data fields?

Firok
  • 269
  • 1
  • 6

2 Answers2

0

Exposed Entities are stateful objects. You shouldn't serialize them directly. Instead, as you mentioned, you should use a simple data class with serialization annotations relevant to you.

For example:

class User(id : EntityID<Int>) : IntEntity(id)
{
    companion object : IntEntityClass<User>(Users)
    var username by Users.username
    var password by Users.password

    fun asResponse(): UserResponse {
        return UserResponse(username, password)
    }
}

@Serializable
data class UserResponse(val username: String, val password: String)
Alexey Soshin
  • 16,718
  • 2
  • 31
  • 40
  • So this means we HAVE to write the same data structure for twice? Exposed has no annotation processing tool or something like that helps us do this job? – Firok Feb 10 '23 at 06:47
  • Correct. Exposed is a DB framework. It has nothing to do with how your HTTP responses are serialized. – Alexey Soshin Feb 10 '23 at 09:35
0

The best way i have tried to solve this issue is by writing 2 extension functions

import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.sql.SizedIterable
import kotlin.reflect.full.declaredMembers

inline fun <reified Dao: Entity<*>, reified Response> Dao.toResponse() : Response = this::class.declaredMembers.let { doaParameters ->
    val response = Response::class
    val responseObjectParameters = Response::class.constructors.first().parameters
    response.constructors.first().call(
        *responseObjectParameters.map { responseParam ->
            doaParameters.find { it.name == responseParam.name }?.call(this)
        }.toTypedArray()
    )
}

inline fun <reified Dao: Entity<*>, reified Response> SizedIterable<Dao>.toResponse() : List<Response> = this.map { doa ->
        doa::class.declaredMembers.let { doaParameters ->
            val response = Response::class
            val responseObjectParameters = Response::class.constructors.first().parameters
            response.constructors.first().call(
                *responseObjectParameters.map { responseParam ->
                    doaParameters.find { it.name == responseParam.name }?.call(doa)
                }.toTypedArray()
            )
        }
    }

this can be executed on all result of the transactions and return a data class or a list of dataclass

The type variables are:
Dao: The entity class
Response: Dataclass representing the data

examples:

class TestDao(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<TestDao>(TestTable)
    var sequelId by TestTable.sequelId
    var name     by TestTable.name
    var director by TestTable.director
}

data class TestResponse(
    var sequelId: Int,
    var name: String,
    var director: String
)

@Router("api/Account")
class Account{
    @Get
    fun testGet() : ResponseEntity<List<TestResponse>> = transaction {
        val parsed = TestDao.all().toResponse<TestDao, TestResponse>()
        ResponseEntity.ok(parsed)
    }

    @Get
    fun testGet(@PathParameter id: Int) : ResponseEntity<TestResponse> = transaction {
        val parsed = TestDao.findById(id)?.toResponse<TestDao, TestResponse>() ?: throw NotFoundException("No entity found with id: $id")
        ResponseEntity.ok(parsed)
    }
}