quarkus-panache-smells について
Quarkus Panache Repository アプリケーションの一般的な ORM アンチパターンを検出し、リファクタリング案を示すスキルです。クエリ効率、関連の読み込み、リソース管理を扱います。
主な機能
- 01EAGER な関連読み込みの検出
- 02ページングなしの全件クエリの指摘
- 03読み取り専用クエリの投影不足を検出
- 04N+1 クエリの確認
- 05未クローズのストリームを検出
- 06Panache クエリ簡略記法の提案
活用シーン
- 01Repository 内の ORM コードの確認
- 02大規模テーブルのクエリとページングの改善
- 03関連アクセスによる追加クエリの削減
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ emvnuel/ skills --skill quarkus-panache-smellsインストール後、Claude Code で/quarkus-panache-smellsを入力して読み込まれたか確認します。
詳細
Quarkus Panache リポジトリパターンのコードスメル検出
リポジトリパターンを使用する Quarkus Panache アプリケーションで、よくある ORM のアンチパターンを特定して修正します。
コードスメルのカテゴリ
1. クラスレベルでの Eager フェッチ
問題:関連エンティティに対して 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 関係にアクセスすると、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 のないクエリ | リポジトリに JOIN FETCH を追加 |
| 単方向の List | mappedBy のない @OneToMany と List | Set または双方向の関係を使用 |
| ストリームを閉じない | try-with-resources を使わない stream() | try() で囲む |
| 冗長な HQL | 単純なクエリに完全な SELECT を使用 | Panache の簡略メソッドを使用 |
| リフレクションの設定漏れ | @RegisterForReflection のない DTO | アノテーションを追加 |
Panache 固有のベストプラクティス
- リポジトリパターン(
PanacheRepository)を使用して、データアクセスロジックをエンティティモデルから分離します。 - リポジトリには
@ApplicationScopedを使用します。 - ページネーションやプロジェクションが必要な場合は、
list()よりfind()を使用します。 - フェッチのバッチサイズをグローバルに設定します:
quarkus.hibernate-orm.fetch.batch-size=25 - サービス層またはリポジトリ層で、書き込み操作に
@Transactionalを使用します。
コメント
Markdown 対応