JavaQuarkusQuarkus 集成 ORM 数据库操作
MeteorCat
官方文档: 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>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-orm-panache</artifactId> </dependency>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-jdbc-postgresql</artifactId> </dependency>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-jdbc-mariadb</artifactId> </dependency>
<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
quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3306/game?useUnicode=true&characterEncoding=utf8mb4&rewriteBatchedStatements=true
quarkus.hibernate-orm.schema-management.strategy=none
quarkus.datasource.jdbc.min-size=5 quarkus.datasource.jdbc.max-size=20 quarkus.datasource.jdbc.initial-size=5
quarkus.datasource.jdbc.max-lifetime=30m quarkus.datasource.jdbc.acquisition-timeout=5s
quarkus.datasource.jdbc.validate-on-borrow=true quarkus.datasource.jdbc.validation-query-sql=SELECT 1
quarkus.hibernate-orm.log.sql=true
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
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
|
其他扩展配置可以参照官方文档处理, 这里只需要配置基础的连接池参数即可.
实体定义与使用
这里假设创建默认的测试表来处理:
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 = "tests", schema = "game") public class TestsModel extends PanacheEntity {
@Column(nullable = false, length = 64) public String username;
@Column(name = "create_time", nullable = false) public Long createTime;
@Column(name = "update_time", nullable = false) public Long updateTime;
@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 public class TestsRepository implements PanacheRepository<TestsModel> {
public Optional<TestsModel> findByUsername(String username) { return find("username", username).firstResultOptional(); }
public boolean enabled(int id, boolean enabled) { int updatedRows = update("enabled = ?1 where id = ?2", enabled, id); return updatedRows > 0; }
@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; }
public void methods() {
PanacheQuery<TestsModel> queries = find("id > ?1", 1);
queries.page(Page.ofSize(25));
List<TestsModel> firstPage = queries.list(); if (queries.hasNextPage()) { List<TestsModel> secondPage = queries.nextPage().list(); }
queries.pageCount(); queries.count();
int validPageNum = 1; int validPageSize = 5;
PanacheQuery<TestsModel> pageResult = find("id > ?1", 1) .page(Page.of(validPageNum - 1, validPageSize)); List<TestsModel> pageData = pageResult.list(); long pageCount = pageResult.count();
TotalPageResult<TestsModel> totalPageResult = new TotalPageResult<>( validPageNum, validPageSize, pageCount, pageData == null ? Collections.emptyList() : pageData );
int validOffset = 0; int validLimit = 5;
PanacheQuery<TestsModel> offsetResult = find("id > ?1", 1) .page(Page.of(validOffset, validLimit)); List<TestsModel> offsetData = offsetResult.list(); long offsetCount = offsetResult.count();
TotalPageResult<TestsModel> offsetPageResult = new TotalPageResult<>( validPageNum, validPageSize, offsetCount, offsetData == null ? Collections.emptyList() : offsetData );
}
public record OffsetPageResult<T>( int offset, int total, long count, List<T> lists ) {
}
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 主要原因:
-
数据库层面:MySQL/PostgreSQL 等数据库的 LIMIT/OFFSET 语法中,OFFSET 虽支持 bigint,但偏移量超过1000万时性能已极差
-
业务层面:常规分页场景(如前端列表)页码极少超过10万,int 完全够用
-
性能层面:int 比 long 更轻量,减少内存占用和计算开销
其中数据数据量极大的情况下, 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() { testsRepository.deleteAll();
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()); }
@Test @Transactional void testDeleteById() { Long test2Id = testsRepository.findByUsername("test2").get().id; boolean deleteResult = testsRepository.deleteById(test2Id);
assertTrue(deleteResult); assertEquals(2, testsRepository.count()); 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 @Table(name = "tests") public class TestsModel extends PanacheEntityBase {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long id;
public String toString() { String var10000 = this.getClass().getSimpleName(); return var10000 + "<" + this.id + ">"; } }
|
这样就完成基础的 ORM 日常使用, 其他分库分表方式篇章比较大需要额外单独说明.