Skip to content

数据库指南

Neton 的数据库层遵循 Entity = 纯数据,Table = 表级入口 的设计原则。没有 companion object 魔法,没有运行时反射,所有代码由 KSP 在编译期生成,确保 Kotlin/Native 原生兼容。

设计原则

概念职责说明
ControllerHTTP 端点接收请求、参数校验、调用 Logic
Logic业务聚合手写,处理 JOIN、事务、缓存等业务用例
Table单表 CRUDKSP 自动生成,提供 get/save/where/destroy 等操作
Entity纯数据类data class,用 @Serializable + @Table 标注

关键约束:

  • Entity 不包含任何数据库逻辑,不使用 companion object
  • Table 由 KSP 根据 Entity 注解自动生成,无需手写
  • 不依赖运行时反射,完全编译期代码生成

定义实体

使用 @Serializable@Table@Id 注解定义实体类:

kotlin
import kotlinx.serialization.Serializable
import neton.database.annotations.Table
import neton.database.annotations.Id

@Serializable
@Table("users")
data class User(
    @Id val id: Long?,
    val name: String,
    val email: String,
    val status: Int,
    val age: Int
)

注解说明

注解作用参数
@Table("表名")标记数据库表,指定表名value: 表名,默认使用类名小写
@Id标记主键字段autoGenerate: 是否自动生成,默认 true
@Column自定义列映射name: 列名;nullable: 是否可空;ignore: 是否忽略
@CreatedAt插入时自动填充当前时间(epoch millis, UTC)
@UpdatedAt插入/更新时自动填充当前时间(epoch millis, UTC)

主键字段类型为 Long?,新建实体时传 null,数据库自动生成。

更多实体示例

kotlin
@Serializable
@Table("roles")
data class Role(
    @Id val id: Long?,
    val name: String
)

@Serializable
@Table("user_roles")
data class UserRole(
    @Id val id: Long?,
    val userId: Long,
    val roleId: Long
)

Table 操作(KSP 生成)

KSP 会为每个标注了 @Table 的 Entity 自动生成对应的 Table 对象(如 User -> UserTable)。Table 实现了 Table<T, ID> 接口(ID 由主键类型推导,常见为 Long),提供完整的单表 CRUD 能力:

基础 CRUD

kotlin
// 按 ID 查询
val user: User? = UserTable.get(1L)

// 查询所有
val allUsers: List<User> = UserTable.findAll()

// 新建(id 传 null,自动生成)
val newUser = UserTable.save(User(null, "Alice", "alice@example.com", 1, 25))

// 更新
UserTable.update(existingUser.copy(name = "New Name"))

// 删除
UserTable.destroy(1L)

// 计数
val total: Long = UserTable.count()

// 是否存在
val exists: Boolean = UserTable.exists(1L)

批量操作

kotlin
// 批量插入
val users = listOf(user1, user2, user3)
UserTable.insertBatch(users)

// 批量保存(返回列表)
val saved = UserTable.saveAll(users)

// 批量更新
UserTable.updateBatch(users)

查询 DSL

Neton 提供类型安全的查询 DSL,通过 query { where { } } 构建条件。where 块内使用 ColumnRefPredicateScopeallandor 等:

kotlin
import neton.database.dsl.ColumnRef

基础查询

kotlin
// 等值查询
val activeUsers = UserTable.query { where { ColumnRef("status") eq 1 } }.list()

// 比较查询
val adults = UserTable.query { where { ColumnRef("age") gt 18 } }.list()

// 模糊查询
val matched = UserTable.query { where { ColumnRef("name") like "%Alice%" } }.list()

// 查询全部
val all = UserTable.query { where { all() } }.list()

组合条件

kotlin
// AND 组合
val result = UserTable.query {
    where { and(ColumnRef("status") eq 1, ColumnRef("age") gt 18) }
}.list()

// OR 组合
val result = UserTable.query {
    where { or(ColumnRef("name") eq "Alice", ColumnRef("name") eq "Bob") }
}.list()

排序、分页

kotlin
// 排序 + 分页(page 从 1 开始)
val sorted = UserTable.query {
    where { ColumnRef("status") eq 1 }
    orderBy(ColumnRef("age").desc())
    limitOffset(20, 0)
}.list()

// 分页(含 total、totalPages)
val pageResult = UserTable.query { where { ColumnRef("status") eq 1 } }.page(1, 20)
// pageResult.items      -> List<User>
// pageResult.total      -> 总记录数
// pageResult.page       -> 当前页
// pageResult.size       -> 每页大小
// pageResult.totalPages -> 总页数

单条查询与计数

kotlin
// 单条(等价于 list().firstOrNull())
val first = UserTable.query { where { ColumnRef("status") eq 1 }; limitOffset(1, 0) }.list().firstOrNull()

// 条件查单条(便捷方法)
val one = UserTable.oneWhere { ColumnRef("email") eq "alice@example.com" }

// 条件是否存在
val exists = UserTable.existsWhere { ColumnRef("email") eq "alice@example.com" }

// 计数
val count = UserTable.query { where { ColumnRef("status") eq 1 } }.count()

安装数据库组件

在应用入口 DSL 中配置 database 组件,注册所有 Table:

kotlin
import neton.core.Neton
import neton.http.http
import neton.database.database
import neton.routing.routing

fun main(args: Array<String>) {
    Neton.run(args) {
        http { port = 8081 }

        database {
            tableRegistry = { clazz ->
                @Suppress("UNCHECKED_CAST")
                when (clazz) {
                    User::class -> UserTable
                    Role::class -> RoleTable
                    UserRole::class -> UserRoleTable
                    else -> null
                }
            }
        }

        routing { }

        onStart {
            // 启动时确保表结构存在
            UserTable.ensureTable()
            RoleTable.ensureTable()
            UserRoleTable.ensureTable()
        }
    }
}

tableRegistry 是一个从 KClassTable 实例的映射函数,框架通过它在运行时查找 Table 对象。ensureTable() 在应用启动时创建表结构(如果不存在)。

CRUD 控制器示例

结合路由注解,构建完整的 RESTful API 控制器。注意:Controller 不直接引用 Table,所有数据操作通过 Logic 层:

kotlin
import logic.UserLogic
import model.User
import neton.core.annotations.*
import neton.core.http.*
import neton.logging.Logger
import neton.logging.Log

@Controller("/api/users")
@Log
class UserController(
    private val log: Logger,
    private val userLogic: UserLogic = UserLogic()
) {

    @Get
    suspend fun all(): List<User> = userLogic.all()

    @Get("/{id}")
    suspend fun get(id: Long): User? {
        log.info("user.get", mapOf("userId" to id))
        return userLogic.get(id)
    }

    @Post
    suspend fun create(@Body user: User): User = userLogic.create(user)

    @Put("/{id}")
    suspend fun update(id: Long, @Body user: User): User =
        userLogic.update(id, user)

    @Delete("/{id}")
    suspend fun delete(id: Long) = userLogic.delete(id)
}

Logic 层:跨表聚合

当需要跨多张表进行联合查询、事务操作或业务聚合时,使用 Logic 层。Logic 通过 DbContext 执行原生 SQL,或通过 Table DSL 进行单表操作,是 Controller 与 Table 之间的唯一业务层。

定义聚合 DTO

kotlin
@Serializable
data class UserWithRoles(
    val user: User,
    val roles: List<Role>
)

实现 Logic

kotlin
import neton.database.api.DbContext
import neton.database.dbContext

class UserLogic(private val db: DbContext = dbContext()) : DbContext by db {

    suspend fun all(): List<User> =
        UserTable.query { where { User::status eq 1 } }.list()

    suspend fun get(id: Long): User? = UserTable.get(id)

    suspend fun create(user: User): User = UserTable.save(user)

    suspend fun getWithRoles(userId: Long): UserWithRoles? {
        val sql = """
            SELECT u.id, u.name, u.email, u.status, u.age,
                   r.id AS role_id, r.name AS role_name
            FROM users u
            LEFT JOIN user_roles ur ON ur.user_id = u.id
            LEFT JOIN roles r ON r.id = ur.role_id
            WHERE u.id = :uid
        """.trimIndent()

        val rows = fetchAll(sql, mapOf("uid" to userId))
        if (rows.isEmpty()) return null

        val first = rows.first()
        val user = User(
            id = first.long("id"),
            name = first.string("name"),
            email = first.string("email"),
            status = first.int("status"),
            age = first.int("age")
        )
        val roles = rows.mapNotNull { r ->
            r.longOrNull("role_id")?.let {
                Role(it, r.string("role_name"))
            }
        }.distinctBy { it.id }

        return UserWithRoles(user, roles)
    }
}

在控制器中使用 Logic

kotlin
@Controller("/api/users")
class UserController(
    private val userLogic: UserLogic = UserLogic()
) {
    @Get
    suspend fun all(): List<User> = userLogic.all()

    @Get("/{id}")
    suspend fun get(id: Long): User? = userLogic.get(id)

    @Get("/{id}/with-roles")
    suspend fun getWithRoles(id: Long): UserWithRoles? =
        userLogic.getWithRoles(id)

    @Post
    suspend fun create(@Body user: User): User = userLogic.create(user)
}

Table vs Logic 职责边界

维度TableLogic
生成方式KSP 自动生成手动编写
操作范围单表 CRUD + Query DSL跨表 JOIN / 事务 / 业务聚合
SQL 编写无需,DSL 自动生成80% 用 Table DSL,20% 用 DbContext(raw SQL 逃生口)
适用场景标准增删改查复合用例、报表、关联查询

数据库配置

config/database.conf 中配置数据库连接(TOML 格式):

toml
# config/database.conf
[default]
driver = "MEMORY"
uri = "sqlite::memory:"
debug = true

配置项说明:

配置项说明示例
driver数据库驱动"MEMORY""SQLITE""POSTGRES"
uri连接 URI"sqlite::memory:""postgres://localhost/mydb"
debug调试模式(打印 SQL)true / false

支持多数据源配置,使用不同的 section 名称:

toml
[default]
driver = "SQLITE"
uri = "sqlite:./data/main.db"
debug = true

[analytics]
driver = "POSTGRES"
uri = "postgres://localhost:5432/analytics"
debug = false

表初始化

onStart 回调中调用 ensureTable() 确保表结构存在:

kotlin
onStart {
    UserTable.ensureTable()
    RoleTable.ensureTable()
    UserRoleTable.ensureTable()
}

ensureTable() 是幂等的,已存在的表不会被重复创建或修改。

事务支持

使用 transaction 在事务中执行多个操作:

kotlin
UserTable.transaction {
    val user = save(User(null, "Alice", "alice@example.com", 1, 25))
    // 如果后续操作失败,整个事务回滚
    destroy(user.id!!)
}

相关文档

Neton Framework Documentation