Quarkus 集成 ORM 数据库操作

官方文档: hibernate-orm-panache

Quarkus 推荐使用 Hibernate ORM with Panache 来处理数据库操作, 并且支持原生 JPA/Hibernate ORM.

支持原生 JPA 主要是为了兼容 spring-boot 迁移过来的项目集, 方便其不需要修改内部数据层代码就可以直接复用.

这部分需要引入扩展第三方库:

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
<!-- 其他略 -->
<dependencies>

<!-- Quarkus ORM 依赖, 后续选择指定数据库 JDBC 驱动 -->
<!-- 具体驱动列表参照: https://quarkus.io/guides/hibernate-orm#setting-up-and-configuring-hibernate-orm -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>

<!-- Quarkus JDBC 依赖: postgresql -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

<!-- Quarkus JDBC 依赖: mariadb -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mariadb</artifactId>
</dependency>

<!-- Quarkus JDBC 依赖: mysql -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mysql</artifactId>
</dependency>
</dependencies>

我这边个人数据库都是采用 MariaDB 版本, 所以后续也是参照这部分来扩展

引入第三方之后需要在 application.properties 追加数据库依赖:

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
## 数据库配置
# 配置数据库相关信息
quarkus.datasource.db-kind=mariadb
quarkus.datasource.username=root
quarkus.datasource.password=root
# useUnicode: 启用 Unicode 字符编码支持
# characterEncoding: 默认字符集
# rewriteBatchedStatements: 批量操作优化
quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3306/game?useUnicode=true&characterEncoding=utf8mb4&rewriteBatchedStatements=true
## 设置数据库 hibernate-orm 的 schema 管理策略
# 简易正式环境设置为 none, 其他配置权限都太高了
# drop-and-create: 启动和关闭会直接销毁数据库所有数据
# update: 就是在数据库字段新增|修改的时候会出现异常
# none: 就是保持默认不做处理
quarkus.hibernate-orm.schema-management.strategy=none
## 连接池的使用
# min-size: 最小空闲连接数(默认0,生产建议5-10)
# max-size: 最大活跃连接数(默认10,生产根据QPS调整)
# initial-size: 初始化连接数(默认等于min-size)
quarkus.datasource.jdbc.min-size=5
quarkus.datasource.jdbc.max-size=20
quarkus.datasource.jdbc.initial-size=5
## 连接存活与超时
# max-lifetime: 连接最大存活时间(默认30分钟,避免长期空闲连接)
# acquisition-timeout: 获取连接超时时间(默认5秒, 超时抛异常)
quarkus.datasource.jdbc.max-lifetime=30m
quarkus.datasource.jdbc.acquisition-timeout=5s
## 连接验证 - 避免无效连接
# validate-on-borrow: 验证链接执行命令,获取连接时验证(默认false,生产建议开启)
# validation-query-sql: 验证SQL, 不同数据库适配, MySQL用SELECT 1, Oracle用SELECT 1 FROM DUAL
quarkus.datasource.jdbc.validate-on-borrow=true
quarkus.datasource.jdbc.validation-query-sql=SELECT 1
## 其他调试查看参数
# 用于 debug 开发的时候查看信息
quarkus.hibernate-orm.log.sql=true
######################################################################################
## 多数据库配置
## 允许同时连接多个数据库做读写分离等操作
## 可单独数据库配置一样, 直接格式需要命名为 quarkus.datasource.{连接名}.db-kind 追加衍生的配置
## 不过目前暂时还不需要分库处理的方式, 所以只需要上面首选数据库信息即可
######################################################################################
## 这里模拟分出个 user
quarkus.datasource.user.db-kind=mariadb
quarkus.datasource.user.username=root
quarkus.datasource.user.password=root
quarkus.datasource.user.jdbc.url=jdbc:mariadb://localhost:3306/user
## 这里模拟分出个 order
quarkus.datasource.order.db-kind=mariadb
quarkus.datasource.order.username=root
quarkus.datasource.order.password=root
quarkus.datasource.order.jdbc.url=jdbc:mariadb://localhost:3306/order
# 在实体类 / DAO 中通过 @PersistenceUnit 指定数据源, 或注入指定数据源的 AgroalDataSource

其他扩展配置可以参照官方文档处理, 这里只需要配置基础的连接池参数即可.

实体定义与使用

这里假设创建默认的测试表来处理:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `tests`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '测试主键',
`username` varchar(64) NOT NULL COMMENT '测试用户名',
`create_time` bigint(20) NOT NULL COMMENT '创建时间',
`update_time` bigint(20) NOT NULL COMMENT '更新时间',
# 留意好 enabled 是 tinyint 而非 boolean, 后续在实体之中是采用 boolean
`enabled` tinyint(1) NOT NULL COMMENT '是否启用账号',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT ='测试写入表'

数据库创建完表之后就是创建实体对象:

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
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

/**
* 建立对象实体
* '@Entity' 用于声明该类为实体类
* '@Table' 用于声明表名信息和其他扩展配置, name 为表名, scheme 为数据库名
* 注意1: 看官方例子的成员权限都是 public, 可以简化引入其他扩展和减少手动编写 getter/setter
* 注意2: 留意 PanacheEntity 父类定义, 底层帮你实现默认主键, 如果想自定义主键就要去按照父类那样重写
*/
@Entity
@Table(name = "tests", schema = "game")
public class TestsModel extends PanacheEntity {

// 因为 PanacheEntity 上层定义好了默认主键(名称为id)和我们数据库默认一样
// 所以这里其实不用编写成员字段 id

/**
* 映射内部字段, '@Column' 用于扩展内部的高级参数
*/
@Column(nullable = false, length = 64)
public String username;


/**
* 默认会将类的 小驼峰命名 的字段自动转化 下划线 字段映射数据之中
* 如果出现表名字段有差异可以通过修改 name 来映射到数据库字段
* 注1: 虽然默认自动转化, 但是为了保险一般会尽可能追加上 name 字段防止异常, 浪费不了多少时间
* 注2: 部分配置修改之后会出现字段不自动转化映射下划线字段, 所以建议全部有 驼峰法 的最好手动映射字段
*/
@Column(name = "create_time", nullable = false)
public Long createTime;


@Column(name = "update_time", nullable = false)
public Long updateTime;

/**
* 这里将 boolean 默认映射到数据库的 tinyint(1)
* 1 = true, 0 = false
*/
@Column(nullable = false, length = 1)
public Boolean enabled;
}

Panache 实际和 Jpa 差不多, 也是需要定义 Repository 层作为操作层, 默认可以将其当作 @Service 层:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Parameters;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

/**
* 数据仓库层, 用于操作数据实体内容
* '@ApplicationScoped': 代表持久全局初始化
* 'PanacheRepository': 代表抽象实现具体的 Repository 工具方法
*/
@ApplicationScoped
public class TestsRepository implements PanacheRepository<TestsModel> {

/**
* 通过用户名查询具体实体
*/
public Optional<TestsModel> findByUsername(String username) {
return find("username", username).firstResultOptional();
}


/**
* 针对某个 id 切换状态
*/
public boolean enabled(int id, boolean enabled) {
// 内部采用 ?1~?n 做递增占位填充
// enabled 是布尔值, 直接映射 tinyint(1), 无需手动转 0/1
int updatedRows = update("enabled = ?1 where id = ?2", enabled, id);
return updatedRows > 0; // 影响的数据行数
}

/**
* 创建用户实体, 创建数据必须啊采用事务 ‘@Transactional’ 来处理
*
*/
@Transactional
public TestsModel create(TestsModel entity) {
persistAndFlush(entity); // 创建持久化实体并立即刷新到数据库
return entity;
}

/**
* 命名参数(可读性更高,避免参数顺序错误)
*/
public Optional<TestsModel> findByUsernameParameter(String username) {
return find("username = :username", Parameters.with("username", username)).firstResultOptional();
}

/**
* 带参数的查询 + 排序
*/
public PanacheQuery<TestsModel> findByUsernameLike(String keyword) {
// 命名参数 + 排序
return find("username like :keyword order by updateTime asc",
Parameters.with("keyword", "%" + keyword + "%"));
}

/**
* 复杂条件查询(链式拼接)
*/
public PanacheQuery<TestsModel> findByCondition(String username, Boolean enabled) {
Sort sorted = Sort.by("username asc , updateTime desc");
PanacheQuery<TestsModel> query = find("1=1", sorted);
if (username != null) {
query = query.filter("username like :username", Parameters.with("username", "%" + username + "%"));
}
if (enabled != null) {
query = query.filter("enabled = :enabled", Parameters.with("enabled", enabled));
}
return query;
}


/**
* 所有对应 Repository 方法
*/
public void methods() {

// 创建混合查询句柄
PanacheQuery<TestsModel> queries = find("id > ?1", 1);


// 设置分页总量, 每一页需要25条数据 - 注意这里不是分页功能, 只是首次分页配置
// Page.ofSize(25) 等价于 Page.of(0, 25)
queries.page(Page.ofSize(25));


// 按照 page({每次数量}) 提交首次提取 25 条数据, 用于需要多次遍历
List<TestsModel> firstPage = queries.list(); // 第1页:0~24条\
// 判断是否有下一页, 有下一页递进获取下一页数据, 这种方式适合递归获取数据
if (queries.hasNextPage()) {
List<TestsModel> secondPage = queries.nextPage().list(); // 再次次获取 25~n+25
}


// 获取当前总页数
// 需要知道 pageCount 和 count 的差异
// pageCount: 代表按照指定 page 分页的页面数量
// count: 代表指定数据的总数量
queries.pageCount();
queries.count();

// 支持两种分页模式:
// 1. page 分页: 按照每页的页码递增, 比如第 1 页查询参数为 (page=1, total = 5) 则 1 * total, 第二页查询参数为 (page=2, total = 5) 则 2 * total
// 2. offset 分页: 按照每页的起始点递增, 比如第 1 页面查询参数为 (offset=0, total = 5) LIMIT 0, total, 第二页面查询参数为 (offset=5, total = 5) 则为 LIMIT 5,5


// page 分页处理
int validPageNum = 1; // 页码≥1
int validPageSize = 5; // 每页条数1

// 构建查询并设置分页 - Page索引从0开始, 需转换
PanacheQuery<TestsModel> pageResult = find("id > ?1", 1)
.page(Page.of(validPageNum - 1, validPageSize));
List<TestsModel> pageData = pageResult.list();
long pageCount = pageResult.count(); // 总记录数

// 最后封装 page 分页处理结果
TotalPageResult<TestsModel> totalPageResult = new TotalPageResult<>(
validPageNum,
validPageSize,
pageCount,
pageData == null ? Collections.emptyList() : pageData
);


// offset 分页处理
int validOffset = 0; // 偏移量≥0
int validLimit = 5; // 条数1~5

// 构建查询并设置分页 - Page索引从0开始, 无需转换
PanacheQuery<TestsModel> offsetResult = find("id > ?1", 1)
.page(Page.of(validOffset, validLimit));
List<TestsModel> offsetData = offsetResult.list();
long offsetCount = offsetResult.count(); // 总记录数


// 最后封装 offset 分页处理结果
TotalPageResult<TestsModel> offsetPageResult = new TotalPageResult<>(
validPageNum,
validPageSize,
offsetCount,
offsetData == null ? Collections.emptyList() : offsetData
);

}


/**
* offset 分页数据结果
* 这数据类可以提取到外层统一工具, 这里放置在这里方便查看而已
*
* @param offset 偏移值, 默认为0, 每次递增 total
* @param total 查询量, 客户端申请查询的每页数据量
* @param count 数据库当中总的数据量
* @param lists 分页列表
* @param <T>
*/
public record OffsetPageResult<T>(
int offset,
int total,
long count,
List<T> lists
) {

}

/**
* page 分页数据结果
*
* @param page 页码值, 默认为1, 每次递增 1
* @param total 查询量, 客户端申请查询的每页数据量
* @param count 数据库当中总的数据量
* @param lists 分页列表
* @param <T>
*/
public record TotalPageResult<T>(
int page,
int total,
long count,
List<T> lists
) {
}
}

页码/偏移量 参数仅支持 int 类型, 这意味着偏移量最大值受限于 int 的取值范围

(Integer.MAX_VALUE = 2147483647, 约 21 亿), 以这个限制作为分页之后的查询影响:

场景 是否受影响 说明
常规分页(页码≤10万) 不受影响 int 可支持页码到 2147483647 / 每页条数(如25)≈ 8589万页,远超常规业务需求
超大偏移量分页(offset > 21亿) 受影响 仅极罕见场景(如千万级/亿级数据批量导出)会触及上限
每页条数 > 21亿 无意义 业务中不可能单次查询21亿条数据(会直接OOM)

Quarkus 限制为 int 主要原因:

  1. 数据库层面:MySQL/PostgreSQL 等数据库的 LIMIT/OFFSET 语法中,OFFSET 虽支持 bigint,但偏移量超过1000万时性能已极差

  2. 业务层面:常规分页场景(如前端列表)页码极少超过10万,int 完全够用

  3. 性能层面intlong 更轻量,减少内存占用和计算开销

其中数据数据量极大的情况下, Page(页码)分页 > Offset(偏移)分页, 所以可以优先采用 Page 分页方式.

最后追加测试单元确认增删改查的具体流程:

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
94
95
96
97
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;


/**
* 测试数据库测试单元
*/
@QuarkusTest
class TestsRepositoryTest {

@Inject
TestsRepository testsRepository;

// 测试前置:插入测试数据
@BeforeEach
@Transactional
void setUp() {
// 清空表(避免测试数据污染)
// 这里涉及修改|删除操作, 没有事务会导致异常警告 'hibernate.connection.provider_disables_autocommit' was enabled.
testsRepository.deleteAll();

// 插入3条测试数据
TestsModel user1 = new TestsModel();
user1.username = "test1";
user1.createTime = System.currentTimeMillis() - 86400;
user1.updateTime = System.currentTimeMillis();
user1.enabled = true;
testsRepository.create(user1);

TestsModel user2 = new TestsModel();
user2.username = "test2";
user2.createTime = System.currentTimeMillis() - 86400;
user2.updateTime = System.currentTimeMillis();
user2.enabled = true;
testsRepository.create(user2);

TestsModel user3 = new TestsModel();
user3.username = "test3";
user3.enabled = true;
user3.createTime = System.currentTimeMillis() - 86400;
user3.updateTime = System.currentTimeMillis();
testsRepository.create(user3);
}


// ==================== 基础查询测试 ====================
@Test
void testFindByUsername() {
// 存在的用户
Optional<TestsModel> existUser = testsRepository.findByUsername("test1");
assertTrue(existUser.isPresent());
assertEquals("test1", existUser.get().username);

// 不存在的用户
Optional<TestsModel> notExistUser = testsRepository.findByUsername("test_not_exist");
assertFalse(notExistUser.isPresent());
}

// ==================== 增删改测试 ====================
@Test
void testCreate() {
// 新增用户
TestsModel newUser = new TestsModel();
newUser.username = "test4";
newUser.createTime = System.currentTimeMillis() - 86400;
newUser.updateTime = System.currentTimeMillis();
newUser.enabled = true;
TestsModel savedUser = testsRepository.create(newUser);

// 验证
assertNotNull(savedUser.id);
assertEquals("test4", savedUser.username);
assertEquals(4, testsRepository.count()); // 总条数=4
}


@Test
@Transactional
void testDeleteById() {
// 找到test2的ID并删除
Long test2Id = testsRepository.findByUsername("test2").get().id;
boolean deleteResult = testsRepository.deleteById(test2Id);

// 验证
assertTrue(deleteResult);
assertEquals(2, testsRepository.count()); // 总条数=2
assertFalse(testsRepository.findByUsername("test2").isPresent());
}

}

这里启动会出现异常:

1
2
3
4
5
2025-12-12 08:46:47,813 DEBUG [org.mar.jdb.cli.imp.StandardClient] (main) execute query: select next value for tests_SEQ
2025-12-12 08:46:47,813 WARN [org.mar.jdb.mes.ser.ErrorPacket] (main) Error: 4091-42S02: Unknown SEQUENCE: 'tests_SEQ'
2025-12-12 08:46:47,814 WARN [org.hib.orm.jdb.error] (main) HHH000247: ErrorCode: 4091, SQLState: 42S02
2025-12-12 08:46:47,814 WARN [org.hib.orm.jdb.error] (main) (conn=22636) Unknown SEQUENCE: 'tests_SEQ'
2025-12-12 08:46:47,814 DEBUG [org.hib.orm.jdb.error] (main) could not extract ResultSet [select next value for tests_SEQ]: java.sql.SQLSyntaxErrorException: (conn=22636) Unknown SEQUENCE: 'tests_SEQ'

因为 Hibernate 试图通过 序列(SEQUENCE) 额外表生成主键, 但 MySQL 不存在 SEQUENCE 表, 导致 Hibernate 误触发了序列查询.

所以这里就需要修改 TestsModel 来重新声明我们需要主键策略:

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

/**
* 建立对象实体
* '@Entity' 用于声明该类为实体类, name 为表名, scheme 为数据库名
* '@Table' 用于声明表名信息和其他扩展配置,
* 注意1: 看官方例子的成员权限都是 public, 可以简化引入其他扩展和减少手动编写 getter/setter
* 注意2: 留意 PanacheEntity 父类定义, 底层帮你实现默认主键, 如果想自定义主键就要去按照父类那样重写
* 注意2: 采用自定义序列就不应该继承 PanacheEntity, 而是需要继承 PanacheEntityBase
*/
@Entity
@Table(name = "tests")
public class TestsModel extends PanacheEntityBase {
/**
* 但是默认的主键不是递增生成(这里默认主键生成需要额外创建表全局管理)
* 声明采用自增ID策略
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;


// 其他略

/**
* 格式化字符串
*/
public String toString() {
String var10000 = this.getClass().getSimpleName();
return var10000 + "<" + this.id + ">";
}
}

这样就完成基础的 ORM 日常使用, 其他分库分表方式篇章比较大需要额外单独说明.