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>
表示当前仅描述表结构,不绑定具体实体。 - ✅ 列类型如
int
、varchar
、decimal
对应标准 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 聚合查询
支持 groupBy
和 having
:
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
是对 JDBCResultSet
的增强封装。 - ✅ 支持通过列对象类型安全访问值。
- ✅ 可离线消费,不依赖数据库连接。
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
前必须确保关联的 Item
和 Customer
已存在,否则外键约束会失败。
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