DEV Community

Andjic Djordje
Andjic Djordje

Posted on • Updated on

How I finally managed to set up Ktor project with ExposedORM

Intro

This is my first tech blog post, so do not go harsh on me :D I am a software engineer with 2 years of experience with the will to share what I already know in order to learn new things and have all my knowledge in one place.

I saw Kotlin for the first time in 2021. The ease of learning and syntax combined with the power it has made me fall in love with the language since writing "Hello World!" to the console. Back then it was used only for Android development, and I stayed away from creating anything more serious with it and stayed in the .NET and Python world I was familiar with. Today I decided to revisit the language and saw that many things have changed and digging deeper made me see that many frameworks like Spring, Quarkus, Vertx and the others do have Kotlin support. Even more, Jetbrains developed it's own framework for writing server side logic and the ORM with it. I decided to play with it and see how it goes. The results were astonishing and I fell in love with the language once more. Let me start with the project.

Creating Ktor project

Creating the project was similar to creating a project in Quarkus ( I tried to use the framework before, but that is a completely different story ) adding different packages on the go in order to set up the project and have some functionality out of the box. The website https://start.ktor.io/#/settings was simple to use and the project is downloaded inside the .zip which I extracted and opened inside InteliJ ( VS Code does not have a nice support for Kotlin ).

Setting up project to connect to database

First things first for me whenever setting up a project is to connect it to some sort of database. This time I decided to use PostreSQL database inside the Docker container. Before doing that, I opened the application.yaml file and added the following code inside:

ktor:
    application:
        modules:
            - com.example.ApplicationKt.module
    deployment:
        port: 8080
jwt:
  domain: "https://jwt-provider-domain/"
  audience: "jwt-audience"
  realm: "ktor sample app"

db:
  url: "jdbc.postgresql://localhost:5432/dbName"
  user: "user"
  pass: "password"
  pool: 2
Enter fullscreen mode Exit fullscreen mode

After that I created docker-compose.yaml file in the root of the project and added the following changes:

version: "1.0"
services:
  db:
    container_name: "postges"
    image: postgres:13.14
    restart: always
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=dbname
    ports:
      - 5432:5432
    volumes:
      - db:/var/lib/postgresql/data

volumes:
  db:
    driver: local

Enter fullscreen mode Exit fullscreen mode

After running the application, everything worked fine and went on to work on the database connection object.

Setting up database connection object

Kotlin project is set up in a way that everything is loaded inside the Application.module() function. So I created a separate DatabaseSingleton object with the following code inside:

import com.example.models.*
import kotlinx.coroutines.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.*
import org.jetbrains.exposed.sql.transactions.experimental.*

object DatabaseSingleton {
    fun init() {
        val driverClassName = "org.postgresql.Driver"
        val jdbcURL = "jdbc:postgresql://localhost:5432/dbName?user=user&password=password"
        val database = Database.connect(jdbcURL, driverClassName)
        transaction(database) {
            SchemaUtils.create(ExampleEntity)
        }
    }

    suspend fun <T> dbQuery(block: suspend () -> T): T =
        newSuspendedTransaction(Dispatchers.IO) { block() }
}
Enter fullscreen mode Exit fullscreen mode

To explain the code here. I am creating an singleton object with two methods, init() and dbQuery() The first one is responsible of initiating the conection to the database and creating the table of my ExampleEntity (which will be covered later) while the second function is responsible of running each new transaction asynchronously inside a coroutine. The Ktor documentation described it the best: ". Instead of using the transaction to access it in a blocking way, let's take advantage of coroutines and start each query in its own coroutine".

Setting up the model

Now comes the easiest part. Setting up the model, in essence, consists of two parts. One is writing a data class to represent the entity itself and the other is writing a companion object for that data class which represents the entity inside the table ( important notice, the ORM operations are done on the object not the data class).
The code was simple and easy to write:

package com.example.models

import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.Table

@Serializable
data class ExampleEntity(val id: Int, val name: String){
    companion object ExampleEntityTable : Table()  {
        val id = integer("id").autoIncrement()
        val name = varchar("name", 255)

        override val primaryKey = PrimaryKey(id)
    }
}
Enter fullscreen mode Exit fullscreen mode

A thing to remember is that the class must have @Serializable tag in order for the kotlinx.serialization library to serialize it into the JSON object.

Setting up the DAO

DAO pattern is used with Exposed ORM so I had to create the DAO interface and it implementation. The code is shown below (these are two separate files):

package com.example.dao.ExampleEntity

import com.example.models.ExampleEntity

interface ExampleEntityDAO {
    suspend fun allExamples(): List<ExampleEntity>
    suspend fun exampleEntity(id: Int): ExampleEntity?
    suspend fun addNewExample(title: String): ExampleEntity?
    suspend fun editExample(id: Int, name:String): ExampleEntity?
    suspend fun deleteExample(id: Int):Unit
}
Enter fullscreen mode Exit fullscreen mode

Implementation:

package com.example.dao.ExampleEntity

import DatabaseSingleton.dbQuery
import com.example.models.ExampleEntity
import com.example.models.ExampleEntity.ExampleEntityTable.id
import com.example.models.ExampleEntity.ExampleEntityTable.name
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq

class ExampleEntityDAOImpl:ExampleEntityDAO {

    private fun resultRowToExampleEntity(row: ResultRow): ExampleEntity = ExampleEntity(
        id = row[id],
        name = row[name],
    )
    override suspend fun allExamples(): List<ExampleEntity> = dbQuery{
        ExampleEntity.selectAll().map(::resultRowToExampleEntity).toList()
    }

    override suspend fun exampleEntity(id: Int): ExampleEntity? = dbQuery{
        ExampleEntity.select { ExampleEntity.id eq id }.map(::resultRowToExampleEntity).singleOrNull()
    }

    override suspend fun addNewExample(title: String): ExampleEntity? = dbQuery {
       ExampleEntity.insert {
           it[name] = title
       }
        ExampleEntity.selectAll().map(::resultRowToExampleEntity).singleOrNull()
    }

    override suspend fun editExample( id:Int,newName:String): ExampleEntity? = dbQuery {
        ExampleEntity.update({ ExampleEntity.id eq id }) {
            it[name] = newName
        }
        ExampleEntity.select{ExampleEntity.id eq id}.map(::resultRowToExampleEntity).singleOrNull()
    }

    override suspend fun deleteExample(id: Int): Unit = dbQuery{
        ExampleEntity.deleteWhere { ExampleEntity.id eq id }
    }
}

Enter fullscreen mode Exit fullscreen mode

Setting up the routes

When downloading the Routing package form the Ktor Generator the Ktor is setting up a helloWorld route for you by default. To add all the routes inside one file does not seem like a good idea so I created another file called exampleRoutes and tried to call it independently. The Ktor did not allow it so I had to call the function that determines the methods for my example routes inside the Routing.kt file.

package com.example.plugins

import com.example.routing.exampleRouting
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Application.configureRouting() {
    routing {
        get("/") {
            call.respondText("Hello World!")
        }
        exampleRouting()
    }

}

Enter fullscreen mode Exit fullscreen mode

The Example Routes:


package com.example.routing
import com.example.dao.ExampleEntity.ExampleEntityDAOImpl
import com.example.models.ExampleEntity
import com.example.models.ExampleEntity.ExampleEntityTable.id
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Route.exampleRouting() {
    val daoImpl = ExampleEntityDAOImpl()
    route("/examples"){
        get("/all"){
            val list = daoImpl.allExamples()
            if(list.isNotEmpty()){
                call.respond(list)
            }
            else{
                call.respondText("List is empty")
            }
        }
        get("/{id}"){
            val id = call.parameters["id"]?.toInt()
            if(id != null){
                val entity = daoImpl.exampleEntity(id)
                if(entity != null){
                    call.respond(HttpStatusCode.OK,entity)
                }
                else{
                    call.respondText("Not found 404", status = HttpStatusCode.NotFound)
                }
            }
            else{
                call.respondText("Id is null", status = HttpStatusCode.BadRequest)
            }

        }
        post("/add"){
        val entity = call.receive<ExampleEntity>()
            daoImpl.addNewExample(entity.name)
            val entityListToReturn = daoImpl.allExamples()
            call.respond(HttpStatusCode.OK,entityListToReturn)
        }
        put("/{id}"){
            val id = call.parameters["id"]?.toInt()
            val body = call.receive<ExampleEntity>()
            if(id != null){
                val entity = daoImpl.exampleEntity(id)
                if(entity != null){
                    daoImpl.editExample(id,body.name)
                    call.respond(HttpStatusCode.OK,entity)
                }
            }

        }
        delete("/{id}"){
            val id = call.parameters["id"]?.toInt()
            if(id != null){
                val entity = daoImpl.exampleEntity(id)
                if(entity != null){
                    daoImpl.deleteExample(id)
                    call.respond(HttpStatusCode.OK,"Deleted successfully")
                }
                else{
                    call.respondText("Not found 404", status = HttpStatusCode.NotFound)
                }
            }
            else{
                call.respondText("Id is null", status = HttpStatusCode.BadRequest)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Final words

After setting up everything I managed to receive the 200 response from my custom route. And added couple of records into the database to see if they would show up (they did). The only thing I did not do was to create a service which will use the DAO implementation, but for the first run it sure is okay. I understand the code is bad, but it is a first step to learning the framework.You can see the code on this github link:https://github.com/neverloved-dev/KtorFinance

I will make sure to create a sample project out of this. The goal is to have a full web application backend with JWT authentication, user flow and business logic. Wish me luck :D

Top comments (0)