JPA、特にJPAの実装にHibernateを使っている場合に遭遇するということで、有名なやつを。
この話自体はちょこちょこ見かけるのですが、実際に自分の目で見たことはなかったので、ここはひとつ確認してみることにしました。
確認に使用する、テーブル定義はこんな感じです。
CREATE TABLE book_shelf ( id INT AUTO_INCREMENT, name VARCHAR(255), PRIMARY KEY(id) ); CREATE TABLE book ( id INT AUTO_INCREMENT, book_shelf_id INT, name VARCHAR(255), publish_date DATE, PRIMARY KEY(id) );
別の本棚に入れるためには、本自体が増えます…。
エンティティクラス。
src/main/scala/javaee6/web/entity/Entities.scala
package javaee6.web.entity import scala.beans.BeanProperty import scala.collection.JavaConverters._ import java.util.Date import javax.persistence.{Column, Entity, GeneratedValue, GenerationType, Id} import javax.persistence.{Table, Temporal, TemporalType} import javax.persistence.{FetchType, JoinColumn, OneToMany} object BookShelf { def apply(name: String): BookShelf = { val bs = new BookShelf bs.name = name bs } } @SerialVersionUID(1L) @Entity @Table(name = "book_shelf") class BookShelf { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @BeanProperty var id: Int = _ @Column @BeanProperty var name: String = _ @OneToMany //@OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "book_shelf_id") @BeanProperty var booksAsJava: java.util.List[Book] = _ def books: Iterable[Book] = booksAsJava.asScala.asInstanceOf[Iterable[Book]] } object Book { def apply(bookShelfId: Int, name: String, publishDate: Date): Book = { val book = new Book book.bookShelfId = bookShelfId book.name = name book.publishDate = publishDate book } } @SerialVersionUID(1L) @Entity @Table(name = "book") class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @BeanProperty var id: Int = _ @Column(name = "book_shelf_id") @BeanProperty var bookShelfId: Int = _ @Column @BeanProperty var name: String = _ @Column(name = "publish_date") @Temporal(TemporalType.DATE) @BeanProperty var publishDate: Date = _ }
OneToManyを貼ったところのフェッチ戦略は、後で切り替えます。
EJB。
src/main/scala/javaee6/web/service/Services.scala
package javaee6.web.service import javax.ejb.{LocalBean, Stateless} import javax.persistence.{EntityManager, PersistenceContext} import javaee6.web.entity.{BookShelf, Book} trait PersistenceContextSupport[T] { @PersistenceContext(unitName = "javaee6.web.pu") var entityManager: EntityManager = _ def create(entity: T): Unit = entityManager.persist(entity) def update(entity: T): Unit = entityManager.merge(entity) def remove(entity: T): Unit = entityManager.remove(entityManager.merge(entity)) } @Stateless @LocalBean class BookShelfService extends PersistenceContextSupport[BookShelf] { def find(id: Int): BookShelf = entityManager.find(classOf[BookShelf], id) def findByFetchQuery(id: Int): BookShelf = entityManager .createQuery("""|SELECT bs | FROM BookShelf bs | LEFT JOIN FETCH bs.booksAsJava | WHERE bs.id = :id""".stripMargin) .setParameter("id", id) .getSingleResult .asInstanceOf[BookShelf] def count: Int = entityManager .createQuery("SELECT bs FROM BookShelf bs") .getResultList .size } @Stateless @LocalBean class BookService extends PersistenceContextSupport[Book]
JAX-RS。Applicationクラスのサブクラスについては、端折ります。
src/main/scala/javaee6/web/jaxrs/BookShelfResource.scala
package javaee6.web.jaxrs import java.net.URI import java.text.SimpleDateFormat import java.util.Date import javax.inject.Inject import javax.ws.rs.{GET, Path, PathParam, Produces} import javax.ws.rs.core.{CacheControl, MediaType, Response, UriBuilder} import javaee6.web.entity.{Book, BookShelf} import javaee6.web.service.{BookService, BookShelfService} @Path("/bookshelf") class BookShelfResource { @Inject var bookShelfService: BookShelfService = _ @Inject var bookService: BookService = _ @GET @Path("init") def init: Response = { val numbering = bookShelfService.count + 1 val bookShelf = BookShelf("My本棚" + numbering) bookShelfService.create(bookShelf) val sdf = new SimpleDateFormat("yyyy/MM/dd") val date: String => Date = sdf.parse Array( Book(bookShelf.id, "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", date("2013/3/9")), Book(bookShelf.id, "マスタリングJavaEE5 第2版", date("2009/11/28")), Book(bookShelf.id, "JavaによるRESTfulシステム構築", date("2010/8/23")) ).foreach(bookService.create) val cc = new CacheControl cc.setMaxAge(0) cc.setMustRevalidate(true) cc.setNoCache(true) cc.setNoStore(true) Response .status(Response.Status.MOVED_PERMANENTLY) .location(UriBuilder .fromPath("/bookshelf/list/{id}") .build(bookShelf.id.toString)) .cacheControl(cc) .build } @GET @Path("list/{id}") def list(@PathParam("id") id: String): Response = { val bookShelf = bookShelfService.find(id.toInt) //val bookShelf = bookShelfService.findByFetchQuery(id.toInt) val sdf = new SimpleDateFormat("yyyy/MM/dd") val dformat: Date => String = sdf.format val responseHtml = <html> <head> <meta charset="UTF-8" /> <title>{if (bookShelf != null) bookShelf.name else "お探しの本棚はありません"}</title> </head> <body> <h1>{if (bookShelf != null) bookShelf.name else "お探しの本棚はありません"}</h1> {if (bookShelf!= null) <table border="1"> <tr><th>書籍ID</th><th>書籍名</th><th>出版日</th></tr> {bookShelf.books.map { b => <tr><td>{b.id}</td><td>{b.name}</td><td>{dformat(b.publishDate)}</td></tr>}} </table> } </body> </html> Response .ok(responseHtml.toString) .build } }
initというGETに対応するメソッドがべき等ではないので、REST好きな人からは怒られそうですが、ここはちょっとご愛嬌ということで…。単に簡単にデータを作りたかっただけなので。
で、initにアクセス後、list/{id}にアクセスすると、こういうことになります。
00:49:09,468 INFO [stdout] (http--127.0.0.1-8080-1) Hibernate: 00:49:09,468 INFO [stdout] (http--127.0.0.1-8080-1) select 00:49:09,468 INFO [stdout] (http--127.0.0.1-8080-1) bookshelf0_.id as id64_0_, 00:49:09,474 INFO [stdout] (http--127.0.0.1-8080-1) bookshelf0_.name as name64_0_ 00:49:09,474 INFO [stdout] (http--127.0.0.1-8080-1) from 00:49:09,475 INFO [stdout] (http--127.0.0.1-8080-1) book_shelf bookshelf0_ 00:49:09,475 INFO [stdout] (http--127.0.0.1-8080-1) where 00:49:09,476 INFO [stdout] (http--127.0.0.1-8080-1) bookshelf0_.id=? 00:49:09,533 ERROR [org.apache.catalina.core.ContainerBase.[jboss.web].[default-host].[/javaee6-web].[javaee6.web.jaxrs.SimpleApplication]] (http--127.0.0.1-8080-1) サーブレット javaee6.web.jaxrs.SimpleApplication のServlet.service()が例外を投げました: org.jboss.resteasy.spi.UnhandledException: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: javaee6.web.entity.BookShelf.booksAsJava, no session or session was closed
はい、見るのはここ。
org.jboss.resteasy.spi.UnhandledException: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: javaee6.web.entity.BookShelf.booksAsJava, no session or session was closed
JAX-RSのリソースクラスは永続化コンテキストの範囲外なので、関連エンティティをレイジーに取得しようとすると、Hibernateの場合にはこうなるということですね。
EclipseLinkだとデータを取り直すらしいですが、それもそれで…。
で、これを解決するためにどうしようか?というところですが、
- 呼び出し元(この場合はJAX-RS)も永続化コンテキストのスコープの範囲に入れる
- OneToManyのフェッチ戦略を、EAGERにする
- 永続化コンテキストを抜ける前に、強引にロードしてしまう
- JPQLで、フェッチジョインする
- 拡張EntityManagerを使用する
などが考えられます。
最初のやつは、考え方的には「Open Session In View」ですね。
強引にロードしてしまおうというのは、永続化コンテキストを抜ける前に単に
for (Book book : bookShelf.getBooks()) { ... }
のようにアクセスしてしまうことや、Listのサイズを取るなどすることです。
拡張EntityManagerは、デフォルトではJTAトランザクション境界に一致するはずのEntityManagerのライフサイクルを、ステートフル・セッションBeanと同じライフサイクルにまで広げることです。
とまあ、ここまで特にやりたくないものを挙げてみました。
個人的には、採用するならそれ以外かなぁと。
では、他の戦略を試してみましょう。
まずはフェッチ戦略をEAGERにする、から。先ほどのエンティティの定義で、以下の部分を
@OneToMany //@OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "book_shelf_id") @BeanProperty var booksAsJava: java.util.List[Book] = _
こう変更します。
//@OneToMany @OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "book_shelf_id") @BeanProperty var booksAsJava: java.util.List[Book] = _
フェッチ戦略をEAGERにしました。
コンパイルしてデプロイ後、確認してみるとHibernateから投げられるSQLがこうなります。
01:05:11,411 INFO [stdout] (http--127.0.0.1-8080-1) Hibernate: 01:05:11,416 INFO [stdout] (http--127.0.0.1-8080-1) select 01:05:11,416 INFO [stdout] (http--127.0.0.1-8080-1) bookshelf0_.id as id68_1_, 01:05:11,416 INFO [stdout] (http--127.0.0.1-8080-1) bookshelf0_.name as name68_1_, 01:05:11,417 INFO [stdout] (http--127.0.0.1-8080-1) booksasjav1_.book_shelf_id as book2_68_3_, 01:05:11,417 INFO [stdout] (http--127.0.0.1-8080-1) booksasjav1_.id as id3_, 01:05:11,417 INFO [stdout] (http--127.0.0.1-8080-1) booksasjav1_.id as id69_0_, 01:05:11,418 INFO [stdout] (http--127.0.0.1-8080-1) booksasjav1_.book_shelf_id as book2_69_0_, 01:05:11,418 INFO [stdout] (http--127.0.0.1-8080-1) booksasjav1_.name as name69_0_, 01:05:11,418 INFO [stdout] (http--127.0.0.1-8080-1) booksasjav1_.publish_date as publish4_69_0_ 01:05:11,418 INFO [stdout] (http--127.0.0.1-8080-1) from 01:05:11,419 INFO [stdout] (http--127.0.0.1-8080-1) book_shelf bookshelf0_ 01:05:11,419 INFO [stdout] (http--127.0.0.1-8080-1) left outer join 01:05:11,420 INFO [stdout] (http--127.0.0.1-8080-1) book booksasjav1_ 01:05:11,420 INFO [stdout] (http--127.0.0.1-8080-1) on bookshelf0_.id=booksasjav1_.book_shelf_id 01:05:11,420 INFO [stdout] (http--127.0.0.1-8080-1) where 01:05:11,428 INFO [stdout] (http--127.0.0.1-8080-1) bookshelf0_.id=?
LEFT OUTER JOINになりましたね。
これで、結果が取得できるようになりました。
それでは、もうひとつの方法のJPQLでフェッチジョインするやり方を試してみましょう。
先ほどの、OneToManyアノテーションのフェッチ戦略はデフォルトに戻しておきます。そして、JAX-RSリソースクラスの以下の部分を
val bookShelf = bookShelfService.find(id.toInt) //val bookShelf = bookShelfService.findByFetchQuery(id.toInt)
このように変更します。
//val bookShelf = bookShelfService.find(id.toInt) val bookShelf = bookShelfService.findByFetchQuery(id.toInt)
このメソッドが指定するJPQLは、以下のものです。
def findByFetchQuery(id: Int): BookShelf = entityManager .createQuery("""|SELECT bs | FROM BookShelf bs | LEFT JOIN FETCH bs.booksAsJava | WHERE bs.id = :id""".stripMargin) .setParameter("id", id) .getSingleResult .asInstanceOf[BookShelf]
コンパイル後、デプロイして動作させた際に得られるSQLは、先ほどのフェッチ戦略を変更した時と同じものです。
ケースバイケースですが、個人的にはJPQLでフェッチジョインする方が好みかなぁ。
JPQLフェッチジョインの参考URL:
http://docs.oracle.com/cd/E28613_01/apirefs.1211/e24396/ejb3_langref.html#ejb3_langref_fetch_joins
参考書籍:
マスタリングJavaEE5 第2版 (DVD付) (Programmer’s SELECTION)
- 作者: 三菱UFJインフォメーションテクノロジー株式会社斉藤賢哉
- 出版社/メーカー: 翔泳社
- 発売日: 2009/11/28
- メディア: 大型本
- 購入: 5人 クリック: 29回
- この商品を含むブログ (11件) を見る