پیاده سازی جستجوی 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 @ اختراعات درخشان