Skip to main content

πŸ’Ύ Database

πŸ“– What is Exposed?​

Exposed is a SQL library for Kotlin. It allows you to perform database operations easily and safely with a type-safe DSL!

πŸ’‘ Configuration​

build.gradle.kts​

dependencies {
implementation("org.jetbrains.exposed:exposed-core:0.44.0")
implementation("org.jetbrains.exposed:exposed-dao:0.44.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.44.0")

// H2 λ°μ΄ν„°λ² μ΄μŠ€ (개발용)
implementation("com.h2database:h2:2.2.224")

// PostgreSQL (ν”„λ‘œλ•μ…˜μš©)
// implementation("org.postgresql:postgresql:42.6.0")
}

🎯 Basic Usage​

Table Definition​

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

object Users : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", 50)
val email = varchar("email", 100)

override val primaryKey = PrimaryKey(id)
}

fun main() {
// λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²°
Database.connect(
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
driver = "org.h2.Driver"
)

transaction {
// λ‘œκΉ… ν™œμ„±ν™”
addLogger(StdOutSqlLogger)

// ν…Œμ΄λΈ” 생성
SchemaUtils.create(Users)

// 데이터 μ‚½μž…
Users.insert {
it[name] = "홍길동"
it[email] = "hong@example.com"
}

// 쑰회
Users.selectAll().forEach {
println("${it[Users.name]}: ${it[Users.email]}")
}
}
}

🎨 CRUD Operations​

Create​

transaction {
// 단건 μ‚½μž…
Users.insert {
it[name] = "κΉ€μ² μˆ˜"
it[email] = "kim@example.com"
}

// ID λ°›κΈ°
val userId = Users.insert {
it[name] = "이영희"
it[email] = "lee@example.com"
} get Users.id

println("μƒμ„±λœ ID: $userId")
}

Read​

transaction {
// 전체 쑰회
val allUsers = Users.selectAll()
allUsers.forEach { row ->
println("${row[Users.id]}: ${row[Users.name]}")
}

// 쑰건 쑰회
val user = Users.select { Users.email eq "hong@example.com" }
.singleOrNull()

if (user != null) {
println("찾음: ${user[Users.name]}")
}

// μ—¬λŸ¬ 쑰건
Users.select {
(Users.name like "κΉ€%") and (Users.email like "%example.com")
}.forEach {
println(it[Users.name])
}
}

Update​

transaction {
// 쑰건에 λ§žλŠ” ν–‰ μˆ˜μ •
Users.update({ Users.email eq "hong@example.com" }) {
it[name] = "홍길동(μˆ˜μ •)"
}

// 전체 μˆ˜μ •
Users.update {
it[email] = concat(email, stringLiteral("_old"))
}
}

Delete​

transaction {
// 쑰건 μ‚­μ œ
Users.deleteWhere { Users.id eq 1 }

// μ—¬λŸ¬ 쑰건
Users.deleteWhere {
(Users.name like "test%") or (Users.email eq "temp@example.com")
}
}

πŸ”§ Practical Example​

User Management System​

object Users : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", 50)
val email = varchar("email", 100).uniqueIndex()
val age = integer("age")
val createdAt = long("created_at")

override val primaryKey = PrimaryKey(id)
}

class UserService {
fun createUser(name: String, email: String, age: Int): Int {
return transaction {
Users.insert {
it[Users.name] = name
it[Users.email] = email
it[Users.age] = age
it[createdAt] = System.currentTimeMillis()
} get Users.id
}
}

fun findById(id: Int): UserData? {
return transaction {
Users.select { Users.id eq id }
.singleOrNull()
?.let {
UserData(
id = it[Users.id],
name = it[Users.name],
email = it[Users.email],
age = it[Users.age]
)
}
}
}

fun findAll(): List<UserData> {
return transaction {
Users.selectAll().map {
UserData(
id = it[Users.id],
name = it[Users.name],
email = it[Users.email],
age = it[Users.age]
)
}
}
}

fun updateUser(id: Int, name: String?, email: String?, age: Int?): Boolean {
return transaction {
val updated = Users.update({ Users.id eq id }) {
name?.let { value -> it[Users.name] = value }
email?.let { value -> it[Users.email] = value }
age?.let { value -> it[Users.age] = value }
}
updated > 0
}
}

fun deleteUser(id: Int): Boolean {
return transaction {
Users.deleteWhere { Users.id eq id } > 0
}
}
}

data class UserData(
val id: Int,
val name: String,
val email: String,
val age: Int
)

🎯 Relational Data​

1:N Relationship​

object Posts : Table() {
val id = integer("id").autoIncrement()
val userId = integer("user_id").references(Users.id)
val title = varchar("title", 200)
val content = text("content")

override val primaryKey = PrimaryKey(id)
}

fun getUserWithPosts(userId: Int): UserWithPosts? {
return transaction {
val user = Users.select { Users.id eq userId }
.singleOrNull() ?: return@transaction null

val posts = Posts.select { Posts.userId eq userId }
.map {
PostData(
id = it[Posts.id],
title = it[Posts.title],
content = it[Posts.content]
)
}

UserWithPosts(
id = user[Users.id],
name = user[Users.name],
posts = posts
)
}
}

data class PostData(val id: Int, val title: String, val content: String)
data class UserWithPosts(val id: Int, val name: String, val posts: List<PostData>)

JOIN Queries​

fun getPostsWithAuthors(): List<PostWithAuthor> {
return transaction {
(Posts innerJoin Users)
.selectAll()
.map {
PostWithAuthor(
postId = it[Posts.id],
title = it[Posts.title],
authorName = it[Users.name]
)
}
}
}

data class PostWithAuthor(
val postId: Int,
val title: String,
val authorName: String
)

πŸ”₯ Advanced Features​

Transactions​

fun transferMoney(fromId: Int, toId: Int, amount: Int) {
transaction {
// 좜금
Accounts.update({ Accounts.id eq fromId }) {
it[balance] = balance - amount
}

// μž…κΈˆ
Accounts.update({ Accounts.id eq toId }) {
it[balance] = balance + amount
}

// μ˜ˆμ™Έ λ°œμƒ μ‹œ μžλ™ λ‘€λ°±
}
}

Batch Insert​

fun createMultipleUsers(users: List<NewUser>) {
transaction {
Users.batchInsert(users) { user ->
this[Users.name] = user.name
this[Users.email] = user.email
this[Users.age] = user.age
}
}
}

data class NewUser(val name: String, val email: String, val age: Int)

Pagination​

fun getUsersPage(page: Int, size: Int): List<UserData> {
return transaction {
Users.selectAll()
.limit(size, offset = ((page - 1) * size).toLong())
.orderBy(Users.id)
.map {
UserData(
id = it[Users.id],
name = it[Users.name],
email = it[Users.email],
age = it[Users.age]
)
}
}
}

πŸ› οΈ Ktor Integration​

Database Initialization​

fun Application.configureDatabases() {
Database.connect(
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
driver = "org.h2.Driver"
)

transaction {
SchemaUtils.create(Users, Posts)
}
}

fun Application.module() {
configureDatabases()

val userService = UserService()

install(ContentNegotiation) {
json()
}

routing {
route("/users") {
get {
val users = userService.findAll()
call.respond(users)
}

get("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
val user = id?.let { userService.findById(it) }

if (user != null) {
call.respond(user)
} else {
call.respond(HttpStatusCode.NotFound)
}
}

post {
val request = call.receive<CreateUserRequest>()
val userId = userService.createUser(
request.name,
request.email,
request.age
)
call.respond(HttpStatusCode.Created, mapOf("id" to userId))
}
}
}
}

πŸ€” Frequently Asked Questions​

Q1. H2 vs PostgreSQL?​

A: H2 for development, PostgreSQL for production!

// H2 (개발 - λ©”λͺ¨λ¦¬)
Database.connect(
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
driver = "org.h2.Driver"
)

// PostgreSQL (ν”„λ‘œλ•μ…˜)
Database.connect(
url = "jdbc:postgresql://localhost:5432/mydb",
driver = "org.postgresql.Driver",
user = "user",
password = "password"
)

Q2. What about migrations?​

A: Using Flyway or Liquibase is recommended!

// Flyway
dependencies {
implementation("org.flywaydb:flyway-core:9.22.0")
}

val flyway = Flyway.configure()
.dataSource(url, user, password)
.load()

flyway.migrate()

Q3. Performance optimization?​

A: Use connection pools!

dependencies {
implementation("com.zaxxer:HikariCP:5.0.1")
}

val config = HikariConfig().apply {
jdbcUrl = "jdbc:postgresql://localhost:5432/mydb"
username = "user"
password = "password"
maximumPoolSize = 10
}

val dataSource = HikariDataSource(config)
Database.connect(dataSource)

🎬 Conclusion​

Secure database operations with Exposed!

Key Takeaways:
βœ… Type-safe SQL DSL
βœ… Simple CRUD operations
βœ… Transaction support
βœ… JOIN and relational data
βœ… Easy Ktor integration

Next Step: Learn how to deploy your server to a production environment in Deployment!