mcpField
Q
quarkus-panache-smells

quarkus-panache-smells

github.com/emvnuelDevelopment242 installsUpdated 10w ago
Repository

About quarkus-panache-smells

A skill for detecting common ORM anti-patterns in Quarkus Panache Repository applications and suggesting refactorings. It covers query efficiency, relationship fetching, and resource handling.

Key features

  1. 01Detect eager relationship loading
  2. 02Flag unpaginated full-table queries
  3. 03Find missing projections in read-only queries
  4. 04Check for N+1 queries
  5. 05Detect streams that are not closed
  6. 06Suggest Panache query shortcuts

Use cases

  1. 01Review ORM code in repositories
  2. 02Improve queries and pagination on large tables
  3. 03Reduce extra queries from relationship access

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/emvnuel/skills --skill quarkus-panache-smells

After installing, type/quarkus-panache-smellsin Claude Code to confirm it loaded.

Details

From the project README

Quarkus Panache Repository Code Smells Detection

Identify and fix common ORM anti-patterns in Quarkus Panache applications using the Repository pattern.

Code Smell Categories

1. Eager Fetching at Class Level

Problem: FetchType.EAGER on relationships always loads related entities.

Detection:

// 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;

Refactoring:

// GOOD: Explicitly LAZY
@ManyToOne(fetch = FetchType.LAZY)
public Person owner;

2. Using listAll() Without Pagination

Problem: listAll() or streamAll() on large tables causes memory issues.

Detection:

// BAD: Loads entire table into memory
List<Person> all = personRepository.listAll();

// BAD: Even with stream, still fetches all
Stream<Person> stream = personRepository.streamAll();

Refactoring:

// 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. Missing Projections for Read-Only Queries

Problem: Fetching entire entities when only specific fields are needed.

Detection:

// BAD: Loads all columns including BLOBs
List<Person> persons = personRepository.list("status", Status.Alive);
// Then only uses person.name

Refactoring:

// 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();

With related entity fields:

@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 Problem: Lazy Access in Loop

Problem: Accessing LAZY relationships inside loops triggers N queries.

Detection:

// BAD: Each iteration triggers a query
List<Person> persons = personRepository.listAll();
for (Person p : persons) {
    p.address.city;  // N+1 queries!
}

Refactoring:

// 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 Problem: Missing JOIN FETCH for Eager Relations

Problem: Queries on entities with EAGER relationships generate N additional queries.

Detection:

// Entity has @ManyToOne(fetch = FetchType.EAGER)
// Query doesn't use JOIN FETCH
List<Dog> dogs = dogRepository.list("breed", "Labrador");  // N+1!

Refactoring:

// GOOD: Explicit JOIN FETCH
List<Dog> dogs = dogRepository.find(
    "FROM Dog d JOIN FETCH d.owner WHERE d.breed = ?1",
    "Labrador"
).list();

6. Unidirectional @OneToMany with List

Problem: Adding/removing elements causes DELETE ALL + N INSERTs.

Detection:

// 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<>();
}

Refactoring:

// 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. Not Closing Streams

Problem: Unclosed streams hold database connections.

Detection:

// BAD: Stream not closed, leaks connection
Stream<Person> persons = personRepository.streamAll();
persons.map(p -> p.name).collect(toList());

Refactoring:

// GOOD: Use try-with-resources
try (Stream<Person> persons = personRepository.streamAll()) {
    return persons.map(p -> p.name).collect(toList());
}

8. Ignoring Panache Query Methods

Problem: Writing verbose HQL when Panache shortcuts exist.

Detection:

// BAD: Verbose HQL for simple queries
personRepository.find("SELECT p FROM Person p WHERE p.status = ?1", Status.Alive);

Refactoring:

// 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));

Quick Reference Table

SmellDetectionFix
Eager class-levelFetchType.EAGER or missing LAZY on @*ToOneAdd FetchType.LAZY
No paginationlistAll() / streamAll() on large tablesUse repo.findAll().page()
No projectionFull entity for read-onlyUse .project(Dto.class)
N+1 in loopLAZY access in for loop@BatchSize or JOIN FETCH
N+1 EagerQuery without FETCH for EAGER relationsAdd JOIN FETCH in repo
Unidirectional List@OneToMany without mappedBy + ListUse Set or bidirectional
Unclosed streamstream() without try-with-resourcesWrap in try()
Verbose HQLFull SELECT for simple queriesUse Panache shortcuts
Missing reflectionDTO without @RegisterForReflectionAdd annotation

Panache-Specific Best Practices

  1. Use Repository Pattern (PanacheRepository) to separate data access logic from the entity model.
  2. Use @ApplicationScoped for your repositories.
  3. Use find() over list() when you need pagination/projection.
  4. Configure fetch batch size globally: quarkus.hibernate-orm.fetch.batch-size=25
  5. Use @Transactional on write operations in your service or repository layer.

Comments

Markdown supported