1. Niye JDBC korutinlarda bloklayir
Insanlar ilk defe “korutinlar” sozunu esidende, beyin tez-tez artiq seyler “cekisir”: guya korutin ele bir gorunmez debilqe kimidir ki, istediyiniz her kodu “avtomatik asinxron” edir. Kash ki, amma yox. Eger JDBC-driver sorgunu sinxron icra edirse, axin (thread) heqiqeten BD-den cavabi gozleyir, korutin ise bu anda sadace axini mesgul edir — pisik klaviaturanı mesgul etdiyi kimi.
Kotlin-de korutinlarin esas ideyasi ondadir ki, onlar asinxron kodu ardicil kimi yazmaga imkan verir ve emeliyyat heqiqeten dayandirilmani desteklediyi halda, sistem resurslarini bloklamadan icrani dayandirir. Amma “adi” JDBC emeliyyatlarinin coxu bloklayicidir: baza cavab verene qeder ax mesgul olur ve hec yere getmir. Buna gore korutinlar oz-ozune problemi hell etmir — problemi dispetcerin ve bloklayici is gorduyumuz serhedlerin duzgun secimi hell edir.
Korutin dispetcerleri ve Dispatchers.IO
Qarisdirmamaq ucun bele dusunmek faydalidir: korutin — “tapsiriq”, dispetcer ise “bu tapsiriq harada ve hansi axinlarda icra olunur”. kotlinx.coroutines-da korutinin konteksti var ve bu kontekstin acqar elementlerinden biri CoroutineDispatcher-dir; o da mehz korutinin hansi axinlarda icra olunacagini muyyen edir.
Eger bloklayici JDBC-sorgusunu Dispatchers.Default-da icra etseniz, esasen CPU isi (hesablama, kolleksiyalarin emali, formatlama ve s.) ucun nezerde tutulmus mehdud axin hovuzunu “doldurmaq” riskiniz var. JDBC ise I/O gozlemesidir ve onun Dispatchers.IO-da yasamasi daha mentiqlidir; bu dispetcer bloklayici daxilolma-cixarilma emeliyyatlari (fayllar, sebekə, BD) ucun hesablanib.
Gelin, zehinde saxlamaga deyer bir sekil tesavvur edek:
flowchart LR A[Coroutine on Default] -->|JDBC query| B[Thread blocks] B --> C[Default pool is busy] C --> D[Other coroutines wait] E[Coroutine on IO] -->|JDBC query| F[Thread blocks] F --> G[IO pool handles blocking] G --> H[Default pool stays free]
Ideya sadedir: siz JDBC-ni “bloklamayan” etmir, sadace bloklamani proqramin qalan hissesine daha az zerer verdiyi yere kocurursunuz.
2. withContext(Dispatchers.IO) ve saxlanma qati serhedi
“Bloklayicini IO-ya cixaraq” dedikde, praktikada cox vaxt bir konkret sablondan danisiriq: bloklayici kodu withContext(Dispatchers.IO) { ... } ile saririq. Bu, parca ucun “icra rejimini deyismek” kimi gorunur: xaricde her hansi kontekst ola biler (meselen, Default), amma blokun daxilinde kod zemanetle IO-dispetcerinde icra olunacaq.
Felsefeni anlamaq vacibdir: withContext JDBC-ni sehrli non-blocking driver-e cevirmir. O sadace deyir: “bu isin bu hissesini basqa axin hovuzunda icra et”. Bu, Kotlin korutinlarinin icra konteksti ve dispetcer etrafinda dizayn olunmasi ile yaxsi uzlasir — dispetcer tapsirigin harada icra olundugunu idare edir.
Layihelerde Dispatchers.IO-nu butun koda “yaymamaq” ucun cox sevilerek yazilan mini-oruyucu:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun <T> dbIo(block: () -> T): T =
withContext(Dispatchers.IO) { block() }
Diqqet edin: burada blok adi lambda () -> T-dir, cunki JDBC-cagirislar adeten suspend deyil. Biz “korutin kimi gozlemirik”; biz heqiqeten sinxron kodu icra edirik, sadace basqa dispetcerde.
Dispatchers.IO harada yasamalidir
Cox adam bele etmek isteyir: “main-de repozitoriyani cagirmamisdin once her seyi withContext(IO) ile sarim”. Kicik layihədə bu, hatta isleyer de. Amma arxitektura baximindan tez bir muddetde qarisigliga cevrilir: bir cagiris sarimadan unudulur — ve birdən sorgularin bir hissesi Default-a gedir. Sonra daha bir ssenari elave edirsiniz — ve artıq hara IO-du, hara deyil, emin olmursunuz.
Buna gore kifayet qeder darixdirici, amma cox faydali qayda teyin edirik: Dispatchers.IO-ya kecid saxlanma qatinda (repository/DAO) olmalidir, CLI-de yox, biznes-mentiqde yox. Bu, “saxlanma qati serhedi”dir: butun xarici kod repozitoriyani “qara qutu” kimi qebul edir ve o, BD ile duzgun islemeyi oz-ozune bacarir.
Xerc ucotu ucun konsol tetbiqimizde repozitoriyanin mini-muqavilesi:
import kotlinx.datetime.Instant
data class Expense(
val id: Long,
val title: String,
val amountCents: Long,
val createdAt: Instant,
)
interface ExpenseRepository {
suspend fun add(title: String, amountCents: Long): Long
suspend fun listLatest(limit: Int): List<Expense>
}
Xaricden her sey sade gorunur: suspend-metodlar, hec bir JDBC tipi yoxdur, hec bir Connection yoxdur. Bu, eladir: xarici kod SQLite-ni hansi acarla acdiginizi ve ResultSet-de ne qeder eziyyet oldugunu bilmemelidir.
3. Tranzaksiyalar ve kontekstler
Siz withContext(IO) istifadesine yeni baslayanda qeribe bir fikir xetasi peyda olur: “her SQL-sorgunu ayrica sarim, daha da tehlukesiz olar”. Ve burada ince bir meqam tuturuq: tranzaksiya atomarligin cercevesidir ve o, bir butov parca kimi icra olunmalidir.
Eger bele etmeye baslasaniz: “birinci sorgu withContext(IO)-da, sonra colə cixdim, sonra ikinci sorgunu yenə withContext(IO)-da”, siz mentiqi olaraq tranzaksiya ssenarisini parcalayirsiniz ve kodu qeyri-aydın etme riskini artirirsiniz. Texniki olaraq JDBC-de tranzaksiya Connection uzerinde yasayir; Connection-u baglamamisinizsa ve commit/rollback etmemisinizse, her seyi bir yerde saxlamaq olar — ve bele etmek lazimdir.
Buna gore qayda cox praktik sekilde formullasir: eger emeliyyat tranzaksiya teleb edirse, onda hem tranzaksiya, hem de onun daxilindeki butun sorgular bir butov sekilde tek bir withContext(Dispatchers.IO) daxilinde olmalidir.
“Tranzaksiya helper kimi” sablonu — qisa ve oxunaqli:
import java.sql.Connection
inline fun <T> inTx(conn: Connection, block: () -> T): T {
conn.autoCommit = false
try {
val result = block()
conn.commit()
return result
} catch (e: Exception) {
conn.rollback()
throw e
} finally {
conn.autoCommit = true
}
}
Diqqet edin: bu, deyil suspend-funksiyadir, cunki icindeki JDBC-cagirislar sinxrondur. suspend bizde colde olacaq — repozitoriya seviyyesinde; orada her seyi withContext(IO) ile saracagiq.
4. Praktiki numune: Expense Tracker
Numuneler ayri-ayri parcalarin mecmusu olmasin deye, layihenin minimal strukturunu razilasdiririq. “Ideal enterprise” qurmuruq, amma kodu bir hefte sonra acanda utanmayacaq qeder seliqeli edirik.
Asililiq sxemi (arxitektura qatlari ideyasini unutmayaq: CLI JDBC-den birbasa asili olmamalidir):
flowchart TD CLI["CLI (main, komandalar)"] --> S["Service (ssenariler)"] S --> R["ExpenseRepository (interface)"] R --> J["JdbcExpenseRepository (impl)"] J --> DB[(SQLite / DB)]
JDBC-siz ve Dispatchers.IO-suz servis
Indi SQL-terminleri ile yox, “insan dilinde” danisan servis eskizini quraq:
class ExpenseService(
private val repo: ExpenseRepository,
) {
suspend fun addExpense(title: String, amountCents: Long): Long {
require(title.isNotBlank()) { "title must not be blank" }
require(amountCents > 0) { "amount must be positive" }
return repo.add(title.trim(), amountCents)
}
}
Servis Dispatchers.IO haqqinda hec ne bilmir. O sadace suspend-repozitoriyani cagirir. Ve “bu harada icra olunur?” sualina cavab vermek mehz repozitoriyanin vezifesidir.
Daxilinde withContext(IO) olan JDBC-repozitoriya
Indi en vacibini edek: (1) JDBC ile isleyen, (2) resurslari baglayan, (3) IO-dispetcerini zemanetleyen repozitoriya realizasiyasini.
Baslayaq kicik “connection provider” ile. Real layihələrdə sonra connection pool-lar peyda olur, amma bu gun bunu qesden ellemirik: bize daha cox qat intizami ve icra konteksti vacibdir.
import java.sql.Connection
import java.sql.DriverManager
class DbConnectionFactory(
private val url: String,
) {
fun open(): Connection = DriverManager.getConnection(url)
}
Indi repozitoriyanin realizasiyasi. Evvelce — add(...). Uc seviyye ic-ice daxilolmaya diqqet edin: withContext(IO) → open().use → prepareStatement(...).use.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.sql.Statement
class JdbcExpenseRepository(
private val connections: DbConnectionFactory,
) : ExpenseRepository {
override suspend fun add(title: String, amountCents: Long): Long =
withContext(Dispatchers.IO) {
connections.open().use { conn ->
val sql = "INSERT INTO expenses(title, amount_cents) VALUES (?, ?)"
conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS).use { st ->
st.setString(1, title)
st.setLong(2, amountCents)
st.executeUpdate()
st.generatedKeys.use { rs ->
rs.next()
rs.getLong(1)
}
}
}
}
}
Burada bir “boyukler ucun” meqam var: generatedKeys ve rs.next() — unutmayin ki, ResultSet “birinci setrden evvel” dayanir. Bu korutin problemi deyil, tamamilə JDBC ritualidir.
Indi — listLatest(limit). Setrleri gotururuk, Expense-e map edirik, siyahi qaytaririq:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.datetime.Instant
override suspend fun listLatest(limit: Int): List<Expense> =
withContext(Dispatchers.IO) {
connections.open().use { conn ->
val sql = """
SELECT id, title, amount_cents, created_at
FROM expenses
ORDER BY id DESC
LIMIT ?
""".trimIndent()
conn.prepareStatement(sql).use { st ->
st.setInt(1, limit)
st.executeQuery().use { rs ->
val items = mutableListOf<Expense>()
while (rs.next()) {
items += Expense(
id = rs.getLong("id"),
title = rs.getString("title"),
amountCents = rs.getLong("amount_cents"),
createdAt = Instant.parse(rs.getString("created_at")),
)
}
items
}
}
}
}
Beli, Instant.parse(...) eger BD-de “cirkli” setr varsa, exception ata biler. Bu normaldir: saxlanma qati ya formatin duzgunluyunu zemanet verir (sxem/validasiya ile), ya da duzgun sekilde “yixilir”. Bu gun bunu ayrica sehv dizayni kimi qurmuruq — sadace problemi xalcaya supurmuruq.
main-den cagiris
Konsol tetbiqlerinde en cox rast gelinen konstruksiya — main-de runBlocking-dir, cunki JVM adi giris noqtesini gozleyir, siz ise suspend-funksiyalari cagirmaq isteyirsiniz. Vacibdir ki, runBlocking “adi dunya” ile “suspend dunyasi” arasinda kordur.
Eyni zamanda, repozitoriyanin her bir cagirisini main-de withContext(IO) ile sarimaq cox vacib deyil — eks halda saxlanma qati serhedi ideyasini pozursunuz.
Mini-numune: “asililiqlari yigdıq ve servisi cagirdiq”:
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val factory = DbConnectionFactory("jdbc:sqlite:app.db")
val repo: ExpenseRepository = JdbcExpenseRepository(factory)
val service = ExpenseService(repo)
val id = service.addExpense("Coffee", 350)
println("Added expense id=$id") // Added expense id=1
}
main temiz qalir: o, JDBC haqqinda bilmir ve daxilde hansi dispetcerlerden istifade olundugunu bilmir. O, sadece bunu bilir: “servis var, repozitoriya var, netice var”.
5. Korutinlarda Exposed
Exposed bezen illuziya yaradir ki, “DSL-dirse, demeli axinlari hansisa hiylə ile optimallasdirir”. Amma Exposed yenə de alt qatinda JDBC uzerinden bazaya gedir, demeli axinlar baximindan bu, eyni bloklayici emeliyyatdir. Buna gore Dispatchers.IO haqqinda qayda hec yere getmir.
Sxem daha da sade olur: biz elle Connection ile islemirik, amma transaction { ... }-u butov sekilde IO-kontekstine saliriq.
Ferz edek ki, sizde kecen muhazireden Users/Expenses cedveli artiq var. Onda korutinli erisim bele gorunur:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction
suspend fun addExpenseExposed(title: String, amountCents: Long): Long =
withContext(Dispatchers.IO) {
transaction {
Expenses.insert {
it[Expenses.title] = title
it[Expenses.amountCents] = amountCents
} get Expenses.id
}.value
}
Menaya diqqet edin: withContext(IO) icinde biz butun tranzaksiyani saxlayiriq. Bu, eyni “tranzaksiyani qirmamaq” prinsipidir; Exposed-da daha da mentiqli gorunur: tranzaksiya — bir blokdur.
Daha bir vacib fikir: eger transaction { } daxilinde coxlu elaqesiz mentiq etmeye baslasaniz (parsinq, fayl oxuma, sebekə cagirilari), tranzaksiyani saxlayib-qoruma vaxtini uzadirsiniz. Buna gore tranzaksiyanin daxilinde yalniz melumat butovluyune aid olanlari saxlayiriq, qalan her seyi ise colde edirik.
6. Sehvler ve istisnalar
Bu merhelede yeni baslayanlar “dusmesin de” diye dusunur: repozitoriyanin daxilinde her seyi try/catch ile sarmaq ve meselen, bos siyahi qaytarmaq. Bu, istifadeciye qaygi kimi gorunur, amma praktikada problemleri maskalamaqdir. Saxlanma qati istisnani sessizce udanda, xarici kod sehv ferziyyelerle islemeye davam edir: “BD-de xerc yoxdur”, halbuki heqiqetde “BD fayli elcatmazdir” ve ya “cedvel yaradilmiyib”.
Daha duzgun yolu iki uslubdan birine sadiq qalmaqdir. Ya repozitoriya istisnalari colə buraxir (ssenari/CLI istifadeciye nece demeyi hell edir), ya da repozitoriya “ugur/sehv” neticesini value-type kimi aciq qaytarir. Bu muhazire cərcivesinde birinci uslubda qaliriq: istisnani gizletmirik, resurslari use { } ile baglayiriq (istisna olsa bele baglanacaq), tranzaksiyalari ise sablonda rollback() ile geri aliriq.
Mini-numune: yuxari seviyye ssenarisi istisnani tutur ve “disk xetasi”ni “sizde 0 xerc var”a cevirmeden normal mesaj cap edir:
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
try {
// ... servis cagirilari
} catch (e: Exception) {
println("Database error: ${e.message}") // Database error: ...
}
}
Beli, bu ideal sehv emali deyil (sehvlere tip verilməsini etmirik ve stack trace-i gozəl loglamiriq), amma bu artiq duzgun modeldir: eger BD sinibse — biz “her sey yaxsidir” kimi davranmiriq.
7. Korutinlarda BD-ye erisimde tipik sehvler
Sehv №1: JDBC/Exposed-u Dispatchers.Default-da icra etmek, “onsuz da isleyir” diye.
Bu en mekrli problemlerden biridir: kicik tetbiqde derdini gormezsiniz, amma bir-iki paralel emeliyyat peyda olan kimi (ve ya sadəcə yavas disk/sebekə olanda) Default bloklamalarda “iliq” qalacaq. Halbuki Default gozleme ucun yox, hesablamalar ucundur; mehz buna gore dispetcer coroutine context-in bir hissesi kimi var.
Sehv №2: withContext(Dispatchers.IO)-nu saxlanma qati yerine butun koda yaymaq.
Evvelde “tez ve rahat” gorunur: CLI-de bir cagiris saridin — hazirdi. Bir hefte sonra ikincini sarmaqi unudursunuz, iki hefte sonra ucuncu ssenari peyda olur ve siz artiq “butun sorgular dogrudan da IO-dadirmi?” sualina cavab veremirsiniz. Saxlanma qati serhedi mehz bu zemaneti merkezlesdirmek ucundur.
Sehv №3: her ayri sorgunu withContext(IO) ile sarimaq ve tranzaksiya ssenarisini qirmaq.
Eger emeliyyat mentiqi olaraq atomar olmalidirsa, onu bir blok kimi saxlayin: butun tranzaksiya etrafinda tek bir withContext(IO). Tranzaksiya parcalara bolunende kod hem daha az effektiv olur, hem de daha pis oxunur: “burada atomar emeliyyat baslayir, burada bitir” hissi itir.
Sehv №4: repozitoriyanin daxilinde Exception tutmaq ve “dusmesin” diye her seyi println ile evez etmek.
Bele repozitoriya sehvləri yeyen bir deşiye cevrilir. Xarici kod bas vereni anlayibilmir: “melumat yoxdur” ile “BD-ye qosulmaq mumkun olmadi” eyni sey olur. Daha yaxsisi ya istisnani colə buraxmaq, ya da aciq netice-sehv qaytarmaqdir; amma “her sey qaydasindadir” kimi davranmaq deyil.
Sehv №5: Connection veya transaction { }-u cox uzun muddet saxlayib, elaqesiz mentiqle qarishdirmak.
Bazen tranzaksiya tesadufen istifadeci girişi, formatlama, fayl oxuma, hesablamalari ozune daxil edir — bu da BD resurslarinin saxlanma vaxtini uzadir. Hele ki, izolasiya seviyelerini ve tranzaksiya reqabetini oxumursunuzsa bele, bir faydali vərdis var: tranzaksiya kompakt olmalidir ve yalniz melumatin uzlasmasina (consistency) aid olanlari ehtiva etmelidir.
Sehv №6: korutinlarin “kodu avtomatik bloklamayan etmediyini” unutmaq.
Korutini ishe sala bilersiniz, amma daxilinde bloklayici JDBC-metodunu cagirirsinizse, ax yenə de gozlemeye mesgul olacaq. Korutin modeli rahat kod stili ve icra kontekstinin idaresini ved edir, amma I/O fizikasini legv etmir. Buna gore Dispatchers.IO ve seliqeli serhedler mecburi olaraq qalir.
GO TO FULL VERSION