1. 概述

关系型数据模型至今仍是大多数数据驱动服务的核心,其灵活性毋庸置疑。但作为开发者,我们都清楚:对象模型与关系模型之间的结构差异,导致两者映射困难、繁琐且容易出错——也就是常说的 “对象-关系阻抗失配(Object-relational impedance mismatch)”

幸运的是,Kotlin 的语言特性催生了新一代 ORM 工具,能够更自然地融入代码体系,提供更流畅的开发体验。

本文将介绍 Ktorm,一个轻量级、易用的 Kotlin ORM 框架。

2. 基础环境搭建

我们先通过 Maven 添加 Ktorm 所需依赖,构建示例运行环境:

<dependency>
    <groupId>org.ktorm</groupId>
    <artifactId>ktorm-core</artifactId>
    <version>3.6.0</version>
</dependency>
<dependency>
    <groupId>org.ktorm</groupId>
    <artifactId>ktorm-support-mysql</artifactId>
    <version>3.6.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

✅ 最新版本可在 Maven Central 查看。
✅ 示例使用 MySQL,因此需引入对应的 JDBC 驱动。Ktorm 支持多种主流数据库,通过方言(Dialect)适配 SQL 语法差异。

3. 核心抽象概念

多年来,ORM 工具试图解决对象与关系模型之间的鸿沟,但部分方案侵入性较强。

  • ❌ JPA 虽覆盖大部分映射需求,但依赖注解,过程略显繁琐。
  • ❌ MyBatis 支持动态 SQL,但牺牲了类型安全。

Ktorm 的设计思路有所不同:

  • 不强制将表映射为特定领域对象,允许在更低层的 SQL DSL 上操作。
  • ✅ 提供类型安全的动态 SQL 构建能力,适用于查询和数据操作。
  • ✅ 当需要实体支持时,可使用基于 SQL DSL 构建的 Entity API。
  • 无需使用注解,映射关系在表结构定义中声明即可。

这意味着你可以根据场景灵活选择:直接操作 DSL 或使用实体抽象。

4. 领域模型设计

以下是我们用于演示的 MySQL 表结构:

create table items(
    id int not null primary key auto_increment,
    description varchar(255) not null
);

create table customers(
    id int not null primary key auto_increment,
    email varchar(255) not null,
    UNIQUE KEY(email)
);

create table orders(
    id int not null primary key auto_increment,
    item_id int not null,
    customer_id int not null,
    free_text_card varchar(255) null,
    amount decimal(10,2),
    last_update timestamp(3)
);

可通过 Docker 快速部署测试环境,例如使用 mysql:8.0 镜像初始化数据库。

5. 表结构定义(Schema)

静态 SQL 不够灵活,多数业务需要运行时动态构建查询条件。Ktorm 提供了类型安全的领域特定语言(DSL),让我们在 Kotlin 中直接构造动态 SQL。

第一步是将数据库表结构映射为 Kotlin 类:

class ItemsTable: Table<Nothing>("items") {
    val id = int("id").primaryKey()
    val description = varchar("description")
}

class CustomersTable: Table<Nothing>("customers") {
    val id = int("id").primaryKey()
    val email = varchar("email")
}

class OrdersTable: Table<Nothing>("orders") {
    val id = int("id").primaryKey()
    val itemId = int("item_id")
    val customerId = int("customer_id")
    val card = base64("free_text_card", Charsets.UTF_8)
    val amount = decimal("amount")
    val timestamp = timestamp("last_update")
}

关键点说明

  • ✅ 继承 Table<Nothing> 表示当前仅描述表结构,不绑定具体实体。
  • ✅ 列类型如 intvarchardecimal 对应标准 JDBC 类型。
  • Nothing 泛型表示无实体绑定,仅用于元数据定义。

5.1 自定义 SQL 类型

OrdersTable 中的 card 字段使用 Base64 编码存储,这不是标准 JDBC 类型。Ktorm 允许自定义 SqlType 实现转换逻辑:

class Base64String(private val charset: Charset): SqlType<String>(Types.VARCHAR, "base64") {

    override fun doGetResult(rs: ResultSet, index: Int): String? {
        val retrievedData = rs.getString(index)
        return when {
            retrievedData.isNullOrBlank() -> null
            else -> Base64.getDecoder().decode(retrievedData).toString(charset)
        }
    }

    override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: String) {
        ps.setString(index, Base64.getEncoder().encodeToString(parameter.toByteArray(charset)))
    }
}

自定义类型核心逻辑

  • doGetResult:从数据库读取后自动解码。
  • doSetParameter:写入前自动编码。
  • ✅ 将转换逻辑下沉到框架层,避免业务代码重复处理。

注册扩展函数以便在 schema 中使用:

fun BaseTable<*>.base64(name: String, charset: Charset) = registerColumn(name, Base64String(charset))

⚠️ 这种方式非常适合处理加密字段、序列化字段等通用转换场景,属于典型的“踩坑后总结的最佳实践”。

6. 创建数据库连接

所有操作始于 Database 实例,它是连接管理和 SQL 执行的入口:

val database = Database.connect(
    url = "jdbc:mysql://localhost:3306/KTORM_TEST",
    driver = "com.mysql.cj.jdbc.Driver",
    user = "root",
    password = "password123",
    dialect = MySqlDialect()
)

参数说明

  • url:JDBC 连接地址。
  • driver:MySQL 驱动类。
  • user/password:登录凭证(已脱敏替换)。
  • dialect:指定生成 SQL 的方言,影响语法细节(如分页、冲突处理等)。

Ktorm 支持主流数据库(PostgreSQL、Oracle、SQL Server 等),每种方言提供特定语法扩展。

7. 初始化数据

准备一些测试数据,先创建表对象实例:

val ordersTable = OrdersTable()
val itemsTable = ItemsTable()
val customersTable = CustomersTable()

使用批量插入填充数据:

database.batchInsert(itemsTable)  {
    (1..10).forEach { idx ->
        item {
            set(it.description, "test_item_$idx")
        }
    }
}

database.bulkInsertOrUpdate(customersTable)  {
    (1..10).forEach { idx ->
        item {
            set(it.email, "user$idx@example.com")
        }
        onDuplicateKey {
            set(it.email, it.email)
        }
    }
}

database.batchInsert(ordersTable)  {
    (1..10).forEach { idx ->
        item {
            set(it.itemId, idx)
            set(it.customerId, idx)
            set(it.amount, 100.20.toBigDecimal())
            set(it.card, "Card nr. $idx")
            set(it.timestamp, Instant.now())
        }
    }
}

注意事项

  • batchInsert:标准批量插入。
  • bulkInsertOrUpdate + onDuplicateKey:MySQL 特有“存在则更新”语义。
  • ⚠️ onDuplicateKey 是 MySQL 方言专属扩展,只有指定 MySqlDialect 时才可用。换其他数据库会编译报错。

这体现了 Ktorm 的设计理念:在保持跨平台能力的同时,允许针对特定数据库暴露原生特性

8. 类型安全查询(DSL 模式)

Ktorm 的 DSL 提供类型安全的 SQL 构建能力。

8.1 基础查询

database
  .from(ordersTable)
  .select(ordersTable.id, ordersTable.card, ordersTable.amount)

8.2 条件查询

database
  .from(ordersTable)
  .select(ordersTable.id, ordersTable.card, ordersTable.amount)
  .where(ordersTable.itemId eq 1)

多个条件可通过 whereWithConditions 组合(AND 逻辑):

database
  .from(ordersTable)
  .select(ordersTable.id, ordersTable.card, ordersTable.amount)
  .whereWithConditions {
       it += (ordersTable.itemId eq 1)
       it += (ordersTable.customerId eq 1)
}

8.3 表连接

database
  .from(ordersTable)
  .innerJoin(customersTable, on = ordersTable.customerId eq customersTable.id)
  .innerJoin(itemsTable, on = ordersTable.itemId eq itemsTable.id )
  .select(ordersTable.id, customersTable.email, itemsTable.description)

8.4 聚合查询

支持 groupByhaving

database
  .from(ordersTable)
  .innerJoin(customersTable, on = ordersTable.customerId eq customersTable.id)
  .innerJoin(itemsTable, on = ordersTable.itemId eq itemsTable.id )
  .select(customersTable.id, itemsTable.id, sum(ordersTable.amount).aliased("item_sales"))
  .groupBy(customersTable.id, itemsTable.id)
  .having(itemsTable.id eq 1)

8.5 结果消费

遍历查询结果:

database
  .from(ordersTable)
  .select(ordersTable.id, ordersTable.card, ordersTable.amount)
  .forEach { 
     println(it[ordersTable.id])
  }
  • QueryRowSet 是对 JDBC ResultSet 的增强封装。
  • ✅ 支持通过列对象类型安全访问值。
  • ✅ 可离线消费,不依赖数据库连接。

9. 实体 API(Entity API)

当需要将数据映射为领域对象时,Ktorm 提供 Entity API。

9.1 定义实体

使用接口继承 Entity<T> 声明实体:

interface Item : Entity<Item> {
    companion object : Entity.Factory<Item>()
    val id: Int
    var description: String
}

interface Customer : Entity<Customer> {
    companion object : Entity.Factory<Customer>()
    val id: Int
    var email: String
}

interface Order : Entity<Order> {
    companion object : Entity.Factory<Order>()
    val id: Int
    var item: Item
    var customer: Customer
    var card: String
    var amount: BigDecimal
    var timestamp: Instant
}

📌 虽然使用接口可能引发争议,但这是 Ktorm 实现代理和延迟加载的基础机制。最新版本也支持普通类作为实体,但会失去部分高级功能。

9.2 映射表结构到实体

更新表定义,绑定列与实体属性:

object Items : Table<Item>("items") {
    val id = int("id").primaryKey().bindTo { it.id }
    val description = varchar("description").bindTo { it.description }
}

object Customers : Table<Customer>("customers") {
    val id = int("id").primaryKey().bindTo { it.id }
    val email = varchar("email").bindTo { it.email }
}

object Orders : Table<Order>("orders") {
    val id = int("id").primaryKey().bindTo { it.id }
    val itemId = int("item_id").references(Items) { it.item }
    val customerId = int("customer_id").references(Customers) { it.customer }
    val card = base64("free_text_card", Charsets.UTF_8).bindTo { it.card }
    val amount = decimal("amount").bindTo { it.amount }
    val timestamp = timestamp("last_update").bindTo { it.timestamp }
}

映射方式对比

方法 用途
bindTo 绑定基本类型字段(如 String、Int)
references 声明外键关联,自动处理 JOIN 和对象图构建

✅ 使用 references 后,查询复杂实体时 Ktorm 会自动 LEFT JOIN 关联表。

9.3 使用实体插入数据

扩展 Database 获取实体序列:

val Database.orders get() = this.sequenceOf(Orders)
val Database.customers get() = this.sequenceOf(Customers)
val Database.items get() = this.sequenceOf(Items)

插入数据示例:

(1..10).forEach { idx ->
    val item = Item {
        description = "test_item_$idx"
    }
    items.add(item)
}

var customer = customers.find {
    it.email eq "user1@example.com"
}

if (customer == null) {
    customer = Customer {
        email = "user1@example.com"
    }
    customers.add(customer)
}

(1..10).forEach { idx ->
    val order = Order()
    val item = items.find { it.id eq idx } ?: items.first()
    order.item = item
    order.customer = customer
    order.amount = 100.20.toBigDecimal()
    order.card = "Card nr. $idx"
    order.timestamp = Instant.now()
    orders.add(order)
}

⚠️ 插入 Order 前必须确保关联的 ItemCustomer 已存在,否则外键约束会失败。

9.4 基础实体查询

val orderEntities = database.orders.toList()

9.5 条件实体查询

val filteredOrderEntities = database
  .orders
  .filter { it.itemId eq 1 }
  .toList()

多条件过滤:

val multiFilteredOrderEntities = database
  .orders
  .filter { it.itemId eq 1 }
  .filter { it.customerId eq 1 }
  .toList()

生成的 SQL 示例

SELECT 
  orders.id AS orders_id, 
  orders.item_id AS orders_item_id, 
  orders.customer_id AS orders_customer_id, 
  orders.free_text_card AS orders_free_text_card, 
  orders.amount AS orders_amount, 
  orders.last_update AS orders_last_update, 
  _ref0.id AS _ref0_id, 
  _ref0.description AS _ref0_description, 
  _ref1.id AS _ref1_id, 
  _ref1.email AS _ref1_email 
FROM orders 
LEFT JOIN items _ref0 ON orders.item_id = _ref0.id 
LEFT JOIN customers _ref1 ON orders.customer_id = _ref1.id 
WHERE (orders.item_id = ?) AND (orders.customer_id = ?)

✅ Ktorm 自动根据 references 声明生成 LEFT JOIN。
⚠️ 若性能敏感,可通过配置禁用自动 JOIN,手动控制查询粒度。

10. 总结

Ktorm 提供了两种编程范式:

  • SQL DSL 模式:适合基础设施层,提供细粒度控制,类型安全。
  • Entity API 模式:贴近领域模型,减少样板代码,自动处理关联。

你可以根据项目需求混合使用两种方式。对于高性能或复杂查询场景,推荐优先使用 DSL;而对于常规 CRUD,Entity API 更加简洁高效。

所有示例代码均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/kotlin-libraries-orm


原始标题:Intro to Ktorm: ORM Framework for Kotlin