今回は Jersey
+JPA
で REST API を Kotlin
で書いてみます。
依存関係の設定
まず gradle
はこんな感じです。
buildscript {
ext.kotlin_version = '1.2.31'
ext.spring_version = '2.0.2.RELEASE'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
plugins {
id 'java'
}
group 'com.example'
version '1.0-SNAPSHOT'
apply plugin: 'kotlin'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compile "org.springframework.boot:spring-boot-starter-jersey:$spring_version"
compile "org.springframework.boot:spring-boot-starter-jdbc:$spring_version"
compile "org.springframework.boot:spring-boot-starter-data-jpa:$spring_version"
compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.5"
compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.5"
compile "mysql:mysql-connector-java:8.0.11"
testCompile group: 'junit', name: 'junit', version: '4.12'
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
アプリケーションのエントリーポイント実装
つづいて、エントリーポイントの実装です。
設定がまとまってた方が見やすいかと思い Jersery
の設定もここに書いちゃいました。
あまり薦められるやり方じゃないです。
@EnableAutoConfiguration
@ComponentScan
@Configuration
open class App
@Configuration
@ApplicationPath("/")
open class JerseyConfig : ResourceConfig() {
init {
packages("com.example")
register(ContextResolver<ObjectMapper> { jacksonObjectMapper() })
}
}
fun main(args: Array<String>) {
runApplication<App>(*args)
}
register(ContextResolver<ObjectMapper> { jacksonObjectMapper() })
をすることで JSON
文字列から Kotlin オブジェクトに変換できます。
PUT
やPOST
でJSON
を扱う場合は設定するといいです。
レスポンスに使うEntityクラスの実装
続いて、レスポンスに使うデータクラスです。
data class User(
val id: Int,
@field:JsonProperty("名前") val name: String,
@field:JsonProperty("歳") val age: Int,
@field:JsonProperty("住所") val address: String?
)
@field:JsonProperty
にある field
ですが、アノテーションをどこに設定したいかを指定するものです。
Kotlinのプロパティは Java で言う所の field
getter
setter
を兼ねていることから、この指定がないと Kotlin的にどこにつけるか困っちゃうみたいです。
なので、getter
につけたい場合は @get:JsonProperty
とします。
REST API のエンドポイント実装
書くのに疲れてきました。。。
REST API部分の実装です。CRUD一通りあります。
data class Parameter(
@field:JsonProperty("名前")
val name: String,
@field:JsonProperty("歳")
@field:Min(18)
val age: Int,
@field:JsonProperty("住所")
val address: String
)
@Path("user")
class UserResource {
@Inject
lateinit var repo: UserRepository
@GET
@Produces(MediaType.APPLICATION_JSON)
fun all() = repo.getAll()
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
fun getById(@PathParam("id") id: Int) = repo.get(id)
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun create(@Valid param: Parameter) = repo.create(param.name, param.age, param.address)
@PUT
@Path("{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun update(@PathParam("id") id: Int, @Valid param: Parameter) = repo.update(User(id, param.name, param.age?: 35, param.address))
@DELETE
@Path("{id}")
fun delete(@PathParam("id") id : Int) = repo.delete(id)
DBアクセス部分の実装
次はDBからデータを取得する部分になりますが、
今回は Repositoryを挟んでREST APIの層とDB層を分離させています。
これを挟むことで、O/Rマッパーは別のものに変えてもResouceに影響がでないようにするという考えです。
実際にExposedに差し替えたパターンはこちらにあります。
JPAで実装
ここからはJPAを利用してDBからデータを取得し、DBに依存しないデータ形式に変換して返すという実装です。
interface UserRepository {
fun get(id: Int): User
fun getAll(): List<User>
fun delete(id: Int)
fun create(name: String, age: Int, address: String): User
fun update(user: User): User
}
テーブルとマッピングするためのクラスです。
@Entity
@Table(name = "user")
data class UserModel(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Int = 0,
var name: String = "",
var age: Int = 0,
var address: String = ""
)
fun UserModel.toEntity() = User(id = id, name = name, address = address, age = age)
@Service
class UserRepositoryImpl : UserRepository {
@Inject
lateinit var repo: internalRepository
override fun get(id: Int) = repo.findById(id).map { it.toEntity() }.orElseGet { throw NotFoundException() }
override fun getAll() = repo.findAll().map { it.toEntity() }
override fun delete(id: Int) = repo.deleteById(id)
override fun create(name: String, age: Int, address: String)
= repo.save(UserModel(name = name, age = age, address = address)).toEntity()
override fun update(user: User)
= repo.save(UserModel(id = user.id, name = user.name, age = user.age, address = user.address?: "不定")).toEntity()
}
@Repository
interface internalRepository : JpaRepository<UserModel, Int>
JPAが関係しているのはこれだけです。
@Repository
interface internalRepository : JpaRepository<UserModel, Int>
あとはDBから取得したUserModel
をUser
に変換しているだけです。
UserModel
もUser
もフィールドが同じなので、作る意味あるのかいって突っ込まれそうですがそこは意図を汲み取ってくれると助かります。
見やすさを意識して改行こそしてますが、処理自体はほとんど1行で書けちゃいました。