Multi Tenant

This chapter will guide you on how to use the multi-tenant feature of Kronos.

Choose a data source per tenant

Kronos reads the default data source from Kronos.dataSource, a function that returns a KronosDataSourceWrapper. In a multi-tenant application, register one wrapper for each tenant during application startup and resolve the wrapper that matches the current request.

kotlin

import com.kotlinorm.Kronos
import com.kotlinorm.wrappers.KronosJdbcWrapper
import com.kotlinorm.interfaces.KronosDataSourceWrapper
import javax.sql.DataSource

val dataSourceMap = mutableMapOf<String, KronosDataSourceWrapper>()

fun registerTenantDataSources(tenant1: DataSource, tenant2: DataSource) {
    dataSourceMap["tenant1"] = KronosJdbcWrapper(tenant1)
    dataSourceMap["tenant2"] = KronosJdbcWrapper(tenant2)
}

fun tenantWrapper(tenantId: String): KronosDataSourceWrapper =
    dataSourceMap[tenantId]
        ?: throw IllegalArgumentException("No data source found for tenant: $tenantId")

KronosJdbcWrapper takes a JDBC DataSource and infers the database type from metadata. If your project already implements KronosDataSourceWrapper, store that implementation in the same registry.

Spring request routing

In a servlet request, set Kronos.dataSource to read the tenant ID from the current request context. Kronos calls without an explicit wrapper can then use the default wrapper.

kotlin

import com.kotlinorm.Kronos
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes

fun configureKronosTenantSource() {
    Kronos.dataSource = {
        val request = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request
        val tenantId = request.getHeader("tenantId") ?: error("Missing tenantId header")
        tenantWrapper(tenantId)
    }
}

@RestController
class UserController {
    @GetMapping("/users")
    fun getUsers(pi: Int, ps: Int): PageResult<User> {
        return User().select().page(pi, ps).withTotal().toList()
    }
}

Kronos.dataSource is evaluated when an operation builds or executes with the default wrapper. When work leaves the request thread, resolve the tenant wrapper before scheduling the work and pass it to the Kronos operation explicitly.

Pass the wrapper explicitly in asynchronous work

Query and mutation execution APIs accept an optional KronosDataSourceWrapper. Use that parameter when the tenant source is already known or when request-local storage is not available.

kotlin

fun loadTenantUsers(tenantId: String, pi: Int, ps: Int): PageResult<User> {
    val wrapper = tenantWrapper(tenantId)

    return User()
        .select()
        .page(pi, ps)
        .withTotal()
        .toList(wrapper)
}

fun updateTenantUser(tenantId: String, id: Int, name: String) {
    val wrapper = tenantWrapper(tenantId)

    User(id = id)
        .update()
        .set { it.name = name }
        .by { it.id }
        .execute(wrapper)
}

Solon request routing

Solon applications can use the same pattern. Read the tenant ID from Context, resolve the wrapper, and pass it to the operation inside the handler.

kotlin

import org.noear.solon.Solon

fun main(args: Array<String>) {
    Solon.start(DemoApp::class.java, args) { app ->
        app.get("/users") { ctx ->
            val tenantId = ctx.header("tenantId") ?: error("Missing tenantId header")
            val wrapper = tenantWrapper(tenantId)
            val pi = ctx.param("pi").toInt()
            val ps = ctx.param("ps").toInt()

            User()
                .select()
                .page(pi, ps)
                .withTotal()
                .toList(wrapper)
        }
    }
}

The same rule applies in other frameworks: create or register wrappers during application startup, resolve the tenant wrapper from request data, and either return it from Kronos.dataSource or pass it to the operation.