Quarkus Panache Repository Code Smells Detection
Identify and fix common ORM anti-patterns in Quarkus Panache applications using the Repository pattern.
Code Smell Categories
- 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;
- 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();
- 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();
- 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(); }
- 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();
- 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<>();
- 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()); }
- 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 @ApplicationScoped for your repositories.
-
Use find() over list() when you need pagination/projection.
-
Configure fetch batch size globally: quarkus.hibernate-orm.fetch.batch-size=25
-
Use @Transactional on write operations in your service or repository layer.