kotlin-exposed-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseKotlin Exposed Patterns
Kotlin Exposed 相关模式
Comprehensive patterns for database access with JetBrains Exposed ORM, including DSL queries, DAO, transactions, and production-ready configuration.
本文提供使用JetBrains Exposed ORM进行数据库访问的各类实用模式,包括DSL查询、DAO、事务处理以及可用于生产环境的配置方案。
When to Use
适用场景
- Setting up database access with Exposed
- Writing SQL queries using Exposed DSL or DAO
- Configuring connection pooling with HikariCP
- Creating database migrations with Flyway
- Implementing the repository pattern with Exposed
- Handling JSON columns and complex queries
- 基于Exposed搭建数据库访问模块
- 使用Exposed DSL或DAO编写SQL查询
- 配置HikariCP连接池
- 通过Flyway创建数据库迁移脚本
- 基于Exposed实现仓库模式
- 处理JSON列与复杂查询
How It Works
工作原理
Exposed provides two query styles: DSL for direct SQL-like expressions and DAO for entity lifecycle management. HikariCP manages a pool of reusable database connections configured via . Flyway runs versioned SQL migration scripts at startup to keep the schema in sync. All database operations run inside blocks for coroutine safety and atomicity. The repository pattern wraps Exposed queries behind an interface so business logic stays decoupled from the data layer and tests can use an in-memory H2 database.
HikariConfignewSuspendedTransactionExposed提供两种查询风格:DSL用于编写类原生SQL的表达式,DAO用于实体生命周期管理。HikariCP通过配置来管理可复用的数据库连接池。Flyway会在应用启动时执行版本化的SQL迁移脚本,确保数据库架构保持同步。所有数据库操作都在代码块中执行,以保证协程安全和原子性。仓库模式将Exposed查询封装在接口之后,使业务逻辑与数据层解耦,同时测试时可使用内存中的H2数据库。
HikariConfignewSuspendedTransactionExamples
示例
DSL Query
DSL查询
kotlin
suspend fun findUserById(id: UUID): UserRow? =
newSuspendedTransaction {
UsersTable.selectAll()
.where { UsersTable.id eq id }
.map { it.toUser() }
.singleOrNull()
}kotlin
suspend fun findUserById(id: UUID): UserRow? =
newSuspendedTransaction {
UsersTable.selectAll()
.where { UsersTable.id eq id }
.map { it.toUser() }
.singleOrNull()
}DAO Entity Usage
DAO实体使用
kotlin
suspend fun createUser(request: CreateUserRequest): User =
newSuspendedTransaction {
UserEntity.new {
name = request.name
email = request.email
role = request.role
}.toModel()
}kotlin
suspend fun createUser(request: CreateUserRequest): User =
newSuspendedTransaction {
UserEntity.new {
name = request.name
email = request.email
role = request.role
}.toModel()
}HikariCP Configuration
HikariCP配置
kotlin
val hikariConfig = HikariConfig().apply {
driverClassName = config.driver
jdbcUrl = config.url
username = config.username
password = config.password
maximumPoolSize = config.maxPoolSize
isAutoCommit = false
transactionIsolation = "TRANSACTION_READ_COMMITTED"
validate()
}kotlin
val hikariConfig = HikariConfig().apply {
driverClassName = config.driver
jdbcUrl = config.url
username = config.username
password = config.password
maximumPoolSize = config.maxPoolSize
isAutoCommit = false
transactionIsolation = "TRANSACTION_READ_COMMITTED"
validate()
}Database Setup
数据库配置
HikariCP Connection Pooling
HikariCP连接池
kotlin
// DatabaseFactory.kt
object DatabaseFactory {
fun create(config: DatabaseConfig): Database {
val hikariConfig = HikariConfig().apply {
driverClassName = config.driver
jdbcUrl = config.url
username = config.username
password = config.password
maximumPoolSize = config.maxPoolSize
isAutoCommit = false
transactionIsolation = "TRANSACTION_READ_COMMITTED"
validate()
}
return Database.connect(HikariDataSource(hikariConfig))
}
}
data class DatabaseConfig(
val url: String,
val driver: String = "org.postgresql.Driver",
val username: String = "",
val password: String = "",
val maxPoolSize: Int = 10,
)kotlin
// DatabaseFactory.kt
object DatabaseFactory {
fun create(config: DatabaseConfig): Database {
val hikariConfig = HikariConfig().apply {
driverClassName = config.driver
jdbcUrl = config.url
username = config.username
password = config.password
maximumPoolSize = config.maxPoolSize
isAutoCommit = false
transactionIsolation = "TRANSACTION_READ_COMMITTED"
validate()
}
return Database.connect(HikariDataSource(hikariConfig))
}
}
data class DatabaseConfig(
val url: String,
val driver: String = "org.postgresql.Driver",
val username: String = "",
val password: String = "",
val maxPoolSize: Int = 10,
)Flyway Migrations
Flyway迁移
kotlin
// FlywayMigration.kt
fun runMigrations(config: DatabaseConfig) {
Flyway.configure()
.dataSource(config.url, config.username, config.password)
.locations("classpath:db/migration")
.baselineOnMigrate(true)
.load()
.migrate()
}
// Application startup
fun Application.module() {
val config = DatabaseConfig(
url = environment.config.property("database.url").getString(),
username = environment.config.property("database.username").getString(),
password = environment.config.property("database.password").getString(),
)
runMigrations(config)
val database = DatabaseFactory.create(config)
// ...
}kotlin
// FlywayMigration.kt
fun runMigrations(config: DatabaseConfig) {
Flyway.configure()
.dataSource(config.url, config.username, config.password)
.locations("classpath:db/migration")
.baselineOnMigrate(true)
.load()
.migrate()
}
// 应用启动
fun Application.module() {
val config = DatabaseConfig(
url = environment.config.property("database.url").getString(),
username = environment.config.property("database.username").getString(),
password = environment.config.property("database.password").getString(),
)
runMigrations(config)
val database = DatabaseFactory.create(config)
// ...
}Migration Files
迁移文件
sql
-- src/main/resources/db/migration/V1__create_users.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
role VARCHAR(20) NOT NULL DEFAULT 'USER',
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);sql
-- src/main/resources/db/migration/V1__create_users.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
role VARCHAR(20) NOT NULL DEFAULT 'USER',
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);Table Definitions
表定义
DSL Style Tables
DSL风格表
kotlin
// tables/UsersTable.kt
object UsersTable : UUIDTable("users") {
val name = varchar("name", 100)
val email = varchar("email", 255).uniqueIndex()
val role = enumerationByName<Role>("role", 20)
val metadata = jsonb<UserMetadata>("metadata", Json.Default).nullable()
val createdAt = timestampWithTimeZone("created_at").defaultExpression(CurrentTimestampWithTimeZone)
val updatedAt = timestampWithTimeZone("updated_at").defaultExpression(CurrentTimestampWithTimeZone)
}
object OrdersTable : UUIDTable("orders") {
val userId = uuid("user_id").references(UsersTable.id)
val status = enumerationByName<OrderStatus>("status", 20)
val totalAmount = long("total_amount")
val currency = varchar("currency", 3)
val createdAt = timestampWithTimeZone("created_at").defaultExpression(CurrentTimestampWithTimeZone)
}
object OrderItemsTable : UUIDTable("order_items") {
val orderId = uuid("order_id").references(OrdersTable.id, onDelete = ReferenceOption.CASCADE)
val productId = uuid("product_id")
val quantity = integer("quantity")
val unitPrice = long("unit_price")
}kotlin
// tables/UsersTable.kt
object UsersTable : UUIDTable("users") {
val name = varchar("name", 100)
val email = varchar("email", 255).uniqueIndex()
val role = enumerationByName<Role>("role", 20)
val metadata = jsonb<UserMetadata>("metadata", Json.Default).nullable()
val createdAt = timestampWithTimeZone("created_at").defaultExpression(CurrentTimestampWithTimeZone)
val updatedAt = timestampWithTimeZone("updated_at").defaultExpression(CurrentTimestampWithTimeZone)
}
object OrdersTable : UUIDTable("orders") {
val userId = uuid("user_id").references(UsersTable.id)
val status = enumerationByName<OrderStatus>("status", 20)
val totalAmount = long("total_amount")
val currency = varchar("currency", 3)
val createdAt = timestampWithTimeZone("created_at").defaultExpression(CurrentTimestampWithTimeZone)
}
object OrderItemsTable : UUIDTable("order_items") {
val orderId = uuid("order_id").references(OrdersTable.id, onDelete = ReferenceOption.CASCADE)
val productId = uuid("product_id")
val quantity = integer("quantity")
val unitPrice = long("unit_price")
}Composite Tables
复合表
kotlin
object UserRolesTable : Table("user_roles") {
val userId = uuid("user_id").references(UsersTable.id, onDelete = ReferenceOption.CASCADE)
val roleId = uuid("role_id").references(RolesTable.id, onDelete = ReferenceOption.CASCADE)
override val primaryKey = PrimaryKey(userId, roleId)
}kotlin
object UserRolesTable : Table("user_roles") {
val userId = uuid("user_id").references(UsersTable.id, onDelete = ReferenceOption.CASCADE)
val roleId = uuid("role_id").references(RolesTable.id, onDelete = ReferenceOption.CASCADE)
override val primaryKey = PrimaryKey(userId, roleId)
}DSL Queries
DSL查询
Basic CRUD
基础CRUD操作
kotlin
// Insert
suspend fun insertUser(name: String, email: String, role: Role): UUID =
newSuspendedTransaction {
UsersTable.insertAndGetId {
it[UsersTable.name] = name
it[UsersTable.email] = email
it[UsersTable.role] = role
}.value
}
// Select by ID
suspend fun findUserById(id: UUID): UserRow? =
newSuspendedTransaction {
UsersTable.selectAll()
.where { UsersTable.id eq id }
.map { it.toUser() }
.singleOrNull()
}
// Select with conditions
suspend fun findActiveAdmins(): List<UserRow> =
newSuspendedTransaction {
UsersTable.selectAll()
.where { (UsersTable.role eq Role.ADMIN) }
.orderBy(UsersTable.name)
.map { it.toUser() }
}
// Update
suspend fun updateUserEmail(id: UUID, newEmail: String): Boolean =
newSuspendedTransaction {
UsersTable.update({ UsersTable.id eq id }) {
it[email] = newEmail
it[updatedAt] = CurrentTimestampWithTimeZone
} > 0
}
// Delete
suspend fun deleteUser(id: UUID): Boolean =
newSuspendedTransaction {
UsersTable.deleteWhere { UsersTable.id eq id } > 0
}
// Row mapping
private fun ResultRow.toUser() = UserRow(
id = this[UsersTable.id].value,
name = this[UsersTable.name],
email = this[UsersTable.email],
role = this[UsersTable.role],
metadata = this[UsersTable.metadata],
createdAt = this[UsersTable.createdAt],
updatedAt = this[UsersTable.updatedAt],
)kotlin
// 插入
suspend fun insertUser(name: String, email: String, role: Role): UUID =
newSuspendedTransaction {
UsersTable.insertAndGetId {
it[UsersTable.name] = name
it[UsersTable.email] = email
it[UsersTable.role] = role
}.value
}
// 根据ID查询
suspend fun findUserById(id: UUID): UserRow? =
newSuspendedTransaction {
UsersTable.selectAll()
.where { UsersTable.id eq id }
.map { it.toUser() }
.singleOrNull()
}
// 带条件查询
suspend fun findActiveAdmins(): List<UserRow> =
newSuspendedTransaction {
UsersTable.selectAll()
.where { (UsersTable.role eq Role.ADMIN) }
.orderBy(UsersTable.name)
.map { it.toUser() }
}
// 更新
suspend fun updateUserEmail(id: UUID, newEmail: String): Boolean =
newSuspendedTransaction {
UsersTable.update({ UsersTable.id eq id }) {
it[email] = newEmail
it[updatedAt] = CurrentTimestampWithTimeZone
} > 0
}
// 删除
suspend fun deleteUser(id: UUID): Boolean =
newSuspendedTransaction {
UsersTable.deleteWhere { UsersTable.id eq id } > 0
}
// 行映射
private fun ResultRow.toUser() = UserRow(
id = this[UsersTable.id].value,
name = this[UsersTable.name],
email = this[UsersTable.email],
role = this[UsersTable.role],
metadata = this[UsersTable.metadata],
createdAt = this[UsersTable.createdAt],
updatedAt = this[UsersTable.updatedAt],
)Advanced Queries
高级查询
kotlin
// Join queries
suspend fun findOrdersWithUser(userId: UUID): List<OrderWithUser> =
newSuspendedTransaction {
(OrdersTable innerJoin UsersTable)
.selectAll()
.where { OrdersTable.userId eq userId }
.orderBy(OrdersTable.createdAt, SortOrder.DESC)
.map { row ->
OrderWithUser(
orderId = row[OrdersTable.id].value,
status = row[OrdersTable.status],
totalAmount = row[OrdersTable.totalAmount],
userName = row[UsersTable.name],
)
}
}
// Aggregation
suspend fun countUsersByRole(): Map<Role, Long> =
newSuspendedTransaction {
UsersTable
.select(UsersTable.role, UsersTable.id.count())
.groupBy(UsersTable.role)
.associate { row ->
row[UsersTable.role] to row[UsersTable.id.count()]
}
}
// Subqueries
suspend fun findUsersWithOrders(): List<UserRow> =
newSuspendedTransaction {
UsersTable.selectAll()
.where {
UsersTable.id inSubQuery
OrdersTable.select(OrdersTable.userId).withDistinct()
}
.map { it.toUser() }
}
// LIKE and pattern matching — always escape user input to prevent wildcard injection
private fun escapeLikePattern(input: String): String =
input.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
suspend fun searchUsers(query: String): List<UserRow> =
newSuspendedTransaction {
val sanitized = escapeLikePattern(query.lowercase())
UsersTable.selectAll()
.where {
(UsersTable.name.lowerCase() like "%${sanitized}%") or
(UsersTable.email.lowerCase() like "%${sanitized}%")
}
.map { it.toUser() }
}kotlin
// 连接查询
suspend fun findOrdersWithUser(userId: UUID): List<OrderWithUser> =
newSuspendedTransaction {
(OrdersTable innerJoin UsersTable)
.selectAll()
.where { OrdersTable.userId eq userId }
.orderBy(OrdersTable.createdAt, SortOrder.DESC)
.map { row ->
OrderWithUser(
orderId = row[OrdersTable.id].value,
status = row[OrdersTable.status],
totalAmount = row[OrdersTable.totalAmount],
userName = row[UsersTable.name],
)
}
}
// 聚合查询
suspend fun countUsersByRole(): Map<Role, Long> =
newSuspendedTransaction {
UsersTable
.select(UsersTable.role, UsersTable.id.count())
.groupBy(UsersTable.role)
.associate { row ->
row[UsersTable.role] to row[UsersTable.id.count()]
}
}
// 子查询
suspend fun findUsersWithOrders(): List<UserRow> =
newSuspendedTransaction {
UsersTable.selectAll()
.where {
UsersTable.id inSubQuery
OrdersTable.select(OrdersTable.userId).withDistinct()
}
.map { it.toUser() }
}
// LIKE与模式匹配 — 务必转义用户输入以防止通配符注入
private fun escapeLikePattern(input: String): String =
input.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
suspend fun searchUsers(query: String): List<UserRow> =
newSuspendedTransaction {
val sanitized = escapeLikePattern(query.lowercase())
UsersTable.selectAll()
.where {
(UsersTable.name.lowerCase() like "%${sanitized}%") or
(UsersTable.email.lowerCase() like "%${sanitized}%")
}
.map { it.toUser() }
}Pagination
分页查询
kotlin
data class Page<T>(
val data: List<T>,
val total: Long,
val page: Int,
val limit: Int,
) {
val totalPages: Int get() = ((total + limit - 1) / limit).toInt()
val hasNext: Boolean get() = page < totalPages
val hasPrevious: Boolean get() = page > 1
}
suspend fun findUsersPaginated(page: Int, limit: Int): Page<UserRow> =
newSuspendedTransaction {
val total = UsersTable.selectAll().count()
val data = UsersTable.selectAll()
.orderBy(UsersTable.createdAt, SortOrder.DESC)
.limit(limit)
.offset(((page - 1) * limit).toLong())
.map { it.toUser() }
Page(data = data, total = total, page = page, limit = limit)
}kotlin
data class Page<T>(
val data: List<T>,
val total: Long,
val page: Int,
val limit: Int,
) {
val totalPages: Int get() = ((total + limit - 1) / limit).toInt()
val hasNext: Boolean get() = page < totalPages
val hasPrevious: Boolean get() = page > 1
}
suspend fun findUsersPaginated(page: Int, limit: Int): Page<UserRow> =
newSuspendedTransaction {
val total = UsersTable.selectAll().count()
val data = UsersTable.selectAll()
.orderBy(UsersTable.createdAt, SortOrder.DESC)
.limit(limit)
.offset(((page - 1) * limit).toLong())
.map { it.toUser() }
Page(data = data, total = total, page = page, limit = limit)
}Batch Operations
批量操作
kotlin
// Batch insert
suspend fun insertUsers(users: List<CreateUserRequest>): List<UUID> =
newSuspendedTransaction {
UsersTable.batchInsert(users) { user ->
this[UsersTable.name] = user.name
this[UsersTable.email] = user.email
this[UsersTable.role] = user.role
}.map { it[UsersTable.id].value }
}
// Upsert (insert or update on conflict)
suspend fun upsertUser(id: UUID, name: String, email: String) {
newSuspendedTransaction {
UsersTable.upsert(UsersTable.email) {
it[UsersTable.id] = EntityID(id, UsersTable)
it[UsersTable.name] = name
it[UsersTable.email] = email
it[updatedAt] = CurrentTimestampWithTimeZone
}
}
}kotlin
// 批量插入
suspend fun insertUsers(users: List<CreateUserRequest>): List<UUID> =
newSuspendedTransaction {
UsersTable.batchInsert(users) { user ->
this[UsersTable.name] = user.name
this[UsersTable.email] = user.email
this[UsersTable.role] = user.role
}.map { it[UsersTable.id].value }
}
// 插入或更新(冲突时更新)
suspend fun upsertUser(id: UUID, name: String, email: String) {
newSuspendedTransaction {
UsersTable.upsert(UsersTable.email) {
it[UsersTable.id] = EntityID(id, UsersTable)
it[UsersTable.name] = name
it[UsersTable.email] = email
it[updatedAt] = CurrentTimestampWithTimeZone
}
}
}DAO Pattern
DAO模式
Entity Definitions
实体定义
kotlin
// entities/UserEntity.kt
class UserEntity(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<UserEntity>(UsersTable)
var name by UsersTable.name
var email by UsersTable.email
var role by UsersTable.role
var metadata by UsersTable.metadata
var createdAt by UsersTable.createdAt
var updatedAt by UsersTable.updatedAt
val orders by OrderEntity referrersOn OrdersTable.userId
fun toModel(): User = User(
id = id.value,
name = name,
email = email,
role = role,
metadata = metadata,
createdAt = createdAt,
updatedAt = updatedAt,
)
}
class OrderEntity(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<OrderEntity>(OrdersTable)
var user by UserEntity referencedOn OrdersTable.userId
var status by OrdersTable.status
var totalAmount by OrdersTable.totalAmount
var currency by OrdersTable.currency
var createdAt by OrdersTable.createdAt
val items by OrderItemEntity referrersOn OrderItemsTable.orderId
}kotlin
// entities/UserEntity.kt
class UserEntity(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<UserEntity>(UsersTable)
var name by UsersTable.name
var email by UsersTable.email
var role by UsersTable.role
var metadata by UsersTable.metadata
var createdAt by UsersTable.createdAt
var updatedAt by UsersTable.updatedAt
val orders by OrderEntity referrersOn OrdersTable.userId
fun toModel(): User = User(
id = id.value,
name = name,
email = email,
role = role,
metadata = metadata,
createdAt = createdAt,
updatedAt = updatedAt,
)
}
class OrderEntity(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<OrderEntity>(OrdersTable)
var user by UserEntity referencedOn OrdersTable.userId
var status by OrdersTable.status
var totalAmount by OrdersTable.totalAmount
var currency by OrdersTable.currency
var createdAt by OrdersTable.createdAt
val items by OrderItemEntity referrersOn OrderItemsTable.orderId
}DAO Operations
DAO操作
kotlin
suspend fun findUserByEmail(email: String): User? =
newSuspendedTransaction {
UserEntity.find { UsersTable.email eq email }
.firstOrNull()
?.toModel()
}
suspend fun createUser(request: CreateUserRequest): User =
newSuspendedTransaction {
UserEntity.new {
name = request.name
email = request.email
role = request.role
}.toModel()
}
suspend fun updateUser(id: UUID, request: UpdateUserRequest): User? =
newSuspendedTransaction {
UserEntity.findById(id)?.apply {
request.name?.let { name = it }
request.email?.let { email = it }
updatedAt = OffsetDateTime.now(ZoneOffset.UTC)
}?.toModel()
}kotlin
suspend fun findUserByEmail(email: String): User? =
newSuspendedTransaction {
UserEntity.find { UsersTable.email eq email }
.firstOrNull()
?.toModel()
}
suspend fun createUser(request: CreateUserRequest): User =
newSuspendedTransaction {
UserEntity.new {
name = request.name
email = request.email
role = request.role
}.toModel()
}
suspend fun updateUser(id: UUID, request: UpdateUserRequest): User? =
newSuspendedTransaction {
UserEntity.findById(id)?.apply {
request.name?.let { name = it }
request.email?.let { email = it }
updatedAt = OffsetDateTime.now(ZoneOffset.UTC)
}?.toModel()
}Transactions
事务处理
Suspend Transaction Support
协程事务支持
kotlin
// Good: Use newSuspendedTransaction for coroutine support
suspend fun performDatabaseOperation(): Result<User> =
runCatching {
newSuspendedTransaction {
val user = UserEntity.new {
name = "Alice"
email = "alice@example.com"
}
// All operations in this block are atomic
user.toModel()
}
}
// Good: Nested transactions with savepoints
suspend fun transferFunds(fromId: UUID, toId: UUID, amount: Long) {
newSuspendedTransaction {
val from = UserEntity.findById(fromId) ?: throw NotFoundException("User $fromId not found")
val to = UserEntity.findById(toId) ?: throw NotFoundException("User $toId not found")
// Debit
from.balance -= amount
// Credit
to.balance += amount
// Both succeed or both fail
}
}kotlin
// 推荐:使用newSuspendedTransaction实现协程支持
suspend fun performDatabaseOperation(): Result<User> =
runCatching {
newSuspendedTransaction {
val user = UserEntity.new {
name = "Alice"
email = "alice@example.com"
}
// 此代码块内的所有操作均为原子性操作
user.toModel()
}
}
// 推荐:带保存点的嵌套事务
suspend fun transferFunds(fromId: UUID, toId: UUID, amount: Long) {
newSuspendedTransaction {
val from = UserEntity.findById(fromId) ?: throw NotFoundException("未找到用户 $fromId")
val to = UserEntity.findById(toId) ?: throw NotFoundException("未找到用户 $toId")
// 转出操作
from.balance -= amount
// 转入操作
to.balance += amount
// 两个操作要么同时成功,要么同时失败
}
}Transaction Isolation
事务隔离级别
kotlin
suspend fun readCommittedQuery(): List<User> =
newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) {
UserEntity.all().map { it.toModel() }
}
suspend fun serializableOperation() {
newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) {
// Strictest isolation level for critical operations
}
}kotlin
suspend fun readCommittedQuery(): List<User> =
newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) {
UserEntity.all().map { it.toModel() }
}
suspend fun serializableOperation() {
newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) {
// 关键操作使用最严格的隔离级别
}
}Repository Pattern
仓库模式
Interface Definition
接口定义
kotlin
interface UserRepository {
suspend fun findById(id: UUID): User?
suspend fun findByEmail(email: String): User?
suspend fun findAll(page: Int, limit: Int): Page<User>
suspend fun search(query: String): List<User>
suspend fun create(request: CreateUserRequest): User
suspend fun update(id: UUID, request: UpdateUserRequest): User?
suspend fun delete(id: UUID): Boolean
suspend fun count(): Long
}kotlin
interface UserRepository {
suspend fun findById(id: UUID): User?
suspend fun findByEmail(email: String): User?
suspend fun findAll(page: Int, limit: Int): Page<User>
suspend fun search(query: String): List<User>
suspend fun create(request: CreateUserRequest): User
suspend fun update(id: UUID, request: UpdateUserRequest): User?
suspend fun delete(id: UUID): Boolean
suspend fun count(): Long
}Exposed Implementation
Exposed实现类
kotlin
class ExposedUserRepository(
private val database: Database,
) : UserRepository {
override suspend fun findById(id: UUID): User? =
newSuspendedTransaction(db = database) {
UsersTable.selectAll()
.where { UsersTable.id eq id }
.map { it.toUser() }
.singleOrNull()
}
override suspend fun findByEmail(email: String): User? =
newSuspendedTransaction(db = database) {
UsersTable.selectAll()
.where { UsersTable.email eq email }
.map { it.toUser() }
.singleOrNull()
}
override suspend fun findAll(page: Int, limit: Int): Page<User> =
newSuspendedTransaction(db = database) {
val total = UsersTable.selectAll().count()
val data = UsersTable.selectAll()
.orderBy(UsersTable.createdAt, SortOrder.DESC)
.limit(limit)
.offset(((page - 1) * limit).toLong())
.map { it.toUser() }
Page(data = data, total = total, page = page, limit = limit)
}
override suspend fun search(query: String): List<User> =
newSuspendedTransaction(db = database) {
val sanitized = escapeLikePattern(query.lowercase())
UsersTable.selectAll()
.where {
(UsersTable.name.lowerCase() like "%${sanitized}%") or
(UsersTable.email.lowerCase() like "%${sanitized}%")
}
.orderBy(UsersTable.name)
.map { it.toUser() }
}
override suspend fun create(request: CreateUserRequest): User =
newSuspendedTransaction(db = database) {
UsersTable.insert {
it[name] = request.name
it[email] = request.email
it[role] = request.role
}.resultedValues!!.first().toUser()
}
override suspend fun update(id: UUID, request: UpdateUserRequest): User? =
newSuspendedTransaction(db = database) {
val updated = UsersTable.update({ UsersTable.id eq id }) {
request.name?.let { name -> it[UsersTable.name] = name }
request.email?.let { email -> it[UsersTable.email] = email }
it[updatedAt] = CurrentTimestampWithTimeZone
}
if (updated > 0) findById(id) else null
}
override suspend fun delete(id: UUID): Boolean =
newSuspendedTransaction(db = database) {
UsersTable.deleteWhere { UsersTable.id eq id } > 0
}
override suspend fun count(): Long =
newSuspendedTransaction(db = database) {
UsersTable.selectAll().count()
}
private fun ResultRow.toUser() = User(
id = this[UsersTable.id].value,
name = this[UsersTable.name],
email = this[UsersTable.email],
role = this[UsersTable.role],
metadata = this[UsersTable.metadata],
createdAt = this[UsersTable.createdAt],
updatedAt = this[UsersTable.updatedAt],
)
}kotlin
class ExposedUserRepository(
private val database: Database,
) : UserRepository {
override suspend fun findById(id: UUID): User? =
newSuspendedTransaction(db = database) {
UsersTable.selectAll()
.where { UsersTable.id eq id }
.map { it.toUser() }
.singleOrNull()
}
override suspend fun findByEmail(email: String): User? =
newSuspendedTransaction(db = database) {
UsersTable.selectAll()
.where { UsersTable.email eq email }
.map { it.toUser() }
.singleOrNull()
}
override suspend fun findAll(page: Int, limit: Int): Page<User> =
newSuspendedTransaction(db = database) {
val total = UsersTable.selectAll().count()
val data = UsersTable.selectAll()
.orderBy(UsersTable.createdAt, SortOrder.DESC)
.limit(limit)
.offset(((page - 1) * limit).toLong())
.map { it.toUser() }
Page(data = data, total = total, page = page, limit = limit)
}
override suspend fun search(query: String): List<User> =
newSuspendedTransaction(db = database) {
val sanitized = escapeLikePattern(query.lowercase())
UsersTable.selectAll()
.where {
(UsersTable.name.lowerCase() like "%${sanitized}%") or
(UsersTable.email.lowerCase() like "%${sanitized}%")
}
.orderBy(UsersTable.name)
.map { it.toUser() }
}
override suspend fun create(request: CreateUserRequest): User =
newSuspendedTransaction(db = database) {
UsersTable.insert {
it[name] = request.name
it[email] = request.email
it[role] = request.role
}.resultedValues!!.first().toUser()
}
override suspend fun update(id: UUID, request: UpdateUserRequest): User? =
newSuspendedTransaction(db = database) {
val updated = UsersTable.update({ UsersTable.id eq id }) {
request.name?.let { name -> it[UsersTable.name] = name }
request.email?.let { email -> it[UsersTable.email] = email }
it[updatedAt] = CurrentTimestampWithTimeZone
}
if (updated > 0) findById(id) else null
}
override suspend fun delete(id: UUID): Boolean =
newSuspendedTransaction(db = database) {
UsersTable.deleteWhere { UsersTable.id eq id } > 0
}
override suspend fun count(): Long =
newSuspendedTransaction(db = database) {
UsersTable.selectAll().count()
}
private fun ResultRow.toUser() = User(
id = this[UsersTable.id].value,
name = this[UsersTable.name],
email = this[UsersTable.email],
role = this[UsersTable.role],
metadata = this[UsersTable.metadata],
createdAt = this[UsersTable.createdAt],
updatedAt = this[UsersTable.updatedAt],
)
}JSON Columns
JSON列处理
JSONB with kotlinx.serialization
结合kotlinx.serialization的JSONB
kotlin
// Custom column type for JSONB
inline fun <reified T : Any> Table.jsonb(
name: String,
json: Json,
): Column<T> = registerColumn(name, object : ColumnType<T>() {
override fun sqlType() = "JSONB"
override fun valueFromDB(value: Any): T = when (value) {
is String -> json.decodeFromString(value)
is PGobject -> {
val jsonString = value.value
?: throw IllegalArgumentException("PGobject value is null for column '$name'")
json.decodeFromString(jsonString)
}
else -> throw IllegalArgumentException("Unexpected value: $value")
}
override fun notNullValueToDB(value: T): Any =
PGobject().apply {
type = "jsonb"
this.value = json.encodeToString(value)
}
})
// Usage in table
@Serializable
data class UserMetadata(
val preferences: Map<String, String> = emptyMap(),
val tags: List<String> = emptyList(),
)
object UsersTable : UUIDTable("users") {
val metadata = jsonb<UserMetadata>("metadata", Json.Default).nullable()
}kotlin
// 自定义JSONB列类型
inline fun <reified T : Any> Table.jsonb(
name: String,
json: Json,
): Column<T> = registerColumn(name, object : ColumnType<T>() {
override fun sqlType() = "JSONB"
override fun valueFromDB(value: Any): T = when (value) {
is String -> json.decodeFromString(value)
is PGobject -> {
val jsonString = value.value
?: throw IllegalArgumentException("列'$name'对应的PGobject值为空")
json.decodeFromString(jsonString)
}
else -> throw IllegalArgumentException("不支持的类型:$value")
}
override fun notNullValueToDB(value: T): Any =
PGobject().apply {
type = "jsonb"
this.value = json.encodeToString(value)
}
})
// 在表中使用
@Serializable
data class UserMetadata(
val preferences: Map<String, String> = emptyMap(),
val tags: List<String> = emptyList(),
)
object UsersTable : UUIDTable("users") {
val metadata = jsonb<UserMetadata>("metadata", Json.Default).nullable()
}Testing with Exposed
Exposed测试
In-Memory Database for Tests
测试用内存数据库
kotlin
class UserRepositoryTest : FunSpec({
lateinit var database: Database
lateinit var repository: UserRepository
beforeSpec {
database = Database.connect(
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL",
driver = "org.h2.Driver",
)
transaction(database) {
SchemaUtils.create(UsersTable)
}
repository = ExposedUserRepository(database)
}
beforeTest {
transaction(database) {
UsersTable.deleteAll()
}
}
test("create and find user") {
val user = repository.create(CreateUserRequest("Alice", "alice@example.com"))
user.name shouldBe "Alice"
user.email shouldBe "alice@example.com"
val found = repository.findById(user.id)
found shouldBe user
}
test("findByEmail returns null for unknown email") {
val result = repository.findByEmail("unknown@example.com")
result.shouldBeNull()
}
test("pagination works correctly") {
repeat(25) { i ->
repository.create(CreateUserRequest("User $i", "user$i@example.com"))
}
val page1 = repository.findAll(page = 1, limit = 10)
page1.data shouldHaveSize 10
page1.total shouldBe 25
page1.hasNext shouldBe true
val page3 = repository.findAll(page = 3, limit = 10)
page3.data shouldHaveSize 5
page3.hasNext shouldBe false
}
})kotlin
class UserRepositoryTest : FunSpec({
lateinit var database: Database
lateinit var repository: UserRepository
beforeSpec {
database = Database.connect(
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL",
driver = "org.h2.Driver",
)
transaction(database) {
SchemaUtils.create(UsersTable)
}
repository = ExposedUserRepository(database)
}
beforeTest {
transaction(database) {
UsersTable.deleteAll()
}
}
test("创建并查询用户") {
val user = repository.create(CreateUserRequest("Alice", "alice@example.com"))
user.name shouldBe "Alice"
user.email shouldBe "alice@example.com"
val found = repository.findById(user.id)
found shouldBe user
}
test("查询不存在的邮箱返回null") {
val result = repository.findByEmail("unknown@example.com")
result.shouldBeNull()
}
test("分页功能正常工作") {
repeat(25) { i ->
repository.create(CreateUserRequest("User $i", "user$i@example.com"))
}
val page1 = repository.findAll(page = 1, limit = 10)
page1.data shouldHaveSize 10
page1.total shouldBe 25
page1.hasNext shouldBe true
val page3 = repository.findAll(page = 3, limit = 10)
page3.data shouldHaveSize 5
page3.hasNext shouldBe false
}
})Gradle Dependencies
Gradle依赖
kotlin
// build.gradle.kts
dependencies {
// Exposed
implementation("org.jetbrains.exposed:exposed-core:1.0.0")
implementation("org.jetbrains.exposed:exposed-dao:1.0.0")
implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0")
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0")
implementation("org.jetbrains.exposed:exposed-json:1.0.0")
// Database driver
implementation("org.postgresql:postgresql:42.7.5")
// Connection pooling
implementation("com.zaxxer:HikariCP:6.2.1")
// Migrations
implementation("org.flywaydb:flyway-core:10.22.0")
implementation("org.flywaydb:flyway-database-postgresql:10.22.0")
// Testing
testImplementation("com.h2database:h2:2.3.232")
}kotlin
// build.gradle.kts
dependencies {
// Exposed
implementation("org.jetbrains.exposed:exposed-core:1.0.0")
implementation("org.jetbrains.exposed:exposed-dao:1.0.0")
implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0")
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0")
implementation("org.jetbrains.exposed:exposed-json:1.0.0")
// 数据库驱动
implementation("org.postgresql:postgresql:42.7.5")
// 连接池
implementation("com.zaxxer:HikariCP:6.2.1")
// 迁移工具
implementation("org.flywaydb:flyway-core:10.22.0")
implementation("org.flywaydb:flyway-database-postgresql:10.22.0")
// 测试
testImplementation("com.h2database:h2:2.3.232")
}Quick Reference: Exposed Patterns
Exposed模式速查
| Pattern | Description |
|---|---|
| Define table with UUID primary key |
| Coroutine-safe transaction block |
| Query with conditions |
| Insert and return generated ID |
| Update matching rows |
| Delete matching rows |
| Efficient bulk insert |
| Join tables |
| Sort and paginate |
| Aggregation functions |
Remember: Use the DSL style for simple queries and the DAO style when you need entity lifecycle management. Always use for coroutine support, and wrap database operations behind a repository interface for testability.
newSuspendedTransaction| 模式 | 描述 |
|---|---|
| 定义使用UUID作为主键的表 |
| 支持协程的事务代码块 |
| 带条件的查询 |
| 插入数据并返回生成的ID |
| 更新符合条件的行 |
| 删除符合条件的行 |
| 高效批量插入 |
| 表连接操作 |
| 排序与分页 |
| 聚合函数 |
注意:简单查询使用DSL风格,需要管理实体生命周期时使用DAO风格。始终使用以支持协程,并将数据库操作封装在仓库接口之后以提升可测试性。
newSuspendedTransaction