Gaurab Paul

Polyglot software developer & consultant passionate about web development, distributed systems and open source technologies

Support my blog and open-source work

Tags

Extending Exposed with new function expressions
Posted  7 months ago

Exposed is a nice ORM for Kotlin by Andrey Tarashevskiy from Jetbrains. Though, not an offically supported product by Jetbrains it has a good following in the Kotlin community.

I recently got involved in a project using Exposed and discovered that the set of database level functions that are mapped by Exposed is quite small. However, the library makes it easy to add support for the missing functions, which is what this post outlines.

Let's say we want to support product of two columns. We can support this by extending the ExpressionWithColumnType class.

class Product<T>(
    private val expr1: ExpressionWithColumnType<T>,
    private val expr2: ExpressionWithColumnType<T>
): ExpressionWithColumnType<T>() {
    override val columnType: IColumnType
        get() = expr1.columnType

    override fun toQueryBuilder(queryBuilder: QueryBuilder) {
        queryBuilder.append("(", expr1, "*", expr2, ")")
    }
}

This accepts two columns (Column class extends ExpressionWithColumnType too) of same type, and constructs the expression for multiplying these column values.

While this class can be used directly, it is often convenient to also write an extension method that enables us to chain the operations similar to other exposed functions.

fun <T> ExpressionWithColumnType<T>.product(expr: ExpressionWithColumnType<T>) =
    Product(this, expr)

So now, we can easily use this in our code as:

DesginationTable
    .slice(
        DesignationTable.name,
        DesginationTable.employeeCount.product(DesginationTable.salary)
    )
    .groupBy(DesignationTable.name)
    .toList()

It is usually also convenient to use an alias to retrieve aggregated values:

data class DesignationSalaryDTO(
    val name: String,
    val totalSalary: Double
)

val totalSalary = DesginationTable.employeeCount.product(DesginationTable.salary).alias("total_salary")

val salaryRow DesginationTable
    .slice(
        DesignationTable.name, 
        // We can use the alias in our slice
        totalSalary
    )
    .groupBy(DesignationTable.name)
    .map { resultRow ->
        // Map results to a DTO
        DesignationSalaryDTO(
            name = DesignationTable.name,
            // We can use the same alias when retrieving the column value
            totalSalary = resultRow[totalSalary]
        )
    }

This illustrates how we can nicely add our extension functions for operations that the library does not support, and use them in pretty much the same way as built in expression composition functions.