概要
Enum を DB カラムや JSON フィールドに保存する場面は業務システムで頻繁に発生します。このとき ordinal() をそのまま整数カラムに入れるコードを見かけることがありますが、これは Enum の定義順を変更しただけで既存データが壊れるという重大なリスクを抱えています。name() による保存は ordinal よりは安全ですが、要素名のリファクタリングで非互換が生じます。もっとも堅牢なのは、Enum に明示的なコード値フィールドを持たせ、そのコード値で保存・復元する方法です。この記事では、ordinal がなぜ危険なのかを具体例で示したうえで、明示的コード値 + fromDbCode メソッドによる永続化パターンを実装します。Java 標準のシリアライズにおける Enum のシングルトン保証についても触れ、DB 保存・JSON 変換・Java シリアライズそれぞれの注意点を整理します。
使いどころ
注文ステータスを DB の VARCHAR カラムに保存する際、ordinal ではなく明示的なコード値(PEND, PROC 等)を使って安全に永続化する
REST API のレスポンス JSON に Enum のコード値を含める際、name() ではなく業務で定義されたコード値を返すよう設計する
既存テーブルの数値コードカラムと Enum を対応づける fromDbCode メソッドを実装し、DAO 層での変換を一元化する
コード例
import java.util.Arrays;
public class EnumSerializeExample {
// 明示的なコード値で永続化する Enum
enum OrderStatus {
PENDING("PEND"),
PROCESSING("PROC"),
COMPLETED("COMP"),
CANCELLED("CANC");
private final String dbCode;
OrderStatus(String dbCode) {
this.dbCode = dbCode;
}
public String getDbCode() { return dbCode; }
// コード値から Enum に復元
public static OrderStatus fromDbCode(String code) {
return Arrays.stream(values())
.filter(s -> s.dbCode.equals(code))
.findFirst()
.orElseThrow(() ->
new IllegalArgumentException("不明なコード: " + code));
}
}
// record でデータクラスを定義(Java 16+)
record Order(String orderId, OrderStatus status) {
@Override
public String toString() {
return "Order{id='" + orderId + "', status="
+ status + "('" + status.getDbCode() + "')}";
}
}
public static void main(String[] args) {
// DB 保存イメージ: Enum → コード値
var status = OrderStatus.PROCESSING;
var dbValue = status.getDbCode(); // "PROC"
System.out.println("DB保存値: " + dbValue);
// DB 読込イメージ: コード値 → Enum
var restored = OrderStatus.fromDbCode(dbValue);
System.out.println("DB復元: " + restored);
// name() による保存(ordinal より安全だがリネームに弱い)
var nameValue = status.name(); // "PROCESSING"
var byName = OrderStatus.valueOf(nameValue);
System.out.println("name() → valueOf(): " + byName);
// ordinal の危険性: 定義順を変えると壊れる
for (var s : OrderStatus.values()) {
System.out.println(s.name()
+ " ordinal=" + s.ordinal()
+ " dbCode=" + s.getDbCode());
}
// record と組み合わせた使用例
var order = new Order("ORD-001", OrderStatus.COMPLETED);
System.out.println(order);
}
}Version Coverage
fromDbCode を Arrays.stream で簡潔に書ける。record を使って Order を定義すれば、equals/hashCode/toString が自動生成される。
// Java 17: Stream + record で簡潔に
public static OrderStatus fromDbCode(String code) {
return Arrays.stream(values())
.filter(s -> s.dbCode.equals(code))
.findFirst()
.orElseThrow(() ->
new IllegalArgumentException("不明なコード: " + code));
}
record Order(String orderId, OrderStatus status)
implements Serializable { }Library Comparison
注意点
ordinal() は定義順に連動するため、要素の挿入・並び替えで過去データとの対応が壊れる。本番データが入った後では修正コストが非常に大きい
name() による保存は要素名の変更(リネーム)で非互換になる。ただし ordinal よりは安全なので、コード値設計の余裕がない場合は name() を選ぶ
fromDbCode で一致しないコードが来た場合は IllegalArgumentException を投げるのが基本。null や空文字の入力も考慮すること
Java 標準のシリアライズ(ObjectOutputStream)では Enum はシングルトンが保証されるが、Enum フィールドの追加・削除は serialVersionUID に影響しない点に注意
FAQ
明示的コード値が最も安全です。name() はリファクタリングに弱く、ordinal は定義順変更に弱い。業務で使うコード体系がすでにある場合はそれを Enum に持たせてください。
Java 標準シリアライズでは Enum のシングルトンが保証されるため、デシリアライズ後も == で正しく比較できます。ただし JSON からの変換は valueOf を経由するので同様に安全です。
移行バッチで既存データの ordinal をコード値に変換し、カラム型を変更します。Enum の定義順を絶対に変えないまま移行を完了させてください。移行完了まで定義順の凍結が必要です。