快速开始¶
本指南以 Jimmer 的经典 Book 实体为例,演示如何从一个空项目起步,把 CRUD 接口跑起来。
环境要求¶
- JDK 17+
- Gradle 8+(或直接使用仓库自带的 wrapper)
- 可选:Docker(用于启动示例项目的 PostgreSQL 数据库)
1. 引入依赖¶
在 settings.gradle.kts 中添加 JitPack 仓库:
dependencyResolutionManagement {
repositories {
maven { url = uri("https://jitpack.io") }
mavenCentral()
}
}
在 build.gradle.kts 中添加依赖:
dependencies {
implementation("com.github.eimsound:ktor-jimmer-rest")
// Jimmer KSP 代码生成(定义实体/DTO 时必需)
ksp("org.babyfish.jimmer:jimmer-ksp:0.11.5")
}
2. 安装插件¶
在 Ktor 应用的 Application.module() 中安装 JimmerRest,并注入 KSqlClient:
fun Application.configureFrameworks() {
install(JimmerRest) {
jimmerSqlClientFactory {
inject<KSqlClient>() // 这里是 Koin 注入示例,也可以是任何提供 KSqlClient 的方式
}
}
}
3. 定义实体¶
@MappedSuperclass
interface BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
}
@Entity
interface Book : BaseEntity {
@Key
val name: String
@Key
val edition: Int
val price: BigDecimal
@ManyToOne
val store: BookStore?
}
4. 声明 API¶
一行 api<Book>("/book") 即可注册全套 REST 路由(block 只在注册时执行一次):
routing {
api<Book>("/book") {
// 过滤:查询参数会自动映射到 where 条件
filter {
where(
`ilike?`(table.name),
`in?`(table.edition),
`between?`(table.price)
)
// 关联过滤(EXISTS 语义,分页安全)
where(table.store) { // 引用关联
`ilike?`(table.name) // ?store_name__start=Amazon
}
where(Book::authors) { // 集合关联
`ilike?`(table.firstName) // ?authors_firstName__start=Alex
}
sort()
orderBy(table.id.desc())
}
// 投影:只返回需要的字段
fetcher {
fetch.by {
allScalarFields()
store {
name()
website()
}
}
}
// 输入:创建/更新时校验和转换
input {
validator {
with(it) {
::name.notBlank { "名称不能为空" }
::price.range(0.toBigDecimal()..100.toBigDecimal()) { range ->
"价格必须在${range.start}和${range.endInclusive}之间"
}
}
}
transformer {
it.copy { name = it.name.uppercase() }
}
}
// 写操作独立配置:保存模式 + 响应投影
create {
saveMode = SaveMode.UPSERT
fetcher {
fetch.by {
name()
edition()
price()
}
}
}
edit {
fetcher {
fetch.by {
name()
edition()
price()
}
}
}
patch { } // 启用 PATCH 部分更新
batch { } // 启用批量端点
// 自定义动作
action {
get("stats") {
val count = sqlClient.createQuery(Book::class) {
select(rowCount())
}.fetchUnlimitedCount()
call.respond(mapOf("count" to count))
}
}
}
}
5. 生成的接口¶
api<Book>("/book") 会注册以下端点:
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/book/{id} |
按 id 查询单个实体,不存在返回 404 |
GET |
/book |
过滤 + 分页的列表查询 |
GET |
/book/count |
过滤后的总数 |
GET |
/book/exists/{id} |
是否存在(true/false) |
POST |
/book |
创建(INSERT_ONLY) |
PUT |
/book |
更新(UPDATE_ONLY,请求体携带 id) |
PATCH |
/book |
部分更新(patch {} 启用,缺省字段不覆盖) |
POST |
/book/batch |
批量创建(batch {} 启用) |
PUT |
/book/batch |
批量更新(batch {} 启用) |
DELETE |
/book/batch?ids=1,2 |
批量删除(batch {} 启用) |
DELETE |
/book/{id} |
按 id 删除 |
| 自定义 | /book/... |
action {} 注册的路由 |
# 创建
curl -X POST http://localhost:8081/book \
-H "Content-Type: application/json" \
-d '{"name":"Learning GraphQL","edition":1,"price":50}'
# 查询单个
curl http://localhost:8081/book/1
# 列表 + 过滤 + 分页
curl "http://localhost:8081/book?name__start=GraphQL&price__ge=50&pageIndex=0&pageSize=10"
# 计数 / 存在性
curl http://localhost:8081/book/count
curl http://localhost:8081/book/exists/1
# 更新
curl -X PUT http://localhost:8081/book \
-H "Content-Type: application/json" \
-d '{"id":1,"name":"Learning GraphQL","edition":1,"price":55}'
# 部分更新(PATCH)
curl -X PATCH http://localhost:8081/book \
-H "Content-Type: application/json" \
-d '{"id":1,"price":55}'
# 批量创建 / 批量删除
curl -X POST http://localhost:8081/book/batch \
-H "Content-Type: application/json" \
-d '[{"name":"A","edition":1,"price":50},{"name":"B","edition":1,"price":60}]'
curl -X DELETE "http://localhost:8081/book/batch?ids=1,2"
# 删除
curl -X DELETE http://localhost:8081/book/1
6. 查询参数与分页¶
filter 中的扩展函数会自动把查询参数映射为条件:
| 扩展函数 | 查询参数 | 示例 |
|---|---|---|
eq? |
{字段名}(无后缀优先,其次 __exact) |
?name=GraphQL |
notEq? |
{字段名} |
?name=GraphQL |
in? / notIn? |
{字段名} 逗号分隔或重复参数 |
?id=1,2 |
lt? / gt? |
{字段名}__lt / {字段名}__gt |
?price__lt=80 |
le? / ge? |
{字段名}__le / {字段名}__ge |
?price__ge=50 |
ilike? |
{字段名} + __anywhere / __exact / __start / __end |
?name__start=GraphQL |
between? |
{字段名}__ge、{字段名}__le |
?price__ge=50&price__le=80 |
isNull / noNull |
静态谓词 | — |
| 关联过滤(EXISTS) | 关联路径用 _ 连接 |
?store_books_name=GraphQL |
sort() |
sort=字段,asc\|desc(可重复) |
?sort=price,desc&sort=id,asc |
参数分隔符可以通过 router 配置修改:
install(JimmerRest) {
router {
extParameterSeparator = "__"
subParameterSeparator = "_"
defaultPathVariable = "{id}"
}
}
列表接口默认分页参数为 pageIndex(默认 0)和 pageSize(默认 10),可通过 pager 配置调整,也可以用 pageFactory 返回自定义分页结构:
install(JimmerRest) {
pager {
defaultPageIndex = 0
defaultPageSize = 10
pageIndexParameterName = "pageIndex"
pageSizeParameterName = "pageSize"
// pageFactory = { rows, totalCount, source -> MyPage(rows, totalCount, ...) }
}
}
端点路径与查询参数名等字面量统一收进 endpoint 配置:
install(JimmerRest) {
endpoint {
batchPath = "batch" // 批量端点路径
batchIdsParameterName = "ids" // 批量删除参数名
sortParameterName = "sort" // 动态排序参数名
countPath = "count" // 计数端点路径
existsPath = "exists/{id}" // 存在性端点路径
}
}
7. 校验与异常处理¶
校验失败会抛出 ValidationException,参数解析失败抛 ParseException。用一行 jimmerRestErrors() 即可统一为 ApiError envelope:
install(StatusPages) {
jimmerRestErrors() // ValidationException → 400、ParseException → 400、Throwable → 500
}
{"status": 400, "code": "BAD_REQUEST", "message": "...", "errors": ["..."]}
框架自身的 404(GET /book/{id} 不存在)也会返回 envelope(code: NOT_FOUND)。
Jimmer 读取未加载字段时抛出的 UnloadedException 已被默认的 catcher 捕获并转为校验错误。
8. 运行示例项目¶
仓库中的 ktor-jimmer-rest-sample 提供了完整的 book-service / order-service 示例:
# 1. 启动 PostgreSQL(并执行 init-postgres.sql 初始化表结构)
docker compose -f .devcontainer/docker-compose.yml up -d db
# 2. 启动 book-service(默认端口 8081)
cd sample
./gradlew :book-service:run
完整的建表 SQL(PostgreSQL / MySQL 两版)见数据准备。