JPA 2.1 の 新機能 Converter まとめ

f:id:Naotsugu:20150313213957p:plain


データベースのカラムと Entity の該当する属性の変換を Converter で定義できるようになりました。

Converter の使用例

Converter は以下の AttributeConverterインターフェース を実装して定義します。

public interface AttributeConverter<X,Y> {
    public Y convertToDatabaseColumn (X attribute);
    public X convertToEntityAttribute (Y dbData);
}

Entity の属性は Boolean 型で、データベース上では数値で扱う場合には以下のような Converter を定義します。

@Converter
public class BooleanToIntegerConverter implements AttributeConverter<Boolean, Integer> {
    @Override
    public Integer convertToDatabaseColumn (Boolean attribute) {
        return attribute ? 1 : 0;
    }
    @Override
    public Boolean convertToEntityAttribute (Integer dbData) {
        return dbData > 0;
    }
}

そして Entity の当該属性に @Convert アノテーションで利用する Converter を指定します。

    @Convert(converter = BooleanToIntegerConverter.class)
    private Boolean bonded;

これによりデータベースへの永続化時とデータベースからの読み込み時に自動的に型変換が適用されるようになります。

JSFjavax.faces.convert.Converter と同じような感じですね。

ID 属性と Version 属性、関連の属性、Enumerated や Temporal アノテーションが付けられている属性は変換の対象にはなりません。

また、複数のデータベースカラムを扱うことは現時点で標準化されていません。

Converter の自動適用

Converter の定義で autoApply を設定すると、Entity の属性値に @Convert を指定しなくとも暗黙的に対象の型に対して変換が適用できます。

URL 型に対して暗黙的に型変換を行うには以下のように定義します。

@Converter(autoApply=true)
public class URLConverter implements AttributeConverter<URL, String> {
    public String convertToDatabaseColumn (URL attribute) {
        return attribute.toString();
    }
    public URL convertToEntityAttribute (String dbData) {
        return new URL(dbData);
    }
}

@ConverterautoApply=true を定義してあげるだけです(URLの変換時の例外コードは省略しています)。

型変換の対象から外したい場合には disableConversion で無効化します。

@Convert(desableConversion=true)
URL homePage;

暗黙的な自動変換が定義されていたとしても、属性に対して @Convert アノテートすることで、自動変換の定義を上書きすることができます。

Embedded 属性の変換

Embedded として属性を定義している場合には、attributeName で対象の属性を指定します。

以下のような @Embeddable なクラスが bonded という属性を持っていた場合、

@Embeddable
public class SecurityInfo {
    private Boolean bonded;
    // ・・・
}

Entity 側の @Convert で属性値を指定します。

@Entity
public class Employee {

    @Embedded
    @Convert(converter = BooleanToIntegerConverter.class, attributeName = "bonded")
    private SecurityInfo securityInfo;
}

より多くの変換が必要な場合は以下のように @Converts の中に複数定義することができます。

    @Embedded
    @Converts({
        @Convert(attributeName = "level", converter = LevelConverter.class),
        @Convert(attributeName = "health", converter = HealthConverter.class),
        @Convert(attributeName = "status.runningStatus", converter = RunningStatusConverter.class)
    })
    protected RunnerInfo info;

この例にあるように "status.runningStatus" のように属性名をドットで連結することでネストした属性に対して変換処理を定義できます。

Collection 属性の変換

ElementCollection の単純な変換は以下のように特別なものは不要です。

@ElementCollection
@Convert(converter = BooleanToIntegerConverter.class)
public List<Boolean> securityClearances;

単純な Map の場合、値に対する変換は以下のように定義します。

@ElementCollection
@Convert(converter = EmployeeNameConverter.class)
Map<String, String> responsibilities;

キーに対する変換が必要な場合には、"key" を指定します。

@ElementCollection
@Converts({
    @Convert(attributeName = "key", converter = DistanceConverter.class),
    @Convert(converter = EmployeeNameConverter.class)
})
protected Map<String, String> responsibilities;

通常の OneToMany 関連のキーに適用する場合にも "key" で指定します。

@OneToMany
@Convert(converter=ResponsibilityCodeConverter.class, attributeName="key")
Map<String, Employee> responsibilities;

ネストして適用する場合には "key." または "value." で連結させて指定することができます。

@Entity 
public class PropertyRecord {
    @ElementCollection
    @Convert(converter=CityConverter.class, attributeName="key.region.city")
    Map<Address, PropertyInfo> parcels;
}

関連の Embeddable に対しても同じような指定で対応できます。

@ManyToMany
@Convert(converter = UpperCaseConverter.class, attributeName="key.lastName")
private Map<EmployeeName, Employee> employees;

クラスレベルでの Converter 指定

Converter は継承した子クラスから指定することもできます。

以下の親クラスがあった場合に、

@MappedSuperclass
public class Athlete {
    protected Integer age;
   
   @ElementCollection
   protected Map<String, Date> accomplishments;
   // ...
}

以下のように継承した属性に対して Convert を定義することもできます。

@Entity
@Converts({
    @Convert(attributeName = "accomplishments.key", converter = AccomplishmentConverter.class),
    @Convert(attributeName = "accomplishments", converter = DateConverter.class),
    @Convert(attributeName = "age", converter = AgeConverter.class)
})
public class Runner extends Athlete {
    ...
}

orm.xml での Converter 指定

CreditCard 番号をデータベースには暗号化した文字列として保存したいとします。

CreditCard Entity は以下のようになり、

@Entity
public class CreditCard {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String ccNumber;

    private String name;

    ...
}

暗号化用のコンバータは以下のようになります。

@Converter
public class CryptoConverter implements AttributeConverter<String, String> {

    private static final String ALGORITHM = "AES/ECB/PKCS5Padding";
    private static final byte[] KEY = "MySuperSecretKey".getBytes();

    @Override
    public String convertToDatabaseColumn(String ccNumber) {
      // do some encryption
      Key key = new SecretKeySpec(KEY, "AES");
      try {
         Cipher c = Cipher.getInstance(ALGORITHM);
         c.init(Cipher.ENCRYPT_MODE, key);
         return Base64.encodeBytes(c.doFinal(ccNumber.getBytes()));
      } catch (Exception e) {
         throw new RuntimeException(e);
      }
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
      // do some decryption
      Key key = new SecretKeySpec(KEY, "AES");
      try {
        Cipher c = Cipher.getInstance(ALGORITHM);
        c.init(Cipher.DECRYPT_MODE, key);
        return new String(c.doFinal(Base64.decode(dbData)));
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
}

orm.xml にて CreditCard の ccNumber にコンバータを適用するには以下のように定義します。

<entity-mappings version="2.1"
  xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd">

  <entity class="sample.entity.CreditCard">
    <convert converter="sample.converter.CryptoConverter" attribute-name="ccNumber"/>
  </entity>
</entity-mappings>

convert タグで対象のコンバータクラスを属性名を指定しています。

注意点

一番最初の例の BooleanToIntegerConverter を定義していた場合、以下の JPQL は上手く変換されます。

SELECT e FROM e WHERE e.bonded = true

true というリテラルはコンバータを介して Integer に変換されるので有効な SQL になります。

ただし、以下の JPQL を書いた場合、

SELECT e FROM e WHERE NOT e.bonded

bonded はデータベース上は Integer なので SQLエラーになる可能性があります。 このように値に対する変換は行われても、演算子に対する変換は行われません。

また UPPER() などの関数を含む場合や LIKE のような演算のリテラル値に対しても変換は適用されない可能性があるため実際に出力されたSQLを良く確認する必要があるでしょう。

詳細は以下の書籍をご覧ください。

Pro JPA 2 (Expert's Voice in Java)

Pro JPA 2 (Expert's Voice in Java)