a fledgling

駆け出しが駆け出してみる

Kotlin+SpringBootでWebアプリを作ってみる その2

加筆(2016/4/3 18時くらい)

はじめてのSpring Bootを書かれている槇さんから以下のツイートをいただきました。

・・・はい、Viewの確認をする前にテストコードで動かしてました。
確認した所、@LazyCollectionでアノテートしなくてもViewおよびControllerで問題なく動作していました。
つまり、lazy initializetrueで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図

f:id:KissyBnts:20160403134427p:plain

こんな感じで実装していきます。

authorcategoryの各テーブルの主キーと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クラスを継承して実装クラスとします。

多対多のauthorscategoriesには@ManyToMany(mappedBy = "books")と、@LazyCollectionを付けます。

一方、publisherには@ManyToOne@JoinTablesをアノテートして、多対1の関係と結合表を示します。

これでBookクラスにAuthorCategoryPublisherの各クラスが紐づけられました。

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という一文が。あぁ、遅延初期化するときにHibernateSessionが閉じてるのかーと。

で、解決策をググるもうまくいかず。
詰んだわって感じだったので、IntelliJ助けてくれーと思って、適当にそれらしいアノテーションをcompletionで探してたら@LazayCollectionを発見。
動くに至りました。
IntelliJありがとう。たぶんeclipseでも出てくると思うけど。

まぁ、何が悪かったかというと上述の通りSession閉じてる時に初期化をしようとしたためです。 Hibernateはデフォルトでlazy initializetrueにしているっぽいです。
たぶん不要な初期化を遅らせることでパフォーマンス向上をはかるためだと思われます。

ということはほんとはfalseにするべきではないのでは?って感じですけど、他に動かし方がよくわからないので一旦はこれでいきます。
他に解決方法を見つけたら修正します。

もう一回動かしてみた結果

f:id:KissyBnts:20160403134046p:plain

SQLは冗長なので省略。
見た目があれなのは置いといて、表示はされてますね。

今回の目標は果たせました。

次回やること

・ページングの実装
・著者、カテゴリ、出版社のRepository作成とそれぞれの書籍リストの表示
あたりをやろうと思います。