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作成とそれぞれの書籍リストの表示
あたりをやろうと思います。

Kotlin勉強会に行った話

Kotlin 1.0リリース記念勉強会 in 京都に行ってきました

ぶっちゃけ勉強会初参加。
期待と不安の入り混じった…

そんな感じで行きました。

以下雑すぎる所感まとめ。

Kotlinのいいとこは?

やっぱりnull safetyをあげてる方が多かった気がする。

次にextensionとかスムーズなJavaとの連携と移行、getter/setterが生えるのでコードがスッキリするとかかな?
バイバイ、Lombok...

あとはJetBrainsが作ってて、Androidに最近力入れてるしってのもあった。

知らなかったこと

javaからnullが渡されるとIllegalStateExceptionが発生する
JRebel
@JvmField
KotlinJavaScriptのRuntimeで動く(実験段階)
・ビルドツールKobaltがある(ver 0.683)
・ドキュメント生成エンジンdokkaがある(ver 0.97)
・関数に対してextensionが書ける
Javaメソッド参照はできない(System.out::printみたいのはNG)
Android関連ほとんど
ラムダ式は消せない!!!

全体を通して

まず思ったのは、1.0がリリースされたのがこの前なので当たり前ですが、発展しはじめたばっかりの言語なんだなぁってことですね。
ベストプラクティスはまだわからないけど、自分はこうしてます。ってのも多かったし、ここはKotlinでは無理・厳しいとかもあった。

Kotlinサイコーってのばっかなのかな?と思ってたけど、ダメなとこはダメだから別で代替しましょうだったり、issue投げようって感じでいいなぁと思いました。

僕はJavaよりKotlinの方が圧倒的に好きなので、Androidだけでなく、そこ以外でも使われるように成長していって欲しいなとか思いました。
あとはAndroid開発やってみたくなったような気がしないでもないです。

発表者さんと発表資料

こざけさん : SIerアーキテクト視点でみたKotlinの紹介

きの子さん : Kotlinはじめてみました

nobuokaさん : Java EE アプリケーションと Kotlin

俺九番さん : Spring Boot + Kotlin 劇的ビフォーアフター

たくじさん : KotlinとモダンなライブラリーでAndroidアプリを作るっ

やんくさん : Kotlinこんなん出ましたけど

山本裕介さん : Dataクラスから始めるKotlin / JetBrains行ってきたよ!

たろうさん : 攻める!ラムダ式禁止おじさん

最後に

勉強会初めて行きましたが、楽しかったです。
関西圏で興味のある勉強会があればどんどん参加していこうと思います。

ちなみにKotlin勉強会は定期開催(歓喜)されるそうなので今後もお邪魔させていただきたいと。

運営・発表者の皆さん、会場等提供してくださったはてなさん、ありがとうございました!

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

lateinit

昨日、Kotlin+SpringBootでWebアプリを作ってみる その1を投稿してから30分くらいでこんなツイートが。

やんくさんや… てか早い。

ということでlateinitで書き換えてみる。

Book.kt

package book.application.domain

import javax.persistence.*

/**
 * bookテーブルのEntity.
 */
@Entity
@Table(name = "book")
class Book() {
    @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

    /**
     * セカンダリコンストラクタ
     * @param id 主キー
     * @param title 書籍名
     * @param subTitle 書籍の副題 ない場合はnull
     * @param leadingSentence リード文
     * @param url リンク先URLパス
     */
    constructor(id: Long? = null, title: String, subTitle: String? = null, leadingSentence: String, imagePath: String, url: String) : this(){
        this.id = id
        this.title = title
        this.subTitle = subTitle
        this.leadingSentence = leadingSentence
        this.imagePath = imagePath
        this.url = url
    }
}

んー、こうかな?
プリミティブ型、nullablelateinitできないから普通にプロパティを書いた。
こっちの方がgetter, setterのアクセス制御ができるのでいいですね。カスタムは持てないようですが。

所感

上記のコードでは、idsubTitleのプロパティ定義が微妙な感じがしますが、lateinitを使う方が良さそうな感じです。
微妙なところはもっと勉強しないと。

それにしても、稚拙でも書いて公開してみるものですね。
まさかフォローしてる方に読んでもらえて軽くコメントしてもらえるとは...!

P.S

4/2(土)Kotlin 1.0リリース勉強会 in 京都 に参加します。
Kotlin初心者なりに学んだこと、感じたことを書こうと思っています。

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

どうもKotlinが楽しそうなので、ためしにWebアプリケーションを作成してみます。
フレームワークは(ほぼ触ったこと無いですが)Spring Bootを使います。

作る予定のもの

・一覧表示、検索機能(今回は本)
・ログイン、ユーザ機能
・いいね的なの
・新着、ランキングとか

KotlinもSpring Bootもチュートリアルレベルなので大変そうですが...
今回はメインクラスの作成と本のエンティティ作成くらいまで。

環境とか概要

・IDEA: IntelliJ IDEA 2016
・Spring Boot Ver: 1.3.3
・Project Name: BookApplication
・Build Tool: Gradle (Ver 2.9)
・Language: Kotlin (Ver 1.0.0)
・DB: MySQL (Ver 5.1.38)

テーブル構造

今回は本のテーブルのみを作成。
UML図はこんな感じ。
f:id:KissyBnts:20160329001134p:plain

ソース

build.gradle

buildscript {
    ext {
        springBootVersion = '1.3.3.RELEASE'
        springLoadedVersion = '1.2.5.RELEASE'
        kotlinVersion = '1.0.0'
        mysqlVersion = '5.1.38'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath "org.springframework:springloaded:${springLoadedVersion}"
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
    }
}

apply plugin: 'kotlin'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'

jar {
    baseName = 'bookapplication'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
    compile("mysql:mysql-connector-java:${mysqlVersion}")
    testCompile('org.springframework.boot:spring-boot-starter-test')
}


eclipse {
    classpath {
         containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
         containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.9'
}

ビルドファイル。
Spring InitializrからProjectを作成し、Library選択でWeb, JPA, Thymeleafを選択して生成。
追加でSpringLoadedとMySQLコネクタを。
SpringLoadedはホットデプロイに必要な感じ。

application.yml

spring:
# DataSource
  datasource:
    url: jdbc:mysql://localhost/book_application
    username: user
    password: user
    driver-class-name: com.mysql.jdbc.Driver
# JPA
  jpa:
    hibernate:
      ddl-auto: update
      show-sql: true
# Thymeleaf
  thymeleaf:
    cache: false

設定ファイル。propertiesファイルは好きじゃないのでYAMLで書きます。

hibernateddl-autoは、
update: テーブルが存在しなければ作成する。存在し、マッピング情報が同じならばそのまま使用。マッピング情報に変更があればテーブル構成をアップデートする。
create: アプリ起動時にテーブルを破棄して新規にテーブルを作成する。そのためアプリ終了後は再度起動するまでデータは残る。
create-drop: アプリ起動時にテーブルを作成、アプリ終了時にテーブルを破棄する。
none: たぶん何もしない。

あとはthymeleafのキャッシュを切って変更が即時反映されるようにしています。

BookApplication.kt

package book.application

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
open class BookApplication{
    companion object {
        @JvmStatic fun main(args: Array<String>) {
            SpringApplication.run(BookApplication::class.java, *args)
        }
    }
}

Book.kt

package book.application.domain

import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table

/**
 * bookテーブルのEntity.
 * @param id 主キー
 * @param title 書籍名
 * @param subTitle 書籍の副題 ない場合はnull
 * @param leadingSentence リード文
 * @param url リンク先URLパス
 */
@Entity
@Table(name = "book")
data class Book(@Id @GeneratedValue var id: Long? = null,
                @Column(nullable = false) var title: String = "",
                @Column var subTitle: String? = null,
                @Column(nullable = false) var leadingSentence: String = "",
                @Column(nullable = false) var imagePath: String = "",
                @Column(nullable = false) var url: String = "") {
}

bookテーブルのエンティティクラス。
default constructor (引数なしのコンストラクタ)が無いとエラーを吐くので、全てのフィールドに初期値を設定しています。
lateinit @Column var ...でも書けますが、セカンダリコンストラクタを書かないといけなくなるのと、プリミティブ型、nullableが書けなくなるのでこちらがいいかなと思ってます。
もっと良い書き方があれば教えて欲しいです。

BookRepository.kt

package book.application.repository

import book.application.domain.Book
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

/**
 * bookテーブルのRepository.
 */
@Repository
interface BookRepository : JpaRepository<Book, Long> {
}

bookテーブルのリポジトリインターフェイス
メソッドはのちのち定義していきます。

BookService.kt

package book.application.service

import book.application.domain.Book
import book.application.repository.BookRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

/**
 * DBからのデータ取得と加工を行う.
 */
@Service
class BookService @Autowired constructor(private val bookRepository: BookRepository) {

    /**
     * 全書籍リストの取得
     * @return 書籍リスト
     */
    fun findAllBook(): MutableList<Book> = bookRepository.findAll()
}

現状bookテーブルしかないので簡易のサービスクラスを作りました。
クラス名は変えないといけない気がしますね。

BookServiceTest.kt

package book.application.service

import book.application.BookApplication
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner
import org.springframework.test.context.web.WebAppConfiguration
import org.hamcrest.CoreMatchers.`is` as be

@RunWith(SpringJUnit4ClassRunner::class)
@SpringApplicationConfiguration(BookApplication::class)
@WebAppConfiguration
class BookServiceTest{
    @Autowired lateinit var bookService: BookService

    @Test
    fun findAllBookTest() {
        var sutList = bookService.findAllBook()[f:id:KissyBnts:20160329000402p:plain]
        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"))
    }
}

INSERT INTO book (title, sub_title, leading_sentence, image_path, url) VALUES ('title', 'subTitle', 'leading', 'imagePath', 'url')が実行されている体でのテスト。(@Beforeでちゃんと書かないといけないですが...)

hamcrestisがKotlinの予約語なのでbeに置き換えています。ここはちょっと微妙ですね。

あとは@Autowiredを使えるようにするためにいろいろアノテーションを書かないといけないです。

実行

f:id:KissyBnts:20160329000114p:plain
うん、ちゃんと実行・取得できてる。
実際はJUnit@Autowiredするのにかなり躓きましたが...

DB使ったテストコードはそのうちちゃんと書きます。
(職場がテストコードを書かない感じなので、JUnit実践入門を使ってちょっとずつ勉強している...)

所感としては、KotlinらしいこともSpring Bootっぽいこともあんまりしてないですが、
仕事で触ってるstrutsとJava6という悲しい組み合わせの万倍
かなり楽しそうな感じがします。

Kotlinはnullに対して堅牢なのがいいですね。
ラムダ書きやすいし、スマートキャストもいい。
スコープ関数も使いやすくて好きです。

あとSwiftに似てるので、Kotlinがある程度書けるようになったらswiftも書いてみようと思います。

次回

・Controllerと一覧画面を作成
・著者、出版社、カテゴリのテーブル・エンティティの作成