برنامه نویسی

پیاده سازی جستجوی RSQL در معرض – انجمن DEV

بررسی اجمالی

در این آموزش، ما قصد داریم با استفاده از تجزیه کننده RSQL jirutka، قابلیت جستجو را در Exposur پیاده سازی کنیم.

RSQL یک زبان پرس و جو برای فیلتر پارامتری شده ورودی ها در RESTful API است.
JetBrains Exposur یک کتابخانه SQL سبک وزن در بالای درایور JDBC برای زبان Kotlin است.

راه اندازی یک برنامه آزمایشی

توجه: اگر چارچوب وب/راه‌اندازی در معرض دید دارید، می‌توانید این بخش را نادیده بگیرید و مستقیماً به بخش اجرای عملکرد جستجوی RSQL بروید.

برای انجام آزمایش، از Ktor استفاده می کنیم – ساده ترین راه برای انجام این کار استفاده از مقداردهی اولیه است.

وقتی فرم را مرور کردیم، فریم برنامه آماده کار است. اکنون، باید عملکرد سریال سازی را اضافه کنیم (زیرا می خواهیم یک شی JSON را به عنوان پاسخ برگردانیم).

build.gradle.ts

plugins {
    ...
    kotlin("plugin.serialization") version "1.8.10"
}

depenendencies {
    ...
    implementation("io.ktor:ktor-server-content-negotiation:2.2.4")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.2.4")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ممکن است به پایگاه داده H2 نیاز داشته باشیم:

build.gradle.ts


dependencies {
    implementation("com.h2database:h2:$h2Version")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به علاوه، ما به jirutka/rsql-parser نیاز داریم

build.gradle.ts

dependencies {
    implementation("cz.jirutka.rsql:rsql-parser:2.1.0")
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اضافه کردن Exposure

اکنون، می‌توانیم لایه پایداری خود را اضافه کنیم – Exposure ORM:

build.gradle.ts

dependencies {
    implementation("org.jetbrains.exposed:exposed-core:0.40.1")
    implementation("org.jetbrains.exposed:exposed-dao:0.40.1")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.40.1")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای ایجاد اتصال پایگاه داده و انجام درج اولیه db، پلاگین Ktor را ایجاد کردم:

Data.kt

import pl.brightinventions.dto.CreatePersonDto
import pl.brightinventions.exposed.Database
import pl.brightinventions.persistance.PersonDaoImpl
import pl.brightinventions.persistance.table.PersonTable
import io.ktor.server.application.*
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction

fun Application.configureData() {
    Database.register()
    TODO("more logic incoming")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و آن را در راه اندازی برنامه ثبت کنید:

Application.kt

import pl.pl.brightinventionsugins.configureData
import pl.pl.brightinventionsugins.configureRouting
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        install(ContentNegotiation) {
            json()
        }
        configureData()
    }
        .start(wait = true)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

منطق لایه پایداری

هنگامی که Exposur را به محیط خود اضافه کردیم، نوبت به معرفی مدل جدول و برخی از DTO می رسد.

در Exposur، نمایش جدول یک شی است:

PersonTable.kt

import org.jetbrains.exposed.sql.Table

object PersonTable : Table("person") {
    val id = uuid("id").autoGenerate()
    val name = text("name")
    val surname = text("surname")
    val age = integer("age")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جدول شی شما باید از جدول Exposur گسترش یابد. محتوای آن گروهی از ستون های تعریف شده است.

بیایید DAO های خود را ایجاد کنیم:

PersonDaoImpl.kt (من از بخش رابط DAO صرف نظر می کنم – می توانید آن را در صفحه مخزن Github بررسی کنید)

import pl.brightinventions.dto.CreatePersonDto
import pl.brightinventions.dto.FoundPersonDto
import pl.brightinventions.exposed.SearchPropertySpecification
import pl.brightinventions.exposed.SearchSpecification
import pl.brightinventions.exposed.search
import pl.brightinventions.persistance.table.PersonTable
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction

class PersonDaoImpl : PersonDao {

    override fun findAll(): List<FoundPersonDto> = transaction {
        PersonTable.selectAll().map(::mapToFoundPerson)
    }

    override fun findByQuery(query: String): List<FoundPersonDto> = transaction {
        TODO("will be implemented soon")
    }

    private fun mapToFoundPerson(it: ResultRow) = FoundPersonDto(
        it[PersonTable.id],
        it[PersonTable.name],
        it[PersonTable.surname],
        it[PersonTable.age]
    )

    override fun create(person: CreatePersonDto) {
        transaction {
            PersonTable.insert {
                it[name] = person.name
                it[surname] = person.surname
                it[age] = person.age
            }
        }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که می بینید، ما سه روش عمومی را تعریف کردیم:

  • findAll – تمام رکوردها را از db برمی گرداند
  • findByQuery(String) – مجموعه ای فیلتر شده از رکوردها را برمی گرداند
  • create – تابع util ما برای درج اولیه استفاده خواهیم کرد

درج اولیه

وقتی داریم_ جدول_ و دائو آماده، می‌توانیم با پیاده‌سازی بیشتر کلاس Data خود پیش برویم:

Data.kt

fun Application.configureData() {
    Database.register()
    transaction {
        SchemaUtils.create(PersonTable)
        PersonDaoImpl().create(CreatePersonDto("John", "Doe", 33))
        PersonDaoImpl().create(CreatePersonDto("George", "Smith", 34))
        PersonDaoImpl().create(CreatePersonDto("Megan", "Miller", 22))
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اینجا چیکار کردیم؟ که در transaction مسدود کردن.

هر دسترسی به پایگاه داده با استفاده از Exposur با به دست آوردن یک اتصال و ایجاد یک تراکنش آغاز می شود.

جدول را در پایگاه داده ایجاد کردیم (SchemaUtils.create call) و DB را با رکوردهای اولیه پر کردیم.

REST نقاط پایانی

درست است، ما پایگاه داده را پر کرده ایم، می توانیم از DAO خود برای ایجاد یک نقطه پایانی REST استفاده کنیم:

Routing.kt

import pl.brightinventions.persistance.PersonDaoImpl
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Application.configureRouting() {
    val dao = PersonDaoImpl()

    routing {
        get("https://dev.to/") {
            call.respond(dao.findAll())
        }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اینجا چیکار کردیم؟ ما (یک بار دیگر) افزونه Ktor را برای ثبت مسیریابی در اینجا ایجاد کردیم. در نقطه پایانی / می خواهیم با تمام اشیاء در جدول Person پاسخ دهیم.

اما باید آن را در برنامه ثبت کنیم:

Application.kt

...
fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        install(ContentNegotiation) {
            json()
        }
        configureRouting()
        configureData()
    }
        .start(wait = true)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اجرا کن عزیزم! GET http://localhost:8080/ باید با لیستی از سه نفر پاسخ دهد: جان، جورج و مگان.
بسیار خوب، اما چه زمانی عملکرد جستجو را اجرا خواهیم کرد؟ اکنون.

اجرای قابلیت جستجوی RSQL

سرانجام! در حال حاضر، ما پشته ktor+exposed را راه‌اندازی کرده‌ایم، همه چیز خوب کار می‌کند و می‌توانیم اشیاء را از پایگاه داده اضافه و فهرست کنیم. زمان ایجاد متد جستجو برای کلاس Query است:

import cz.jirutka.rsql.parser.RSQLParser
import cz.jirutka.rsql.parser.ast.Node
import org.jetbrains.exposed.sql.Query
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.transactions.transaction

fun Query.search(query: String, specification: SearchSpecification): Query =
    transaction {
        val rootNode: Node = RSQLParser().parse(query)
        val queryExpression = rootNode.accept(ExposedRSQLVisitor(specification))
        andWhere { queryExpression }
    }
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اینجا چیکار کردیم؟ ما تابع افزونه Query.search را که پرس و جو را تجزیه می‌کند، اعلام کردیم: رشته‌ای را در توکن‌ها فراخوانی می‌کند و ExposedRSQLVisitor ما را برای تفسیر کوئری فراخوانی می‌کند که خود شی Query را برمی‌گرداند، بنابراین ما می‌توانیم عملکرد استاندارد Exposur را انجام دهیم.

ExposureRSQLVisitor چیست؟ این پیاده سازی سفارشی ما است – jirutka/rsql-parser “تنها” تجزیه کننده برای اجرای منطق به منظور تغییر “age=in=(33,22)” به درخت گره است. به این صورت است که به نظر می رسد:

ExposedRSQLVisitor.kt

import cz.jirutka.rsql.parser.ast.*
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.notInList
import java.time.Instant
import java.time.format.DateTimeParseException

class ExposedRSQLVisitor(
    private val searchSpecification: SearchSpecification
) : NoArgRSQLVisitorAdapter<Op<Boolean>>() {

    override fun visit(node: AndNode): Op<Boolean> {
        TODO("Not yet implemented")
    }

    override fun visit(node: OrNode): Op<Boolean> {
        TODO("Not yet implemented")
    }

    @Suppress("UNCHECKED_CAST")
    override fun visit(node: ComparisonNode): Op<Boolean> {
        val arguments =
            node.arguments.map {
                it.toLongOrNull()
                    ?: it.toBooleanStrictOrNull()
                    ?: it.toDateOrNull()
                    ?: it.toDoubleOrNull()
                    ?: it
            }
        val argument = arguments.first()

        val property = searchSpecification.properties.first { it.name == node.selector }
        val column = property.column as Column<Any>
        return when (val operator = node.operator) {
            RSQLOperators.EQUAL -> column eq argument
            RSQLOperators.NOT_EQUAL -> column neq argument
            RSQLOperators.GREATER_THAN -> column greater argument as Comparable<Any>
            RSQLOperators.GREATER_THAN_OR_EQUAL -> column greaterEq argument as Comparable<Any>
            RSQLOperators.LESS_THAN -> column less argument as Comparable<Any>
            RSQLOperators.LESS_THAN_OR_EQUAL -> column lessEq argument as Comparable<Any>
            RSQLOperators.IN -> column inList arguments
            RSQLOperators.NOT_IN -> column notInList arguments

            else -> throw Exception("Filter operator '$operator' not supported")
        }
    }
}

private fun String.toDateOrNull(): Instant? = try {
    Instant.parse(this)
} catch (e: DateTimeParseException) {
    null
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اینجا چیکار کردیم؟ در اصل ما اجرا کردیم NoArgRSQLVisitorAdapter که قرار است از گره بازدید کند تا مشخص کند چه عملیات منطقی (اکسپوز) را باید در ستون/آرگمون داده شده انجام دهیم.

بنابراین، اگر ما یک رشته کوئری مانند age=in=(33,22) داشته باشیم، بازدیدکننده ما تولید خواهد کرد

column<PersonTable.age> inList listOf(33,22) 
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین پرس و جوی SQL ما به صورت زیر خواهد بود:

SELECT PERSON.ID, PERSON."NAME", PERSON.SURNAME, PERSON.AGE FROM PERSON WHERE PERSON.AGE IN (33, 22)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

وقت آن است که از روش جستجو در DAO خود استفاده کرده و آن را در کنترلر فراخوانی کنیم:

PersonDaoImpl.kt

It's time to use the search method in our DAO and call it in the controller:

PersonDaoImpl.kt
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

لطفا توجه داشته باشید SearchSpecifictation ساختار – راهی برای گفتن به بازدید کننده از چه نوع فیلدهایی می تواند در فیلتر کردن استفاده شود و چگونه باید آنها را نگاشت کرد. PersonTable ستون ها

SearchSpecification.kt

data class SearchSpecification(
    val properties: List<SearchPropertySpecification>
)

data class SearchPropertySpecification(
    val name: String,
    val column: Column<*>
)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و تماس کنترلرها:

Routing.kt

...
get("/filtered/") {
    call.respond(dao.findByQuery(call.request.queryParameters["query"] ?: ""))
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

GET http://localhost:8080/filtered/?query=age=in=(33,22) دو رکورد را به ما برمی گرداند – برای جان دو و مگان میلر.

نتیجه

در این مقاله، ما یاد گرفتیم که چگونه قابلیت جستجوی عمومی را به پشته Exposur اضافه کنیم. Ktor فقط طعم چیزی غیر از Spring Boot را اضافه کرد – من آن را عمداً انجام دادم – شاید برای شما آنقدر جالب باشد که در مورد این چارچوب عمیق تر بگردید.

می توانید کد کامل را از طریق GitHub پیدا کنید.

توسط Patryk Szlagowski، توسعه‌دهنده ارشد Backend @ اختراعات درخشان

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا