DEV Community

Cover image for Exposed in Your Project - JSON support
bright inventions
bright inventions

Posted on • Originally published at brightinventions.pl

Exposed in Your Project - JSON support

Overview

In this tutorial, we are going to learn how to implement JSON support in our JetBrains/Exposed app - we will create new
ColumnType and how to modify the database query.

As an extra point, I will show you, how to establish connection pooling and why it's important.

In the previous post we modified our initial
code to follow DAO approach. Now, based on this, we will create a little thinner application, add additional information
to our Person entity and use json PostgreSQL' datatype.

Setting up a test application

NOTE: Because we did all the preparation in the previous blog post, we will skip this part - if you need to set up
the environment, please, go to Step 1

Setting up a database

For the sake of this tutorial, we are going to use PostgreSQL database. Let's create a docker container:

# docker-compose.yml
version: '3.3'
services:
  db:
    image: postgres
    restart: always
    environment:
      POSTGRES_PASSWORD: foo
    ports:
      - "55432:5432"

Enter fullscreen mode Exit fullscreen mode

We are just using the official postgres image, with a defined password and exposed port.

connection pooling

// Database.kt
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.server.config.*
import org.jetbrains.exposed.sql.Database

object Database {

    fun register(config: ApplicationConfig) {
        Database.connect(
            hikari(
                config.property("db.url").getString(),
                config.property("db.user").getString(),
                config.property("db.password").getString(),
                config.property("db.maximumPoolSize").getString().toInt()
            )
        )
    }

    private fun hikari(dbUrl: String, dbUser: String, dbPassword: String, maximumPoolSize: Int): HikariDataSource {
        val config = HikariConfig()
        config.driverClassName = "org.postgresql.Driver"
        config.jdbcUrl = dbUrl
        config.username = dbUser
        config.password = dbPassword
        config.maximumPoolSize = maximumPoolSize
        config.validate()
        return HikariDataSource(config)
    }
}
Enter fullscreen mode Exit fullscreen mode

Plus, the configuration file:

# src/main/resources/application.conf
db {
    url = "jdbc:postgresql://localhost:55432/postgres"
    user = postgres
    password = foo
    maximumPoolSize = 10
}
...
Enter fullscreen mode Exit fullscreen mode

Now, why connection pooling is important?

Using connection pools helps to both alleviate connection management overhead and decrease development tasks for data
access. Each time an application attempts to access a backend store (such as a database), it requires resources to
create, maintain, and release a connection to that data store.

With PostgreSQL, each new connection can take up to
1.3MB in memory. In a production environment where we expect to receive thousands or millions of concurrent
connections
to the backend service, this can quickly exceed your memory resources (or if you have a scalable cloud, it can get
very
expensive very quickly).

Add JsonColumnType

First, let's create and register a new column type - JsonColumnType:

// JsonColumnType.kt
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.ColumnType
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi
import org.postgresql.util.PGobject

fun <T : Any> Table.json(name: String, serialize: (Any) -> String, deserialize: (String) -> Any): Column<T> =
    registerColumn(name, JsonColumnType(serialize, deserialize))

class JsonColumnType(
    private val serialize: (Any) -> String,
    private val deserialize: (String) -> Any
) : ColumnType() {
    override fun sqlType() = "JSON"

    override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) {
        super.setParameter(
            stmt,
            index,
            value.let {
                PGobject().apply {
                    this.type = sqlType()
                    this.value = value as String?
                }
            }
        )
    }

    override fun valueFromDB(value: Any): Any {
        if (value !is PGobject) {
            return value
        }
        return deserialize(checkNotNull(value.value))
    }

    override fun notNullValueToDB(value: Any): String = serialize(value)
}
Enter fullscreen mode Exit fullscreen mode

The result of this implementation is that we have an available .json() method on Table level - which means that we can
use our new columnType:

// PersonTable.kt

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.dao.id.IntIdTable
import pl.brightinventions.exposed.json

object PersonTable : IntIdTable("person") {
    ...
    val details = json<PersonDetails>(
        "details",
        { Json.encodeToString(it as PersonDetails) },
        { Json.decodeFromString(it) as PersonDetails }
    )
}

@Serializable
data class PersonDetails(
    val nickname: String
)
Enter fullscreen mode Exit fullscreen mode

Because of our choice of kotlinx serialization, we need to declare the context for serialization explicitly (casting for
methods Json.encodeToString and Json.decodeFromString) - the easiest way was to declare it on the column registering
step.

Add JsonValue

Once we registered our new column, we can store serialized and read the object value. But what if we want to use our
column in WHERE or ORDER BY clause?\
We need to register JsonValue and a new method:

// JsonValue.kt
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.Function
import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect
import org.jetbrains.exposed.sql.vendors.currentDialect
import kotlin.reflect.KClass

inline fun <reified T : Any> Column<*>.jsonValue(vararg jsonPath: String): Function<T> =
    this.jsonValue(T::class, *jsonPath)

fun <T : Any> Column<*>.jsonValue(clazz: KClass<T>, vararg jsonPath: String): Function<T> {
    if (this.columnType !is JsonColumnType) {
        throw IllegalArgumentException("Cannot perform jsonValue call on the column which is not related to JsonbColumnType")
    }

    val columnType = when (clazz) {
        Boolean::class -> BooleanColumnType()
        Int::class -> IntegerColumnType()
        Float::class -> FloatColumnType()
        Long::class -> LongColumnType()
        String::class -> TextColumnType()
        else -> TextColumnType()
    }

    return when (currentDialect) {
        is PostgreSQLDialect -> PostgreSQLJsonValue(this, columnType, jsonPath.toList())
        else -> throw NotImplementedError()
    }
}

class PostgreSQLJsonValue<T>(
    private val expr: Expression<*>,
    override val columnType: ColumnType,
    private val jsonPath: List<String>
) : Function<T>(columnType) {
    override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder {
        append("(")
        append(expr)
        append("${jsonPath.joinToString { it }})::${columnType.sqlType()}")
    }
}

Enter fullscreen mode Exit fullscreen mode

We created PostgreSQLJsonValue which is a representation of (column->>jsonPath)::type PostgreSQL' syntax. Plus, we can
use jsonValue method in order to utilize JSON functionality inside the query, which will look like this:

fun findByNickname(nickname: String): FoundPersonWithAddressDto? = transaction {
    PersonEntity
        .find { PersonTable.details.jsonValue<String>("->>'nickname'") eq nickname }
        .firstOrNull()
        ?.load(PersonEntity::addresses)?.toFoundPersonWithAddressDto()
}
Enter fullscreen mode Exit fullscreen mode

Testing

GET http://localhost:8080/person \
This one should respond with a list of three persons: John, George, and Megan. It will respond with short
information about saved records, but the rows will be ordered by person.details->>'nickname'.

If we want to receive data about a particular person BY nickname, you can call:
GET http://localhost:8080/person/nickname/johny,\
which will respond with full Person DTO.

Conclusion

In this article, we've learned:

  • how to add json column type to our JetBrains/Exposed application,
  • how to add jsonValue method which allows us to search/sort by the JSON specific property in the query.

As extra points we did:

  • exercise to create Docker container for our PostgreSQL database,
  • configured connection pooling, which is important to our production environment.

Did you like the article? Maybe you have some other way for DAO implementation? Leave a comment below and stay in
touch!

You can find the complete code over GitHub.

By Patryk Szlagowski, Senior Backend Developer @ Bright Inventions

Top comments (0)