多租户

本文将指导您如使用Kronos的多租户功能

按租户选择数据源

Kronos 会从 Kronos.dataSource 读取默认数据源。该属性是一个返回 KronosDataSourceWrapper 的函数。多租户应用可以在启动阶段为每个租户注册一个 wrapper,并在请求进入时解析当前租户对应的 wrapper。

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 接收 JDBC DataSource,并从数据库元数据推断数据库类型。如果项目已经有自己的 KronosDataSourceWrapper 实现,也可以直接放入同一个注册表。

Spring 请求路由

在 servlet 请求中,可以让 Kronos.dataSource 从当前请求上下文读取租户 ID。没有显式传入 wrapper 的 Kronos 调用随后会使用默认 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()
    }
}

使用默认 wrapper 时,Kronos.dataSource 会在操作构建或执行时求值。任务离开请求线程后,应先解析租户 wrapper,再显式传给 Kronos 操作。

在异步任务中显式传入 wrapper

查询和写入执行 API 可以接收可选的 KronosDataSourceWrapper。当租户数据源已经确定,或请求上下文不可用时,直接使用该参数。

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 请求路由

Solon 应用可以使用同一模式:从 Context 读取租户 ID,解析 wrapper,并在 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)
        }
    }
}

其他框架也遵循同一规则:应用启动时创建或注册 wrapper,从请求信息解析租户 wrapper,然后从 Kronos.dataSource 返回它,或显式传给本次操作。