Exposed для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое Exposed и зачем он нужен?

Exposed — это легковесная, но мощная ORM-библиотека (Object-Relational Mapping) для языка программирования Kotlin, разработанная компанией JetBrains. Она предоставляет два основных подхода к работе с базами данных: DSL (Domain Specific Language) и DAO (Data Access Objects).

Зачем нужен Exposed?

  • Типобезопасность: В отличие от сырых SQL-запросов, Exposed использует типизированные конструкции Kotlin, что позволяет отлавливать ошибки на этапе компиляции.
  • Лаконичность: Код на Exposed значительно короче и читаемее, чем эквивалентные SQL-запросы или код на Java с JPA.
  • Производительность: Exposed не использует рефлексию и прокси-объекты (как Hibernate), что делает его очень быстрым и предсказуемым.
  • Гибкость: Вы можете легко переключаться между DSL (для сложных запросов) и DAO (для простых CRUD-операций) в рамках одного проекта.
  • Поддержка множества БД: PostgreSQL, MySQL, MariaDB, SQLite, H2, Oracle, SQL Server.

Установка Exposed

Exposed устанавливается через систему сборки Gradle или Maven. Добавьте следующие зависимости в ваш build.gradle.kts:

dependencies {
    // Основная библиотека
    implementation("org.jetbrains.exposed:exposed-core:0.44.1")
    // DSL для запросов
    implementation("org.jetbrains.exposed:exposed-dao:0.44.1")
    // JDBC-драйвер для вашей БД (пример для PostgreSQL)
    implementation("org.postgresql:postgresql:42.7.1")
    // Пул соединений HikariCP (рекомендуется)
    implementation("com.zaxxer:HikariCP:5.1.0")
    // Логирование SQL (опционально)
    implementation("ch.qos.logback:logback-classic:1.4.14")
}

Для Maven (pom.xml):

<dependency>
    <groupId>org.jetbrains.exposed</groupId>
    <artifactId>exposed-core</artifactId>
    <version>0.44.1</version>
</dependency>

Быстрый старт: пример кода

Создадим простое приложение для управления задачами (ToDo):

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import java.time.LocalDateTime

// 1. Определяем таблицу
object Tasks : Table() {
    val id = integer("id").autoIncrement()
    val title = varchar("title", 255)
    val description = text("description").nullable()
    val isCompleted = bool("is_completed").default(false)
    val createdAt = datetime("created_at").default(LocalDateTime.now())

    override val primaryKey = PrimaryKey(id)
}

// 2. Data class для удобства
data class Task(
    val id: Int,
    val title: String,
    val description: String?,
    val isCompleted: Boolean,
    val createdAt: LocalDateTime
)

fun main() {
    // 3. Подключаемся к БД (H2 в памяти для примера)
    Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")

    transaction {
        // 4. Создаем таблицу
        SchemaUtils.create(Tasks)

        // 5. Вставляем данные
        val insertId = Tasks.insert {
            it[title] = "Изучить Exposed"
            it[description] = "Прочитать документацию и написать пример"
        } get Tasks.id

        println("Вставлена задача с ID: $insertId")

        // 6. Выбираем все задачи
        val allTasks = Tasks.selectAll().map { row ->
            Task(
                id = row[Tasks.id],
                title = row[Tasks.title],
                description = row[Tasks.description],
                isCompleted = row[Tasks.isCompleted],
                createdAt = row[Tasks.createdAt]
            )
        }
        println("Все задачи: $allTasks")

        // 7. Обновляем задачу
        Tasks.update({ Tasks.id eq 1 }) {
            it[isCompleted] = true
        }

        // 8. Удаляем задачу
        Tasks.deleteWhere { Tasks.id eq 1 }
    }
}

Основные методы и функции Exposed (минимум 10)

1. Database.connect()

Что делает: Устанавливает соединение с базой данных. Это обязательный шаг перед выполнением любых операций.

Параметры:

  • url (String) — JDBC URL подключения
  • driver (String) — класс JDBC-драйвера
  • user (String?) — имя пользователя (опционально)
  • password (String?) — пароль (опционально)
  • databaseConfig (DatabaseConfig?) — дополнительная конфигурация

Пример:

Database.connect(
    url = "jdbc:postgresql://localhost:5432/mydb",
    driver = "org.postgresql.Driver",
    user = "postgres",
    password = "secret"
)

2. transaction { ... }

Что делает: Оборачивает блок кода в транзакцию. Все операции внутри блока выполняются атомарно. При ошибке происходит откат.

Параметры: Принимает лямбда-выражение с кодом.

Пример:

transaction {
    // Все операции здесь
    Users.insert { it[name] = "Alice" }
    // Если здесь будет исключение, вставка откатится
}

3. SchemaUtils.create()

Что делает: Создает таблицы в базе данных на основе определенных объектов Table. Если таблицы уже существуют — ничего не делает (безопасно для повторного вызова).

Параметры: Принимает vararg объектов Table.

Пример:

transaction {
    SchemaUtils.create(Tasks, Users, Categories)
}

4. Table.insert { ... }

Что делает: Вставляет новую строку в таблицу. Возвращает InsertStatement, из которого можно получить сгенерированные значения.

Параметры: Лямбда, где вы устанавливаете значения колонок через it[columnName] = value.

Пример:

val userId = Users.insert {
    it[name] = "Bob"
    it[email] = "bob@example.com"
    it[age] = 25
} get Users.id

5. Table.selectAll()

Что делает: Выбирает все строки из таблицы. Возвращает Query, который можно преобразовать в список.

Параметры: Нет (выбирает все колонки).

Пример:

val allUsers = Users.selectAll().map { row ->
    User(
        id = row[Users.id],
        name = row[Users.name]
    )
}

6. Table.select()

Что делает: Выбирает строки с условием. Позволяет фильтровать, сортировать и группировать данные.

Параметры: Принимает условие (обычно через лямбду с where).

Пример:

val adults = Users.select {
    Users.age greaterEq 18
}.orderBy(Users.age to SortOrder.DESC)
  .limit(10)

7. Table.update()

Что делает: Обновляет существующие строки, удовлетворяющие условию.

Параметры: Условие (первый параметр) и лямбда с новыми значениями.

Пример:

Users.update({ Users.id eq 5 }) {
    it[name] = "Updated Name"
    it[age] = it[age] + 1  // Инкремент
}

8. Table.deleteWhere()

Что делает: Удаляет строки, удовлетворяющие условию.

Параметры: Условие в виде лямбды.

Пример:

Users.deleteWhere { Users.age less 18 }
// Или с AND
Users.deleteWhere {
    (Users.age less 18) and (Users.name eq "Guest")
}

9. Table.count()

Что делает: Возвращает количество строк в таблице (с учетом условия).

Параметры: Опциональное условие.

Пример:

val totalUsers = Users.count()
val activeUsers = Users.select { Users.isActive eq true }.count()

10. Table.slice() с count()

Что делает: Позволяет выполнять агрегатные функции (SUM, AVG, COUNT, MAX, MIN) над определенными колонками.

Параметры: Колонки для агрегации и группировки.

Пример:

// Средний возраст по группам
val avgAgeByGroup = Users
    .slice(Users.groupId, Users.age.avg())
    .selectAll()
    .groupBy(Users.groupId)
    .map { row ->
        Pair(row[Users.groupId], row[Users.age.avg()])
    }

// Сумма зарплат
val totalSalary = Users.slice(Users.salary.sum()).selectAll().first()[Users.salary.sum()]

11. innerJoin(), leftJoin()

Что делает: Выполняет JOIN между таблицами.

Параметры: Другая таблица и условие соединения.

Пример:

val usersWithOrders = (Users innerJoin Orders)
    .select { Users.id eq Orders.userId }
    .map { row ->
        UserWithOrder(
            userName = row[Users.name],
            orderTotal = row[Orders.total]
        )
    }

12. batchInsert()

Что делает: Вставляет множество строк за один SQL-запрос (значительно быстрее, чем множество отдельных insert).

Параметры: Данные (список) и лямбда для маппинга.

Пример:

val newUsers = listOf(
    "Alice" to 25,
    "Bob" to 30,
    "Charlie" to 35
)

Users.batchInsert(newUsers) { (name, age) ->
    this[Users.name] = name
    this[Users.age] = age
}

Реальный практический пример: Система управления заказами

Создадим полноценный пример с двумя связанными таблицами:

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.math.BigDecimal
import java.time.LocalDateTime

// Таблица клиентов
object Customers : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 100)
    val email = varchar("email", 255).uniqueIndex()
    val createdAt = datetime("created_at").default(LocalDateTime.now())

    override val primaryKey = PrimaryKey(id)
}

// Таблица заказов
object Orders : Table() {
    val id = integer("id").autoIncrement()
    val customerId = integer("customer_id").references(Customers.id)
    val totalAmount = decimal("total_amount", 10, 2)
    val status = varchar("status", 20).default("pending")
    val orderDate = datetime("order_date").default(LocalDateTime.now())

    override val primaryKey = PrimaryKey(id)
}

data class Customer(val id: Int, val name: String, val email: String)
data class Order(val id: Int, val customerId: Int, val total: BigDecimal, val status: String)

fun main() {
    Database.connect("jdbc:h2:mem:shop;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")

    transaction {
        // Создаем таблицы                    

Рекомендаций