Kotlin
spring
Doma
spring-boot
SpringBoot

サーバーサイドKotlin (Spring Boot / Doma 2) 入門

この記事は エムスリー Advent Calendar 2017 の18日目の記事です。

今年はKotlinが熱い1年でした!今年の5月に、Googleがandroid開発言語としてKotlinを公式にサポートするとアナウンスしてから、急速に利用が広がっているようですね:arrow_heading_up:

KotlinAdoption.png

出典:https://blog.jetbrains.com/jp/2017/11/29/828

私が所属するエムスリーでも

とKotlinに関する話題が盛り沢山の年となりました:sunny:

KotlinはJetBrains社が開発したいわゆるJVM言語です。型推論・Null Safety・便利な標準リスト操作関数などモダンな開発言語の特徴を備えつつ、既存のJavaコードともシームレスに相互運用できることが特徴です。Javaの有名なWebアプリケーションフレームワークであるSpring Bootも特に問題なく使うことができます。

この記事では、KotlinをSpring BootとDoma2(Java製のDBアクセスライブラリ)とともに使って、簡単なCRUDアプリケーションを立ち上げる手順を紹介します。まだサーバーサイドKotlinに触れたことが無い方は是非この機会に触ってみてもらえると嬉しいです!なお、本記事で作るサンプルのソースコードはgithubにあげています

エディタ: IntelliJ IDEA
Kotlin 1.1.61, Spring Boot 1.5.9, Doma 2.19.0
* Kotlinは2017/12/16現在、spring initializrでDLしたバージョン

Hello World! (たった3ステップ!)

まずはhello!と表示するREST APIを作ってみます。spring initializrを使うと、とても簡単です!

1. spring initializrからひな形をダウンロード

以下のサイトにアクセスし、所定の項目を選択した上で、「Generate Project」ボタンを押してzipをダウンロードしてください。

https://start.spring.io

  • Gradle、Kotlin、Spring Boot 1.5.9
  • Project Metadata: 好みに合わせて入力ください
  • Dependencies: Web、DevTools、Thymeleaf

スクリーンショット 2017-12-17 22.33.58.png

2. controllerを作る

ダウンロードしたzipを解凍。IntelliJ IDEAでOpenを選択し、解凍したディレクトリを選択します

スクリーンショット 2017-12-17 22.57.16.png

左メニューのファイルを作成したいフォルダにカーソルをあわせ、右クリック => New => Kotlin File/Classと選択します

スクリーンショット 2017-12-17 22.41.54.png

NameをHomeController。KindをClassにしてOK

スクリーンショット 2017-12-18 11.48.58.png

hello!と返すRestControllerを作成します。@RestController@GetMappingはspringframeworkが提供するアノテーションです

src/main/kotlin/com/example/kotlinspringbootdomademo/HomeController.kt
package com.example.kotlinspringbootdomademo

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HomeController {
    @GetMapping("")
    fun index(): String = "hello!"
}

3. 起動コマンドを叩く

$ ./gradlew clean bootRun

http://localhost:8080 にアクセスすればhello!と表示されます!

Thymeleafを使って、home画面を作成

今度はh1タグにhello Kotlin!と表示するhome画面を作成していきます。なお、htmlテンプレートにはThymeleafを使います

まずは、htmlを作成します。src/main/resources/templates/home.htmlというファイルを作成し、messageという変数をh1に展開するようにしておきます

src/main/resources/templates/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
</head>
<body>
    <h1 th:text="${message}"></h1>
</body>
</html>

次に、先程作ったHomeControllerを修正します

src/main/kotlin/com/example/kotlinspringbootdomademo/HomeController.kt
+import org.springframework.stereotype.Controller
+import org.springframework.ui.Model
 import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.RestController

-@RestController
+@Controller
 class HomeController {
     @GetMapping("")
-    fun index(): String = "hello!"
+    fun index(model: Model): String {
+        model.addAttribute("message", "hello Kotlin!")
+        return "home"
+    }

ビルドしなおして、 http://localhost:8080 にアクセスすると、h1タグにhello Kotlin!と表示されます!

スクリーンショット 2017-12-17 23.19.56.png

ついでに、CSSも使って背景を赤くしてみましょう。cssファイルを作成します

src/main/resources/static/css/app.css
body {
    background-color: red;
}

作成したcssをhtmlから読み込みます

src/main/resources/templates/home.html
 <head>
     <meta charset="UTF-8" />
     <title>Title</title>
+    <link th:href="@{/css/app.css}" rel="stylesheet" />
 </head>

背景が赤くなればOKです

スクリーンショット 2017-12-17 23.20.45.png

DB(Postgresql、Docker)の準備

さて、ここからはDBアクセスです。インメモリDBのH2を使ってもよいのですが、今回はDockerでpostgresqlを立てることにします。以下のファイルを作成してください

docker-compose.yml
version: "3"
services:
  postgres:
    build: ./docker/postgres/
    image: kotlin-demo-postgres
    ports:
      - 5432:5432
    container_name: kotlin-demo-postgres-container
docker/postgres/Dockerfile
FROM postgres:9.5.3

COPY ./docker-entrypoint-initdb.d /docker-entrypoint-initdb.d
docker/postgres/docker-entrypoint-initdb.d/init-db.sh
#!/bin/bash
set -e

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
    CREATE DATABASE kotlindemo;
EOSQL

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" kotlindemo <<-EOSQL
    BEGIN;

    create table customer (
        id serial,
        name varchar(50),
        email varchar(50)
    );

    insert into customer(name, email) values ('hoge', 'hoge@example.com');
    insert into customer(name, email) values ('fuge', 'fuge@example.com');
    insert into customer(name, email) values ('piyo', 'piyo@example.com');

    COMMIT;
EOSQL

起動します

$ docker-machine start
$ eval $(docker-machine env default)
$ docker-compose up

コンテナにアクセスして、初期データが入っていることを確認できればOKです

$ eval $(docker-machine env default)
$ docker exec -it kotlin-demo-postgres-container psql -U postgres kotlindemo

kotlindemo=# select * from customer;
 id | name |      email
----+------+------------------
  1 | hoge | hoge@example.com
  2 | fuge | fuge@example.com
  3 | piyo | piyo@example.com
(3 rows)

Doma 2でDBアクセス

DBアクセスには、Doma 2というライブラリを使います。Java製のライブラリで、2-way SQL(SQLファイルを外部ファイル化できる)のが特徴です(詳しくは公式のドキュメントをご覧ください)。ちなみに、私達のチームでは、Domaの開発で大切にしている10のことという開発方針がよいと思ったのと、社内のJavaプロジェクトで導入実績があったことからDoma 2を採用しました。

なお、Doma2はKotlin1.1.2を実験的にサポートしているのですが、私のチームでは、将来Kotlinのバージョンが上がっていった際にハマるリスクを回避するために「Domaの層は全てJavaで書く」という選択をしています(本記事で紹介するようにDomain層でKotlinにマッピングすれば、実際に触るコードは殆どKotlinになります)。今回も同じように「Domaの層は全てJavaで書く」という選択をとります

まずは依存ライブラリを入れます。doma-spring-boot-starterを使うことで、Spring BootへのDomaの導入が簡単になります。

build.gradle
        ext {
                kotlinVersion = '1.1.61'
                springBootVersion = '1.5.9.RELEASE'
+               postgresqlVersion = '42.1.4'
+               springBootDomaVersion = '1.1.1'
+               domaVersion = '2.19.0'
        }
        repositories {
                mavenCentral()
@@ -32,6 +35,12 @@ repositories {
        mavenCentral()
 }

+// -------------------------------------------
+// for Doma2 with gradle
+// see: http://doma.readthedocs.io/ja/stable/build/#gradle
+// -------------------------------------------
+processResources.destinationDir = compileJava.destinationDir
+compileJava.dependsOn processResources

 dependencies {
        compile('org.springframework.boot:spring-boot-starter-thymeleaf')
@@ -40,4 +49,15 @@ dependencies {
        compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
        runtime('org.springframework.boot:spring-boot-devtools')
        testCompile('org.springframework.boot:spring-boot-starter-test')
+
+       // -------------------------------------------
+       // postgresql
+       // -------------------------------------------
+       compile "org.postgresql:postgresql:${postgresqlVersion}"
+
+       // -------------------------------------------
+       // Doma2
+       // -------------------------------------------
+       compile "org.seasar.doma.boot:doma-spring-boot-starter:${springBootDomaVersion}"
+       compile "org.seasar.doma:doma:${domaVersion}"
 }

application.ymlにdomaがpostgresを使うことを記載します。(application.propertiesはyml形式にしたいので、ファイル名を変更しておきます)

$ mv src/main/resources/application.properties src/main/resources/application.yml
src/main/resources/application.yml
doma:
  dialect: postgres

DomaのEntity(Java)を作成します。

src/main/java/com/example/kotlinspringbootdomademo/infrastructure/doma/entity/CustomerDomaEntity.java
package com.example.kotlinspringbootdomademo.infrastructure.doma.entity;

import org.seasar.doma.*;
import org.seasar.doma.jdbc.entity.NamingType;

@Entity(naming = NamingType.SNAKE_UPPER_CASE)
@Table(name = "customer")
public class CustomerDomaEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Integer id;

    public String name;

    public String email;
}

DomaのDaoインターフェース(Java)を作成します

src/main/java/com/example/kotlinspringbootdomademo/infrastructure/doma/dao/CustomerDomaDao.java
package com.example.kotlinspringbootdomademo.infrastructure.doma.dao;

import com.example.kotlinspringbootdomademo.infrastructure.doma.entity.CustomerDomaEntity;
import org.seasar.doma.Dao;
import org.seasar.doma.Select;
import org.seasar.doma.boot.ConfigAutowireable;

import java.util.List;

@ConfigAutowireable
@Dao
public interface CustomerDomaDao {
    @Select
    List<CustomerDomaEntity> selectAll();
}

SQLファイルを作成します

src/main/resources/META-INF/com/example/kotlinspringbootdomademo/infrastructure/doma/dao/CustomerDomaDao/selectAll.sql
select
    /*%expand*/*
from
    customer
order by
    id desc

ここまででがDomaのコード(Javaで書く部分)となります。補足ですが、既存DBが数100テーブルある場合、DomaのEntity、Dao、SQLを手動で作成していくのは骨の折れる作業です。Doma-Genを使うとこれらを自動生成することができます。

さて、ここからはKotlinです

ドメインのmodelを作成します

src/main/kotlin/com/example/kotlinspringbootdomademo/domain/model/Customer.kt
package com.example.kotlinspringbootdomademo.domain.model

data class Customer(val id: Int? = null, val name: String, val email: String)

Repositoryのインターフェースを作成します

src/main/kotlin/com/example/kotlinspringbootdomademo/domain/repository/CustomerRepository.kt
package com.example.kotlinspringbootdomademo.domain.repository

import com.example.kotlinspringbootdomademo.domain.model.Customer

interface CustomerRepository {
    fun findAll(): List<Customer>
}

Repositoryの実装を作成します。ここでDomaのEntity(Java)をドメインのModel(Kotlin)に詰め替えます

src/main/kotlin/com/example/kotlinspringbootdomademo/infrastructure/domarepository/CustomerRepositoryDomaImpl.kt
package com.example.kotlinspringbootdomademo.infrastructure.domarepository

import com.example.kotlinspringbootdomademo.domain.model.Customer
import com.example.kotlinspringbootdomademo.domain.repository.CustomerRepository
import com.example.kotlinspringbootdomademo.infrastructure.doma.dao.CustomerDomaDao
import com.example.kotlinspringbootdomademo.infrastructure.doma.entity.CustomerDomaEntity
import org.springframework.stereotype.Repository

@Repository
class CustomerRepositoryDomaImpl(
        private val customerDomaDao: CustomerDomaDao
): CustomerRepository {
    override fun findAll(): List<Customer> {
        return customerDomaDao.selectAll().map { _mapToModel(it) }
    }

    // ここでDomaのEntity(Java)をドメインのModel(Kotlin)に詰め替える
    private fun _mapToModel(domaEntity: CustomerDomaEntity): Customer {
        return Customer(
                id = domaEntity.id,
                name = domaEntity.name,
                email = domaEntity.email
        )
    }
}

Controllerを作成します

src/main/kotlin/com/example/kotlinspringbootdomademo/application/controller/web/CustomerController.kt
package com.example.kotlinspringbootdomademo.application.controller.web

import com.example.kotlinspringbootdomademo.domain.repository.CustomerRepository
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping

@Controller
@RequestMapping("/customers")
class CustomerController(
        private val customerRepository: CustomerRepository
) {
    @GetMapping("")
    fun index(model: Model): String {
        val customers = customerRepository.findAll()
        model.addAttribute("customers", customers)
        return "customers/index"
    }
}

htmlを作成します

src/main/resources/templates/customers/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <link th:href="@{/css/app.css}" rel="stylesheet" />
</head>
<body>
    <h1>顧客一覧</h1>
    <ul th:unless="${customers.isEmpty()}">
        <li th:each="customer: ${customers}">
            <span th:text="${customer.id}"></span>
            <span th:text="${customer.name}"></span>
            <span th:text="${customer.email}"></span>
        </li>
    </ul>
</body>
</html>

最後にSpring Bootがデータベースに接続するための環境変数を設定

.env.dev
DOCKER_IP=$(docker-machine ip)

export SPRING_DATASOURCE_URL=jdbc:postgresql://${DOCKER_IP}:5432/kotlindemo
export SPRING_DATASOURCE_USERNAME=postgres
export SPRING_DATASOURCE_PASSWORD=
export SPRING_DATASOURCE_DRIVER_CLASS_NAME="org.postgresql.Driver"

環境変数を読み込んだうえで、起動します

$ source .env.dev
$ ./gradlew clean bootRun

http://localhost:8080/customers にアクセスしてDBの中身が表示されればOKです!

スクリーンショット 2017-12-18 1.23.09.png

詳細ページ

一覧ページを作成することができたので、今度は詳細ページを作成していきます

domaのDaoインターフェース

src/main/java/com/example/kotlinspringbootdomademo/infrastructure/doma/dao/CustomerDomaDao.java
 public interface CustomerDomaDao {
     @Select
     List<CustomerDomaEntity> selectAll();
+
+    @Select
+    CustomerDomaEntity selectById(int id);
 }

domaのSQL

src/main/resources/META-INF/com/example/kotlinspringbootdomademo/infrastructure/doma/dao/CustomerDomaDao/selectById.sql
select
    /*%expand*/*
from
    customer
where
    id = /* id */1

repository(インターフェース)

src/main/kotlin/com/example/kotlinspringbootdomademo/domain/repository/CustomerRepository.kt
 interface CustomerRepository {
     fun findAll(): List<Customer>
+    fun findById(id: Int): Customer?

repository(実装)

src/main/kotlin/com/example/kotlinspringbootdomademo/infrastructure/domarepository/CustomerRepositoryDomaImpl.kt
+    override fun findById(id: Int): Customer? {
+        return customerDomaDao.selectById(id)?.let { _mapToModel(it) }
+    }

controller

src/main/kotlin/com/example/kotlinspringbootdomademo/application/controller/web/CustomerController.kt
+import com.example.kotlinspringbootdomademo.application.RecordNotFoundException
 import com.example.kotlinspringbootdomademo.domain.repository.CustomerRepository
 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

 @Controller
@@ -17,4 +19,14 @@ class CustomerController(
         model.addAttribute("customers", customers)
         return "customers/index"
     }
+
+    @GetMapping("{id}")
+    fun show(
+            @PathVariable id: Int,
+            model: Model
+    ): String {
+        val customer = customerRepository.findById(id) ?: throw RecordNotFoundException()
+        model.addAttribute("customer", customer)
+        return "customers/show"
+    }

idがなかった場合のExceptionを追加

src/main/kotlin/com/example/kotlinspringbootdomademo/application/exceptions.kt
package com.example.kotlinspringbootdomademo.application

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus

@ResponseStatus(HttpStatus.NOT_FOUND)
class RecordNotFoundException(): RuntimeException()

htmlを作成

src/main/resources/templates/customers/show.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <link th:href="@{/css/app.css}" rel="stylesheet" />
</head>
<body>
    <h1 th:text="${customer.name}"></h1>
    <span th:text="${customer.email}"></span>
</body>
</html>

http://localhost:8080/customers/1 にアクセスして詳細情報が表示されればOKです

スクリーンショット 2017-12-18 2.07.58.png

作成ページ

このまま作成ページも作成してみましょう。

domaのdaoインターフェース

src/main/java/com/example/kotlinspringbootdomademo/infrastructure/doma/dao/CustomerDomaDao.java
@@ -1,7 +1,9 @@
 package com.example.kotlinspringbootdomademo.infrastructure.doma.dao;

 import com.example.kotlinspringbootdomademo.infrastructure.doma.entity.CustomerDomaEntity;
 import org.seasar.doma.Dao;
+import org.seasar.doma.Insert;
 import org.seasar.doma.Select;
 import org.seasar.doma.boot.ConfigAutowireable;

@@ -15,4 +17,7 @@ public interface CustomerDomaDao {

     @Select
     CustomerDomaEntity selectById(int id);
+
+    @Insert
+    int insert(CustomerDomaEntity entity);
 }

repository(インターフェース)

src/main/kotlin/com/example/kotlinspringbootdomademo/domain/repository/CustomerRepository.kt
 interface CustomerRepository {
     fun findAll(): List<Customer>
     fun findById(id: Int): Customer?
+    fun create(customer: Customer): Int
 }

repository(実装)。ここでドメインのModel(Kotlin)をDomaのEntity(Java)をに詰め替える

src/main/kotlin/com/example/kotlinspringbootdomademo/infrastructure/domarepository/CustomerRepositoryDomaImpl.kt
@@ -18,6 +18,12 @@ class CustomerRepositoryDomaImpl(
         return customerDomaDao.selectById(id)?.let { _mapToModel(it) }
     }

+    override fun create(customer: Customer): Int {
+        val domaEntity = _mapToDomaEntity(customer)
+        customerDomaDao.insert(domaEntity)
+        return domaEntity.id
+    }
+
     private fun _mapToModel(domaEntity: CustomerDomaEntity): Customer {
         return Customer(
                 id = domaEntity.id,
@@ -25,4 +31,12 @@ class CustomerRepositoryDomaImpl(
                 email = domaEntity.email
         )
     }
+
+    // ここでドメインのModel(Kotlin)をDomaのEntity(Java)をに詰め替える
+    private fun _mapToDomaEntity(customer: Customer): CustomerDomaEntity {
+        return CustomerDomaEntity().also {
+            it.id = customer.id
+            it.name = customer.name
+            it.email = customer.email
+        }
+    }

applicationservice層を追加(@Transactionalでトランザクション境界を宣言)

src/main/kotlin/com/example/kotlinspringbootdomademo/application/service/CustomerApplicationService.kt
package com.example.kotlinspringbootdomademo.application.service

import com.example.kotlinspringbootdomademo.application.input.CustomerInput
import com.example.kotlinspringbootdomademo.domain.model.Customer
import com.example.kotlinspringbootdomademo.domain.repository.CustomerRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class CustomerApplicationService(
        private val customerRepository: CustomerRepository
) {
    fun create(customerInput: CustomerInput): Int {
        val customer = Customer(
                name = customerInput.name!!,
                email = customerInput.email!!
        )

        return customerRepository.create(customer)
    }
}

ユーザーの入力を表現するクラスを作成

src/main/kotlin/com/example/kotlinspringbootdomademo/application/input/CustomerInput.kt
package com.example.kotlinspringbootdomademo.application.input

import org.hibernate.validator.constraints.NotBlank
import javax.validation.constraints.Size

class CustomerInput {
    @NotBlank
    @Size(max = 20)
    var name: String? = null

    @NotBlank
    @Size(max = 50)
    var email: String? = null
}

controller

kotlin
+import com.example.kotlinspringbootdomademo.application.input.CustomerInput
+import com.example.kotlinspringbootdomademo.application.service.CustomerApplicationService
 import com.example.kotlinspringbootdomademo.domain.repository.CustomerRepository

class CustomerController(
-        private val customerRepository: CustomerRepository
+        private val customerRepository: CustomerRepository,
+        private val customerApplicationService: CustomerApplicationService
) {

+    @GetMapping("new")
+    fun new(input: CustomerInput): String {
+        return "customers/new"
+    }
+
+    @PostMapping("")
+    fun create(
+            @Validated customerInput: CustomerInput,
+            bindingResult: BindingResult
+    ): String {
+        if(bindingResult.hasErrors()) {
+            return "customers/new"
+        }
+
+        val id = customerApplicationService.create(customerInput)
+
+        return "redirect:/customers/${id}"
+    }

htmlを作成

src/main/resources/templates/customers/new.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <link th:href="@{/css/app.css}" rel="stylesheet" />
</head>
<body>
    <h1>顧客作成</h1>
    <form th:method="post" th:action="@{./}" th:object="${customerInput}">
        氏名: <input type="text" th:field="*{name}" />
        <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></p>

        email: <input type="text" th:field="*{email}" />
        <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></p>

        <input type="submit" value="作成" />
    </form>
</body>
</html>

http://localhost:8080/customers/new にアクセス

スクリーンショット 2017-12-18 2.15.11.png

間違った入力をして「作成」ボタンを押すとエラーメッセージが表示されます

スクリーンショット 2017-12-18 2.44.14.png

正しい値を入力して「作成」を押すと、データが作成されている様子を確認できればOKです

編集ページ

最後に編集ページを作成します

domaのdaoインターフェース

src/main/java/com/example/kotlinspringbootdomademo/infrastructure/doma/dao/CustomerDomaDao.java
@@ -4,6 +4,7 @@ import com.example.kotlinspringbootdomademo.infrastructure.doma.entity.CustomerD
 import org.seasar.doma.Dao;
 import org.seasar.doma.Insert;
 import org.seasar.doma.Select;
+import org.seasar.doma.Update;
 import org.seasar.doma.boot.ConfigAutowireable;

 import java.util.List;
@@ -19,4 +20,7 @@ public interface CustomerDomaDao {

     @Insert
     int insert(CustomerDomaEntity entity);
+
+    @Update
+    int update(CustomerDomaEntity entity);
 }

repositoryインターフェース

src/main/kotlin/com/example/kotlinspringbootdomademo/domain/repository/CustomerRepository.kt
@@ -6,4 +6,5 @@ interface CustomerRepository {
     fun findAll(): List<Customer>
     fun findById(id: Int): Customer?
     fun create(customer: Customer): Int
+    fun update(customer: Customer)

repository実装

src/main/kotlin/com/example/kotlinspringbootdomademo/infrastructure/domarepository/CustomerRepositoryDomaImpl.kt
@@ -24,6 +24,11 @@ class CustomerRepositoryDomaImpl(
         return domaEntity.id
     }

+    override fun update(customer: Customer) {
+        val domaEntity = _mapToDomaEntity(customer)
+        customerDomaDao.update(domaEntity)
+    }

applicationserviceを修正

src/main/kotlin/com/example/kotlinspringbootdomademo/application/service/CustomerApplicationService.kt
@@ -1,5 +1,6 @@
 package com.example.kotlinspringbootdomademo.application.service

+import com.example.kotlinspringbootdomademo.application.RecordNotFoundException
 import com.example.kotlinspringbootdomademo.application.input.CustomerInput
 import com.example.kotlinspringbootdomademo.domain.model.Customer
 import com.example.kotlinspringbootdomademo.domain.repository.CustomerRepository
@@ -11,6 +12,14 @@ import org.springframework.transaction.annotation.Transactional
 class CustomerApplicationService(
         private val customerRepository: CustomerRepository
 ) {
+    fun findAll(): List<Customer> {
+        return customerRepository.findAll()
+    }
+
+    fun findById(id: Int): Customer {
+        return customerRepository.findById(id) ?: throw RecordNotFoundException()
+    }
+
     fun create(customerInput: CustomerInput): Int {
         val customer = Customer(
                 name = customerInput.name!!,
@@ -19,4 +28,15 @@ class CustomerApplicationService(

         return customerRepository.create(customer)
     }
+
+    fun update(id: Int, customerInput: CustomerInput) {
+        val existingCustomer = customerRepository.findById(id) ?: throw RecordNotFoundException()
+
+        val customer = existingCustomer.copy(
+                name = customerInput.name!!,
+                email = customerInput.email!!
+        )
+
+        customerRepository.update(customer)
+    }
 }

controller(repositoryを直接触るのはやめて、applicationserviceを介すように統一しています)

src/main/kotlin/com/example/kotlinspringbootdomademo/application/controller/web/CustomerController.kt
@@ -1,27 +1,21 @@
 package com.example.kotlinspringbootdomademo.application.controller.web

-import com.example.kotlinspringbootdomademo.application.RecordNotFoundException
 import com.example.kotlinspringbootdomademo.application.input.CustomerInput
 import com.example.kotlinspringbootdomademo.application.service.CustomerApplicationService
-import com.example.kotlinspringbootdomademo.domain.repository.CustomerRepository
 import org.springframework.stereotype.Controller
 import org.springframework.ui.Model
 import org.springframework.validation.BindingResult
 import org.springframework.validation.annotation.Validated
-import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.PathVariable
-import org.springframework.web.bind.annotation.PostMapping
-import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.*

 @Controller
 @RequestMapping("/customers")
 class CustomerController(
-        private val customerRepository: CustomerRepository,
         private val customerApplicationService: CustomerApplicationService
 ) {
     @GetMapping("")
     fun index(model: Model): String {
-        val customers = customerRepository.findAll()
+        val customers = customerApplicationService.findAll()
         model.addAttribute("customers", customers)
         return "customers/index"
     }
@@ -31,7 +25,7 @@ class CustomerController(
             @PathVariable id: Int,
             model: Model
     ): String {
-        val customer = customerRepository.findById(id) ?: throw RecordNotFoundException()
+        val customer = customerApplicationService.findById(id)
         model.addAttribute("customer", customer)
         return "customers/show"
     }
@@ -54,4 +48,32 @@ class CustomerController(

         return "redirect:/customers/${id}"
     }
+
+    @GetMapping("{id}/edit")
+    fun edit(
+            @PathVariable id: Int,
+            customerInput: CustomerInput
+    ): String {
+        val customer = customerApplicationService.findById(id)
+
+        customerInput.name = customer.name
+        customerInput.email = customer.email
+
+        return "customers/edit"
+    }
+
+    @PatchMapping("{id}")
+    fun update(
+            @PathVariable id: Int,
+            @Validated customerInput: CustomerInput,
+            bindingResult: BindingResult
+    ): String {
+        if(bindingResult.hasErrors()) {
+            return "customers/edit"
+        }
+
+        customerApplicationService.update(id, customerInput)
+
+        return "redirect:/customers"
+    }

htmlファイル

src/main/resources/templates/customers/edit.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <link th:href="@{/css/app.css}" rel="stylesheet" />
</head>
<body>
    <h1>顧客編集</h1>
    <form th:method="patch" th:action="@{./}" th:object="${customerInput}">
        氏名: <input type="text" th:field="*{name}" />
        <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></p>

        email: <input type="text" th:field="*{email}" />
        <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></p>

        <input type="submit" value="編集" />
    </form>
</body>
</html>

http://localhost:8080/customers/1/edit にアクセス

スクリーンショット 2017-12-18 3.02.19.png

こちらも不正な値を入れるとエラーメッセージが表示されます

スクリーンショット 2017-12-18 3.02.35.png

正しい値を入力して「編集」を押すと、データが編集されている様子を確認できればOKです

削除

最後に削除を実装して終わりです

Domaのdaoインターフェース

src/main/java/com/example/kotlinspringbootdomademo/infrastructure/doma/dao/CustomerDomaDao.java
@@ -1,10 +1,7 @@
 package com.example.kotlinspringbootdomademo.infrastructure.doma.dao;

 import com.example.kotlinspringbootdomademo.infrastructure.doma.entity.CustomerDomaEntity;
-import org.seasar.doma.Dao;
-import org.seasar.doma.Insert;
-import org.seasar.doma.Select;
-import org.seasar.doma.Update;
+import org.seasar.doma.*;
 import org.seasar.doma.boot.ConfigAutowireable;

 import java.util.List;
@@ -23,4 +20,7 @@ public interface CustomerDomaDao {

     @Update
     int update(CustomerDomaEntity entity);
+
+    @Delete
+    int delete(CustomerDomaEntity entity);
 }

repositoryインターフェース

src/main/kotlin/com/example/kotlinspringbootdomademo/domain/repository/CustomerRepository.kt
@@ -7,4 +7,5 @@ interface CustomerRepository {
     fun findById(id: Int): Customer?
     fun create(customer: Customer): Int
     fun update(customer: Customer)
+    fun delete(customer: Customer)
 }

repository実装

src/main/kotlin/com/example/kotlinspringbootdomademo/infrastructure/domarepository/CustomerRepositoryDomaImpl.kt
@@ -29,6 +29,11 @@ class CustomerRepositoryDomaImpl(
         customerDomaDao.update(domaEntity)
     }

+    override fun delete(customer: Customer) {
+        val domaEntity = _mapToDomaEntity(customer)
+        customerDomaDao.delete(domaEntity)
+    }

applicationservice

rc/main/kotlin/com/example/kotlinspringbootdomademo/application/service/CustomerApplicationService.kt
@@ -39,4 +39,9 @@ class CustomerApplicationService(

         customerRepository.update(customer)
     }
+
+    fun delete(id: Int) {
+        val customer = customerRepository.findById(id) ?: throw RecordNotFoundException()
+        customerRepository.delete(customer)
+    }

controller

src/main/kotlin/com/example/kotlinspringbootdomademo/application/controller/web/CustomerController.kt
@@ -76,4 +76,12 @@ class CustomerController(

         return "redirect:/customers"
     }
+
+    @DeleteMapping("{id}")
+    fun delete(
+            @PathVariable id: Int
+    ): String {
+        customerApplicationService.delete(id)
+        return "redirect:/customers"
+    }

html

src/main/resources/templates/customers/index.html
@@ -12,6 +12,9 @@
             <span th:text="${customer.id}"></span>
             <span th:text="${customer.name}"></span>
             <span th:text="${customer.email}"></span>
+            <form th:object="${customer}" th:action="@{/customers/__${customer.id}__}" th:method="delete">
+                <input type="submit" value="削除" />
+            </form>
         </li>
     </ul>
 </body>

削除ボタンを押して該当レコードが削除されればOKです

スクリーンショット 2017-12-18 11.39.23.png

まとめ

いかがでしたでしょうか。このように、Kotlinはandroidだけでなく、サーバーサイドでも使える言語です。既存のJavaライブラリもシームレスに使えますので、是非一度触ってみてください!have a nice Kotlin!