Kotlin/Maven/Thymeleaf/MyBatisFrameworkを用いたSpringBootプロジェクトの導入から立ち上げまで

Gradleで構築する例は見つかりますが、どうしてもMavenを使いたい場面が出てきたのでプロジェクトの構築から実行方法までまとめます。

作成したプロジェクト一式はこちらです。
https://github.com/ysd-marrrr/springboot-kt-mvn-sample-20201019


データベースを使うため別途MySQLの環境が必要です(接続できないとアプリケーションを起動することができません)
データベースを使いますが、開発環境内にDocker(`docker-compose`)を導入している場合は同梱の `docker-compose.yml` を使って立ち上げることができます。接続設定のデフォルト値はこの環境に合わせています。

```bash:同梱のデータベースを立ち上げる場合
cd docker-mysql
docker-compose up -d db
```
 
## SpringBootプロジェクトの作成: Spring Initilizr
 
Webの画面でプロジェクトを設定できる[Spring Initilizr](https://start.spring.io/)を使います。Spring Initilizrでは次のように設定します。
 
 * Language: Kotlin
 * Project Maven Project
 * Spring Boot: 2.3.4
 * Packaging: Jar
 * Java: 11
 
### Dependencies

Dependeciesは`ADD DEPENDENCIES`から追加できます。

 * Spring Boot DevTools
 * Thymeleaf
 * Spring Security
 * MyBatis Framework
 * MySQL Driver
 * Spring Web
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/1151c346-9a35-7ffa-40cf-44aa0cff2101.png)

`GENERATE`をクリックするとこの設定でSpringBootのプロジェクトがダウンロードされます。
解凍するとプロジェクトのディレクトリが出てきますが、 **ディレクトリ内は `.mvn` など隠しファイルが含まれるため** ファイル単位で移動しようとする場合は注意が必要です。
 
IntelliJ IDEAで開く際は"Open or Import"で先程のディレクトリを選択します。先程のディレクトリ内にIntelliJのプロジェクトファイルは含まれていませんが、自動で認識されます。

プロジェクトの読み込みとDependenciesの導入(右下に進捗が出ます)が進むと左側のProjectから `./src/`にアクセスできるようになります。`./src/main/kotlin/<Package name>/*Application.kt` を開いてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/6b177570-0a11-aac6-b98c-fe7fb536bd5c.png)


## IntelliJ IDEAでのプロジェクト初期設定

まずIntelliJを導入したばかりの環境ではJavaのSDKが必要になります。
導入していない場合は"Project SDK is not defined."と表示されるため、Setup SDK -> Install SDKからAdopt OpenJDK 11.08を導入します。

Adopt OpenJDKもHotspotかOpenJ9か選択できます。ここでは広く使われているHotspotを選択します。
詳しくは https://adoptopenjdk.net/index.html の"Help Me Choose"で説明しています(気になるようでしたら調べてみましょう🔍👀)
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/66e2f3f0-5d2c-28e5-eb51-eb11ab505b45.png)

 
次に **実行** してみたくなりますが、プロジェクトを立ち上げたばかりではRunボタンがグレーアウトしてクリックできません(もしくはテストしか実行できません)
`main()` 関数の行にあるボタン(▶)を1回クリックします。するとRunの選択肢が出るのでクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/257ec10c-1992-83ee-8362-80206b6cce93.png)


このあとは `Control + R` でSpringBootが起動できます。
ちなみに `mvn springboot:run`, 右側に表示されているMavenのパネルからPlugins->spring-boot->springboot:run でも起動できます。
 
 
アプリケーションを起動させると次のメッセージが出て止まりますが、 **データベースを使うDependenciesを設定しながらデータベースの設定がされていないため** なのでいったん無視します。

```
***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).


Process finished with exit code 0
```
 
 
## プロジェクトインポート後の立ち上げ
 
### データベースの設定: 設定ファイルはyamlを使う
 
先程のデータベースの設定は設定ファイルを介して行います。 

`./src/main/resources/applicaiton.properties` を編集するのですが、このファイルを`applicaiton.yaml` に名前を変えておきます。

設定ファイルは次の2種類が使え、resourcesディレクトリに配置すると自動で認識しますが、
 
 *  application.properties
 *  application.yml
 
yamlのほうが設定値の階層が視覚的にわかるのでおすすめです。こちらが2種類の書き方でデータベースの設定をした例になります。
 
```text:application.properties
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:13306}/${MYSQL_DB:sample_db}
spring.datasource.username=${MYSQL_USER:sample_user}
spring.datasource.password=${MYSQL_PASS:sample_password}
```

```yml:application.yaml
spring:
  jpa:
    hibernate:
      ddl-auto: update
  datasource:
    url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:13306}/${MYSQL_DB:sample_db}
    username: ${MYSQL_USER:sample_user}
    password: ${MYSQL_PASS:sample_password}
```

データベースのホスト・ポート・ユーザー・パスワード・データベース名はこの設定ファイルからわかります。
MySQLを使う場合は `url` が `jdbc:mysql://` から始まります。

`spring.jpa.hibernate.ddl-auto`は`update`にしておきます。詳しくは次の記事で説明されています。


### 環境変数の設定

設定ファイル内でところどころ `${MYSQL_HOST:127.0.0.1}` のような書き方をしていますが、これは実行時に

 * `MYSQL_HOST` の環境変数がセットされていたらその値
 * セットされていないのであれば `127.0.0.1`

を使うようにしています。

IntelliJで開発する場合、環境変数の設定はRunの設定を変更します。Edit Configulations...からEnvironment variablesから追加/削除できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/33b90c38-37d9-32f9-5ba5-3015b0a52cc7.png)



## データベース(MySQL)を使う

### Controllerの作成

`./src/main/kotlin/<Package name>/` 以下ならどこに配置しても動作しますが、controllerディレクトリ(パッケージ)などを作って整理します。

GETパラメーター (`?itemtype=`)は`@RequestParam`アノテーションを、
パスパラメーター(`/{id}`)は`@PathVariable`アノテーションを使います。

```kotlin:controller/itemsController.kt
package dev.ymix.springbootsamplemaven.controller

import dev.ymix.springbootsamplemaven.domain.Item
import dev.ymix.springbootsamplemaven.mapper.ItemMapper
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam

@Controller
@RequestMapping("/items")
class ItemsController(private val itemMapper: ItemMapper) {

    @GetMapping("/")
    fun listByType(@RequestParam itemType: String, model: Model) :String {
        val itemData: List<Item> = itemMapper.selectItemsByType(itemType)
        val typeData: List<Item> = itemMapper.selectTypes()

        model.addAttribute("itemType", itemType)
        model.addAttribute("itemData", itemData)
        model.addAttribute("typeData", typeData)
        return "items_list"
    }

    @GetMapping("/{id}")
    fun itemDetail(@PathVariable id: String, model: Model) :String {
        val itemData: Item = itemMapper.selectItem(id)

        model.addAttribute("itemData", itemData)
        println(itemData)
        return "items"
    }
}
```


### Domain, Mapperの作成

DBにアクセスするにはDomain, Mapperを作成します。

#### Domain

Domainには使用するデータベーステーブルのカラムとKotlinで使用するデータ型を指定します。

Javaで実装する際はgetter/setterを別途記述する必要がありますが、Kotlinでは自動で作成されるため記述する必要がありません。

Kotlinで(Javaと同様に)書く場合はメンバー変数の初期化が必要なため、初期値を設定します。初期値が未定の場合は型に `?` をつけてnullを入れます。

```kotlin:domain/Item.kt
package dev.ymix.springbootsamplemaven.domain

import java.util.*

class Item {
    var id: Int = 0
    var name: String = ""
    var type: String = ""
    var created_at: Date? = null
}
```

もしくはKotlinで採用されたヘッダーに入れる形のプライマリコンストラクターを使うと初期値の設定を省略できます。

```kotlin:domain/Item.kt
package dev.ymix.springbootsamplemaven.domain

import java.util.*

data class Item(var id: Int, var name: String, var type: String, var created_at: Date) {
}
```

ただし、「 `SELECT *` した結果から `SELECT DISTINCT(item)` した結果も取得したいけどDomainはそのまま流用したい」等という悪い（？）使い方はできなくなります。SELECTするカラムに応じてDomainを作成する必要があります。

> There was an unexpected error (type=Internal Server Error, status=500).
> Error attempting to get column 'type' from result set. Cause: java.sql.SQLDataException: Cannot determine value type from string 'ぶき' ; Cannot determine value type from string 'ぶき'; nested exception is java.sql.SQLDataException: Cannot determine value type from string 'ぶき'


#### Mapper(on Kotlin Class)

複雑なクエリーはXMLでMapperを作成することが推奨されますが、今回はKotlinのアノテーションでサクッと書きます。
Mapperには`select()`関数の返り値によって複数件取得するか1件のみ取得するか自動的に決定されます。間違えると実行時にエラーが発生します。

```kotlin:mapper/ItemMapper.kt
package dev.ymix.springbootsamplemaven.mapper

import dev.ymix.springbootsamplemaven.domain.Item
import org.apache.ibatis.annotations.Mapper
import org.apache.ibatis.annotations.Select

@Mapper
interface ItemMapper {
    @Select("""
        SELECT id, name, type, created_at
        FROM items
        WHERE id = #{id}
    """)
    fun selectItem(id: String): Item

    @Select("""
        SELECT DISTINCT(type)
        FROM items
    """)
    fun selectTypes(): List<Item>

    @Select("""
        SELECT id, name, type, created_at
        FROM items
        WHERE type = #{type}
    """)
    fun selectItemsByType(type: String): List<Item>
}
```


### 初期データ・スキーマの定義

アプリケーション起動時にテーブルを自動作成・初期データを投入することができます。

ただし、自動作成(初期データ投入)を常にするか・しないかの制御しかできないようで、重複して作成・投入しないようにするためにはSQL側で対応する必要があります(対応しないとアプリケーション起動時にエラーが発生します)
また、他のアプリケーションフレームワークでいう「マイグレーション」はFlywayなど別のパッケージを使う必要があります。

自動作成するテーブルは `./src/main/resources/schema.sql` で定義します。MySQLを使う場合は次のとおりです。

```sql:./src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(280),
    type VARCHAR(280),
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```

初期データは `./src/main/resources/data.sql`で定義します。MySQLを使う場合は次のとおりです。

```sql:./src/main/resources/data.sql
INSERT INTO items (id, name, type) VALUES(1, "ひのきのぼう", "ぶき") ON DUPLICATE KEY UPDATE id=id;
INSERT INTO items (id, name, type) VALUES(2, "きのいた", "ぼうぐ") ON DUPLICATE KEY UPDATE id=id;
INSERT INTO items (id, name, type) VALUES(3, "おもちゃのつるぎ", "ぶき") ON DUPLICATE KEY UPDATE id=id;
```

さらに、`./src/main/resources/application.yaml`を変更します。 `spring.datasource.initialization–mode`を `always`にします。

```yaml:./src/main/resources/application.yaml
spring:
  jpa:
    hibernate:
      ddl-auto: update
  datasource:
    initialization–mode: always
    url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:13306}/${MYSQL_DB:sample_db}
    username: ${MYSQL_USER:sample_user}
    password: ${MYSQL_PASS:sample_pass}
```

設定値の組みあわせでテーブルの自動作成・初期データの投入がどのように実行されるかは次の記事でまとめられています。

Springで初期データをdata.sqlで投入する方法 | 株式会社CONFRAGE ITソリューション事業部
https://confrage.jp/spring%E3%81%A7%E5%88%9D%E6%9C%9F%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92data-sql%E3%81%A7%E6%8A%95%E5%85%A5%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/


### Thymeleafを使う

`resources/templates` にテンプレートファイル(`*.html`)を配置すると認識されます。
Kotlinで実装したControllerからデータを渡す`addAttribute()`や使用するテンプレートの指定、Thymeleafの構文はJavaと同じように実装します。

ひとつだけ、IntelliJ IDEAでHTMLファイルを作成したあとHTMLの開始タグを

```html
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
```

に置き換えるとThymeleafのタグでエラーを吐きません(エラーが出なくなる程度です。IntelliJ IDEAがUltimateでないと補完などのサポートが効きません)


### 動作確認

ここまでできるとブラウザ上で動作確認できるはずです。アプリケーションをRunしたあとブラウザで `http://127.0.0.1:8080` にアクセスします。
Please sign inというメッセージが表示されたら後の"Spring Security"のセクションを見てユーザー名とパスワードを入力してアクセスしてみてください。

## プロジェクト立ち上げ時に導入したパッケージについて

### Spring Boot DevTools
 
Mavenのdependenciesに追加すると、アプリケーション実行中変更されたファイルに応じて自動でビルド・反映する機能など開発に便利なツールを提供します。
IntelliJ IDEAのRunから実行した場合はDevelopment、JARパッケージを作って起動させた場合はProductionとして扱われるため環境ごとにDevToolsの読み込み設定を書く必要はありません。
 
IntelliJ IDEAの初期設定ではファイルを変更しても自動的に反映されません。
次の設定を **両方とも** 変更する必要があります。
 
  * Shiftを2回押してRegistryを開き `compiler.automake.allow.when.app.running` にチェックを入れる
  * Preferencesを開きBuild->Compilerの `Build prpoject automatically`にチェックを入れる
 
 
### Spring Security
 
初期設定だとすべてのpathに対してパスワード認証がかかります。この状態でアプリケーションにアクセスするとPlease sign inというメッセージが表示されます。

ページごとにパスワード認証をするかなどの制御は別にクラスを作成して記述する必要があります。
 
 ユーザーは `user`、パスワードはSpringBoot起動時に生成されたものが(IntelliJ IDEAのRunに)表示されています(起動する度に生成されます)
 
```
 Using generated security password: 06042d52-7968-48c2-af15-c9dc575d6a8d
```

 

## 参考

IntelliJ IDEAでKotlinを始めてHello Worldする - Qiita
https://qiita.com/suin/items/e941aa249c235350b8eb

Kotlinでmybatis-spring-boot-starterを使う - Qiita
https://qiita.com/kazuki43zoo/items/9dfe35c47b08ce5f2ebb

spring.jpa.hibernate.ddl-auto に設定可能な値 - Qiita
https://qiita.com/KenjiOtsuka/items/8450c407ba121fea8151

(JavaですがMaven+SpringBootの構成でIntelliJ IDEAを使った例です)
IntelliJ IDEAを使ってSpring Bootのアプリケーションを作成する - Qiita
https://qiita.com/masayuki_200801/items/6a3be801211257934864

(AppCodeの画面ですが、IntelliJ IDEAの画面と同一です)
Environment variables and program arguments - Help | AppCode
https://www.jetbrains.com/help/objc/add-environment-variables-and-program-arguments.html

Intellij IDEAの環境にSpring Boot DevToolsを導入する
https://bsblog.casareal.co.jp/archives/2953
 
Spring Bootでデフォルトで表示されるログイン画面のユーザIDとパスワード - あるSEのつぶやき・改
https://www.aruse.net/entry/2019/02/10/153738