Kuina-Dao 開発記 Detached Entity と Open Session in View

某巨大掲示板へのレス.
あっちに書こうかと思ったけど長くなるからこっちへ.
っていうか,以前のスレであっちに書かずにこっちへ書けって言われたからこっちなのだ (←根に持ってる).

380 名前:デフォルトの名無しさん[sage] 投稿日:2006/06/12(月) 08:04:20
KuinaでもDTOを利用しちゃうんなら
List Query#getResultList(Class)
が欲しいずら

中の人はdetach関連について、どう考えてるんかね

ふむ.
まずは DTO について.
Kuina-Dao では DTO は扱いません.エンティティのみ.
というのも,Dxo の登場によって DTO とエンティティの相互変換がラクチンにできるようになるため,DAO や Criteria に DTO を渡す必要が無くなるからです.
代わりに (?) エンティティを使った Query by Example はサポートします.


次に detach 関連について.
一言でいうと,考えていません.(^^;
Detached Entity は使わないし,おそらく使うべきじゃないし,きっと使わない方が身のためです.
せっかくなので,

Pro EJB 3: Java Persistence API

Pro EJB 3: Java Persistence API

の「Chapter 5 Entity Manager」から「Detachment and Merging」をかいつまんで紹介しましょう.


最初に JSP 中で <c:out value="${emp.department.name}"> みたいに書いた場合を例として Detachment の必要性が書かれています.
そして P149

Planning for Detachment

detachment の戦略はいくつかありますが,ここでは 2 つを紹介するとか何とか.「Chapter 7 Query Language」では別の方法 (Fetch Join) を紹介するとのこと.

Triggering Lazy Loading

まずは 1 つめの戦略.
これは EntityManagerclose() する前に関連を全部たどって Lazy Load を引き起こしちゃおうというもの.
出てくるサンプルはこんな感じ.

@Stateless
public class EmployeeServiceBean implements EmployeeService {
    @PersistenceContext(unitName="EmployeeService")
    private EntityManager em;

    public List findAll() {
        List<Employee> emps = (List<Employee>)
            em.createQuery("SELECT e FROM Employee e")
                .getResultList();
        for (Employee emp : emps) {
            Department dept = emp.getDepartment();
            if (dept != null) {
                dept.getName();
            }
        }
        return emps;
    }
}

この戦略自体はともかくとして,findAll() みたいなメソッドでこれをやるのはどうかと.
必要性に関わらず,無条件に Department までフェッチするなんてあり得なくなくなくない?
こんなんだったら Eager フェッチすべきだよね.
ともあれ (JW),これが 1 つめ.

Configuring Eager Loading

2 つめの戦略は,常に EAGER フェッチするようにマッピングすること.
JPA では,@OneToMany および @ManyToMany では Lazy フェッチするのがデフォルトですが,fetch = FetchType.EAGER を明示的に指定しろってことね.
これもまたどんな場合でも EAGER フェッチするのはいかがなものか?
関連が延々続く場合はどうすんのよ?
ともあれ (JW),これが 2 つめ.

Avoiding Detachment

結局,Detached Entity を使った方法がどっちもイケてないので,いっそのこと Detached Entity を使うのをやめればいいじゃん,っていうのがここからの話.
ここでもそのやり方には 2 つあるということになっています.
1 つはエンティティを JSP (ビュー) から使わないようにすること.それには DTO を使う方法 (冗長になるということで著者達の好みではないみたい) と projection (SELECT e.name, ... みたいな) を使う方法があるとのこと.
もう 1 つは JSP (ビュー) の実行も Persistence Context の中でやっちゃうこと.いわゆる Open Session in View ですね.
これについてこの後詳述となります.

Transaction View

そんなわけで (どんなわけで?),トランザクション境界を広げればいいじゃん,ってわけで,こんなコードが出てきます.

public class EmployeeServlet extends HttpServlet {
    @Resource UserTransaction tx;
    @EJB EmployeeService bean;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            tx.begin();
            List emps = bean.findAll();
            request.setAttribute("employees", emps);
            getServletContext().getRequestDispatcher("/listEmployees.jsp")
                .forward(request, response);
        } finally {
            tx.commit();
        }
    }
}

...
Servlet でやるの?
普通は Servlet Filter を使うんじゃないかと思うわけですが,そうすると @Resource とか使えないので「non-portable」ってことでこうなるらしい.
っていうか,Servlet で @EJB って安全に使えるの? SessionBean ってスレッドセーフじゃないよ? 再入不可だよ?
ともあれ (JW),こんな風にトランザクション境界を広げることで,JSP (ビュー) の実行中でも Lazy フェッチをできるようにしようというわけです.
それを Servlet でやるかどうかはともかく,この方法は問題がいくつかあります.
本書ではその一つとして,REQUIRES_NEWトランザクションを必要とする SessionBean を使うとうまくいかないよ,ってな話が出てきます.
他にも,JSP (ビュー) で何か起きた場合にトランザクションを無条件にロールバックしていいのかとか,いろいろあるよなぁとか思ったり.

EntityManager per Request

Transaction View と似たようなやり方として,アプリケーション管理の EntityManager を使おうということで,こんなコードが出てきます.

public class EmployeeServlet extends HttpServlet {
    @PersistenceUnit(unitNmae="EmployeeService")
    EntityManagerFactory emf;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        EntityManager em = emf.createEntityManager();
        try {
            List emps = em.createQuery("SELECT e FRM Employee e")
                .getResultList();
            request.setAttribute("employees", emps);
            getServletContext().getRequestDispatcher("/listEmployees.jsp")
                .forward(request, response);
        } finally {
            em.close();
        }
    }
}

...
どっちもどっち (苦笑).
ServletEntityManager を直接使って問い合わせするだなんて,サンプルコードとはいえ掲載しない方がいいと思うんだけど.
それは著者も認識していて,その対策として Stateful SessionBean を使った例がこの後出てきます.
えーっと...
せっかくだから,そのコードも紹介しましょうか.

@Stateful
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class EmployeeQueryBean implements EmployeeQuery {
    @PersistenceContext(type=PersistenceContextType.EXTENDED,
        unitName="EmployeeService")
    EntityManager em;

    public List findAll() {
        return em.createQuery("SELECT e FROM Employee e")
            .getResultList();
    }

    @Remove
    public void finished() {
    }
}

そして,上の SFSB を使う Servlet

@EJB(name="queryBean", beanInterface=EmployeeQuery.class)
public class EmployeeServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        EntityQuery bean = createQueryBean();
        try {
            List emps = bean.findAll();
            request.setAttribute("employees", emps);
            getServletContext().getRequestDispatcher("/listEmployees.jsp")
                .forward(request, response);
        } finally {
            bean.finished();
        }
    }

    private EmployeeQuery createQueryBean() throws ServletException {
        // look up queryBean
    }
}

look up ってのは JNDI 使うってことね.
えーっと...
何がしたかったんだっけ? (^^;
リクエスト毎に SFSB をインスタンス化するとは随分と貴族的なソリューション.
まぁ,その辺は著者達も認識しているようですが,読み手によってはこれが正解だとか思っちゃいそうで怖い...
年末から来年にかけて,Open Session in View と SFSB,そして Extended Context な EntityManager を使って開発して,トランザクションが増えてきたらメモリ不足で不安定になるシステムが続出するんじゃないかと予想している今日この頃です.


さて.
JPA 仕様の Spec Lead,Mike Keith が著者の一人でもある「Pro EJB 3 Java Persistence API」でさえ,この調子.
Detached Entity を安全に使うのはひどく効率の悪いやり方になるし,Open Session in View の使用例も非道いもんだ.
一応,上のようなコードを無条件に推奨しているというわけではないのだけど,教科書的に参照されそうな本だけに,こんなコードは掲載しないで欲しかったなぁとか思う今日この頃です.
だいたい,なんでこんな劣悪な例しか出てこないのか?
それは,エンティティを JSP (ビュー) でも使おうというところに諸悪の根源があると思います.
それについてはかつて,「Re: hibernateを利用してはいけない5つのシチュエーション」でねっとりと書いているわけですが,このアプローチを取る限り,どこまでいっても真っ当なやり方にはたどり着けないのではないでしょうか.


そんなわけで (どんなわけで?).
Kuina-Dao では Detached Entity を支援するために何か考える予定はありません.
Open Session in View を支援するために Filter とか提供する予定はありません.
おいらは S2Hibernate からも Filter 削除しちゃったくらい,Open Session in View キライなのだ.
トランザクション境界はプレゼンテーションとロジックの間,サービス層に設定すること.
エンティティはプレゼンテーション層には見せないこと.
以上.