データ構造の設計

CRUD行列の中核データ構造は次の入れ子 Map です。

Java — 行列データ型
// クラス名 → (テーブル名 → CRUD操作セット)
Map<String, Map<String, Set<CrudClassifier.CrudOp>>> matrix;

外側の Map のキーがクラス名(行)、 内側の Map のキーがテーブル名(列)、 値が操作種別のセット(C/R/U/D)です。 複数の SQL がひとつのクラスから同じテーブルを参照しても、 Set に追加するだけで自動的にマージされます。

CrudMatrixAggregator の実装

Java — CrudMatrixAggregator
import java.util.*;

public class CrudMatrixAggregator {

    // クラス名 → テーブル名 → CRUD操作セット
    private final Map<String, Map<String, Set<CrudClassifier.CrudOp>>> matrix
        = new TreeMap<>();  // クラス名でソート

    /**
     * 1クラス分の解析結果を行列に追加する
     *
     * @param className  クラス名(FQCNまたは単純名)
     * @param tableOps   テーブル名 → 操作セットのマップ(CrudClassifier.classify()の戻り値)
     */
    public void add(String className,
                    Map<String, Set<CrudClassifier.CrudOp>> tableOps) {
        Map<String, Set<CrudClassifier.CrudOp>> row =
            matrix.computeIfAbsent(className, k -> new TreeMap<>());

        tableOps.forEach((table, ops) ->
            row.computeIfAbsent(table, k -> new LinkedHashSet<>()).addAll(ops)
        );
    }

    /** 構築済み行列を返す(読み取り専用ビュー) */
    public Map<String, Map<String, Set<CrudClassifier.CrudOp>>> getMatrix() {
        return Collections.unmodifiableMap(matrix);
    }

    /** 行列に登場する全テーブル名をソートして返す */
    public List<String> getAllTables() {
        return matrix.values().stream()
            .flatMap(row -> row.keySet().stream())
            .distinct()
            .sorted()
            .toList();
    }
}

解析パイプライン統合

ProjectScannerSqlExtractorCrudClassifierCrudMatrixAggregator を繋いで、プロジェクト全体のCRUD行列を構築するメインクラスです。

Java — パイプライン統合
import java.nio.file.*;
import java.util.*;

public class CrudMatrixBuilder {

    public CrudMatrixAggregator build(Path srcRoot) throws Exception {
        SqlExtractor extractor = new SqlExtractor();
        CrudClassifier classifier = new CrudClassifier();
        CrudMatrixAggregator aggregator = new CrudMatrixAggregator();

        try (var stream = Files.walk(srcRoot)) {
            stream.filter(p -> p.toString().endsWith(".java"))
                  .forEach(javaFile -> {
                      try {
                          String className = resolveClassName(javaFile, srcRoot);
                          List<String> sqls = extractor.extract(javaFile);

                          for (String sql : sqls) {
                              try {
                                  Map<String, Set<CrudClassifier.CrudOp>> ops =
                                      classifier.classify(sql);
                                  if (!ops.isEmpty()) {
                                      aggregator.add(className, ops);
                                  }
                              } catch (Exception e) {
                                  // パース失敗は読み飛ばし
                              }
                          }
                      } catch (Exception e) {
                          System.err.println("Skip: " + javaFile);
                      }
                  });
        }
        return aggregator;
    }

    /** ソースルートからの相対パスを Java FQCN に変換 */
    private String resolveClassName(Path file, Path srcRoot) {
        String rel = srcRoot.relativize(file).toString();
        return rel.replace(FileSystems.getDefault().getSeparator(), ".")
                  .replace(".java", "");
    }
}

クラス名の正確な取得

上記の resolveClassName() はパス文字列からFQCNを求める簡易実装です。 JavaParser を使ってパッケージ宣言とクラス定義名を取得するより正確な方法も示します。

Java — JavaParserでFQCN取得
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;

public String getFqcn(Path javaFile) throws Exception {
    CompilationUnit cu = StaticJavaParser.parse(javaFile);

    String pkg = cu.getPackageDeclaration()
        .map(pd -> pd.getNameAsString() + ".")
        .orElse("");

    String cls = cu.getTypes().isEmpty()
        ? javaFile.getFileName().toString().replace(".java", "")
        : cu.getType(0).getNameAsString();

    return pkg + cls;
}
// 例: "com.example.service.OrderService"

テーブル・クラスのソート

出力時の見やすさのため、テーブルはアルファベット順、クラスはパッケージ→クラス名の順で並べます。 TreeMap を使うと自動ソートされますが、カスタム比較も可能です。

構築済み行列のデバッグ出力例
com.example.repository.OrderRepository
  order_items → [C, R, D]
  orders      → [C, R, U]

com.example.repository.UserRepository
  users       → [C, R, U]

com.example.service.ReportService
  order_items → [R]
  orders      → [R]
  users       → [R]

次の章では…

PART 06 では構築済みの行列を Markdownテーブル・Mermaid・PlantUML 形式で出力するコードを実装します。

→ PART 06 — CRUD図の出力へ