Что такое 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 {
// Создаем таблицы