はじめに
JPAで出荷状態を保存し、アプリケーションから再読込するとSHIPPEDを正しく返します。それでも、DBを直接照会する帳票や外部連携では状態コードが一致しないことがあります。原因は、JPAの再読込だけを確認し、物理列に何が保存されているかを契約として検証していない点です。
この記事では、ShipmentStatus.SHIPPEDを保存したとき、shipment.status列にも文字列"SHIPPED"を保存する契約を、Spring Data JPAとH2の再現教材で確認します。@Enumeratedを明示しないenumフィールドは、@EnumeratedValueを持たない場合に既定でEnumType.ORDINALとなります。Jakarta Persistence APIのEnumerated
問題を最小再現する
出荷エンティティの状態は、次のように保存されていました。
@Entity
public class Shipment {
private ShipmentStatus status;
}
ShipmentStatusは次の固定値です。
public enum ShipmentStatus {
READY,
SHIPPED
}
この実装では、JPAを通した再読込は成功します。しかし、外部照会に使うstatus列の値は文字列ではなく整数になります。
| 観測点 | 期待 | バグ状態 |
|---|---|---|
| JPAで再読込した状態 | SHIPPED |
SHIPPED |
JDBCで読むshipment.status
|
"SHIPPED" |
"1" |
| 外部照会用の文字列コード | 一致する | 一致しない |
失敗する契約テストでは、JPAの再読込値とJDBCで読む列値を別々に確認します。
assertAll(
() -> assertEquals(ShipmentStatus.SHIPPED, reloadedStatus),
() -> assertEquals("SHIPPED", storedStatus)
);
バグ状態では二つ目だけが失敗し、expected: <SHIPPED> but was: <1>と観測できます。これにより、保存自体の失敗ではなく、保存形式の不一致へ原因を絞れます。
原因を直接観測する
ShipmentStatus.SHIPPED.ordinal()は1です。別の出荷を保存してからJDBCで物理列を読むと、保存値はこの序数と一致しました。
assertEquals(
ShipmentStatus.SHIPPED.ordinal(),
storedStatus
);
EnumType.ORDINALはenumを整数として保存し、EnumType.STRINGは文字列として保存します。Jakarta Persistence APIのEnumType @Enumeratedの既定値もORDINALです。つまり、JPAの変換が成功しているために問題が隠れているのではなく、enum保存形式を明示していないことが直接原因です。
最小修正
対象フィールドに保存形式を一つ追加します。
@Enumerated(EnumType.STRING)
private ShipmentStatus status;
この修正後、JPAの再読込とJDBCで読む物理列はどちらもSHIPPEDを表します。アプリケーションのDTO、外部HTTP連携、全enumの一括設定を変更する必要はありません。
再発を防ぐ確認
再発防止には、ORMを通した再読込だけでなく、今回の契約に対応する物理列または外部出力を独立して確認します。本教材では、次の二つのテストを残します。
-
ShipmentRepositoryTestは、JPAの再読込値とJDBCの物理列がともにSHIPPEDであることを検証します。 -
EnumStorageObservationTestは、STRING指定で物理列へ列挙名が保存されることを直接検証します。
この例は新規に保存する単一enumフィールドの形式に限定しています。既存整数データの移行、独自コードを持つenum、AttributeConverter、DBネイティブenum、enum名変更時の互換性は、別途設計・検証が必要です。
実行できる教材
バグ状態、直接観測、最小修正、回帰テスト、実行証跡は、公開されているSpring Data JPA enum保存形式デバッグ教材にあります。バグコミット215fb8cでは契約テストの失敗を、修正コミット10ce3ba以降では全テストの成功を確認できます。
まとめ
JPAのenumを保存するとき、アプリケーションで再読込できることと、DBの物理値が外部契約に適合することは別の確認項目です。@Enumeratedを省略すると既定のORDINAL保存が選ばれるため、文字列コードを必要とする場合は@Enumerated(EnumType.STRING)を明示します。再読込と物理列を分けてテストすれば、ORMの内部変換で隠れる不一致を回帰テストで検出できます。