生活中的Design.

初识Ktorm

字数统计: 2.8k阅读时长: 14 min
2021/08/09 Share

初识Ktorm

Ktorm是什么?

一个基于纯JDBC的轻量级ORM框架

  • kotlin
  • 强类型
  • SqlDSL
  • 序列化API 使用 filtermapsortedBy 等序列函数进行查询,就像使用 Kotlin 中的原生集合一样方便
  • 无配置文件无xml,没有注解,没有其他第三方依赖
  • 易扩展的设计,可以灵活编写扩展,支持更多运算符、数据类型、 SQL 函数、数据库方言等

Install

build.gradle.kts:

1
2
3
4
5
implementation("org.ktorm:ktorm-core:3.4.1")
//可选
implementation("org.ktorm:ktorm-jackson:3.4.1")
//可选
implementation("org.ktorm:ktorm-support-mysql:3.4.1")

序列化

除了 JDK 序列化,ktorm-jackson 模块还为你提供了使用 JSON 格式进行序列化的功能。该模块为 Java 中著名的 JSON 框架 Jackson 提供了一个扩展,它支持将 Ktorm 中的实体对象格式化为 JSON,以及从 JSON 中解析实体对象。我们只需要将 KtormModule 注册到 ObjectMapper 中:

1
2
val objectMapper = ObjectMapper()
objectMapper.registerModule(KtormModule())

连接数据库

1
val database = Database.connect("jdbc:mysql://localhost:3306/你的数据库名称", user = "数据库用户名", password = "密码")

表、实体类

描述表结构,我试着把原来项目中JPA定义的Entity类转换成Ktorm的表描述:

原先的JPA Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@Entity
@TableName("t_admin_user")
@Table( name = "t_admin_user" )
data class AdminUser @JvmOverloads constructor(
@TableId(type = IdType.AUTO)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@field:Positive(message = "用户ID不能为空", groups = [GroupEdit::class])
@ApiModelProperty("用户ID")
val id: Long? = 0,
@field:NotBlank(message = "用户名不能为空", groups = [GroupEdit::class, GroupAdd::class])
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY, updateStrategy = FieldStrategy.NOT_EMPTY)
@ApiModelProperty("用户名")
@Column( unique = true )
var username: String? = null,

@ApiModelProperty("用户手机号")
var mobile: String? = "",

@field:Email(message = "无效的邮箱地址")
@ApiModelProperty("用户邮箱")
@Column( unique = true )
var mail: String? = "",

@field:JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@field:NotBlank(message = "密码不能为空", groups = [GroupEdit::class, GroupAdd::class])
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY, updateStrategy = FieldStrategy.NOT_EMPTY)
@ApiModelProperty("密码")
var password: String? = null,

@field:Max(value = 2, message = "错误的性别选项", groups = [GroupEdit::class, GroupAdd::class])
@field:Min(value = 0, message = "错误的性别选项", groups = [GroupEdit::class, GroupAdd::class])
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY, updateStrategy = FieldStrategy.NOT_EMPTY)
@ApiModelProperty("性别")
var gender: Int? = 0,

@ApiModelProperty("生日")
var birthday: LocalDate? = LocalDate.ofEpochDay(0),

@TableField("last_login_time")
@ApiModelProperty("上次登陆时间")
var lastLoginTime: LocalDateTime? = LocalDateTime.now(),

@TableField("last_login_ip")
@ApiModelProperty("上次登陆ip")
var lastLoginIp: String? = "",

@field:Max(value = 100, message = "错误的用户等级", groups = [GroupEdit::class, GroupAdd::class])
@field:Min(value = 0, message = "错误的用户等级", groups = [GroupEdit::class, GroupAdd::class])
@TableField("user_level")
@ApiModelProperty("用户等级")
var userLevel: Int? = 0,

@ApiModelProperty("用户昵称")
var nickname: String? = "",

@ApiModelProperty("用户头像")
var avatar: String? = "",

@TableField("weixin_open_id")
@ApiModelProperty("用户微信openId")
var weixinOpenId: String? = "",

@TableField("session_key")
@ApiModelProperty("微信登录会话KEY")
var sessionKey: String? = "",

@field:Max(value = 2, message = "错误的用户状态", groups = [GroupEdit::class, GroupAdd::class])
@field:Min(value = 0, message = "错误的用户状态", groups = [GroupEdit::class, GroupAdd::class])
@field:Column(length = 32)
@ApiModelProperty("用户状态")
var status: Int? = 0,

@TableField("add_time")
@ApiModelProperty("用户创建时间")
var addTime: LocalDateTime? = LocalDateTime.now(),

@TableField("update_time")
@ApiModelProperty("用户更新时间")
var updateTime: LocalDateTime? = LocalDateTime.now(),

@TableLogic
@ApiModelProperty("逻辑删除")
@field:Column(length = 1)
var deleted: Int? = 0,

@Transient
var roles: List<Role> = arrayListOf(),

@Transient
var menus: List<Menu> = arrayListOf()

)

先不绑定到Entity,只需Table的泛型设为Nothing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
object AdminUsers: Table<Nothing>("t_admin_user"){
val id = int("id").primaryKey()
val username = varchar("username")
val mobile = varchar("mobile")
val mail = varchar("mail")
val password = varchar("password")
val gender = int("gender")
val birthday = date("birthday")
val lastLoginTime = date("last_login_time")
val lastLoginIp = varchar("last_login_ip")
val userLevel = int("user_level")
val nickname = varchar("nickname")
val avatar = varchar("avatar")
val weixinOpenId = varchar("weixin_open_id")
val sessionKey = varchar("session_key")
val status = int("status")
val addTime = datetime("add_time")
val updateTime = datetime("update_time")
val deleted = int("deleted")
}

ktorm的表定义与实体类定义是分开的,Table的泛型可以指定绑定到哪个实体类,我们写个实体类,然后想办法把原来JPA Entity上Swagger、Validated相关的注解迁移到新的Ktorm实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface AdminUser: Entity<AdminUser>{

companion object : Entity.Factory<AdminUser>()

val id: Long
var username: String
var mobile: String
var mail: String
var password: String
var gender: Int
var birthday: LocalDate
var lastLoginTime: LocalDateTime
var lastLoginIp: String
var userLevel: Int
var nickname: String
var avatar: String
var weixinOpenId: String
var sessionKey: String
var status: Int
var addTime: LocalDateTime
var updateTime: LocalDateTime
var deleted: Int
var roles: List<Role>
var menus: List<Menu>

}

可以看到,Ktorm 中的实体类都继承了 Entity<E> 接口,这个接口为实体类注入了一些通用的方法。实体类的属性则使用 var 或 val 关键字直接定义即可,根据需要确定属性的类型及是否为空。有一点可能会违背你的直觉,Ktorm 中的实体类并不是 data class,甚至也不是一个普通的 class,而是 interface。这是 Ktorm 的设计要求,通过将实体类定义为 interface,Ktorm 才能够实现一些特别的功能,以后你会了解到它的意义。
众所周知,接口并不能被实例化,既然实体类被定义为接口,我们要如何才能创建一个实体对象呢?Ktorm 提供了一个 Entity.create 函数,这个函数会使用 JDK 动态代理生成实体类接口的实现,并为我们创建一个实体对象。要创建对象,可以这样写:

1
val user = Entity.create<AdminUser>()

因为 Entity.Factory 类重载了 invoke 运算符,所以你可以把这个伴随对象当函数一样直接加上括号进行调用,创建对象的代码变成了这样:

1
val user = AdminUser()

但是interface上没法直接加注解,所以得定义自己的实体类(2.5版本之后可以绑定任意实体类)

所以我们想办法把注解加到interface的field上面,既然field没有back-field无法添加注解,那我们试着加在get方法上,@get:注解名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
interface AdminUser: Entity<AdminUser>{

companion object : Entity.Factory<AdminUser>()
@get:Positive(message = "用户ID不能为空", groups = [GroupEdit::class])
@get:ApiModelProperty("用户ID")
val id: Long
@get:NotBlank(message = "用户名不能为空", groups = [GroupEdit::class, GroupAdd::class])
@get:ApiModelProperty("用户名")
var username: String
@get:ApiModelProperty("用户手机号")
var mobile: String
@get:Email(message = "无效的邮箱地址")
@get:ApiModelProperty("用户邮箱")
var mail: String
@get:JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@get:NotBlank(message = "密码不能为空", groups = [GroupEdit::class, GroupAdd::class])
@get:ApiModelProperty("密码")
var password: String
@get:Max(value = 2, message = "错误的性别选项", groups = [GroupEdit::class, GroupAdd::class])
@get:Min(value = 0, message = "错误的性别选项", groups = [GroupEdit::class, GroupAdd::class])
@get:ApiModelProperty("性别")
var gender: Int
@get:ApiModelProperty("生日")
var birthday: LocalDate
@get:ApiModelProperty("上次登陆时间")
var lastLoginTime: LocalDateTime
@get:ApiModelProperty("上次登陆ip")
var lastLoginIp: String
@get:Max(value = 100, message = "错误的用户等级", groups = [GroupEdit::class, GroupAdd::class])
@get:Min(value = 0, message = "错误的用户等级", groups = [GroupEdit::class, GroupAdd::class])
@get:ApiModelProperty("用户等级")
var userLevel: Int
@get:ApiModelProperty("用户昵称")
var nickname: String
@get:ApiModelProperty("用户头像")
var avatar: String
@get:ApiModelProperty("用户微信openId")
var weixinOpenId: String
@get:ApiModelProperty("微信登录会话KEY")
var sessionKey: String
@get:Max(value = 2, message = "错误的用户状态", groups = [GroupEdit::class, GroupAdd::class])
@get:Min(value = 0, message = "错误的用户状态", groups = [GroupEdit::class, GroupAdd::class])
@get:ApiModelProperty("用户状态")
var status: Int
@get:ApiModelProperty("用户创建时间")
var addTime: LocalDateTime
@get:ApiModelProperty("用户更新时间")
var updateTime: LocalDateTime
@get:ApiModelProperty("逻辑删除")
var deleted: Int
var roles: List<Role>
var menus: List<Menu>
}

这样我们就可吧实体类绑定到表上了,稍微修改了下几个表字段的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
object AdminUsers: Table<AdminUser>("t_admin_user"){
val id = long("id").primaryKey().bindTo { it.id }
val username = varchar("username").bindTo { it.username }
val mobile = varchar("mobile").bindTo { it.mobile }
val mail = varchar("mail").bindTo { it.mail }
val password = varchar("password").bindTo { it.password }
val gender = int("gender").bindTo { it.gender }
val birthday = date("birthday").bindTo { it.birthday }
val lastLoginTime = datetime("last_login_time").bindTo { it.lastLoginTime }
val lastLoginIp = varchar("last_login_ip").bindTo { it.lastLoginIp }
val userLevel = int("user_level").bindTo { it.userLevel }
val nickname = varchar("nickname").bindTo { it.nickname }
val avatar = varchar("avatar").bindTo { it.avatar }
val weixinOpenId = varchar("weixin_open_id").bindTo { it.weixinOpenId }
val sessionKey = varchar("session_key").bindTo { it.sessionKey }
val status = int("status").bindTo { it.status }
val addTime = datetime("add_time").bindTo { it.addTime }
val updateTime = datetime("update_time").bindTo { it.updateTime }
val deleted = int("deleted").bindTo { it.deleted }
}

写个简单的接口验证下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/user")
class UserController {

@Autowired
lateinit var database: Database

@IgnoreTokenInvalidation
@PostMapping("/register/ktorm")
@ApiOperation("Ktorm注册用户")
@Transactional
fun registerWithKtorm(
@RequestBody @Validated(value = [GroupAdd::class]) user: AdminUser
): BaseResponse<AdminUser> {
database.adminUsers.add(user)
return BaseResponse.ok(user)
}
}

但是实际上并不行,因为我们这个AdminUser是一个接口,jackson并不能把他deserialize,我们还是得新建个参数类(所以ktorm版的AdminUser上的@Validated相关接口可以去掉,移到新的参数接口上啦):

参数data class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

data class JsonRegister @JvmOverloads constructor(
@field:Positive(message = "用户ID不能为空", groups = [GroupEdit::class])
val id: Long? = 0,

@field:NotBlank(message = "用户名不能为空", groups = [GroupEdit::class, GroupAdd::class])
var username: String = "",

@field:NotBlank(message = "用户邮箱不能为空", groups = [GroupEdit::class, GroupAdd::class])
var mail: String = "",

@field:JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@field:NotBlank(message = "密码不能为空", groups = [GroupEdit::class, GroupAdd::class])
var password: String = "",

@field:Max(value = 2, message = "错误的性别选项", groups = [GroupEdit::class, GroupAdd::class])
@field:Min(value = 0, message = "错误的性别选项", groups = [GroupEdit::class, GroupAdd::class])
var gender: Int? = 0,

@field:NotBlank(message = "用户Nick不能为空", groups = [GroupEdit::class, GroupAdd::class])
var nickname: String = "",
@field:NotBlank(message = "用户联系方式不能为空", groups = [GroupEdit::class, GroupAdd::class])
var mobile: String = "",
@field:NotBlank(message = "用户头像不能为空", groups = [GroupEdit::class, GroupAdd::class])
var avatar: String = "",
)

接口改一改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

@RestController
@RequestMapping("/user")
class UserController {

@PostMapping("/test")
fun test():BaseResponse<Any>{
return BaseResponse.ok("666")
}

@Autowired
lateinit var database: Database
@Autowired
lateinit var tokenUtils: TokenUtils

@IgnoreTokenInvalidation
@PostMapping("/register/ktorm")
@ApiOperation("Ktorm注册用户")
fun registerWithKtorm(
@RequestBody @Validated(value = [GroupAdd::class]) user: JsonRegister
): BaseResponse<AdminUser> {
val adminUser = AdminUser {
username = user.username
mail = user.mail
password = user.password
gender = user.gender
nickname = user.nickname
mobile = user.mobile
avatar = user.avatar
}

val insert = try {
database.adminUsers.add(adminUser)
} catch (e: Exception) {
when (e.cause) {
is SQLIntegrityConstraintViolationException -> throw ServiceException(500, "账号已存在")
else -> throw ServiceException(500, "未知错误,注册用户失败")
}
}
val databaseUser = if (insert > 0) {
database.adminUsers.find {
it.username eq adminUser.username
}
} else {
null
}
return BaseResponse.depends(
databaseUser!=null,
errMsg = "注册失败",
successMsg = "注册成功",
token = tokenUtils.createTokenWithKtormUser(databaseUser),
data = databaseUser
)
}
}

测试下参数校验:

1
2
3
4
5
6
{
"code": 400,
"msg": "参数验证失败(mobile):用户联系方式不能为空",
"data": null,
"token": null
}

再试着填完参数测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"code": 200,
"msg": "注册成功",
"data": {
"id": 23,
"username": "admin-test",
"mobile": "17376508275",
"mail": "835170486@qq.com",
"gender": 0,
"nickname": "测试管理员",
"avatar": "https://www.ktorm.org/images/logo-middle.png"
}
}
CATALOG
  1. 1. 初识Ktorm
    1. 1.1. Ktorm是什么?
    2. 1.2. Install
    3. 1.3. 序列化
    4. 1.4. 连接数据库
    5. 1.5. 表、实体类