Exposed란?
Kotlin용 ORM 프레임워크이자 JDBC 드라이버를 사용하는 경량 SQL 라이브러리. Kotlin DSL을 활용하여 SQL을 보다 직관적으로 작성할 수 있으며, DAO 방식도 지원하여 객체지향적인 데이터 접근이 가능합니다.
지원 데이터베이스
- MariaDB
- MySQL
- Oracle
- Postgres
- Microsoft SQL
- SQLITE
- H2 (versions 2.x; 1.x version은 deprecated 예정)
- (Also, PostgreSQL using the pgjdbc-ng JDBC driver)
Exposed 모듈
- exposed-core - 기본 모듈로, DSL API와 매핑 기능을 포함
- exposed-crypt - 암호화된 데이터를 데이터베이스에 저장할 수 있도록 추가 컬럼 타입을 제공하며, 클라이언트 측에서 인코딩/디코딩 지원
- exposed-dao - DAO API 제공
- exposed-java-time - Java 8 Time API 기반의 날짜 및 시간 확장 기능 제공
- exposed-jdbc - Java JDBC API 기반의 전송 계층 구현
- exposed-jodatime - JodaTime 라이브러리를 기반으로 한 날짜 및 시간 확장 기능 제공
- exposed-json - JSON 및 JSONB 데이터 타입 확장 기능 제공
- exposed-kotlin-datetime - kotlinx-datetime 기반의 날짜 및 시간 확장 기능 제공
- exposed-money - "javax.money:money-api"의 MonetaryAmount를 지원하는 확장 기능 제공
- exposed-spring-boot-starter - Hibernate 대신 Exposed를 ORM으로 활용할 수 있도록 지원하는 Spring Boot 스타터
지원 데이터베이스 접근 방식
- DSL 방식
- DAO 방식
Exposed 사용 예시
DSL & DAO 사용 권장 케이스
DSL 방식
- 복잡한 조인이나 집계가 필요한 경우
- 성능이 중요한 쿼리
- 동적 쿼리가 필요한 경우
- 데이터 분석이나 리포트 생성
DAO 방식
- 단순한 CRUD 작업
- 객체 중심의 비즈니스 로직
- 작은 규모의 데이터 처리
- 타입 안전성이 중요한 경우
DSL & DAO 방식 예시
예시 데이터
object Users : Table() {
val id: Column<String> = varchar("id", 10)
val name: Column<String> = varchar("name", length = 50)
val cityId: Column<Int?> = (integer("city_id") references Cities.id).nullable()
override val primaryKey = PrimaryKey(id, name = "PK_User_ID") // name is optional here
}
object Cities : Table() {
val id: Column<Int> = integer("id").autoIncrement()
val name: Column<String> = varchar("name", 50)
override val primaryKey = PrimaryKey(id, name = "PK_Cities_ID")
}
// DAO 방식을 사용하기 위한 Entity
class City(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<City>(Cities)
var name by Cities.name
}
object Users : StringIdTable() {
val name = varchar("name", 50)
val city = reference("city_id", Cities).nullable()
}
단순 조회
transaction {
// DSL
Users.selectAll().map {
it[Users.id] to it[Users.name]
}.forEach { (id, name) ->
println("ID: $id, Name: $name")
}
// DAO
User.all().forEach {
println("ID: ${it.id}, Name: ${it.name}, City: ${it.city?.name}")
}
}
Where를 사용한 조회
transaction {
// DSL
Users.select { Users.name eq "Alice" }
.map { it[Users.id] to it[Users.name] }
.forEach { (id, name) -> println("ID: $id, Name: $name") }
// DAO
User.find { Users.name eq "Alice" }.forEach {
println("ID: ${it.id}, Name: ${it.name}, City: ${it.city?.name}")
}
}
In절을 사용한 조회
transaction {
// DSL
Users.select { Users.id inList listOf("user1", "user2", "user3") }
.map { it[Users.id] to it[Users.name] }
.forEach { (id, name) -> println("ID: $id, Name: $name") }
// DAO
User.find { Users.id inList listOf("user1", "user2", "user3") }.forEach {
println("ID: ${it.id}, Name: ${it.name}, City: ${it.city?.name}")
}
}
Join & Group By를 사용한 조회
transaction {
// DSL
(Users innerJoin Cities)
.slice(Cities.name, Users.id.count())
.selectAll()
.groupBy(Cities.name)
.map { it[Cities.name] to it[Users.id.count()] }
.forEach { (cityName, count) -> println("City: $cityName, User Count: $count") }
// DAO
// GROUP BY 같이 복잡한 쿼리는 DSL 방식 이용 추천
}
Left Join을 사용한 조회
transaction {
// DSL
(Users leftJoin Cities)
.selectAll()
.map { it[Users.id] to it[Users.name] to it[Cities.name] }
.forEach { (user, city) -> println("User: ${user.first}, Name: ${user.second}, City: $city") }
// DAO
User.all().forEach {
val cityName = it.city?.name ?: "No city"
println("User: ${it.id}, Name: ${it.name}, City: $cityName")
}
}
페이징 조회
transaction {
// DSL
Users.selectAll()
.limit(10, offset = 20)
.map { it[Users.id] to it[Users.name] }
.forEach { (id, name) -> println("ID: $id, Name: $name") }
// DAO
User.all().limit(10, offset = 20).forEach {
println("ID: ${it.id}, Name: ${it.name}, City: ${it.city?.name}")
}
}
데이터 삽입
transaction {
// DSL
Users.insert {
it[id] = "user123"
it[name] = "John Doe"
it[cityId] = 1
}
// DAO
val city = City.new {
name = "Seoul"
}
User.new("user123") {
name = "John Doe"
this.city = city
}
}
데이터 변경
transaction {
// DSL
Users.update({ Users.id eq "user123" }) {
it[name] = "John Updated"
it[cityId] = 2
}
// DAO
val user = User.findById("user123")
user?.apply {
name = "John Updated"
city = City.findById(2)
}
}
데이터 삭제
transaction {
// DSL
Users.deleteWhere { Users.id eq "user123" }
// DAO
User.findById("user123")?.delete()
}
사용시 참고할 점
- 트랜잭션(transaction)이 필수
- Exposed는 모든 데이터베이스 작업이 트랜잭션 내에서 실행되도록 설계됨
- 불필요한 트랜잭션을 줄이려면 readOnly = true 옵션 사용 가능
transaction(readOnly = true) { Users.selectAll().forEach { println(it[Users.name]) } }
- 여러 데이터베이스 동시 사용
- Database.connect()를 각각 설정하여 동시에 여러 DB 연결 가능
- transaction을 중첩하여 사용
- 스키마 지정 가능
- 테이블 선언 시 Table("schema.table_name")으로 스키마 지정 가능
참조
Exposed Github -https://github.com/JetBrains/Exposed?tab=readme-ov-file
Exposed Document - https://www.jetbrains.com/help/exposed/home.html
반응형