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
テーブルが用意されていて, いくつかデータも存在しています。 クライアント側のコードとして, かなり適当ですが, 次のようなサーブレットから AccountManager
の findAll
メソッドを呼び出して, 一覧を表示するということをやってみました。
// .. @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
でした。 しかも, その例外メッセージには AccountType
を AccountType
にキャストすることができません, という趣旨のことが書かれていました。
どうしたらよいか
どうしたらよいのか, わかりません。
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 に何か不具合があるのか, まったくちんぷんかんぷんでよくわかりません。 何となくクラスローダー絡みの何かがあるような気がしなくもないのですが, ググってもさっぱり関連情報を見つけられる感じもしないので, もう挫折することにしました。
おしまい
同じ現象にハマりました。
ReplyDelete私の環境だとglassfish-web.xmlに以下の設定を加えることで解消しました。
タグが受け付けられなかったので再投稿。
ReplyDeleteclass-loader delegate="false"