3K = Kotlin + Ktor + Ktorm,不要记错了哦
一直以来都用惯了 MyBatis,但是随着我越来越多的使用 Kotlin 而不是 Java,在 MyBatis 上遇到的问题也在不断增多。比如说那个经典的 kotlin.Int 作为参数的问题,由于 Kotlin 在编译期间会依据不同的情况将 kotlin.Int 映射为 int 或者 java.lang.Integer,从而导致 MyBatis 有可能无法找到正确的函数签名从而调用失败。诸如此类的问题还有很多,让我不得不想要用一种新的方案来替代掉 MyBatis。
同时,由于我将主力的服务端开发框架从 Springboot 切换到 Ktor,使得整个研发体系都已经是完整的 Kotlin 环境,因此也需要有一个更加 Kotlin 的 ORM 框架,来实现对技术栈的统一。
在我的认知中,我所需要的 ORM 框架应当具备以下特点:
- 尽可能的不直接编写 SQL 语句,但是又要能精准的控制生成的 SQL 语句
- 更加 Kotlin 风格的链式调用
- 能够简单的处理复杂的连表查询等情况
- 效率优先,尽量让我不要思考或更少的思考
在经过一番搜索后,我找到了 Ktorm,简单看了下它是 2018 年年底发布的,至今迭代到 3.6.0 版本,3.7 也正在开发中,看了一下 issue,也是比较活跃的,作者亲自解答问题。目前也算是很稳定了,又简单看了一下文档,感觉非常简单且有意思,应该是我理想中的 ORM 框架。嗯,就决定是你了!
在实际场景中,我需要连接池以及序列化的能力,参考了 Ktorm 的文档,作者也是强烈建义与连接池一起使用,那正中下怀,赶紧动手吧。
dependencies {
implementation("org.ktorm:ktorm-core:3.6.0")
implementation("org.ktorm:ktorm-jackson:3.6.0")
implementation("com.alibaba:druid:1.2.18")
implementation("mysql:mysql-connector-java:8.0.33")
implementation("org.ktorm:ktorm-support-mysql:3.6.0")
}
data class DatabaseConfig(
val driverClassName: String, // 驱动的类名
val url: String, // jdbc url
val username: String, // 用户名
val password: String, // 密码
val initialSize: Int = 10, // 默认连接数
val maxActive: Int = 25, // 最大连接数
val maxWait: Long = 3000, // 最大等待时间
val logLevel: LogLevel = LogLevel.DEBUG // 输出的日志级别
)
object WHDatabase {
lateinit var database: Database
private lateinit var dataSource: DataSource
fun initDatabase(config: DatabaseConfig) {
val prop = Properties()
config.javaClass.declaredFields.forEach {
prop[it.name] = "${it.apply { isAccessible = true }.get(config)}"
}
dataSource = DruidDataSourceFactory.createDataSource(prop)
database = Database.connect(
dataSource = dataSource,
dialect = MySqlDialect(),
logger = ConsoleLogger(config.logLevel)
)
}
}
现在我们可以直接使用 WHDatabase.database
来访问 database 对象,这个东西是后面一切 Ktorm 操作的开端。
下面重头戏就来了,Ktorm 独特的地方就是它的表结构与实体类,在这里可以玩出很多花来,不得不说作者在 Kotlin 上的深度是非常优秀的,从这块的设计上来看就可见一斑。下面我直接写一个真实案例。
以下是建表语句:
create table sys_user (
user_id bigint primary key auto_increment,
user_name varchar(32) not null default '',
dept_id bigint not null default 0, /* 部门 id */
email varchar(255),
status char(1) not null default '0', /* 0正常, 1已停用 */
update_time datetime /* 更新时间 */
)
create table sys_dept (
dept_id bigint primary key auto_increment,
dept_name varchar(32) not null default '',
update_time datetime
)
针对这数据结构,先定义实体类,也就是说我们在程序里传递的对象形态,这里先定义实体类是因为后面定义表结构时需要进行绑定,以便实体类的改动可以简单的传递到数据库。
interface SysUser : Entity<SysUser> {
companion object : Entity.Factory<SysUser>()
var userId: Long
var userName: String
var deptId: Long
var email: String?
var status: Boolean // true正常, false已停用
var updateTime: LocalDate?
val dept: SysDept? // 部门实体
}
interface SysDept: Entity<SysDept> {
companion object : Entity.Factory<SysDept>()
var deptId: Long
var deptName: String
var updateTime: LocalDate?
}
好了,这就是我们需要传递的实体类了,下面将要实现的就是表结构定义了,在定义表结构的时候,我们可以同时定义表结构与实体类的绑定关系,可以引用其他的表结构,也可以对数据类型进行转换,这也是 Ktorm 为我们提供的巨大方便。
object SysUsers : Table<SysUser>("sys_user") {
var userId = long("user_id").primaryKey().bindTo { it.userId }
var userName = varchar("user_name").bindTo { it.userName }
var deptId = long("dept_id").references(SysDepts) { it.dept }.bindTo { it.deptId }
var email = varchar("email").bindTo { it.email }
var status = varchar("status").transform({ it == "0" }, { if (it) "0" else "1" }).bindTo { it.status }
var updateTime = date("update_time").bindTo { it.updateTime }
}
val Database.sysUsers get() = this.sequenceOf(SysUsers)
object SysDepts : Table<SysDept>("sys_dept") {
var deptId = long("dept_id").primaryKey().bindTo { it.deptId }
var deptName = varchar("dept_name").bindTo { it.deptName }
var updateTime = date("update_time").bindTo { it.updateTime }
}
val Database.sysDepts get() = this.sequenceOf(SysDepts)
在这里就能看到很多有意思的东西了,比如说 SysUser 实体类中包含了 dept 对象,它是一个 SysDept 实体,也就是说需要从 sys_dept 表中查出相应的数据,这类需求在 Ktorm 里可以轻松被满足,只需要在绑定 dept_id 的时候顺便做一个 reference 就好了。
还有一个需求是 status 字段,在数据库里是 char 型,而在实体类里是 Boolean 型,这里的数据类型映射是需要进行转换的,Ktorm 也提供了 transform 方法来实现这一能力。
最后我们可以把这两个表结构挂到 Database 上,以便从 Database 直接进行访问,提供更方便的操作。
现在我们已经可以利用 Ktorm 提供的便利来进行增删改查了,比如说这样:
// 新增
val user = SysUser {
userName = "Sample User"
deptId = 1
status = true
}
database.sysUsers.add(user)
// 修改
user.status = false
user.flushChanges()
// 删除
user.delete()
// 查询
val user = database.sysUsers.find { SysUsers.userId eq 1 }
除此之外,Ktorm 还提供了非常符合 SQL 书写习惯的 DSL,可以很方便的进行联表查询,比如说知道部门名称,要查该部门有哪些人,就可以这样做:
fun queryUsersInDept(deptName: String): List<SysUser> =
database.from(SysUsers)
.leftJoin(SysDepts, on = SysUsers.deptId eq SysDepts.deptId)
.select(SysUsers.columns).where {
SysDepts.deptName eq deptName
}.map {
SysUsers.createEntity(it)
}
到此,目前我们已经可以正常的使用 Ktorm 来进行对数据的操作了,同时也将 Druid 连接池成功配置。
目前还需要知道的一点是,Ktorm 支持使用 Jackson 进行 JSON 序列化,这个将对后面的内容起到至关重要的作用,敬请等待 3K 整合系列的第二篇。