mcpField
Q
quarkus-panache-smells

quarkus-panache-smells

github.com/emvnuel开发242 次安装10 周前更新
代码仓库

关于 quarkus-panache-smells

用于检测 Quarkus Panache Repository 应用中常见 ORM 反模式的技能,并给出重构建议。覆盖查询效率、关联加载和资源管理等问题。

主要功能

  1. 01检测 EAGER 关联加载
  2. 02识别未分页的全表查询
  3. 03发现缺少投影的只读查询
  4. 04检查 N+1 查询问题
  5. 05发现未关闭的流
  6. 06建议 Panache 查询简写

适用场景

  1. 01检查 Repository 中的 ORM 代码异味
  2. 02优化大型表查询与分页
  3. 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));

快速参考表

异味检测修复
类级别 EagerFetchType.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 专属最佳实践

  1. 使用 Repository 模式(PanacheRepository),将数据访问逻辑与实体模型分离。
  2. 对 Repository 使用 @ApplicationScoped。
  3. 需要分页或投影时优先使用 find(),而非 list()。
  4. 全局配置抓取批次大小:quarkus.hibernate-orm.fetch.batch-size=25
  5. 在服务层或 Repository 层的写操作上使用 @Transactional。

评论

支持 Markdown