编译器插件

配置 Kronos 编译器插件,并读取用户可见的 DSL 诊断。

设置 Kotlin 项目基线

Kronos 编译期支持由 Kotlin 构建加载。项目使用 JDK 8+、Kotlin 2.4.21+、Maven 3.9+,或 Kotlin 2.4.21 支持的 Gradle 版本。

gradle(kts)

import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
    kotlin("jvm") version "2.4.21"
}

kotlin {
    compilerOptions {
        jvmTarget.set(JvmTarget.JVM_1_8)
    }
}

maven

<properties>
    <kotlin.version>2.4.21</kotlin.version>
    <maven.compiler.release>8</maven.compiler.release>
</properties>

先让基础 Kotlin 构建通过,再添加 Kronos 数据源和 ORM 代码。

output

> Task :compileKotlin
BUILD SUCCESSFUL

启用kronos-gradle-plugin

在 JVM Kotlin 项目中添加 Kronos Gradle 插件和 kronos-core。Gradle 插件会把 Kronos 编译期支持接入 compileKotlin。

build.gradle.kts

plugins {
    kotlin("jvm") version "2.4.21"
    id("com.kotlinorm.kronos-gradle-plugin") version "0.4.0"
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xcollection-literals", "-Xcontext-sensitive-resolution")
    }
}

dependencies {
    implementation("com.kotlinorm:kronos-core:0.4.0")
}

运行 Kotlin 编译任务,并检查插件加载输出。

shell

./gradlew compileKotlin

output

Loaded Gradle plugin com.kotlinorm.compiler.plugin.KronosGradlePlugin version 0.4.0
Loaded Compiler plugin com.kotlinorm.kronos-compiler-plugin version 0.4.0
BUILD SUCCESSFUL

启用kronos-maven-plugin

在 Maven 中,将 kronos-core 作为应用依赖,并在 kotlin-maven-plugin 中注册 kronos-maven-plugin。

pom.xml

<project>
    <properties>
        <kotlin.version>2.4.21</kotlin.version>
        <kronos.version>0.4.0</kronos.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.kotlinorm</groupId>
            <artifactId>kronos-core</artifactId>
            <version>${kronos.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <args>
                        <arg>-Xcollection-literals</arg>
                        <arg>-Xcontext-sensitive-resolution</arg>
                    </args>
                    <compilerPlugins>
                        <plugin>kronos-maven-plugin</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>com.kotlinorm</groupId>
                        <artifactId>kronos-maven-plugin</artifactId>
                        <version>${kronos.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>

使用 Maven 编译,并检查 Kronos 插件加载输出。

shell

mvn compile

output

Loaded Maven plugin com.kotlinorm.compiler.plugin.KronosMavenPlugin
[INFO] BUILD SUCCESS

编译每个 Kronos source set

每个声明 KPojo 类或使用 Kronos DSL 的 JVM source set 都需要启用 Kronos 构建插件。只消费已编译实体的模块可以正常依赖上游模块;但只要本模块声明实体或编写查询 DSL,就需要在本模块编译时加载 Kronos 插件。

module layout

// :domain 声明 KPojo,需要应用 Kronos Gradle 或 Maven 插件。
@Table("tb_user")
data class User(
    @PrimaryKey
    var id: Int? = null,
    var name: String? = null
) : KPojo

// :service 基于 User 编写 DSL,也需要经过 Kronos 编译期支持。
val names = User()
    .select { [it.id, it.name] }
    .where { it.name like "A%" }
    .toList()

最快的检查方式是编译拥有这些源码的模块。

shell

./gradlew :service:compileKotlin

output

[Kronos] Kronos compiler plugin K2 initialized
BUILD SUCCESSFUL

检查生成的KPojo 成员

构建插件生效后,KPojo 可以在运行时检查中暴露生成的元数据和 Map 转换方法。

User.kt

import com.kotlinorm.annotations.PrimaryKey
import com.kotlinorm.annotations.Table
import com.kotlinorm.interfaces.KPojo

@Table("tb_user")
data class User(
    @PrimaryKey
    var id: Int? = null,
    var name: String? = null
) : KPojo

fun main() {
    val user = User(7, "Ada")

    println(user.__tableName)
    println(user.toDataMap())
    println(user.__columns.map { it.name })
}

输出应包含表元数据和 Kronos 收集到的属性值。

output

tb_user
{id=7, name=Ada}
[id, name]

如果出现 __tableName must be overridden by the compiler plugin,表示该 source set 没有经过 Kronos 编译期支持处理。

检查 DSL SQL 输出

使用一个小的 select 查询确认 Kotlin 属性访问和条件表达式已转换为 Kronos SQL。

kotlin

import com.kotlinorm.orm.select.select

val (sql, params) = User()
    .select { [it.id, it.name] }
    .where { it.id == 7 }
    .build()

println(sql)
println(params)

使用 MySQL 数据源时,检查输出会包含选择字段和命名参数。

output

SELECT `id`, `name` FROM `tb_user` WHERE `tb_user`.`id` = :id
{id=7}

条件操作符和 query-by-example 行为请参考 📚Where、Having 和 On 条件。

使用 kronos-syntax 的 DSL 规则

Kronos 的查询、变更和 DDL API 都会在执行前生成可检查的 SQL 任务。需要先查看 SQL 和参数时,调用 build(),不需要连接数据库。

projection and condition

val task = User()
    .select {
        [
            it.id,
            it.name.alias("username"),
            f.length(it.name).alias("nameLength")
        ]
    }
    .where { (it.id in listOf(1, 2, 3)) && it.name like "A%" }
    .build()

println(task.sql)
println(task.paramMap)

output

SELECT `id`, `name` AS `username`, LENGTH(`name`) AS `nameLength`
FROM `tb_user`
WHERE `tb_user`.`id` IN (:idList) AND `tb_user`.`name` LIKE :name
{idList=[1, 2, 3], name=A%}

同一套规则适用于 where、having 和 on:Kotlin 比较会变成 SQL 谓词,&& 和 || 保留布尔组合,in 可以接集合或可查询子查询,非直接字段投影需要 .alias("name")。

derived projection

val nameLengths = User()
    .select { [it.id, f.length(it.name).alias("nameLength")] }

val rows = nameLengths
    .select { [it.id, it.nameLength] }
    .where { it.nameLength > 8 }
    .toList()

投影、子查询和生成结果形态示例见 📚投影 和 📚子查询。

读取 Kronos 诊断

Kronos 诊断会出现在 IDE 和构建输出中。KRONOS_ 代码用于定位 DSL 规则,诊断消息会说明修复方式。

invalid select

import com.kotlinorm.orm.select.select

User()
    .select { [it.id, f.length(it.name)] }

这个选择项是函数表达式,因此诊断会要求显式设置别名。

output

User.kt:6:23: error: Non-field select item must declare .alias("name")
KRONOS_SELECT_ITEM_REQUIRES_ALIAS

添加别名后重新编译。

fixed select

User()
    .select { [it.id, f.length(it.name).alias("nameLength")] }

Kronos 发出的编译插件错误也会以 [Kronos] 开头,并可能带有 Fix: 行。

kronos output

User.kt:12:13: error: [Kronos] Unsupported field
  Fix: Use a property reference

修复常见诊断

通过诊断代码定位需要调整的 DSL 形态。

诊断检查点修复方式
__tableName must be overridden by the compiler plugin当前 source set 没有经过 Kronos 编译期支持为该模块启用 Gradle 或 Maven 插件后重新编译
KRONOS_GENERIC_KPOJO_NOT_SUPPORTEDKPojo 类声明了类级泛型参数移除 KPojo 类的泛型参数,并使用具体的属性类型
KRONOS_SELECT_ITEM_REQUIRES_ALIASselect { ... } 中包含函数、聚合、标量子查询、窗口函数或原生 SQL添加 .alias("resultName")
KRONOS_DUPLICATE_PROJECTION_FIELD两个投影项生成了同名结果属性删除重复字段,或给其中一个投影项设置不同 alias
OPT_IN_USAGE_ERRORselected alias 替换了仍存在的同名 source 字段修改 alias、先移除 source 字段,或使用 @OptIn(UnsafeProjectionOverride::class) 明确确认
KRONOS_SCALAR_SUBQUERY_REQUIRES_LIMIT标量子查询可能返回多行作为值使用前添加 .limit(1)
KRONOS_SCALAR_SUBQUERY_REQUIRES_SINGLE_COLUMN标量子查询选择了多列只选择一个字段,或拆成多个表达式
KRONOS_PREDICATE_SUBQUERY_COLUMN_COUNT_MISMATCHfield in query、ANY、SOME、ALL 或 row-value IN 左右列数不一致让左侧表达式和右侧查询返回相同列数
KRONOS_ROW_VALUE_TUPLE_REQUIRES_MULTIPLE_FIELDS使用了 [it.id] in query 这样的单字段 tuple改写为 it.id in query
KRONOS_INSERT_SELECT_VALUE_COUNT_MISMATCHinsert<Target> { ... } 值数量与目标字段数量不一致选择与目标可插入列数量一致的值
KRONOS_INSERT_SELECT_VALUE_TYPE_MISMATCHinsert-select 值类型和目标字段类型不匹配调整顺序,或把标量表达式 cast 成预期 Kotlin 类型

显式 projection alias 通常不能替换仍存在的同名 source 字段。如果这种替换是有意的,请使用 Kotlin 标准 opt-in 机制。标记为 com.kotlinorm.annotations.UnsafeProjectionOverride,严重级别为 error;提示会说明 selected 值和 Kotlin 类型可能发生变化。

import com.kotlinorm.annotations.UnsafeProjectionOverride

@OptIn(UnsafeProjectionOverride::class)
val query = User().select { [it.id, f.length(it.name).alias("name")] }

先移除字段再恢复同名 alias 时,不需要 opt-in:

val query = User().select { [it - it.name, f.length(it.name).alias("name")] }

构建配置从 📚Quick Start 开始。数据库执行和 SQL 日志请参考 📚连接到数据库 和 📚日志。IDE 检查行为请参考 📚KEFS 编辑器支持。