LoginSignup
2
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-07

今回は 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行で書けちゃいました。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4