Українська
Репозиторій, міграції та тести
Репозиторій та корутини
Репозиторій дає предметний API, наприклад listBooks, а SQL залишає всередині. Назовні повертаються data-класи, не ліниві Query чи DAO-сутності. Результат потрібно матеріалізувати до завершення транзакції. Це спрощує тестування та не прив’язує екран до активного JDBC-з’єднання.
JDBC є блокувальним. Модифікатор suspend не перетворює його на неблокувальний драйвер. У прикладі withContext(IO) переносить коротку звичайну транзакцію на відповідний пул. У транзакції немає додаткових призупинень і передавання її стану іншому потоку.
Приклад 4. Асинхронний репозиторій назв
kotlin
import java.nio.file.Files
import kotlinx.coroutines.*
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
object Titles : Table("titles") {
val id = integer("id").autoIncrement()
val name = varchar("name", 120).uniqueIndex()
override val primaryKey = PrimaryKey(id)
}
data class Title(val id: Int, val name: String)
class TitleRepository(private val db: Database) {
suspend fun add(name: String): Unit =
withContext(Dispatchers.IO) {
require(name.isNotBlank() && name.length <= 120)
transaction(db) {
Titles.insert { it[Titles.name] = name.trim() }
}
}
suspend fun all(): List<Title> = withContext(Dispatchers.IO) {
transaction(db) {
Titles.selectAll().orderBy(Titles.id).map {
Title(it[Titles.id], it[Titles.name])
}
}
}
}
fun main() = runBlocking {
val path = Files.createTempFile("titles-", ".db")
val db = Database.connect("jdbc:sqlite:$path",
"org.sqlite.JDBC")
withContext(Dispatchers.IO) {
transaction(db) { SchemaUtils.create(Titles) }
}
val repository = TitleRepository(db)
repository.add("Кобзар")
println(repository.all())
path.toFile().deleteOnExit()
}text
[Title(id=1, name=Кобзар)]Exposed має окремий модуль exposed-r2dbc з реактивним драйвером і suspendTransaction. Це інша модель виконання; не слід копіювати її імпорти в JDBC-проєкт. Старі приклади newSuspendedTransaction потребують звірення з поточною міграцією API. Для нашої SQLite-програми достатньо явної короткої JDBC-транзакції на Dispatchers.IO.
Міграції, захист і тести
Після появи реальних даних схему змінюють версійними міграціями. Міграція додає стовпець, переносить значення, встановлює індекс і має бути перевірена на копії попередньої версії бази. Exposed надає засоби формування міграцій; Flyway є окремим інструментом керування SQL-міграціями. Автоматичний diff схеми потрібно прочитати перед застосуванням, особливо при видаленні даних.
Параметризовані DSL-умови захищають значення від SQL-ін’єкції. Якщо потрібний raw SQL, використовуйте параметри драйвера, а не "... WHERE name = '$name'". Імена таблиць чи напрям сортування, обрані користувачем, звіряють із дозволеним списком: параметр значення не підставляє довільний SQL-синтаксис.
Тест репозиторію створює власну тимчасову базу, готує схему, виконує операцію й читає результат новою транзакцією. Перевірте порожній список, дублікат унікального поля, невідомий зовнішній ключ, видалення відсутнього id, відкат після першої зміни та повторне відкриття файла. У пам’яті SQLite база може бути прив’язана до життя з’єднання, тому файл часто простіший для тесту.
H2 швидкий для частини тестів, але не є точним емулятором SQLite чи PostgreSQL. Тести ключових обмежень і транзакцій слід повторити на тій СКБД, яку використає застосунок. Підроблений репозиторій перевіряє логіку UI, а інтеграційний тест із драйвером – SQL; ці перевірки доповнюють одна одну.
Перевірка схеми перед предметною логікою
Спочатку перевірте, що база справді виконує заявлені обмеження. Це окреме питання від правильності Kotlin-форми: користувач може відкрити файл іншим клієнтом. Підготуйте три сценарії, кожний у власній транзакції на новому тимчасовому файлі.
- Вставте автора та книгу з його id. Після COMMIT повторно прочитайте їх новою транзакцією й порівняйте поля.
- Спробуйте вставити книгу з id автора, якого немає. Виняток повинен вийти з транзакції, а кількість книг залишитися сталою.
- Спробуйте видалити автора, на якого посилається книга. Результат має відповідати обраному ON DELETE: заборона, каскад або очищення nullable-посилання. Не приймайте будь-яку поведінку як успіх лише тому, що процес не аварійно завершився.
Перевірка PRAGMA foreign_keys у тому самому з’єднанні допомагає знайти вимкнений контроль посилань. Значення 0 пояснює, чому «правильна» схема пропустила невідомий ключ. Увімкнення на одному з’єднанні пулу недостатньо: налаштування потрібне кожному.
Далі перевірте інваріант переказу: сума двох балансів до й після успішної операції однакова. Після штучного збою обидва баланси мають дорівнювати початковим. Для недостатнього залишку не повинна змінитися жодна таблиця, включно з журналом операцій. Такий тест перевіряє результат, який важливий предметній області.
Робота зі збоями та журналом
Розрізняйте неправильні дані, тимчасову зайнятість бази й помилку програмування. Користувачеві потрібне зрозуміле повідомлення про невдалу операцію; розробникові – причина та контекст без паролів і зайвих особистих даних. Повторення SQL-запиту після кожного винятку може приховати пошкоджену схему або багаторазово виконати зовнішню дію.
На JDK 27 SQLite JDBC використовує нативну бібліотеку. Для довіреної залежності JVM-параметр --enable-native-access=ALL-UNNAMED явно дозволяє такий доступ. Попередження про SLF4J без provider означає, що не підключений backend журналювання; це не доказ помилки SQL. Для навчальних результатів відділяйте предметне stdout від діагностичного stderr. Український текст у перенаправленому виведенні перевіряйте з узгодженим UTF-8 кодуванням термінала та JVM.