mcpField
Q
quarkus-panache-smells

quarkus-panache-smells

github.com/emvnuel開発242 回インストール10 週間前に更新
リポジトリ

quarkus-panache-smells について

Quarkus Panache Repository アプリケーションの一般的な ORM アンチパターンを検出し、リファクタリング案を示すスキルです。クエリ効率、関連の読み込み、リソース管理を扱います。

主な機能

  1. 01EAGER な関連読み込みの検出
  2. 02ページングなしの全件クエリの指摘
  3. 03読み取り専用クエリの投影不足を検出
  4. 04N+1 クエリの確認
  5. 05未クローズのストリームを検出
  6. 06Panache クエリ簡略記法の提案

活用シーン

  1. 01Repository 内の ORM コードの確認
  2. 02大規模テーブルのクエリとページングの改善
  3. 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));

クイックリファレンステーブル

スメル検出修正
クラスレベルの EagerFetchType.EAGER または @*ToOne で LAZY が未指定FetchType.LAZY を追加
ページネーションなし大きなテーブルでの listAll() / streamAll()repo.findAll().page() を使用
プロジェクションなし読み取り専用でエンティティ全体を取得.project(Dto.class) を使用
ループ内の N+1for ループ内での LAZY アクセス@BatchSize または JOIN FETCH を使用
Eager による N+1EAGER 関係に対して FETCH のないクエリリポジトリに JOIN FETCH を追加
単方向の ListmappedBy のない @OneToMany と ListSet または双方向の関係を使用
ストリームを閉じないtry-with-resources を使わない stream()try() で囲む
冗長な HQL単純なクエリに完全な SELECT を使用Panache の簡略メソッドを使用
リフレクションの設定漏れ@RegisterForReflection のない DTOアノテーションを追加

Panache 固有のベストプラクティス

  1. リポジトリパターン(PanacheRepository)を使用して、データアクセスロジックをエンティティモデルから分離します。
  2. リポジトリには @ApplicationScoped を使用します。
  3. ページネーションやプロジェクションが必要な場合は、list() より find() を使用します。
  4. フェッチのバッチサイズをグローバルに設定します:quarkus.hibernate-orm.fetch.batch-size=25
  5. サービス層またはリポジトリ層で、書き込み操作に @Transactional を使用します。

コメント