データ構造の設計
CRUD行列の中核データ構造は次の入れ子 Map です。
// クラス名 → (テーブル名 → CRUD操作セット)
Map<String, Map<String, Set<CrudClassifier.CrudOp>>> matrix;
外側の Map のキーがクラス名(行)、
内側の Map のキーがテーブル名(列)、
値が操作種別のセット(C/R/U/D)です。
複数の SQL がひとつのクラスから同じテーブルを参照しても、
Set に追加するだけで自動的にマージされます。
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();
}
}
解析パイプライン統合
ProjectScanner・SqlExtractor・CrudClassifier・CrudMatrixAggregator
を繋いで、プロジェクト全体のCRUD行列を構築するメインクラスです。
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 を使ってパッケージ宣言とクラス定義名を取得するより正確な方法も示します。
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]