a fledgling

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

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と一覧画面を作成
・著者、出版社、カテゴリのテーブル・エンティティの作成