Spring JPAを使用したアプリケーション開発において、AttributeConverterはエンティティ属性とデータベースカラム間の型変換を柔軟に制御するための重要なインターフェースです。本記事では、@ConverterアノテーションとそのautoApply設定、Enum変換パターン(文字列/数値)、JSON型カラムへのオブジェクト保存、暗号化フィールドの実装、Java 8日時型の変換(レガシーシステム対応)まで、AttributeConverterの実践的な活用方法を網羅的に解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Java |
17以上 |
| Spring Boot |
3.4.x |
| Spring Data JPA |
3.4.x(Spring Boot Starterに含まれる) |
| Hibernate |
6.6.x(Spring Data JPAに含まれる) |
| Jakarta Persistence |
3.2 |
| データベース |
H2 Database / PostgreSQL / MySQL |
| ビルドツール |
Maven または Gradle |
| IDE |
VS Code または IntelliJ IDEA |
事前に以下の準備を完了してください。
- JDK 17以上のインストール
- Spring Boot + Spring Data JPAプロジェクトの基本構成
- JPAエンティティとリポジトリの基本知識
AttributeConverterインターフェースの基本構造#
AttributeConverterは、Jakarta Persistence(旧JPA)で定義されたインターフェースで、エンティティの属性とデータベースカラムの間で双方向の型変換を行います。
インターフェースの定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package jakarta.persistence;
public interface AttributeConverter<X, Y> {
/**
* エンティティ属性値をデータベース格納用の値に変換する
* @param attribute エンティティ属性値
* @return データベースに格納する値
*/
Y convertToDatabaseColumn(X attribute);
/**
* データベースの値をエンティティ属性値に変換する
* @param dbData データベースから取得した値
* @return エンティティ属性値
*/
X convertToEntityAttribute(Y dbData);
}
|
型パラメータの意味は以下の通りです。
| 型パラメータ |
説明 |
X |
エンティティ側の属性型(Javaオブジェクト) |
Y |
データベース側のカラム型(JDBC型) |
変換フローの図解#
flowchart LR
subgraph Entity["エンティティ"]
A["Java属性<br/>(型 X)"]
end
subgraph Converter["AttributeConverter"]
B["convertToDatabaseColumn()"]
C["convertToEntityAttribute()"]
end
subgraph Database["データベース"]
D["カラム値<br/>(型 Y)"]
end
A -->|"INSERT/UPDATE時"| B
B --> D
D -->|"SELECT時"| C
C --> A@Converterアノテーションの設定方法#
@Converterアノテーションは、変換クラスをJPAに登録するために使用します。このアノテーションにはautoApplyという重要な設定があります。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter
public class BooleanToStringConverter implements AttributeConverter<Boolean, String> {
@Override
public String convertToDatabaseColumn(Boolean attribute) {
if (attribute == null) {
return null;
}
return attribute ? "Y" : "N";
}
@Override
public Boolean convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
return "Y".equals(dbData);
}
}
|
autoApply設定の違い#
autoApplyの値によって、変換の適用範囲が変わります。
| 設定 |
動作 |
autoApply = false(デフォルト) |
エンティティで@Convertを明示的に指定した属性にのみ適用 |
autoApply = true |
同じ型のすべての属性に自動適用(一部例外あり) |
autoApply = false の場合#
1
2
3
4
|
@Converter // autoApply = false がデフォルト
public class BooleanToStringConverter implements AttributeConverter<Boolean, String> {
// 実装省略
}
|
エンティティ側で明示的に指定が必要です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Convert(converter = BooleanToStringConverter.class) // 明示的な指定が必要
private Boolean active;
// getter/setter省略
}
|
autoApply = true の場合#
1
2
3
4
|
@Converter(autoApply = true) // すべてのBoolean型属性に自動適用
public class BooleanToStringConverter implements AttributeConverter<Boolean, String> {
// 実装省略
}
|
エンティティ側での指定は不要です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Boolean active; // 自動的にBooleanToStringConverterが適用される
private Boolean verified; // これにも自動適用される
// getter/setter省略
}
|
autoApplyの適用除外#
以下の属性にはautoApply = trueでも自動適用されません。
@Idが付与された属性
@Versionが付与された属性
- リレーション属性(
@OneToMany、@ManyToOneなど)
@Enumeratedが明示的に付与された属性
@Temporalが明示的に付与された属性
特定の属性で自動適用を無効化したい場合は、disableConversionを使用します。
1
2
3
4
5
6
|
@Entity
public class User {
@Convert(disableConversion = true) // この属性では変換を無効化
private Boolean legacyFlag;
}
|
Enumの文字列変換パターン#
Enumをデータベースに保存する際、デフォルトでは序数(ordinal: 0, 1, 2…)または名前(name: 文字列)で保存されますが、独自のコード値で保存したいケースが多くあります。
標準的な@Enumeratedの問題点#
1
2
3
4
5
6
|
public enum OrderStatus {
PENDING, // ordinal = 0
CONFIRMED, // ordinal = 1
SHIPPED, // ordinal = 2
DELIVERED // ordinal = 3
}
|
@Enumerated(EnumType.ORDINAL)を使用すると、Enum定数の順序変更で既存データとの整合性が崩れます。@Enumerated(EnumType.STRING)を使用しても、Enum定数名の変更で問題が発生します。
AttributeConverterによる解決策#
業務コードをEnumに持たせ、AttributeConverterでコード変換を行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
public enum OrderStatus {
PENDING("PND", "保留中"),
CONFIRMED("CNF", "確認済み"),
SHIPPED("SHP", "発送済み"),
DELIVERED("DLV", "配達完了"),
CANCELLED("CAN", "キャンセル");
private final String code;
private final String displayName;
OrderStatus(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
public String getCode() {
return code;
}
public String getDisplayName() {
return displayName;
}
/**
* コード値からEnumを取得する
*/
public static OrderStatus fromCode(String code) {
if (code == null) {
return null;
}
for (OrderStatus status : values()) {
if (status.code.equals(code)) {
return status;
}
}
throw new IllegalArgumentException("Unknown OrderStatus code: " + code);
}
}
|
対応するConverterを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter(autoApply = true)
public class OrderStatusConverter implements AttributeConverter<OrderStatus, String> {
@Override
public String convertToDatabaseColumn(OrderStatus attribute) {
if (attribute == null) {
return null;
}
return attribute.getCode();
}
@Override
public OrderStatus convertToEntityAttribute(String dbData) {
return OrderStatus.fromCode(dbData);
}
}
|
エンティティでの使用例は以下の通りです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", unique = true)
private String orderNumber;
@Column(name = "status", length = 3)
private OrderStatus status; // "PND", "CNF" などのコード値で保存される
@Column(name = "created_at")
private LocalDateTime createdAt;
// getter/setter省略
}
|
データベースには以下のように保存されます。
| id |
order_number |
status |
created_at |
| 1 |
ORD-001 |
PND |
2026-01-13 10:00:00 |
| 2 |
ORD-002 |
CNF |
2026-01-13 11:30:00 |
| 3 |
ORD-003 |
DLV |
2026-01-13 14:00:00 |
Enumの数値変換パターン#
数値コードでEnumを管理したい場合のパターンです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public enum Priority {
LOW(1),
MEDIUM(2),
HIGH(3),
CRITICAL(4);
private final int value;
Priority(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static Priority fromValue(Integer value) {
if (value == null) {
return null;
}
for (Priority priority : values()) {
if (priority.value == value) {
return priority;
}
}
throw new IllegalArgumentException("Unknown Priority value: " + value);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Converter(autoApply = true)
public class PriorityConverter implements AttributeConverter<Priority, Integer> {
@Override
public Integer convertToDatabaseColumn(Priority attribute) {
if (attribute == null) {
return null;
}
return attribute.getValue();
}
@Override
public Priority convertToEntityAttribute(Integer dbData) {
return Priority.fromValue(dbData);
}
}
|
汎用的なEnum Converterの実装#
同様のパターンを複数のEnumに適用する場合、汎用的な基底クラスを作成できます。
1
2
3
4
5
6
|
/**
* コード値を持つEnumのインターフェース
*/
public interface CodedEnum<T> {
T getCode();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public enum PaymentMethod implements CodedEnum<String> {
CREDIT_CARD("CC"),
BANK_TRANSFER("BT"),
CASH_ON_DELIVERY("COD");
private final String code;
PaymentMethod(String code) {
this.code = code;
}
@Override
public String getCode() {
return code;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/**
* CodedEnumの汎用コンバーター基底クラス
*/
public abstract class AbstractCodedEnumConverter<E extends Enum<E> & CodedEnum<C>, C>
implements AttributeConverter<E, C> {
private final Class<E> enumClass;
protected AbstractCodedEnumConverter(Class<E> enumClass) {
this.enumClass = enumClass;
}
@Override
public C convertToDatabaseColumn(E attribute) {
if (attribute == null) {
return null;
}
return attribute.getCode();
}
@Override
public E convertToEntityAttribute(C dbData) {
if (dbData == null) {
return null;
}
for (E enumConstant : enumClass.getEnumConstants()) {
if (enumConstant.getCode().equals(dbData)) {
return enumConstant;
}
}
throw new IllegalArgumentException(
"Unknown code '" + dbData + "' for enum " + enumClass.getSimpleName()
);
}
}
|
1
2
3
4
5
6
7
8
|
@Converter(autoApply = true)
public class PaymentMethodConverter
extends AbstractCodedEnumConverter<PaymentMethod, String> {
public PaymentMethodConverter() {
super(PaymentMethod.class);
}
}
|
JSON型カラムへのオブジェクト保存#
複雑なオブジェクトをJSON文字列としてデータベースに保存するパターンです。PostgreSQLのJSONB型やMySQLのJSON型と組み合わせて使用できます。
JSONコンバーターの実装#
まず、Jacksonの依存関係を追加します(Spring Boot Starterに含まれています)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter
public class JsonAttributeConverter<T> implements AttributeConverter<T, String> {
private static final ObjectMapper objectMapper = new ObjectMapper();
private final Class<T> targetClass;
public JsonAttributeConverter(Class<T> targetClass) {
this.targetClass = targetClass;
}
@Override
public String convertToDatabaseColumn(T attribute) {
if (attribute == null) {
return null;
}
try {
return objectMapper.writeValueAsString(attribute);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(
"Error converting object to JSON: " + e.getMessage(), e
);
}
}
@Override
public T convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isEmpty()) {
return null;
}
try {
return objectMapper.readValue(dbData, targetClass);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(
"Error converting JSON to object: " + e.getMessage(), e
);
}
}
}
|
具体的な使用例:住所情報の保存#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
/**
* 住所情報を表す値オブジェクト
*/
public class Address {
private String postalCode;
private String prefecture;
private String city;
private String street;
private String building;
// デフォルトコンストラクタ(Jacksonのデシリアライズに必要)
public Address() {
}
public Address(String postalCode, String prefecture,
String city, String street, String building) {
this.postalCode = postalCode;
this.prefecture = prefecture;
this.city = city;
this.street = street;
this.building = building;
}
// getter/setter省略
}
|
1
2
3
4
5
6
7
|
@Converter
public class AddressJsonConverter extends JsonAttributeConverter<Address> {
public AddressJsonConverter() {
super(Address.class);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "shipping_address", columnDefinition = "TEXT")
@Convert(converter = AddressJsonConverter.class)
private Address shippingAddress;
@Column(name = "billing_address", columnDefinition = "TEXT")
@Convert(converter = AddressJsonConverter.class)
private Address billingAddress;
// getter/setter省略
}
|
データベースには以下のように保存されます。
1
2
3
4
5
6
7
|
{
"postalCode": "100-0001",
"prefecture": "東京都",
"city": "千代田区",
"street": "丸の内1-1-1",
"building": "東京ビル5F"
}
|
リスト型のJSON保存#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.List;
@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(List<String> attribute) {
if (attribute == null || attribute.isEmpty()) {
return null;
}
try {
return objectMapper.writeValueAsString(attribute);
} catch (Exception e) {
throw new IllegalArgumentException("Error converting list to JSON", e);
}
}
@Override
public List<String> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isEmpty()) {
return List.of();
}
try {
return objectMapper.readValue(dbData, new TypeReference<List<String>>() {});
} catch (Exception e) {
throw new IllegalArgumentException("Error converting JSON to list", e);
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "tags", columnDefinition = "TEXT")
@Convert(converter = StringListConverter.class)
private List<String> tags; // ["electronics", "smartphone", "5G"]
// getter/setter省略
}
|
暗号化フィールドの実装#
機密情報をデータベースに保存する際、暗号化して保存し、取得時に復号化するパターンです。
AES暗号化コンバーターの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Base64;
@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
// 本番環境では環境変数やSecret Managerから取得すること
private static final String SECRET_KEY = System.getenv("ENCRYPTION_KEY");
private final SecretKeySpec keySpec;
public EncryptedStringConverter() {
if (SECRET_KEY == null || SECRET_KEY.length() != 32) {
throw new IllegalStateException(
"ENCRYPTION_KEY must be set and be 32 characters (256 bits)"
);
}
this.keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
}
@Override
public String convertToDatabaseColumn(String attribute) {
if (attribute == null) {
return null;
}
try {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec);
byte[] encryptedData = cipher.doFinal(attribute.getBytes());
// IVと暗号化データを結合
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + encryptedData.length);
byteBuffer.put(iv);
byteBuffer.put(encryptedData);
return Base64.getEncoder().encodeToString(byteBuffer.array());
} catch (Exception e) {
throw new RuntimeException("Encryption failed", e);
}
}
@Override
public String convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
try {
byte[] decoded = Base64.getDecoder().decode(dbData);
ByteBuffer byteBuffer = ByteBuffer.wrap(decoded);
byte[] iv = new byte[GCM_IV_LENGTH];
byteBuffer.get(iv);
byte[] encryptedData = new byte[byteBuffer.remaining()];
byteBuffer.get(encryptedData);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec);
return new String(cipher.doFinal(encryptedData));
} catch (Exception e) {
throw new RuntimeException("Decryption failed", e);
}
}
}
|
エンティティでの使用例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Entity
@Table(name = "user_credentials")
public class UserCredential {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "api_key", length = 512)
@Convert(converter = EncryptedStringConverter.class)
private String apiKey; // 暗号化されて保存
@Column(name = "secret_token", length = 512)
@Convert(converter = EncryptedStringConverter.class)
private String secretToken; // 暗号化されて保存
// getter/setter省略
}
|
Springの@Valueを使用した改善版#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter
@Component
public class EncryptedStringConverter implements AttributeConverter<String, String> {
private static SecretKeySpec keySpec;
@Value("${app.encryption.key}")
public void setEncryptionKey(String key) {
if (key != null && key.length() == 32) {
keySpec = new SecretKeySpec(key.getBytes(), "AES");
}
}
// 暗号化/復号化メソッドは同様
}
|
Java 8日時型の変換(レガシーシステム対応)#
Hibernate 6.x以降ではJava 8の日時型(LocalDateTime、LocalDateなど)はネイティブサポートされていますが、レガシーシステムとの連携で特殊な形式が必要な場合があります。
数値型タイムスタンプへの変換#
エポックミリ秒で日時を保存するレガシーシステムとの連携例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
@Converter
public class LocalDateTimeToEpochConverter
implements AttributeConverter<LocalDateTime, Long> {
private static final ZoneId ZONE_ID = ZoneId.of("Asia/Tokyo");
@Override
public Long convertToDatabaseColumn(LocalDateTime attribute) {
if (attribute == null) {
return null;
}
return attribute.atZone(ZONE_ID).toInstant().toEpochMilli();
}
@Override
public LocalDateTime convertToEntityAttribute(Long dbData) {
if (dbData == null) {
return null;
}
return LocalDateTime.ofInstant(
Instant.ofEpochMilli(dbData),
ZONE_ID
);
}
}
|
文字列形式の日時変換#
特定のフォーマットで日時を保存する必要がある場合の例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Converter
public class LocalDateTimeToStringConverter
implements AttributeConverter<LocalDateTime, String> {
// レガシーシステムで使用されているフォーマット
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Override
public String convertToDatabaseColumn(LocalDateTime attribute) {
if (attribute == null) {
return null;
}
return attribute.format(FORMATTER);
}
@Override
public LocalDateTime convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isEmpty()) {
return null;
}
return LocalDateTime.parse(dbData, FORMATTER);
}
}
|
使用例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Entity
@Table(name = "legacy_transactions")
public class LegacyTransaction {
@Id
@Column(name = "transaction_id", length = 20)
private String transactionId;
@Column(name = "amount")
private Long amount;
@Column(name = "transaction_time")
@Convert(converter = LocalDateTimeToEpochConverter.class)
private LocalDateTime transactionTime; // 1736755200000 のように保存
@Column(name = "created_date", length = 14)
@Convert(converter = LocalDateTimeToStringConverter.class)
private LocalDateTime createdDate; // "20260113170000" のように保存
// getter/setter省略
}
|
よくある誤解とアンチパターン#
誤解1: autoApplyは常に有効にすべき#
autoApply = trueは便利ですが、意図しない属性にも適用されてしまうリスクがあります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// アンチパターン: 広範囲に影響するautoApply
@Converter(autoApply = true)
public class StringTrimConverter implements AttributeConverter<String, String> {
@Override
public String convertToDatabaseColumn(String attribute) {
return attribute != null ? attribute.trim() : null;
}
@Override
public String convertToEntityAttribute(String dbData) {
return dbData != null ? dbData.trim() : null;
}
}
// すべてのString属性に適用され、意図しない副作用を引き起こす可能性がある
|
推奨アプローチは以下の通りです。
1
2
3
4
5
|
// 推奨: 特定の型専用のConverterを作成
@Converter(autoApply = true)
public class EmailAddressConverter implements AttributeConverter<EmailAddress, String> {
// EmailAddress型にのみ適用される
}
|
誤解2: nullチェックの省略#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// アンチパターン: nullチェックの省略
@Converter
public class StatusConverter implements AttributeConverter<Status, String> {
@Override
public String convertToDatabaseColumn(Status attribute) {
return attribute.getCode(); // NullPointerException発生の可能性
}
@Override
public Status convertToEntityAttribute(String dbData) {
return Status.fromCode(dbData); // nullの場合に例外発生
}
}
|
正しい実装は以下の通りです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 推奨: 適切なnullハンドリング
@Converter
public class StatusConverter implements AttributeConverter<Status, String> {
@Override
public String convertToDatabaseColumn(Status attribute) {
if (attribute == null) {
return null;
}
return attribute.getCode();
}
@Override
public Status convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null; // またはデフォルト値を返す
}
return Status.fromCode(dbData);
}
}
|
誤解3: 複雑なビジネスロジックの混入#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// アンチパターン: ビジネスロジックの混入
@Converter
public class PriceConverter implements AttributeConverter<Price, Long> {
@Autowired // Converterでの依存注入は推奨されない
private TaxService taxService;
@Override
public Long convertToDatabaseColumn(Price attribute) {
// 税金計算などのビジネスロジックを含めるべきではない
BigDecimal withTax = taxService.calculateWithTax(attribute.getAmount());
return withTax.multiply(BigDecimal.valueOf(100)).longValue();
}
}
|
Converterは純粋な型変換に徹するべきです。
誤解4: IDフィールドへのConverter適用#
1
2
3
4
5
6
7
|
@Entity
public class Product {
@Id
@Convert(converter = UuidConverter.class) // IDには適用されない
private UUID id;
}
|
@Idが付与されたフィールドにはConverterは適用されません。UUIDをIDとして使用する場合は、Hibernateの型マッピングを使用します。
まとめと実践Tips#
AttributeConverterの使いどころ#
| ユースケース |
推奨度 |
備考 |
| Enumのコード値変換 |
高 |
保守性と安全性が向上 |
| JSON型カラムへの保存 |
中 |
PostgreSQLのJSONB型と組み合わせると効果的 |
| 暗号化フィールド |
中 |
Spring Security Cryptoも検討 |
| レガシーシステム連携 |
高 |
日時型の形式変換など |
| 単純な型変換 |
低 |
標準機能で対応可能なら不要 |
実践Tips#
- Converterはステートレスに保つ: インスタンス変数を持たず、スレッドセーフにする
- 例外メッセージを明確に: 変換失敗時のデバッグを容易にする
- テストを必ず書く: 双方向変換の整合性を確認する
- autoApplyは慎重に: 影響範囲を把握した上で使用する
- パフォーマンスを考慮: JSONパースなど重い処理は適切にキャッシュする
テストコード例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class OrderStatusConverterTest {
private final OrderStatusConverter converter = new OrderStatusConverter();
@Test
void convertToDatabaseColumn_正常変換() {
assertThat(converter.convertToDatabaseColumn(OrderStatus.PENDING))
.isEqualTo("PND");
assertThat(converter.convertToDatabaseColumn(OrderStatus.CONFIRMED))
.isEqualTo("CNF");
}
@Test
void convertToDatabaseColumn_nullの場合() {
assertThat(converter.convertToDatabaseColumn(null)).isNull();
}
@Test
void convertToEntityAttribute_正常変換() {
assertThat(converter.convertToEntityAttribute("PND"))
.isEqualTo(OrderStatus.PENDING);
assertThat(converter.convertToEntityAttribute("CNF"))
.isEqualTo(OrderStatus.CONFIRMED);
}
@Test
void convertToEntityAttribute_nullの場合() {
assertThat(converter.convertToEntityAttribute(null)).isNull();
}
@Test
void convertToEntityAttribute_不正なコード() {
assertThatThrownBy(() -> converter.convertToEntityAttribute("INVALID"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Unknown OrderStatus code");
}
@Test
void 双方向変換の整合性() {
for (OrderStatus status : OrderStatus.values()) {
String dbValue = converter.convertToDatabaseColumn(status);
OrderStatus restored = converter.convertToEntityAttribute(dbValue);
assertThat(restored).isEqualTo(status);
}
}
}
|
参考リンク#