概要
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
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のインサート設定、コンソール表示設定を記載する。
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で定義されたテーブル情報が、自動インサートの対象になる。
データクラスを使用することでスッキリ書けた。
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用のインターフェイスを定義する。
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
ビジネスロジック書く。
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
作成機能に対応したメソッドを作成。
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を編集する。
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の接続設定を記載。
spring:
datasource:
url: jdbc:h2:./db;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
初期実行sql
テスト実行じに呼び出せれる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で実行される。
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
良い感じに作れた。