| name | quarkus-panache-smells |
| description | Detects and refactors ORM code smells in Quarkus Panache applications using the Repository pattern. Use when reviewing PanacheRepository code, diagnosing N+1 queries, data overfetching, or pagination issues. |
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
| Smell | Detection | Fix |
|---|---|---|
| Eager class-level | FetchType.EAGER or missing LAZY on @*ToOne |
Add FetchType.LAZY |
| No pagination | listAll() / streamAll() on large tables |
Use repo.findAll().page() |
| No projection | Full entity for read-only | Use .project(Dto.class) |
| N+1 in loop | LAZY access in for loop |
@BatchSize or JOIN FETCH |
| N+1 Eager | Query without FETCH for EAGER relations | Add JOIN FETCH in repo |
| Unidirectional List | @OneToMany without mappedBy + List |
Use Set or bidirectional |
| Unclosed stream | stream() without try-with-resources |
Wrap in try() |
| Verbose HQL | Full SELECT for simple queries |
Use Panache shortcuts |
| Missing reflection | DTO without @RegisterForReflection |
Add annotation |
Panache-Specific Best Practices
- Use Repository Pattern (
PanacheRepository) to separate data access logic from the entity model. - Use
@ApplicationScopedfor your repositories. - Use
find()overlist()when you need pagination/projection. - Configure fetch batch size globally:
quarkus.hibernate-orm.fetch.batch-size=25 - Use
@Transactionalon write operations in your service or repository layer.