CodeGym /Kurslar /Kotlin SELF /Exposed: cedveller ve DSL ile CRUD

Exposed: cedveller ve DSL ile CRUD

Kotlin SELF
Səviyyə, Dərs
Mövcuddur

1. Exposed niye lazimdir ve niye bu ORM deyil

Siz artiq bir neche JDBC-metodu yazdiqdan sonra guclu bir hiss yaranir ki, siz proqramchi kimi yox, kopiyalama mashini kimi ishle(y)irsiniz: her yerde prepareStatement, her yerde parametr indeksleri, her yerde eyni try/use, ustelik unutmaq olmaz ki, parametrlerin indekslesdirilmesi 1-den bashlayir. Bu anda Exposed xilas kimi gorunur: “sorgulari Kotlin-sayaqi yazmaq olar”. Amma vacibdir ki, Exposed-in “SQL-i legv etdiyi” illuziyasina dusmeyesiniz — o sadəcə onu daha az “səs-küy”lu edir.

Exposed — JetBrains-in Kotlin u-chun SQL DSL veren kitabxanasidir. Bu leksiyada biz mehz Exposed DSL-den (bazen ona SQL DSL deyirler) danishacagiq, Exposed DAO API-den yox; hemin DAO API Entity/EntityClass ile gelir. DAO-hisse movcuddur, amma o, ozunun ardinca daha chox abstraksiya ve “magiya” aparir; bizim meqsed ise yeni bashlayan u-chun aydin kod-dur: “cedveli gorurem, insert-i gorurem, select-i gorurem”.

Ferqi hiss etmek u-chun muqayiseni bashda saxlamaq rahatdir:

Ne edirik JDBC (birbasha) Exposed DSL
INSERT
SQL setri + setString(1, …)
Table.insert { it[column] = value }
SELECT
ResultSet + rs.getLong("id")
select { … }.map { row -> … }
Tranzaksiya
autoCommit=false + commit/rollback
transaction { … }

Ve he: biz hele de tranzaksiyalar, melumatlarin butovluyu ve onun haqqinda dusunmeye borcluyuq ki, UPDATE WHERE-siz — “ozunu-mehv et” duymesidir. Sadəcə indi kod ele gorunecek ki, guya siz Kotlin yazirsiniz, “setrlerde SQL + resurslar etrafinda shamanciliq” yox.

2. Qoshulma: ashililiqlar ve Database.connect

Exposed ile bir “duz” an var: “magiya bashlasin” deye kitabxanani qoshmaq lazimdir, ustelik sizin DB-niz u-chun JDBC-driver de gerekdir. Exposed driver-i evez etmir: o, onun ustunde ishle(y)ir. Ona gore proyektin sazlanmasi iki addimdir: ashililiqlar + DB-ye qoshulma. Daha sonra siz DSL-kod yazirsiniz ve her sorquda asagi-seviyye detallari haqda dusunmursunuz.

Ashagida — ashililiqlarin minimal numunesi (konsepsiya kimi). Versiyalar proyektinizden asilidir, buna gore bunu “forma” kimi qebul edin, “tek duz reqemler” kimi yox:

// build.gradle.kts (ideya)
dependencies {
    implementation("org.jetbrains.exposed:exposed-core:<version>")
    implementation("org.jetbrains.exposed:exposed-jdbc:<version>")

    implementation("org.xerial:sqlite-jdbc:<version>") // eger SQLite istifade edirikse
}

Koddaki DB-ye qoshulmani adeten tetbiqin startinda bir defe edirlər. Tedris proyektimiz u-chun SQLite goturek ve fayl "app.db" proqramla eyni qovluqda olsun.

import org.jetbrains.exposed.sql.Database

fun connectDb() {
    Database.connect(
        url = "jdbc:sqlite:app.db",
        driver = "org.sqlite.JDBC"
    )
}

Burada yadda saxlamaq lazim olan sade bir fikir var: Database.connect(...) — “Exposed-e hara getmeli oldugunu dedik” demekdir. Bu, hech bir sorqu icra etmir; bu sadəcə sazlamadir. Emeliyyatlar ise transaction { ... } daxilinde icra olunacaq.

3. Cedvelin tesviri: LongIdTable ve sutunlar

Bu ana qeder cedvel bizde SQL-setr kimi movcud idi: CREATE TABLE .... Exposed-de cedvel Kotlin-obyektine chevrilir, ve biz sutunlari xasseler kimi elan edirik. Bu iki sebebe gore rahatdir: birincisi, IDE avto-tamamlama ile komek etmeye bashlayir (sutun adlarinda sehvler az olur). Ikincisi, siz “tipleme” alirsiniz sutunlar seviyyesinde: Long-a setri tesadufen yerlesdire bilmezsiniz, kompilyator sizi bazadan evvel saxlayacaq.

Praktiki numuneni davam etdirmek u-chun, xerc u-chotu olan konsol proyektimizi inkishaf etdirdiyimizi tesəvvur edek. Bizde Expense varliq olacaq: mebleg (qepik/cent ile), kateqoriya ve qisa tesvir.

data class Expense(
    val id: Long,
    val title: String,
    val amountCents: Long,
    val category: String
)

Indi cedveli tesvir edek. Yeni bashlayan u-chun en dost variant — LongIdTable: onda artiq avto-inkrementli id var ve rutinin bir hissesini aradan qaldirir.

import org.jetbrains.exposed.dao.id.LongIdTable

object Expenses : LongIdTable("expenses") {
    val title = varchar("title", length = 200)
    val amountCents = long("amount_cents")
    val category = varchar("category", length = 50)
}

Demek olar ki, melumat strukturunun elani kimidir. Ve bu, bir fikri sabitlemek u-chun ela momentdir: Exposed bu obyektda melumat saxlamir. object Expenses — sxemin tesviridir, yaddaşda xerc kollekciyasi deyil.

Goz onune getirmek u-chun bunu bele tesəvvur etmek olar:

flowchart TD
    A["object Expenses : LongIdTable(...)
sutunlarin tesviri"] -->|DSL SQL qurur| B["SQL sorqu"] B --> C["JDBC driver"] C --> D["Melumat bazasi (SQLite fayli app.db)"]

4. Tranzaksiyalar: transaction {} ish chercivesi kimi

JDBC-kod sizi daima autoCommit ve seliqeli rollback haqda dusunmeye mecbur edirdise, Exposed daha rahat “cerchive” teklif edir: transaction { ... }. Blok daxilinde sorgulari yazirsiniz, blokdan kenarda ise aydin netice alirsiniz. Amma burada tez-tez tuta bildikleri bir “tələ” var: transaction {} sadəcə “sintaksis shekeri” deyil, bu serheddir — daxilinde Exposed-in birlesme ve tranzaksiya ile duzgun ishlediyi zemanet verilir.

Tedris proyektinde adeten lazim olan birinci praktiki addim — cedvelleri yaratmaqdir. Exposed bunu SchemaUtils ile bacarir.

import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction

fun initSchema() {
    transaction {
        SchemaUtils.create(Expenses)
    }
}

Bu, “cedvel yoxdursa yarat” demekdir (konkret DB-nin imkanlari chercivesinde). He, bu migration-lara oxshayir, amma migration-lari ayrica disciplina kimi bu gun toxunmuruq — bizim u-chun vacibdir ki, proyekt sadəcə ishlesin.

Bu stile oyrenmeyi chox tovsiye edirem: DB ile danishan her shey — daxilinde transaction, kenarda ise adi Kotlin-kod qalir. Bu menada Exposed memariligi “har yerde JDBC”den daha yaxshi intizama salır.

5. Exposed DSL-de CRUD

Create: insert { ... }

Exposed-de insert-i ilk defe gordukde, “hedden artiq gozel, dogru ola bilmez” kimi gorunur. Esasda o, sadəcə parametrlesdirilmis SQL-sorqu qurur ve JDBC vasitesile icra edir. Yəni tehlukesizlik ve parametr ideyasi hech yere getmeyib — siz sadəcə ? ve setString(1, ...) yazmagı el ile dayandirdiniz.

Xerc yaratma funksiyasi elave edek. Sadelik u-chun biz id qaytarmayacagiq (bunu etmek olar, amma indiki anda kritik olmayan detallar elave edir). Biz Unit qaytaracagiq, ugur/sehv ise tedris proyektinde helelik exception ile ifadelensin.

import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction

fun addExpense(title: String, amountCents: Long, category: String) {
    transaction {
        Expenses.insert {
            it[Expenses.title] = title
            it[Expenses.amountCents] = amountCents
            it[Expenses.category] = category
        }
    }
}

Formaya diqqet edin: it[Sutun] = deyer. Burada it — INSERT qurulushunun DSL-kontekstidir.

Eger “bir az daha boyukler kimi” istersinizse, domen seviyyesinde sadə girish melumatlari validasiyasi elave ede bilersiniz (bunu daha evvel require/check ile etmisdik), ki, DB-ye evvelden pis olan deyerler getməsin:

fun addExpenseSafe(title: String, amountCents: Long, category: String) {
    require(title.isNotBlank()) { "title must not be blank" }
    require(amountCents > 0) { "amountCents must be positive" }

    addExpense(title.trim(), amountCents, category.trim())
}

Bu, “Exposed feature”-i deyil, tetbiqin intizamidir: DB zibil qabı deyil; o, onsuz da chox seye dozur.

Read: select ve ResultRow → Expense map-pingi

Melumatin oxunmasi — en tez-tez CRUD ssenarisidir: xerclerin siyahisini goster, kateqoriya uzre tap, id uzre tap. JDBC-de siz ResultSet-i kursorla rs.next() uzerinden oxuyurdunuz. Exposed-de oxuma neticesini ResultRow setri kimi alirsiniz ve sonra adeten oz data class-iniza map edirsiniz. Bu, kolleksiyalara daha evvel tetbiq etdiyimiz eyni yanasmadir: “elementler destini aldıq → cevrirdik”. Bu stil standart kolleksiya emeliyyatlarina, meselen map-e chox yaxsi oturur.

“Her sheyi oxu” ile bashlayaq. Exposed-de bu chox birbasha gorunur:

import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction

fun listExpenses(): List<Expense> = transaction {
    Expenses.selectAll().map { row ->
        Expense(
            id = row[Expenses.id].value,
            title = row[Expenses.title],
            amountCents = row[Expenses.amountCents],
            category = row[Expenses.category]
        )
    }
}

Beli, burada .value var. Chunki LongIdTable id-ni EntityID<Long> kimi (obertka) saxlayir, halbuki domeniniz adeten adi Long isteyir.

Indi meselen, kateqoriya uzre filtrleme. Burada eq (beraberdir) peyda olur. Onu “category equals …” kimi oxumaq rahatdir.

import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction

fun listByCategory(category: String): List<Expense> = transaction {
    Expenses.select { Expenses.category eq category }.map { r ->
        Expense(r[Expenses.id].value, r[Expenses.title], r[Expenses.amountCents], r[Expenses.category])
    }
}

Burada vacib “tedris” fikri var: chole qaytarmamalisiniz ResultRow. Bu, Exposed-in daxili formatidir. Chole daha yaxshisi domen modelinizi (Expense) qaytarmaqdir. Onda qalan kod DB ile ishlemek u-chun sechdiyiniz kitabxanadan asili olmur — bu, daha evvel muzakire etdiyimiz memarli serhedlerin ruhuna tam uyğundur.

Update ve Delete: update, deleteWhere ve setrlerin yoxlanmasi

Deyishiklik ve silme — sehv etmek asan olan yerdir, chunki “guya kod kompilyasiya olundu”. JDBC-de muzakire edirdik ki, executeUpdate() toxunulmus setrlerin sayini qaytarir ve bunu yoxlamaq lazimdir. Exposed-de de eyni ideyadir: update ve deleteWhere Int qaytarir — nechə setrin heqiqeten deyishdiyini/silindiyini. Eger siz 1 gozləyirdinizse, amma 0 aldinizsa, bu “yaxshi, olar” deyil — bu siqnaldir ki, ya yazı tapilmadi, ya da kriteriya sehvdir.

Gəlin xercin basligini id uzre yenilemek edek:

import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update

fun renameExpense(id: Long, newTitle: String): Boolean = transaction {
    val updated = Expenses.update({ Expenses.id eq id }) {
        it[Expenses.title] = newTitle
    }
    updated == 1
}

Id uzre silme:

import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.transactions.transaction

fun deleteExpense(id: Long): Boolean = transaction {
    val deleted = Expenses.deleteWhere { Expenses.id eq id }
    deleted == 1
}

Gorduyunuz kimi, burada bir pattern tekrarlanir: “icra etdik → 1 ile muqayise etdik”. Bu artiqliq deyil, bu tetbiqinizin kontraktidir: “rename/delete by id” emeliyyati deqiq olaraq bir yazıya toxunmalidir. Qalan her shey — ssenari xetasi demekdir.

Gozellik u-chun balaca bir cedvel-shpargalka da etmek olar: hansı emeliyyat ne qaytarir:

Emeliyyat Exposed DSL Qaytarir
Elave et
insert { ... }
adeten InsertStatement (biz onu nezere almiriq)
Oxu
select { ... }
setrler desti, adeten List<T>-e map edirik
Yenile
update({ ... }) { ... }
Int (neche setr yenilendi)
Sil
deleteWhere { ... }
Int (neche setr silindi)

6. Burada DAO haradadir: Exposed-i repository arxasinda gizledirik

Exposed-i ilk defe qoshanda, chox istiyirsiniz ki, transaction { Expenses.selectAll() ... } birbasha main-den yazasiniz. Axı ishle(y)ir! Amma bir hefte sonra anlayacaqsiniz ki, kodunuz “tranzaksiyalar spagetti”sine chevrilib, sxemde her bir deyishiklik tetbiqin yarısını sındırır. Buna gore bize serhed lazimdir: melumatlara girish yashayan yer ve ssenarilerin logikasi yashayan yer. Evvelki memarliq gunlerinde buna repository/saxlama qati deyirdik; pattern terminlerinde yaxinda DAO sozu durur.

DAO (Data Access Object) tetbiqi menada — melumatlari nece oxuyub-yazmagi bilen obyekt/klass/moduldur. O, SQL/Exposed detallarini gizledir ve chole addExpense, listExpenses, deleteExpense kimi aydin metodlar verir.

Repository kontrakti ile bashlayaq. Mutleq generics olmasi sert deyil, amma “interfeys kimi kontrakt” prinsipi Kotlin tipleri ve umumilesdirmelerle chox yaxsi uzlashir; biz bunlarla artiq dilde rastlashmisiq.

interface ExpenseRepository {
    fun add(title: String, amountCents: Long, category: String)
    fun listAll(): List<Expense>
    fun deleteById(id: Long): Boolean
}

Indi Exposed uzerinde realizasiya:

import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction

class ExposedExpenseRepository : ExpenseRepository {
    override fun add(title: String, amountCents: Long, category: String) = transaction {
        Expenses.insert {
            it[Expenses.title] = title
            it[Expenses.amountCents] = amountCents
            it[Expenses.category] = category
        }
    }

    override fun listAll(): List<Expense> = transaction {
        Expenses.selectAll().map { r ->
            Expense(r[Expenses.id].value, r[Expenses.title], r[Expenses.amountCents], r[Expenses.category])
        }
    }

    override fun deleteById(id: Long): Boolean = transaction {
        Expenses.deleteWhere { Expenses.id eq id } == 1
    }
}

Beli, burada 10 setrden bir az artiqdir — amma bu, “butun DB-logikasi bir yerdedir” ideyasini gosteren bütov bir hissedir. Real proyektde siz bunu private funksiyalara boler ve daha qisa ederdiniz, amma indi “skeleti tam halda” gormek daha faydalidir.

Indi main (ve ya CLI qati) Exposed ile birbasha yox, ExpenseRepository ile ishle(y)ir:

fun main() {
    connectDb()
    initSchema()

    val repo: ExpenseRepository = ExposedExpenseRepository()
    repo.add("Kofe", 19900, "food")

    println(repo.listAll()) // [Expense(id=1, title=Kofe, amountCents=19900, category=food)]
}

Belelikle “DAO haradadir”? Bax budur: ExposedExpenseRepository. Tetbiqi menada DAO/Repository mehz budur. Siz Exposed-i daxile gizlettiniz, ve qalan kod hec bilmeye borclu deyil ki, sizde orada SQL-cedveller var.

Chox vacibdir qarisdirmayasiniz: Exposed DAO API (Entity ile) — Exposed-in konkret API-sidir. DAO pattern kimi — memarlıq roludur. Bu gun biz pattern-den danishiriq ve Exposed DSL-i onun daxilinde alət kimi istifade edirik.

7. Tipik sehvler

Sehv №1: sorqulari transaction {}-den kenarda icra edib, qeribe crash-lara heyretlenmek.
Exposed ele qurulub ki, emeliyyatlarin ekseriyyeti tranzaksiya daxilinde icra olunmalidir, chunki birlesmeni ve icra kontekstini mehz orada kitabxana idare edir. Eger kenarda “bir az select edim” desəniz, “no transaction in context” seviyyeli sehvler ve ya oxshar surprizler alacaqsiniz. Bunun mualicesi sehr deyil, intizamdir: oxu/yaz emeliyyatlarinin hamisi — yalniz transaction daxilinde.

Sehv №2: domen modelinin evezine ResultRow-u chole dashimaq.
Evvelce funksiyadan ResultRow siyahisi qaytarmaq ve “qoy chagirən ozü sahələri cixartsin” demek rahat gorunur. Amma bu, butun proyekti derhal Exposed-e baglayir. Bir neche gunden sonra row[Expenses.title] DB olmamali qatlarda da gorunmeye bashlayacaq. Duzgun stil — repository daxilinde birbasha Expense-e map etmek ve temiz modelləri qaytarmaqdir. Ve he, map { ... } — sizin en yaxsi dostunuzdur; o, standart kitabxanadan artiq tanishdir.

Sehv №3: update/deleteWhere neticesini yoxlamamaq.
Eger siz id uzre yenileyir ve ya silirsiniz, amma emeliyyat 0 setre toxunubsa — bu “olsun” deyil, bu “biz yazinin oldugunu sandiq, amma yoxdur” demekdir. Eger birden chox setre toxunubsa — bu daha da pisdir; demek, shert sehvdir. Neticeni gozlənilən setr sayi ile muqayise etmek vərdişi proqramin etibarliligini ciddi artirir.

Sehv №4: transaction {}-i butun koda, UI/CLI qatina qeder yaymaq.
En yaygin memarliq ugursuzlugu bele gorunur: “men main-de emri oxuyuram, derhal tranzaksiya aciram, derhal konsola cap edirem”. Bu, mesuliyyetleri qarisdirir: saxlamaya aid qat, istifadechi ssenarisi olan yerle eyni yerde yashayir. Proyekt, tranzaksiyalar repository/DAO daxilinde yashayanda, chole ise “add/list/delete” seviyyeli metodlar chixanda daha asan saxlanilir.

Sehv №5: Exposed-in SQL ve sxemi anlamaq ehtiyacini legv etdiyini dusunmek.
Exposed kodu qisalidir, amma emeliyyatlarning menasi SQL-menalidir: SELECT oxuyur, INSERT yaradir, UPDATE deyishir, DELETE silir. Cedvel mehdudiyyetleri, acarlar, unikalliq ve tranzaksiya mentiqi hele de vacibdir. Eger siz sorqunun ne etdiyini anlamirsinizsa, DSL xilas etmeyəcək — sadəcə sehv daha “elegant” gorunecek.

Sehv №6: transaction {} daxilinde hedden artiq logika saxlamag.
Tranzaksiya qisa olmali ve melumatlarin uzlasmasi ile bagli olmalidir. Eger daxilinde istifadechi girişi, boyuk metnin formatlanmasi, uzun hesablamalar ve ya “gəlin bir de 20 defe istifadeciden nese sorushum” kimi sheyler varsa, tranzaksiya vaxt baximindan uzanir, ve siz ozunuz ozunuze problem yaradırsiniz. Tranzaksiya daxilinde yalniz DB uzerinde emeliyyatlari ve neticenin minimal elaqeli emalini saxlamağa chalisin.

1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Qeydlərin başlanğıcı
Qeydlərin başlanğıcı
1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Kitab rəfi
Kitab rəfi
1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Tapşırıq filtrəsi
Tapşırıq filtrəsi
1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Kontaktın redaktəsi
Kontaktın redaktəsi
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION