Вступление
В этой статье мы рассмотрим как реализовать функциональность 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