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

KotlinでJersey+JPAを使ったRESTサーバーを作る

More than 1 year has passed since last update.

今回は Jersey+JPA で REST API を Kotlin で書いてみます。

依存関係の設定

まず gradle はこんな感じです。

build.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 の設定もここに書いちゃいました。
あまり薦められるやり方じゃないです。

App.kt
@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 オブジェクトに変換できます。
PUTPOSTJSONを扱う場合は設定するといいです。

レスポンスに使うEntityクラスの実装

続いて、レスポンスに使うデータクラスです。

User.kt
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一通りあります。

UserResource.kt
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に依存しないデータ形式に変換して返すという実装です。

UserRepository.kt
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
}

テーブルとマッピングするためのクラスです。

UserModel.kt
@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 = ""
)
UserRepositoryImpl.kt
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から取得したUserModelUserに変換しているだけです。
UserModelUserもフィールドが同じなので、作る意味あるのかいって突っ込まれそうですがそこは意図を汲み取ってくれると助かります。

見やすさを意識して改行こそしてますが、処理自体はほとんど1行で書けちゃいました。

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
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