关于 quarkus-panache-smells
用于检测 Quarkus Panache Repository 应用中常见 ORM 反模式的技能,并给出重构建议。覆盖查询效率、关联加载和资源管理等问题。
主要功能
- 01检测 EAGER 关联加载
- 02识别未分页的全表查询
- 03发现缺少投影的只读查询
- 04检查 N+1 查询问题
- 05发现未关闭的流
- 06建议 Panache 查询简写
适用场景
- 01检查 Repository 中的 ORM 代码异味
- 02优化大型表查询与分页
- 03减少关联访问引发的额外查询
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ emvnuel/ skills --skill quarkus-panache-smells安装后在 Claude Code 中输入/quarkus-panache-smells确认已加载。
详细介绍
Quarkus Panache Repository 代码异味检测
识别并修复采用 Repository 模式的 Quarkus Panache 应用中的常见 ORM 反模式。
代码异味类别
1. 在类级别使用 Eager Fetching
问题:关系上的 FetchType.EAGER 总是会加载相关实体。
检测:
// BAD: Always loads Person even when not needed
@ManyToOne(fetch = FetchType.EAGER)
public Person owner;
// BAD: @ManyToOne/@OneToOne default to EAGER
@ManyToOne // implicitly EAGER
public Person owner;
重构:
// GOOD: Explicitly LAZY
@ManyToOne(fetch = FetchType.LAZY)
public Person owner;
2. 使用 listAll() 时未进行分页
问题:对大型表调用 listAll() 或 streamAll() 会导致内存问题。
检测:
// BAD: Loads entire table into memory
List<Person> all = personRepository.listAll();
// BAD: Even with stream, still fetches all
Stream<Person> stream = personRepository.streamAll();
重构:
// GOOD: Use PanacheQuery with pagination
PanacheQuery<Person> query = personRepository.findAll();
query.page(Page.ofSize(25));
List<Person> page = query.list();
// Or with range
List<Person> range = personRepository.findAll().range(0, 24).list();
3. 只读查询缺少投影
问题:只需要特定字段时,却获取了整个实体。
检测:
// BAD: Loads all columns including BLOBs
List<Person> persons = personRepository.list("status", Status.Alive);
// Then only uses person.name
重构:
// GOOD: Use DTO projection
@RegisterForReflection
public class PersonName {
public final String name;
public PersonName(String name) {
this.name = name;
}
}
// Only 'name' column loaded from database
List<PersonName> names = personRepository.find("status", Status.Alive)
.project(PersonName.class)
.list();
包含相关实体字段时:
@RegisterForReflection
public class DogDto {
public String name;
public String ownerName;
public DogDto(String name,
@ProjectedFieldName("owner.name") String ownerName) {
this.name = name;
this.ownerName = ownerName;
}
}
List<DogDto> dogs = dogRepository.findAll().project(DogDto.class).list();
4. N+1 问题:在循环中进行 Lazy 访问
问题:在循环中访问 LAZY 关系会触发 N 次查询。
检测:
// BAD: Each iteration triggers a query
List<Person> persons = personRepository.listAll();
for (Person p : persons) {
p.address.city; // N+1 queries!
}
重构:
// Option 1: Use @BatchSize on relationship
@OneToMany(fetch = FetchType.LAZY)
@BatchSize(size = 25)
public List<Order> orders;
// Option 2: JOIN FETCH in query through Repository
List<Person> persons = personRepository.find(
"FROM Person p LEFT JOIN FETCH p.address"
).list();
// Option 3: Use EntityGraph in Repository method
public List<Person> findAllWithAddress() {
return find("FROM Person p").withHint("javax.persistence.fetchgraph", "person-with-address").list();
}
5. N+1 问题:Eager 关系缺少 JOIN FETCH
问题:对包含 EAGER 关系的实体执行查询时,会额外产生 N 次查询。
检测:
// Entity has @ManyToOne(fetch = FetchType.EAGER)
// Query doesn't use JOIN FETCH
List<Dog> dogs = dogRepository.list("breed", "Labrador"); // N+1!
重构:
// GOOD: Explicit JOIN FETCH
List<Dog> dogs = dogRepository.find(
"FROM Dog d JOIN FETCH d.owner WHERE d.breed = ?1",
"Labrador"
).list();
6. 使用 List 的单向 @OneToMany
问题:添加或移除元素会导致 DELETE ALL + N 次 INSERT。
检测:
// BAD: Unidirectional with List
@Entity
public class Person { // Standard Entity (no PanacheEntity)
@Id @GeneratedValue public Long id;
@OneToMany(cascade = CascadeType.ALL)
public List<Order> orders = new ArrayList<>();
}
重构:
// Option 1: Make bidirectional
@Entity
public class Person {
@Id @GeneratedValue public Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
public List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id @GeneratedValue public Long id;
@ManyToOne(fetch = FetchType.LAZY)
public Person person;
}
// Option 2: Use Set (single INSERT per operation)
@OneToMany(cascade = CascadeType.ALL)
public Set<Order> orders = new HashSet<>();
7. 未关闭流
问题:未关闭的流会占用数据库连接。
检测:
// BAD: Stream not closed, leaks connection
Stream<Person> persons = personRepository.streamAll();
persons.map(p -> p.name).collect(toList());
重构:
// GOOD: Use try-with-resources
try (Stream<Person> persons = personRepository.streamAll()) {
return persons.map(p -> p.name).collect(toList());
}
8. 忽略 Panache 查询方法
问题:明明有 Panache 快捷方法,却编写冗长的 HQL。
检测:
// BAD: Verbose HQL for simple queries
personRepository.find("SELECT p FROM Person p WHERE p.status = ?1", Status.Alive);
重构:
// GOOD: Panache simplified query
personRepository.find("status", Status.Alive);
// GOOD: Multiple parameters
personRepository.find("status = ?1 and name = ?2", Status.Alive, "John");
// GOOD: Named parameters
personRepository.find("status = :status",
Parameters.with("status", Status.Alive));
快速参考表
| 异味 | 检测 | 修复 |
|---|---|---|
| 类级别 Eager | FetchType.EAGER 或 @*ToOne 缺少 LAZY | 添加 FetchType.LAZY |
| 未分页 | 对大型表调用 listAll() / streamAll() | 使用 repo.findAll().page() |
| 无投影 | 只读时获取完整实体 | 使用 .project(Dto.class) |
| 循环中的 N+1 | 在 for 循环中访问 LAZY | 使用 @BatchSize 或 JOIN FETCH |
| Eager 导致的 N+1 | 查询 EAGER 关系时未使用 FETCH | 在 repo 中添加 JOIN FETCH |
| 单向 List | @OneToMany 未使用 mappedBy 且类型为 List | 使用 Set 或双向关系 |
| 未关闭流 | 使用 stream() 时未使用 try-with-resources | 使用 try() 包裹 |
| 冗长的 HQL | 简单查询却使用完整的 SELECT | 使用 Panache 快捷方法 |
| 缺少反射配置 | DTO 未使用 @RegisterForReflection | 添加注解 |
Panache 专属最佳实践
- 使用 Repository 模式(
PanacheRepository),将数据访问逻辑与实体模型分离。 - 对 Repository 使用
@ApplicationScoped。 - 需要分页或投影时优先使用
find(),而非list()。 - 全局配置抓取批次大小:
quarkus.hibernate-orm.fetch.batch-size=25 - 在服务层或 Repository 层的写操作上使用
@Transactional。
评论
支持 Markdown