JSqlParserの基本

JSqlParser の CCJSqlParserUtil.parse() はSQL文字列を受け取り、 Statement(文のAST)を返します。 返ってくる型を instanceof で分岐して SELECT/INSERT/UPDATE/DELETE を判別します。

Java — JSqlParserの基本使用
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.delete.Delete;

String sql = "SELECT id, name FROM users WHERE active = 1";

Statement stmt = CCJSqlParserUtil.parse(sql);

if (stmt instanceof Select)  System.out.println("READ");
if (stmt instanceof Insert)  System.out.println("CREATE");
if (stmt instanceof Update)  System.out.println("UPDATE");
if (stmt instanceof Delete)  System.out.println("DELETE")

操作種別の判定

CRUDJSqlParser型SQL例
CreateInsertINSERT INTO orders ...
ReadSelectSELECT * FROM orders ...
UpdateUpdateUPDATE orders SET ...
DeleteDeleteDELETE FROM orders ...

テーブル名の取得

各 Statement 型からテーブル名を取得する方法は型によって異なります。 TablesNamesFinder ユーティリティを使うと型を意識せず全テーブル名を取得できます。

Java — TablesNamesFinderでテーブル名取得
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;

import java.util.List;

public class TableExtractor {

    public List<String> getTableNames(String sql) throws Exception {
        Statement stmt = CCJSqlParserUtil.parse(sql);
        TablesNamesFinder finder = new TablesNamesFinder();
        return finder.getTableList(stmt);
    }
}

// 使用例
TableExtractor te = new TableExtractor();

// JOINを含むSQLでも全テーブルを返す
List<String> tables = te.getTableNames(
    "SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id"
);
// → ["users", "orders"]

// サブクエリのテーブルも含む
List<String> tables2 = te.getTableNames(
    "SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE active=1)"
);
// → ["orders", "users"]
TablesNamesFinder の動作確認
SQL: SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id
テーブル: [users, orders]

SQL: DELETE FROM order_items WHERE order_id = ?
テーブル: [order_items]

JOIN・サブクエリへの対応

TablesNamesFinder はサブクエリ・CTE・UNION内のテーブルも再帰的に収集します。 ただし操作種別の割り当てには注意が必要です。 JOIN 先のテーブルは SELECT で参照されているため R ですが、 UPDATE ... SET ... FROM join_table のように JOIN を伴う UPDATE の場合、 主テーブルは U、結合テーブルは R と分ける必要があります。

Java — 操作種別を考慮したテーブル分類
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.*;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.util.TablesNamesFinder;

import java.util.*;

public class CrudClassifier {

    public enum CrudOp { C, R, U, D }

    /**
     * SQL文字列から テーブル名 → 操作種別セット のマップを返す
     */
    public Map<String, Set<CrudOp>> classify(String sql) throws Exception {
        Statement stmt = CCJSqlParserUtil.parse(sql);
        Map<String, Set<CrudOp>> result = new LinkedHashMap<>();

        if (stmt instanceof Select) {
            // SELECTは全テーブルをR扱い
            allTables(stmt).forEach(t -> add(result, t, CrudOp.R));

        } else if (stmt instanceof Insert ins) {
            // 主テーブルはC
            add(result, ins.getTable().getName(), CrudOp.C);
            // SELECT部分(INSERT INTO ... SELECT)はR
            if (ins.getSelect() != null) {
                new TablesNamesFinder()
                    .getTableList(ins.getSelect())
                    .forEach(t -> add(result, t, CrudOp.R));
            }

        } else if (stmt instanceof Update upd) {
            // 主テーブルはU
            add(result, upd.getTable().getName(), CrudOp.U);
            // FROM句のJOINテーブルはR
            if (upd.getFromItem() != null) {
                new TablesNamesFinder()
                    .getTableList(upd.getFromItem())
                    .forEach(t -> add(result, t, CrudOp.R));
            }

        } else if (stmt instanceof Delete del) {
            // 主テーブルはD
            add(result, del.getTable().getName(), CrudOp.D);
            // JOINはR
            if (del.getJoins() != null) {
                del.getJoins().forEach(j ->
                    add(result, j.getRightItem().toString(), CrudOp.R));
            }
        }
        return result;
    }

    private List<String> allTables(Statement stmt) throws Exception {
        return new TablesNamesFinder().getTableList(stmt);
    }

    private void add(Map<String, Set<CrudOp>> map, String table, CrudOp op) {
        map.computeIfAbsent(table.toLowerCase(), k -> new LinkedHashSet<>()).add(op);
    }
}

CRUD結果データクラス

解析結果を保持するシンプルなデータクラスを定義します。

Java — CrudEntry データクラス
import java.util.*;

/** 1件分のCRUD情報 */
public record CrudEntry(
    String className,          // 発見元クラス名
    String tableName,          // テーブル名
    Set<CrudClassifier.CrudOp> operations  // C/R/U/D の集合
) {}

// 使用例
CrudClassifier classifier = new CrudClassifier();
Map<String, Set<CrudClassifier.CrudOp>> result = classifier.classify(
    "SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id"
);

result.forEach((table, ops) -> {
    System.out.println(table + " → " + ops);
});
// users  → [R]
// orders → [R]

パースエラーのハンドリング

JSqlParser が対応していない方言や、SQL以外の文字列が混入した場合にパース例外が発生します。 これらは静かに読み飛ばし、ログに記録するのが実用的です。

Java — エラーハンドリング
public Optional<Map<String, Set<CrudClassifier.CrudOp>>> safeClassify(
        String sql, String sourceClass) {
    try {
        return Optional.of(new CrudClassifier().classify(sql));
    } catch (Exception e) {
        // SQL以外の文字列やJSqlParser非対応構文は読み飛ばす
        System.err.printf("[SKIP] %s — %s%n", sourceClass, e.getMessage());
        return Optional.empty();
    }
}

次の章では…

PART 05 では PART 03・04 の結果をクラス横断で集約し、最終的なCRUD行列データ構造を構築する実装を解説します。

→ PART 05 — CRUD行列の構築へ