WinstoneでTopLink

JPAの手習い兼,Winstone/DerbyプラグインによるIDEAで完結した開発環境の検証なんてのをやってみたんだけど,意外なところでハマってしまった。一応,解決できたんで,経緯をメモっておく。


えーと,この構成をMacBookで試したときは,なんの問題もなく実施できたんだけど,Windowsでやったらダメだった。理由は,Winstoneのクラスローダに問題があるらしく,TopLinkがpersistence.xmlの読み込みに失敗してしまう(エラーコードはTOPLINK-30005)。
原因はハッキリしていて,persistence.xmlのURLのパス情報に"/"じゃなくて"\"が使われているからだった。で,どこでその設定をしているのか見つけるのが,少々面倒だったのだが,突き止めた先はこちら。

== WebAppConfiguration.java ==
  private ClassLoader buildWebAppClassLoader(Map startupArgs, ClassLoader parentClassLoader,
          String webRoot, List classPathFileList) {
    List urlList = new ArrayList();

    try {
        :
      // Classes folder
      File classesFolder = new File(webInfFolder, CLASSES);
      if (classesFolder.exists()) {
        Logger.log(Logger.DEBUG, Launcher.RESOURCES,
                   "WebAppConfig.WebAppClasses");
        urlList.add(new URL("file", "", classesFolder.getCanonicalPath() + "/"));
                                 ここ→ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        :

classesFolder.getCanonicalPath()なんてやってるから,おかしな事になるので,ここを以下のように書き換える。

urlList.add(new URL("file", "", classesFolder.getCanonicalPath() + "/"));
    ↓
urlList.add(classesFolder.toURL());

これで解決と思いきや。今度は,TopLinkがDerbyのJDBCドライバの読み込みに失敗する(TOPLINK-4002)。:-(


TopLinkもDerbyも同じくcommonLibFolderに入れてるんだけどなぁ。同じ親のクラスローダを共有しているはずだから,お互いを認識できると思うんだけど,なぜかダメみたい(なんでOSXで平気だったんだ?)。こっちは,しゃあないんで,DerbyをTopLinkより上のクラスローダに配置することで,急場をしのぐこととした。


ちょっと釈然としないところもあるが,まあいいや。
#ちなみに,Tomcatでは何もしなくてもすんなり動くよ。

TopLink Essentials JPA Extensions Reference

persistence.xmlのproperties要素に書けるオプションとか,いろいろ載ってて便利。これで,キレイに印刷できるように工夫してあれば,なお良いのに。


とりあえず,TopLink+Derby(非データソース利用)んときの,persistence.xmlの記述例をメモっておく(IDEAのpersistence.xmlの入力支援は,こうゆう実装依存ネタはてんで弱いから*1)。

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
  <persistence-unit name="TestPersistenceUnit" transaction-type="RESOURCE_LOCAL">
    <provider>oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider</provider>
      :
    <properties>
      <property name="toplink.jdbc.driver" value="org.apache.derby.jdbc.ClientDriver"/>
      <property name="toplink.jdbc.url" value="jdbc:derby://localhost:1527/testdb;create=true"/>
      <property name="toplink.ddl-generation" value="create-tables"/>
      <property name="toplink.jdbc.user" value="app"/>
      <property name="toplink.jdbc.password" value="app"/>
      <property name="toplink.logging.level" value="ALL"/>
    </properties>
  </persistence-unit>
</persistence>

注目すべき点はtoplink.ddl-generationプロパティ。
って,別にTopLinkに限った機能じゃないし,今となっては珍しくもないんだけど,ちょいちょい組んでみるときなんかは便利よね。ただ,無茶な話だとわかってはいるが,エンティティの追加(CREATE TABLE)はインクリメンタルにできるけど,エンティティの属性の変更(ALTER TABLE)は直接DBをいじってあげないとダメなのね。
Railsマイグレーション的な事を一瞬期待してしまった。:-P

*1:そのためにLive Templateがあるんだろう

JPAは悪くないと思う

しばらくO/Rマッピングな世界から遠ざかっていたけど,今となってはJPAにすがっていいんじゃないかと思う(世の中の流れ的にもズレてないし)。「JavaEE5はちょっと」っと思ってるなら,JPAだけ抜き出して使う事はできるから,そんなに気にする話でもないかと。
ちょっと手習い程度とは言え,「JDK5/TopLink(JPA)/Winstone(Servlet2.4)/Guice」の組み合わせでサックリ使えたし。


問題は「トランザクション管理をどいつにやらせるか」くらいだろうから,それこそSpringでもSeasarでも好きなのを使ってクレイって話だ。DAOを作るだ,作らないだなんて話は,それこそどうでもよろしかろう。

warp-persist試す

と言っても,ここに書いてあるとおりにやっただけなんだけどさ。Dynamic Findersで,気になったところがあったんで,そこだけメモっとく。


Dynamic Findersってのは,@Finderアノテーションを付けたインターフェイスだけを作っておけば,実装いらずで検索用DAOが作れるっていうwarp-persistの機能のことだ。
こんな感じでFinderのコードを書いて,

import com.wideplay.warp.persist.dao.Finder;
import sample.entity.Blog;

import java.util.List;

public interface BlogFinder {
    @Finder(query = "select o from Blog o")
    List<Blog> getAllBlogs();
}

こんな感じで,Guicewarp-persist)に登録すると,Finderの実装がなくても使えるようになる。

public class BlogAppFactory {
    private static Injector injector;

    public static void init() {
        injector = Guice.createInjector(
                new BlogModule(),
                PersistenceService
                        .usingJpa()
                        .across(UnitOfWork.REQUEST)
                        .addAccessor(BlogFinder.class) // ← ここ
                        .transactedWith(TransactionStrategy.LOCAL)
                        .buildModule());
    }

    public static <T> T getInstacne(Class<T> clazz) {
        return injector.getInstance(clazz);
    }
}

一見便利そうに思えたんだけど,実装いらずは検索処理だけで,更新処理は実装がいるのだ。
更新用DAOってのは,こんなコードを書く(JavaEEによるリソース管理しないから,@Resourceアノテーションはないよ)。

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.wideplay.warp.persist.Transactional;
import sample.entity.Blog;

import javax.persistence.EntityManager;

public class BlogDAO {
    @Inject
    private Provider<EntityManager> em;

    @Transactional
    public Blog create(String title, String link) {
        Blog blog = new Blog(title, link);
        em.get().persist(blog);
        return blog;
    }
}

つまり,検索用と更新用のDAOを用意するハメになるんで,めんどくせぇなぁ〜なんて思ったわけだ。

BlogDAO blogDAO = BlogAppFactory.getInstance(BlogDAO.class);
BlogFinder blogFinder = BlogAppFactory.getInstance(BlogFinder.class);


で,試したことは「BlogFinderをBlogDAOに実装したらどうなるか?」っての。やれば,できるんだけど,アノテーションとかどうなるのか,興味あったんで試してみた。結果はこう。

public interface BlogFinder {
    // @Finder(query = "select o from Blog o") → アノテーションは要らない
    List<Blog> getAllBlogs();
}

public class BlogDAO implements BlogFinder {
    @Inject
    private Provider<EntityManager> em;

    @Transactional
    public Blog create(String title, String link) {
        Blog blog = new Blog(title, link);
        em.get().persist(blog);
        return blog;
    }

    @Finder(query = "select o from Blog o") // ← 実装側にアノテーションが必要
    public List<Blog> getAllBlogs() {
        // 空実装でよい
        return null;
    }
}

んー,こんなだったら,あえてDynamic Findersを使わず,ふつーにJPAクエリを使った方がよさげだの。あまり異端なことしても,ついてくる人いなさそうだし。
それと,なるべくJavaEE風な記述のほうが,いろいろ便利な予感がする。:-D