KESR Works by generating DTO classes from your Database Entities using KSP as a compiler Plugin. These classes then resolve the given relationship on the fly and evaluate them in the runtime. The DTOs generated by the compiler are then accessible using the toModel() extension function on your entities. Using the with() selector allows to specify which relationships should be resolved when serializing the models class.
Actually the relationship will be evaluated, as soon as the responsible function is called
User.kt
@Model
class User(id: EntityID<Int>) : Entity<Int>(id) {
companion object : EntityClass<Int, User>(Users)
var name by Users.name
@HasMany
val posts by Post referrersOn Posts.user
@HasMany
val comments by Comment referrersOn Comments.user
}
Post.kt
@Model
class Post(id: EntityID<Int>) : Entity<Int>(id) {
companion object : EntityClass<Int, Post>(Posts)
var content by Posts.content
@BelongsTo
var user by User referencedOn Posts.user
@HasMany
val comments by Comment referrersOn Comments.post
}
Comment.kt
@Model
class Comment(id: EntityID<Int>) : Entity<Int>(id) {
companion object : EntityClass<Int, Comment>(Comments)
var content by Comments.content
@BelongsTo
var post by Post referencedOn Comments.post
@BelongsTo
var user by User referencedOn Comments.user
}
Easily serialize your relationship by using a DLS selector
user.toModel().with {
posts {
comments()
}
comments {
post()
}
}KSER can handle nullable attributes, and will render then in the final model
KSER allows for nullable relationships, only exposing resolved relations at run time.
Because this library is based on KSP, Kotlin Exposed and kotlinx serialization, you will need to also add these libraries to your project.
Currently KSER uses kotlin 2.0.21 and exposed 0.55.0
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.25"
kotlin("plugin.serialization") version "2.0.20"
}
//for json serialization.
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("org.jetbrains.exposed:exposed-core:$kotlinExposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$kotlinExposedVersion")Now the KSER libraries can be added as well
The latest release of KSER is 1.0.2
repositories {
maven { setUrl("https://jitpack.io") }
}
implementation("com.github.StaticFX.kotlin-exposed-relationships:annotations:$kserVerion")
ksp("com.github.StaticFX.kotlin-exposed-relationships:processor:$kserVerion")To test if you have installed the compiler plugin correctly, use
gradle kspKotlin
Check my recommendations to work with KSER.
KSER builds heavily on Nullable values. Therefore attributes being null, when not set. At this time, it is not possible to use kotlinx serialization to only for example handle resolved relations. Therefore unresolved relations will be set to null. This behaviour can be controlled by configuring your formatter.
private val jsonSerializer = Json {
prettyPrint = true
encodeDefaults = false
explicitNulls = true
}The most important value is encodeDefaults as this will tell the serializer to not serialize the default values. KSER will set the default values to null and when resolving the relationship override them. Afterwards, because the default was modified, the values will be correctly encoded.
Play around with the values as you please, the configuration above, is just my personal recommendation.
You can easily use KSER with KTOR by configuring the serializer in the setup, for example:
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(json = Json {
encodeDefaults = false
})
}
}- To create model classes you can use the
@Modelannotation on your desired entity. - Then rebuild your project using
gradle clean build. This will generate all necessary classes and functions. - Afterward you should be able to import the generated classed and functions into your project.
- Use the
toModel()extension function to get a reference to your model. - Use the
with()function to select which relationships should be resolved. - optional - select more relationships in the
with()context
KSER allows you to add custom attributes to a model using a map. These will then be serialized at runtime.
Usage
model.attributes["some value"] = JsonPrimitive("Test")Use kotlinx.serialization's inbuilt Json functions to build a JSON Element!
- Because this library uses KSP, there is no on the fly code generation. So you will have to run
gradle kspKotlineverytime you annotate a new model. - Currently, there is no real way to get database entities from models, but this on the roadmap.
Because not all types which are commonly used in KTOR have default serializers, KSER will automatically transform their type by mapping and transforming them. For example if a property is type UUID, it will be mapped to a String and transformed using the .toString() call.
More mappings can be found here
KESR leverages KSP as a compiler addon to analyze the code you annotated with the @Model annotation.
All declared properties are received and filtered into generic and model properties. Generic properties include all properties not related to another model.
Model properties are related to another model. The processor finds the by looking for the EntityID type in the property.
Then a new data class based on these properties is generated. The constructor includes all generic properties, and transient fields for the relations. An inner Relations class is used to lazy load the relationships and provide context to the selector.
Let's assume the given class:
@Model
class User(id: EntityID<Int>) : Entity<Int>(id) {
companion object : EntityClass<Int, User>(Users)
var name by Users.name
@HasMany
val posts by Post referrersOn Posts.user
@HasMany
val comments by Comment referrersOn Comments.user
@HasMany
val likes by Like referrersOn Likes.user
}The processor will generate the following based on this:
@Serializable
public data class UserModelDTO(
public val name: String,
public val id: Int,
@Transient
private val postsRelation: List<Post>? = null,
@Transient
private val commentsRelation: List<Comment>? = null,
@Transient
private val likesRelation: List<Like>? = null,
) {
public var posts: List<PostModelDTO>? = null
public var comments: List<CommentModelDTO>? = null
public var likes: List<LikeModelDTO>? = null
@Transient
private val relations: Relations = Relations()
public suspend fun with(block: suspend Relations.() -> Unit): UserModelDTO {
relations.block()
return this
}
public inner class Relations {
private val posts: List<PostModelDTO> by lazy { postsRelation!!.map { it.toModel() }}
private val comments: List<CommentModelDTO> by lazy { commentsRelation!!.map { it.toModel() }}
private val likes: List<LikeModelDTO> by lazy { likesRelation!!.map { it.toModel() }}
public suspend fun posts(block: suspend PostModelDTO.Relations.() -> Unit = {}) {
dbQuery {
this@UserModelDTO.posts = posts
}
posts.forEach { it.with(block) }
}
public suspend fun comments(block: suspend CommentModelDTO.Relations.() -> Unit = {}) {
dbQuery {
this@UserModelDTO.comments = comments
}
comments.forEach { it.with(block) }
}
public suspend fun likes(block: suspend LikeModelDTO.Relations.() -> Unit = {}) {
dbQuery {
this@UserModelDTO.likes = likes
}
likes.forEach { it.with(block) }
}
}
}IntelliJ's default code generation does not really handle KSP plugins well. When adding a new model, you will need to rerun the ksp task to generate the required files. This can be frustrating to work with, so i recomment to create a custom tast which executed gradle clean build which will make sure to generate all the files. If your build times are long, you can also try to use gradle clean kspKotlin.
Also adding the generated files to your IntelliJ's sources adds code completion to your project.
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin") <-- Check your build/generated folder structure and insert here.
}
}- Add CI/CD Pipeline with automatic release
- Enhance documentation, by generating javaDocs
- Find a workaround for nullable attributes
- Support more custom datatypes like DateTimes
- Support more attributes
Every contribution is welcomed, please start by opening a new issue.