Saturday, February 1, 2014

まだ AttributeConverter が使いこなせていません

JPA で enum を永続化する際, ordinal を使うのも name を使うのも一長一短があると思っていて, それ以外にも何かおもしろい手はないものかなと探していました。 ふと見かけた Mapping enums done right with @Convert in JPA 2.1 (June 3, 2013) という記事に, AttributeConverter を使うという方法が紹介されていました。 おもしろそうだなと思って試してみたのですが, これまでのところほぼ挫折した状態にあります。

歳のせいか, 気が短くなってきているのでしょう, 簡単に挫折しているようではだめなんですが, とりあえず書き留めておきたいと思います。

エンティティクラスなど

経理科目を題材として, 次のようなクラスを作ってみました。 Account は経理科目をあらわすためのもので,

package tt4cs.bookkeeper;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="account")
public class Account implements Serializable {
    
    @Id @GeneratedValue
    @Column(name="account_id")
    private Long id;
    
    @Column(name="account_type")
    @Convert(converter=AccountTypeConverter.class)
    private AccountType type;
    
    @Column(name="account_title")
    private String title;
    
    protected Account() {}
    
    public Account(AccountType type, String title) {
        this.type = type;
        this.title = title;
    }
    
    // ..
    
}

AccountType は経理科目の種類 (資産なのか負債なのか等) をあらわすためのものです。

package tt4cs.bookkeeper;

public enum AccountType {
    
    ASSETS, LIABILITIES, OWNERS_EQUITY,
    
}

また, Account オブジェクトには AccountType 型のフィールドがありますが, これを永続化する際に利用するコンバーターとして AccountTypeConverter という名前のクラスを作ってみました。

package tt4cs.bookkeeper;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter
public class AccountTypeConverter
implements AttributeConverter<AccountType, String> {
    
    @Override
    public String convertToDatabaseColumn(AccountType attribute) {
        switch (attribute) {
            case ASSETS:
                return "A";
            case LIABILITIES:
                return "L";
            case OWNERS_EQUITY:
                return "O";
            default:
                return " ";
        }
    }
    
    @Override
    public AccountType convertToEntityAttribute(String dbData) {
        switch (dbData) {
            case "A":
                return AccountType.ASSETS;
            case "L":
                return AccountType.LIABILITIES;
            case "O":
                return AccountType.OWNERS_EQUITY;
            default:
                return null;
        }
    }
    
}

Account エンティティを操作するクラスとして AccountManager というものを作りました。 ステートレスなセッションビーンです。

package tt4cs.bookkeeper;

import java.util.Collection;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Stateless
public class AccountManager {
    
    @PersistenceContext(unitName="tt4cs-bookkeeper")
    private EntityManager em;
    
    public Collection<Account> findAll() {
        return em.createQuery("select a from Account a",
                Account.class).getResultList();
    }
    
    // ..
    
}

実際に使ってみる

データベースにはすでに account テーブルが用意されていて, いくつかデータも存在しています。 クライアント側のコードとして, かなり適当ですが, 次のようなサーブレットから AccountManagerfindAll メソッドを呼び出して, 一覧を表示するということをやってみました。

// ..

    @EJB
    private AccountManager accountManager;
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse
            response) throws ServletException, IOException {
        response.setContentType("text/plain; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("--- Account List ---");
        out.println();
        for (Account account : accountManager.findAll())
            out.println(account.toString());
        out.println("");
        out.println("--------------------");
        out.flush();
    }

// ..

ここでは一応, 次のような結果が返って来ることを期待しています。

--- Account List ---

Cash (A)
Loan (L)
Capital (O)

--------------------

このような結果は返って来ます。 たしかに返っては来るのですが, 問題がありました。 手元の環境 (Windows 7 x86, JDK 7u51, GlassFish 4.0 build 89) では, 次のようなことがほぼ一貫して観測されました。

(1) はじめてデプロイしたときは, 何も問題は感じられませんでした。 ウェブブラウザからサーブレットにアクセスすると期待どおりの結果を閲覧することができました。

(2) 同じものを再デプロイして, 再び閲覧しようとすると, なぜか期待した結果とはならず, EJBException がスローされました。

(3) GlassFish を再起動してから再び閲覧してみると, 今度は期待どおりの結果が返って来ました。

(4) 同じものを再び再デプロイして, 再び閲覧しようとすると, また EJBException がスローされてしまいました。

スタックトレース上で原因を遡っていくと, 最初にスローされたのは IllegalStateException でした。

Caused by: java.lang.IllegalStateException: This web container has not yet been started
    at org.glassfish.web.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1652)
    at org.glassfish.web.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1611)
    at tt4cs.bookkeeper.AccountTypeConverter.convertToEntityAttribute(AccountTypeConverter.java:28)
    at tt4cs.bookkeeper.AccountTypeConverter.convertToEntityAttribute(AccountTypeConverter.java:6)
    at org.eclipse.persistence.internal.jpa.metadata.converters.ConverterClass.convertDataValueToObjectValue(ConverterClass.java:75)
    ... 88 more

ちなみに, 上で掲載したコードは Read のみでしたが, 実際には em.persist(account) のような Create も試してみたのですが, その場合は IllegalStateException ではなく ClassCastException でした。 しかも, その例外メッセージには AccountTypeAccountType にキャストすることができません, という趣旨のことが書かれていました。

どうしたらよいか

どうしたらよいのか, わかりません。

Account エンティティを書き直して, もう AccountTypeConverter を使わないことにし,

// ..

import javax.persistence.EnumType;
import javax.persistence.Enumerated;

// ..

    @Column(name="account_type")
    //@Convert(converter=AccountTypeConverter.class)
    @Enumerated(EnumType.STRING)
    private AccountType type;

// ..

それとともに, データベース上のデータを修正 ('A''ASSETS' にする等) してみたら, それ以降はいまのところ全く問題があらわれなくなりました。

--- Account List ---

Cash (ASSETS)
Loan (LIABILITIES)
Capital (OWNERS_EQUITY)

--------------------

EclipseLink の実装のせいなのか, わたしが馬鹿なのがいけないのか, それとも GlassFish に何か不具合があるのか, まったくちんぷんかんぷんでよくわかりません。 何となくクラスローダー絡みの何かがあるような気がしなくもないのですが, ググってもさっぱり関連情報を見つけられる感じもしないので, もう挫折することにしました。

おしまい

2 comments:

  1. 同じ現象にハマりました。
    私の環境だとglassfish-web.xmlに以下の設定を加えることで解消しました。

    ReplyDelete
  2. タグが受け付けられなかったので再投稿。
    class-loader delegate="false"

    ReplyDelete