null

Пишем CRUD REST API на Kotlin с помощью Vert.x и Redis

Вступление

В этой статье мы рассмотрим как реализовать функциональность CRUD в REST API-сервисе с помощью Redis.

Прежде чем мы начнем, я рекомендую ознакомиться с одной из предыдущих статей - "Пишем простое REST API на Kotlin с помощью Vert.x"Ссылка здесь. Если Вы её еще не читали, то начните с неё, так как текущая статья очень тесно с ней связана, - в качестве основы мы используем приложение, созданное в той статье.

Добавляем поддержку реактивного клиента Redis

Сначала мы должны добавить зависимость клиента Redis в наш файл build.gradle, чтобы иметь возможность работать с Redis в коде приложения.

Файл build.gradle:

buildscript {
    ext.kotlin_version = '1.7.0'
    ext.vertx_version = '4.3.1'

    repositories {
        mavenCentral()
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }

    group = 'CrudRestApi'
    version = '1.0.0'
}

plugins {
    id 'org.jetbrains.kotlin.jvm' version "$kotlin_version"
    id 'java'
}

repositories {
    mavenCentral()
}

sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

dependencies {
    // Kotlin
    implementation 'org.jetbrains.kotlin:kotlin-stdlib'

    // Vertx Core
    implementation "io.vertx:vertx-core:$vertx_version"
    implementation "io.vertx:vertx-lang-kotlin:$vertx_version"

    // Vertx Web
    implementation "io.vertx:vertx-web:$vertx_version"

    // Vertx Rxjava
    implementation "io.vertx:vertx-rx-java3:$vertx_version"
    implementation "io.vertx:vertx-rx-java3-gen:$vertx_version"

    // Vertx Redis
    implementation "io.vertx:vertx-redis-client:$vertx_version"
}

jar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    manifest {
        attributes 'Implementation-Title': rootProject.name
        attributes 'Implementation-Version': archiveVersion
        attributes 'Main-Class': 'AppKt'
    }

    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

task cleanAndJar {
    group = 'build'
    description = 'Clean and create jar'

    dependsOn clean
    dependsOn jar
}

compileKotlin {
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_11
    }
}

Как Вы могли заметить, зависимость добавляется строчкой:

implementation "io.vertx:vertx-redis-client:$vertx_version"

 

Изменяем файл HttpServerVerticle.kt для реализации клиента Redis

Мы сделаем несколько доработок в этом файле, - добавим настройку соединения (redisOptions) и инициализируем клиент Redis (Redis.createClient).
Ну и конечно же переделаем все функции эндпоинта, чтобы они взаимодействовали с сервером Redis.

В конечном счете файл HttpServerVerticle.kt будет выглядеть так:

package verticle

import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.vertx.core.Promise
import io.vertx.core.json.JsonObject
import io.vertx.redis.client.RedisOptions
import io.vertx.rxjava3.core.AbstractVerticle
import io.vertx.rxjava3.ext.web.Router
import io.vertx.rxjava3.ext.web.RoutingContext
import io.vertx.rxjava3.ext.web.handler.BodyHandler
import io.vertx.rxjava3.redis.client.Redis
import io.vertx.rxjava3.redis.client.RedisAPI
import io.vertx.rxjava3.redis.client.Response

class HttpServerVerticle : AbstractVerticle() {
    private lateinit var redisApi: RedisAPI

    override fun start(promise: Promise<Void>) {
        val redisOptions = RedisOptions()
            .setConnectionString("redis://:this_is_password@localhost/1")

        Redis(io.vertx.redis.client.Redis.createClient(vertx.delegate, redisOptions))
            .rxConnect()
            .subscribe(
                { redisConnection ->
                    redisApi = RedisAPI.api(redisConnection)
                },
                { failure -> promise.fail(failure.cause) })

        val router = Router.router(vertx).apply {
            get("/api/users").handler(this@HttpServerVerticle::getUsers)
            post("/api/users").handler(BodyHandler.create()).handler(this@HttpServerVerticle::setUser)
            put("/api/users").handler(BodyHandler.create()).handler(this@HttpServerVerticle::updateUser)
            delete("/api/users").handler(this@HttpServerVerticle::deleteUser)
        }

        vertx
            .createHttpServer()
            .requestHandler(router)
            .rxListen(8282)
            .subscribe(
                { promise.complete() },
                { failure -> promise.fail(failure.cause) })
    }

    private fun getUsers(context: RoutingContext) {
        var response: JsonObject
        var count = 0
        var dataSize = 0
        val users = ArrayList<JsonObject>()

        redisApi
            .rxKeys("heroes:*")
            .toObservable()
            .map { keys ->
                ArrayList<Observable<Response>>().apply {
                    keys.forEach {
                        add(redisApi
                            .rxHmget(ArrayList<String>().apply {
                                add(it.toString())
                                add("userId")
                                add("userName")
                                add("nameAlias")
                                add("company")
                            })
                            .toObservable())
                    }
                }
            }
            .flatMap {
                dataSize = it.size
                Observable.concat(it)
            }
            .subscribe(
                {
                    count++
                    val data = it.iterator()
                    users.add(JsonObject().apply {
                        put("userId", data.next().toString())
                        put("userName", data.next().toString())
                        put("nameAlias", data.next().toString())
                        put("company", data.next().toString())
                    })

                    if (count >= dataSize) {
                        response = JsonObject().apply {
                            put("success", true)
                            put("data", users)
                        }

                        putResponse(context, 200, response)
                    }
                },
                {
                    response = JsonObject().apply {
                        put("success", false)
                        put("message", it.message)
                    }

                    putResponse(context, 500, response)
                })
    }

    private fun setUser(context: RoutingContext) {
        var response: JsonObject

        val userId = context.request().getParam("user_id")
        val userName = context.request().getParam("user_name")
        val nameAlias = context.request().getParam("name_alias")
        val company = context.request().getParam("company")

        redisApi
            .rxHmset(ArrayList<String>().apply {
                add("heroes:${userId}")
                add("userId")
                add(userId)
                add("userName")
                add(userName)
                add("nameAlias")
                add(nameAlias)
                add("company")
                add(company)
            })
            .subscribe(
                {
                    response = JsonObject().apply {
                        put("success", true)
                        put("action", "insert")
                    }

                    putResponse(context, 200, response)
                },
                {
                    response = JsonObject().apply {
                        put("success", false)
                        put("message", it.message)
                    }

                    putResponse(context, 500, response)

                })
    }

    private fun updateUser(context: RoutingContext) {
        var response: JsonObject

        val userId = context.request().getParam("user_id")
        val userName = context.request().getParam("user_name")
        val nameAlias = context.request().getParam("name_alias")
        val company = context.request().getParam("company")

        redisApi
            .rxHmget(ArrayList<String>().apply {
                add("heroes:${userId}")
                add("userName")
            })
            .toObservable()
            .map {
                null != it.iterator().next()
            }
            .flatMapMaybe { exist ->
                if (exist) {
                    redisApi
                        .rxHmset(ArrayList<String>().apply {
                            add("heroes:${userId}")

                            add("userId")
                            add(userId)

                            if (null != userName) {
                                add("userName")
                                add(userName)
                            }

                            if (null != nameAlias) {
                                add("nameAlias")
                                add(nameAlias)
                            }

                            if (null != company) {
                                add("company")
                                add(company)
                            }
                        })
                } else {
                    Maybe.just(false)
                }
            }
            .subscribe(
                {
                    response = if (it is Boolean && !it) {
                        JsonObject().apply {
                            put("success", false)
                            put("message", "Data doesn't exist")
                        }
                    } else {
                        JsonObject().apply {
                            put("success", true)
                            put("action", "update")
                        }
                    }

                    putResponse(context, 200, response)
                },
                {
                    response = JsonObject().apply {
                        put("success", false)
                        put("message", it.message)
                    }

                    putResponse(context, 500, response)
                })
    }

    private fun deleteUser(context: RoutingContext) {
        var response: JsonObject

        val userId = context.request().getParam("user_id")

        redisApi
            .rxDel(ArrayList<String>().apply { add("heroes:$userId") })
            .subscribe(
                {
                    response = JsonObject().apply {
                        put("success", true)
                        put("action", "delete")
                    }

                    putResponse(context, 200, response)
                },
                {
                    response = JsonObject().apply {
                        put("success", false)
                        put("message", it.message)
                    }

                    putResponse(context, 500, response)

                })
    }

    private fun putResponse(context: RoutingContext, statuscode: Int, response: JsonObject) {
        context.response().statusCode = statuscode

        context.response().putHeader("Content-Type", "application/json")
        context.response().end(response.encode())
    }
}

 

Разберем этот код на примере функции .getUsers().

Обращаясь к API Redis, мы вызываем метод .rxKeys(), для того чтобы получить все ключи, хранящиеся в Redis, которые соответствуют шаблону "heroes:*". Далее мы преобразуем результат в тип Observable. Затем мы вызываем функцию .map(), чтобы преобразовать данные в нужный нам формат. После этого выполняем проход по всем найденным ключам, чтобы получить значения внутри хэша (вызываем .rxHmget()), соответствующие каждому из ключей. Значения преобразуем в Observable и сохраняем в ArrayList. Следующее что мы делаем - вызываем .flatMap(), чтобы преобразовать ArrayList в Observable. Однако делаем это с помощью вызова Observable.concat(), так как хотим подписаться на каждую из Observable из списка.
Важно отметить, что .subscribe() будет вызвана столько раз, сколько элементов содержит ArrayList, который мы передаем в функцию Observable.concat(). Поэтому в коде мы отслеживаем эти вызовы (с помощью переменных count и dataSize) и возвращаем итоговый результат только после того, как все их обработаем.

Рассмотрим также функцию .updateUser().

Сначала мы вызываем функцию .rxHmget(), чтобы получить из Redis хэш-значения, связанные с ключом "heroes:${userId}". Далее преобразуем результат при помощи .map() и проверяем существуют ли данные или нет. Результат проверки - Булево значение. Если данные существуют, тогда мы с помощью функции .rxHmset() обновляем хэш, в противном случае - просто возвращаем Maybe.just(false). Затем мы подписываемся и возвращаем ответ, в зависимости от результата. 

Мы не будем рассматривать работу оставшихся функций, так как их реализация схожа с той, что мы увидели в функциях .getUsers() и .updateUser(). Вы сами легко сможете во всем разобраться, просто анализируя код, строчка за строчкой.

Вместо заключения

Выражаю благодарность автору Mei Rizal F, так как данная статья является моим отредактированным переводом его статьи "CRUD REST API Using Vert.x and Redis in Kotlin".

Полный исходный код рассмотренного нами приложения, Вы можете найти на странице автора в GitHub по ссылке  https://github.com/merizrizal/vertx-simple-rest-api/tree/crud-redis