Help us understand the problem. What is going on with this article?

Kotlin/SpringBoot/MySQLでRESTAPIを作ってみる

概要

Kotlinを触ってみたかったので簡単なRESTAPIを作成してみた。

環境

  • Kotlin: 1.3.50
  • Springboot: 2.2.1
  • MySQL: 5.6.46

リポジトリ

今回、説明するものはこちらのリポジトリです。

git clone git@github.com:yamee-dev/kotlin_restapi.git
git checkout 831e1028078cfe796077d6f8f6df230361da1c66

作成機能

HTTPメソッド URL 概要
GET /api/v1/characters 全件取得
POST /api/v1/characters 新規登録
GET /api/v1/characters/{id} 一件取得
PUT /api/v1/characters/{id} 編集
DELETE /api/v1/characters/{id} 削除

雛形の作成

Spring Initializrを用いて雛形を作成する。

  • Project : Gradle Project
  • Language : Kotlin
  • Spring Boot : 2.2.1
  • Project MetaData : com.example.kotlin_restapi
  • Dependencies : Web,MySQL,JPA,H2 image.png

Generateされたzipファイルを展開して、適当なディレクトリに配置。
生成されたプロジェクトは以下のような構成になる。


$ tree
.
├── HELP.md
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    ├── main
    │   ├── kotlin
    │   │   └── com
    │   │       └── example
    │   │           └── kotlin_restapi
    │   │               └── KotlinRestapiApplication.kt
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── kotlin
            └── com
                └── example
                    └── kotlin_restapi
                        └── KotlinRestapiApplicationTests.kt

DBの設定

DBを作成する。今回は予めローカル環境に設置したものを使用する。

$ mysql -u ${username} -p
Enter password: 
mysql> CREATE DATABASE kotlin_restapi_db;
Query OK, 1 row affected (0.00 sec)

実装

設定ファイル

作成したDBへの接続設定と、JPAのインサート設定、コンソール表示設定を記載する。

/src/main/resources/application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/kotlin_restapi_db
    username: root
    password:
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

Entity

Entityで定義されたテーブル情報が、自動インサートの対象になる。
データクラスを使用することでスッキリ書けた。

/src/main/kotlin/com/example/kotlin_restapi/entity/Character.kt
package com.example.kotlin_restapi.entity

import javax.persistence.*

@Entity
@Table(name = "characters")
data class Character (
    @Id
    @GeneratedValue
    val id: Long? = null,

    @Column(name = "username", length = 100, nullable = false)
    val username: String,

    @Column(name = "age", nullable = false)
    val age: Long,

    @Column(name = "jobs", length = 100)
    val jobs: String? = null
)

Repository

JPA用のインターフェイスを定義する。

/src/main/kotlin/com/example/kotlin_restapi/repository/CharacterRepository.kt
package com.example.kotlin_restapi.repository

import com.example.kotlin_restapi.entity.Character
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface CharacterRepository : JpaRepository<Character, Long> {
}

Service

ビジネスロジック書く。

/src/main/kotlin/com/example/kotlin_restapi/service/CharacterService.kt
package com.example.kotlin_restapi.service

import com.example.kotlin_restapi.entity.Character
import com.example.kotlin_restapi.repository.CharacterRepository
import org.springframework.stereotype.Service

@Service
class CharacterService(private val characterRepository: CharacterRepository) {

    fun findAll(): List<Character> = characterRepository.findAll()

    fun findById(id: Long) = characterRepository.findById(id)

    fun save(character: Character) = characterRepository.save(character)

    fun delete(id: Long) = characterRepository.deleteById(id)
}

Controller

作成機能に対応したメソッドを作成。

/src/main/kotlin/com/example/kotlin_restapi/controller/CharacterController.kt
package com.example.kotlin_restapi.controller

import com.example.kotlin_restapi.entity.Character
import com.example.kotlin_restapi.service.CharacterService
import org.springframework.web.bind.annotation.*
import java.util.*


@RestController
@RequestMapping("/api/v1/characters")
class CharacterController (private val characterService: CharacterService){

    @GetMapping("")
    fun findAll(): List<Character> {
        return characterService.findAll()
    }

    @PostMapping("")
    fun create(@RequestBody character: Character): Character {
        characterService.save(character)
        return character
    }

    @GetMapping("{id}")
    fun findById(@PathVariable id: Long): Optional<Character> {
        return characterService.findById(id)
    }

    @PutMapping("{id}")
    fun update(@PathVariable id: Long, @RequestBody character: Character): Character {
        characterService.save(character.copy(id = id))
        return character.copy(id = id)
    }

    @DeleteMapping("{id}")
    fun delete(@PathVariable id: Long): String {
        characterService.delete(id)
        return "Delete Complete"
    }
}

テスト

build.gradle.ktsの編集

テスト用の環境と整えるために、dependenciesを編集する。

/build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    runtimeOnly("com.h2database:h2")
    runtimeOnly("mysql:mysql-connector-java")

//  追加
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.assertj:assertj-core:3.8.0")

//  削除
//  testImplementation("org.springframework.boot:spring-boot-starter-test") {
//      exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
//  }
}

設定ファイル

./src/test/kotlin以下にresources/application.ymlを作成。
テスト環境では、DBをH2に変更したいので、H2の接続設定を記載。

/src/test/resources/application.yml
spring:
  datasource:
    url: jdbc:h2:./db;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:
    driver-class-name: org.h2.Driver

初期実行sql

テスト実行じに呼び出せれるsqlを記載。

/src/test/resources/setup.sql
DROP TABLE IF EXISTS characters;
CREATE TABLE IF NOT EXISTS characters (
    id bigint(20) NOT NULL,
    age bigint(20) DEFAULT NULL,
    jobs varchar(100) NOT NULL,
    username varchar(100) NOT NULL,
    PRIMARY KEY (id)
);

テストの実装

Controllerのメソッドを呼び出し、正常動作を確認するテストを書いた。
テストが呼び出されると、setup.sqlが実行され、初期状態のDBで実行される。

/src/test/kotlin/com/example/kotlin_restapi/CharacterControllerTest.kt
package com.example.kotlin_restapi

import com.example.kotlin_restapi.controller.CharacterController
import com.example.kotlin_restapi.entity.Character
import org.assertj.core.api.Assertions
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.context.junit4.SpringRunner


@RunWith(value = SpringRunner::class)
@SpringBootTest
@Sql("classpath:setup.sql")
class CharacterControllerTest {
    @Autowired
    private lateinit var characterController: CharacterController

    @Test
    fun noDataTest() {
        val characters = characterController.findAll()
        Assertions.assertThat(characters).isEmpty()
    }

    @Test
    fun createTest() {
        val createCharacter = characterController.create(Character(username = "Kazuma", age = 16, jobs = "NEET"))
        Assertions.assertThat(createCharacter.username).isEqualTo("Kazuma")
        Assertions.assertThat(createCharacter.age).isEqualTo(16)
        Assertions.assertThat(createCharacter.jobs).isEqualTo("NEET")
        Assertions.assertThat(createCharacter.id).isEqualTo(createCharacter.id)
    }

    @Test
    fun findByIdTest() {
        val createCharacter = characterController.create(Character(username = "Kazuma", age = 16, jobs = "NEET"))
        val findCharacter = characterController.findById(createCharacter.id ?: 0)
        Assertions.assertThat(findCharacter.orElse(null).username).isEqualTo("Kazuma")
        Assertions.assertThat(findCharacter.orElse(null).age).isEqualTo(16)
        Assertions.assertThat(findCharacter.orElse(null).jobs).isEqualTo("NEET")
    }

    @Test
    fun findAllTest() {
        val createCharacter1 = characterController.create(Character(username = "Kazuma", age = 16, jobs = "NEET"))
        val createCharacter2 = characterController.create(Character(username = "Megumin", age = 13, jobs = "Arc Wizard"))
        val findCharacters = characterController.findAll()
        Assertions.assertThat(findCharacters[0].id).isEqualTo(createCharacter1.id)
        Assertions.assertThat(findCharacters[0].username).isEqualTo("Kazuma")
        Assertions.assertThat(findCharacters[0].age).isEqualTo(16)
        Assertions.assertThat(findCharacters[0].jobs).isEqualTo("NEET")
        Assertions.assertThat(findCharacters[1].id).isEqualTo(createCharacter2.id)
        Assertions.assertThat(findCharacters[1].username).isEqualTo("Megumin")
        Assertions.assertThat(findCharacters[1].age).isEqualTo(13)
        Assertions.assertThat(findCharacters[1].jobs).isEqualTo("Arc Wizard")
    }

    @Test
    fun updateTest() {
        val createCharacter = characterController.create(Character(username = "Kazuma", age = 16, jobs = "NEET"))
        val findCharacter = characterController.findById(createCharacter.id ?: 0)
        Assertions.assertThat(findCharacter.orElse(null).username).isEqualTo("Kazuma")
        Assertions.assertThat(findCharacter.orElse(null).age).isEqualTo(16)
        Assertions.assertThat(findCharacter.orElse(null).jobs).isEqualTo("NEET")

        characterController.update(createCharacter.id ?: 0, Character(username = "Aqua", age = 999, jobs = "Arc Priest"))
        val updateCharacter = characterController.findById(createCharacter.id ?: 0)
        Assertions.assertThat(updateCharacter.orElse(null).username).isEqualTo("Aqua")
        Assertions.assertThat(updateCharacter.orElse(null).age).isEqualTo(999)
        Assertions.assertThat(updateCharacter.orElse(null).jobs).isEqualTo("Arc Priest")
    }

    @Test
    fun deleteTest() {
        val createCharacter = characterController.create(Character(username = "Kazuma", age = 16, jobs = "NEET"))
        val findCharacter = characterController.findById(createCharacter.id ?: 0)
        Assertions.assertThat(findCharacter.orElse(null).username).isEqualTo("Kazuma")
        Assertions.assertThat(findCharacter.orElse(null).age).isEqualTo(16)
        Assertions.assertThat(findCharacter.orElse(null).jobs).isEqualTo("NEET")
        characterController.delete(createCharacter.id ?: 0)
        val deleteCharacter = characterController.findById(createCharacter.id ?: 0)
        Assertions.assertThat(deleteCharacter).isEmpty
    }
}

動作確認

テスト

$ gradle test

> Task :test
2019-11-08 17:25:21.860  INFO 5154 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
2019-11-08 17:25:21.861  INFO 5154 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2019-11-08 17:25:21.861  INFO 5154 --- [extShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2019-11-08 17:25:21.864  INFO 5154 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2019-11-08 17:25:21.869  INFO 5154 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

BUILD SUCCESSFUL in 6s
5 actionable tasks: 2 executed, 3 up-to-date

起動

$ gradle build

BUILD SUCCESSFUL in 1s
7 actionable tasks: 2 executed, 5 up-to-date

$ java -jar ./build/libs/kotlin_restapi-0.0.1-SNAPSHOT.jar

省略

2019-11-08 17:30:40.437  INFO 5222 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-11-08 17:30:40.690  INFO 5222 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-11-08 17:30:40.692  INFO 5222 --- [           main] c.e.k.KotlinRestapiApplicationKt         : Started KotlinRestapiApplicationKt in 4.306 seconds (JVM running for 4.71)

APIを叩いてみる

jqをインストール

$ brew install jq

新規作成

$ curl -XPOST -H "Content-type: application/json" -d '{"username": "Kazuma","age": 17,"jobs": "NEET"}' 'http://localhost:8080/api/v1/characters'
{
  "id": 1,
  "username": "Kazuma",
  "age": 17,
  "jobs": "NEET"
}

一件取得

$ curl http://localhost:8080/api/v1/characters/1 |jq
{
  "id": 1,
  "username": "Kazuma",
  "age": 17,
  "jobs": "NEET"
}

全件取得

$ curl http://localhost:8080/api/v1/characters |jq
[
  {
    "id": 1,
    "username": "Kazuma",
    "age": 17,
    "jobs": "NEET"
  },
  {
    "id": 2,
    "username": "Megumin",
    "age": 13,
    "jobs": "Arc Wizard"
  }
]

編集

$ curl -XPUT -H "Content-type: application/json" -d '{"username": "Aqua","age": 999,"jobs": "Arc Priest"}' 'http://localhost:8080/api/v1/characters/1' |jq
{
  "id": 1,
  "username": "Aqua",
  "age": 999,
  "jobs": "Arc Priest"
}

$ curl http://localhost:8080/api/v1/characters/1 |jq
{
  "id": 1,
  "username": "Aqua",
  "age": 999,
  "jobs": "Arc Priest"
}

削除

$ curl -XDELETE -H "Content-type: application/json" 'http://localhost:8080/api/v1/characters/1'

Delete Complete

$ curl http://localhost:8080/api/v1/characters/1 |jq

null

良い感じに作れた。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした