Kotlin+SpringBootでWebアプリを作ってみる その2
加筆(2016/4/3 18時くらい)
はじめてのSpring Bootを書かれている槇さんから以下のツイートをいただきました。
@KissyBnts spring-boot-starter-data-jpa使っています?つかっていたらviewでLazyInitializationExceptionでないと思いますが。
— Toshiaki Maki (@making) 2016年4月3日
・・・はい、View
の確認をする前にテストコードで動かしてました。
確認した所、@LazyCollection
でアノテートしなくてもView
およびController
で問題なく動作していました。
つまり、lazy initialize
はtrue
でOKってことですね。
よって下記の@LazyCollection(value = LazyCollectionOption.FALSE)
は不要となります。
んー、テストコードがまずいのかな? 以下は失敗したテストです。
@RunWith(SpringJUnit4ClassRunner::class) @SpringApplicationConfiguration(BookApplication::class) @WebAppConfiguration class BookServiceTest{ @Autowired lateinit var bookService: BookService @Test fun findAllBookTest() { var sutList = bookService.findAllBook() var sut = sutList[0] assertThat(sutList.size, be(1)) assertThat(sut.id, be(1L)) assertThat(sut.title, be("title")) assertThat(sut.subTitle, be("subTitle")) assertThat(sut.leadingSentence, be("leading")) assertThat(sut.imagePath, be("imagePath")) assertThat(sut.url, be("url")) assertThat(sut.authors[0].name, be("author")) assertThat(sut.likeCount, be(0)) assertThat(sut.categories[0].name, be("category")) assertThat(sut.publisher.name, be("publisher")) } }
ここら辺は要調査ですね。
テストコードがまずかったです。
槇さんが教えてくださりました。詳細はこちら
槇さん、ありがとうございました!
やること
Kotlin+SpringBootでWebアプリを作ってみる その1.1の続きです。
今回は
・Controller
と一覧ページの作成
・著者、出版社、カテゴリのEntity
作成
をやっていきます。
Controllerと一覧ページの作成
MainController.kt
package book.application.controller import book.application.service.BookService import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.servlet.ModelAndView /** * 一覧画面のコントローラ. */ @Controller class MainController @Autowired constructor(private val bookService: BookService) { @RequestMapping("/main") fun main(): ModelAndView = ModelAndView("/main").apply { addObject("books", bookService.findAllBook()) } }
/main
を受けるメソッドを定義したコントローラクラス。
BookService
をコンストラクタでAutowired
してるのはval
で定数プロパティにしたかったから。
別に変数でいいですって時は@Autowired lateinit var hoge: Hoge
でいいと思います。
メソッドの方はview
に/main.html
を指定して、書籍リストを渡すだけ。
apply{}
でレシーバを返せるのでちょっとスッキリ。
個人的にapply{}
とかlet{}
は便利でとても気に入ってます。
使い方はKotlin スコープ関数 用途まとめがわかりやすくていいと思います。
main.html
<!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>一覧ページ</title> <style> dl{ display: inline-block; width: 300px; float: left; margin: 5px; border: 1px solid gray; } dd{ max-height: 50px; overflow: auto; } </style> </head> <body> <h1>一覧ページ</h1> <dl th:each="book : ${books}" th:object="${book}"> <dt>Title</dt> <dd th:text="*{title}">本の名前</dd> <dt>SubTitle</dt> <dd th:if="*{subTitle != null}" th:text="*{subTitle}">サブタイトル</dd> <dt>LeadingSentence</dt> <dd th:text="*{leadingSentence}">リード文</dd> <dt>ImagePath</dt> <dd th:text="*{imagePath}">本当は画像を表示するぞ</dd> <dt>URL</dt> <dd><a href="#" th:href="*{url}" th:text="*{url}">どっかのURL</a></dd> </dl> </body> </html>
一覧ページを表示するhtml
ファイル。
Spring Boot
のテンプレートエンジンといえばThymeleaf
でしょってことで、使ってます。meta
タグはちゃんと閉じましょう...
MainController.main()
でModelAndView
に詰めたbooks
をある分だけ表示しています。
サブタイトルはなかったら表示されません。
Thymeleaf
はチュートリアル見ればだいたいわかる気がします。
レイアウトはCSS
書くか、Bootstrap
を使うか迷ってますが、とりあえず適当に。現状は表示できればいいのだ。
Controllerと一覧ページの作成 : done
著者、カテゴリ、出版社のEntity作成
適当なER図
こんな感じで実装していきます。
author
、category
の各テーブルの主キーとbook
テーブルの主キーで結合表を作って多対多の関係にします。共著、複数カテゴリの書籍への対応です。
publisher
テーブルとbook
テーブルは1対多の関係にします。 複数の出版社が共同で出版することはないでしょうし、翻訳書が複数の出版社から出ることはありますがそれは別の書籍ということにします。
あと、いいね的なのを実装するとか書いてたのでとりあえずいいね数を格納するカラムをそれぞれ作りました。
LikeTarget.kt
package book.application.domain import javax.persistence.Column /** * いいね対象の抽象エンティティクラス. */ abstract class LikeTarget { /** * いいね数を保持するカラム */ @Column(nullable = false) var likeCount: Int = 0 private set /** * いいね数をインクリメントする. * @return 処理後のいいね数 */ fun likeUp(): Int = likeCount++ /** * いいね数をデクリメントする. * @return 処理後のいいね数 */ fun likeDown(): Int = if(likeCount === 0){ 0 } else { likeCount-- } }
いいね対象のEntity
クラスに共通するプロパティとメソッドを定義した抽象クラスです。
interface
でいい気もする。
Author.kt
package book.application.domain import org.hibernate.annotations.LazyCollection import org.hibernate.annotations.LazyCollectionOption import javax.persistence.* /** * authorテーブルのEntity. */ @Entity @Table(name = "author") class Author() : LikeTarget() { @Id @GeneratedValue var id: Long? = null /** * 著者名. */ @Column(nullable = false) lateinit var name: String /** * 執筆した書籍リスト. */ @ManyToMany @JoinTable(name = "author_book", joinColumns = arrayOf(JoinColumn(name = "author_id")), inverseJoinColumns = arrayOf(JoinColumn(name = "book_id"))) // @LazyCollection(value = LazyCollectionOption.FALSE) ←不要です! lateinit var books: MutableList<Book> /** * セカンダリコンストラクタ. * @param id 主キー * @param name 著者名 * @param likeCount いいね数 * @param books 執筆した本リスト */ constructor(id: Long? = null, name: String, likeCount: Int = 0, books: MutableList<Book> = mutableListOf()): this(){ this.id = id this.name = name this.likeCount = likeCount this.books = books } }
上記のLikeTarget
を継承したauthor
テーブルのEntity
クラスです。
@ManyToMany
は多対多の関係を示すアノテーション。
所有者側にtargetEntity
属性を、被所有者側にmappedBy
属性を付ける感じ。
プロパティがGenerics
を使用したCollection
の場合は省略してもOKなようです。
@JoinTable
は結合表を指定するアノテーション。
joinColumns
属性で所有者側の主キーを参照する結合表のカラムを指定し、inverseJoinColumns
属性で被所有者側の主キーを参照するカラムを指定する模様。
@LazyCollection
は遅延初期化(lazy initialize)の設定をするアノテーション。
これにたどり着くまでかなり時間がかかりました。詳細は後述。
Category.kt
package book.application.domain import org.hibernate.annotations.LazyCollection import org.hibernate.annotations.LazyCollectionOption import javax.persistence.* /** * categoryテーブルのEntity. */ @Entity @Table(name = "category") class Category() : LikeTarget() { @Id @GeneratedValue var id: Long? = null private set /** * カテゴリ名. */ @Column(nullable = false) lateinit var name: String private set /** * カテゴリに属する書籍リスト. */ @ManyToMany @JoinTable(name = "category_book", joinColumns = arrayOf(JoinColumn(name = "category_id")), inverseJoinColumns = arrayOf(JoinColumn(name = "book_id"))) // @LazyCollection(value = LazyCollectionOption.FALSE) ←不要です lateinit var books: MutableList<Book> /** * セカンダリコンストラクタ * @param id 主キー * @param name カテゴリ名 * @param likeCount いいね数 * @param books カテゴリに属する書籍リスト */ constructor(id: Long? = null, name: String, likeCount: Int, books: MutableList<Book>): this(){ this.id = id this.name = name this.likeCount = likeCount this.books = books } }
Author
クラスとほぼ一緒。
Publisher.kt
package book.application.domain import org.hibernate.annotations.LazyCollection import org.hibernate.annotations.LazyCollectionOption import javax.persistence.* /** * publisherテーブルのEntity. */ @Entity @Table(name = "publisher") class Publisher() : LikeTarget() { @Id @GeneratedValue var id: Long? = null private set @Column(nullable = false) lateinit var name: String private set /** * 出版している書籍リスト. */ @OneToMany(mappedBy = "publisher") // @LazyCollection(value = LazyCollectionOption.FALSE) ←不要です lateinit var books: MutableList<Book> /** * セカンダリコンストラクタ. * @param id 主キー * @param name 出版社名 * @param likeCount いいね数 * @param books 出版している書籍リスト */ constructor(id: Long? = null, name: String, likeCount: Int = 0, books: MutableList<Book> = mutableListOf()): this(){ this.id = id this.name = name this.likeCount = likeCount this.books = books } }
publisher
テーブルのEntity
です。
@OneToMany
に変わってるくらいです。
Book.kt
package book.application.domain import org.hibernate.annotations.LazyCollection import org.hibernate.annotations.LazyCollectionOption import javax.persistence.* /** * bookテーブルのEntity. */ @Entity @Table(name = "book") class Book() : LikeTarget() { @Id @GeneratedValue var id: Long? = null private set @Column(nullable = false) lateinit var title: String private set @Column var subTitle: String? = null private set @Column(nullable = false) lateinit var leadingSentence: String private set @Column(nullable = false) lateinit var imagePath: String private set @Column(nullable = false) lateinit var url: String private set /** * 著者リスト. */ @ManyToMany(mappedBy = "books") // @LazyCollection(value = LazyCollectionOption.FALSE) ←不要です lateinit var authors: MutableList<Author> private set /** * カテゴリリスト. */ @ManyToMany(mappedBy = "books") // @LazyCollection(value = LazyCollectionOption.FALSE) ←不要です lateinit var categories: MutableList<Category> private set /** * 出版社リスト. */ @ManyToOne @JoinTable(name = "publisher_book", joinColumns = arrayOf(JoinColumn(name = "book_id")), inverseJoinColumns = arrayOf(JoinColumn(name = "publisher_id"))) lateinit var publisher: Publisher /** * セカンダリコンストラクタ * @param id 主キー * @param title 書籍名 * @param subTitle 書籍の副題 ない場合はnull * @param leadingSentence リード文 * @param url リンク先URLパス * @param likeCount いいね数 * @param authors 著者リスト * @param categories カテゴリリスト * @param publisher 出版社リスト */ constructor(id: Long? = null, title: String, subTitle: String? = null, leadingSentence: String, imagePath: String, url: String, likeCount: Int = 0, authors: MutableList<Author> = mutableListOf(), categories: MutableList<Category> = mutableListOf(), publisher: Publisher = Publisher()) : this(){ this.id = id this.title = title this.subTitle = subTitle this.leadingSentence = leadingSentence this.imagePath = imagePath this.url = url this.likeCount = likeCount this.authors = authors this.categories = categories this.publisher = publisher } }
上記Entity
クラスを追加したので、それに合わせてBook.kt
を修正。
まず、LikeTarget
クラスを継承して実装クラスとします。
多対多のauthors
とcategories
には@ManyToMany(mappedBy = "books")
と、@LazyCollection
を付けます。
一方、publisher
には@ManyToOne
と@JoinTables
をアノテートして、多対1の関係と結合表を示します。
これでBook
クラスにAuthor
、Category
、Publisher
の各クラスが紐づけられました。
main.htmlの修正
h1まで省略 <dl th:each="book : ${books}" th:object="${book}"> <dt>Title</dt> <dd th:text="*{title}">本の名前</dd> <dt>SubTitle</dt> <dd th:if="*{subTitle != null}" th:text="*{subTitle}">サブタイトル</dd> <dt>LeadingSentence</dt> <dd th:text="*{leadingSentence}">リード文</dd> <dt>ImagePath</dt> <dd th:text="*{imagePath}">本当は画像を表示するぞ</dd> <dt>Author</dt> <dd th:each="author : *{authors}" th:text="${author.name}">著者の名前</dd> <dt>Category</dt> <dd th:each="category : *{categories}" th:text="${category.name}">カテゴリ名</dd> <dt>Publisher</dt> <dd th:text="*{publisher.name}">出版社名</dd> <dt>URL</dt> <dd><a href="#" th:href="*{url}" th:text="*{url}">どっかのURL</a></dd> </dl> </body> </html>
著者・カテゴリ・出版社を追加したので修正を行います。
著者とカテゴリはth:each
であるだけ表示しています。
著者・カテゴリ・出版社のEntity作成:Done
動かしてみる
※@LazyCollection
でアノテートしなくても問題ありません。下記のもう一回動かしてみたと同様の結果なります。 (2016/4/3 追記)
最初は前述の@LazyCollection
なしで動かしました。
はい、エラー。
LazyInitializationException
これで結構はまりました。
最初は@LazyCollection
アノテーションなしで書いていて、動かしてみると org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role:...
とエラーが...
初見の例外だったのとほとんどJPA触ったことなかったので、@ManyToMany
とか@JoinTable
らへんの指定が悪いのかなと思っていじってたのですが治らず。
スタックトレースを見返すとno session or session was closed
という一文が。あぁ、遅延初期化するときにHibernate
のSession
が閉じてるのかーと。
で、解決策をググるもうまくいかず。
詰んだわって感じだったので、IntelliJ助けてくれーと思って、適当にそれらしいアノテーションをcompletionで探してたら@LazayCollection
を発見。
動くに至りました。
IntelliJありがとう。たぶんeclipseでも出てくると思うけど。
まぁ、何が悪かったかというと上述の通りSession
閉じてる時に初期化をしようとしたためです。 Hibernate
はデフォルトでlazy initialize
をtrue
にしているっぽいです。
たぶん不要な初期化を遅らせることでパフォーマンス向上をはかるためだと思われます。
ということはほんとはfalse
にするべきではないのでは?って感じですけど、他に動かし方がよくわからないので一旦はこれでいきます。
他に解決方法を見つけたら修正します。
もう一回動かしてみた結果
SQLは冗長なので省略。
見た目があれなのは置いといて、表示はされてますね。
今回の目標は果たせました。
次回やること
・ページングの実装
・著者、カテゴリ、出版社のRepository作成とそれぞれの書籍リストの表示
あたりをやろうと思います。