jOOQ (Kotlin)
jOOQ(Java Object Oriented Querying)は「SQL重視のType-safeなクエリビルダー」として開発された、Kotlinでも活用できるデータベースアクセスライブラリです。「SQLファースト」の哲学を掲げ、SQLの表現力を損なうことなく、コンパイル時型安全性を提供します。データベーススキーマからKotlinコードを自動生成し、IDEの自動補完とコンパイル時チェックにより、実行時SQLエラーを大幅に削減できます。複雑なJOIN、ウィンドウ関数、集約クエリなどのSQLの全機能をKotlinのタイプセーフなDSLで記述でき、エンタープライズレベルのデータベースアプリケーション開発における信頼性と生産性を同時に実現します。
GitHub概要
トピックス
スター履歴
ライブラリ
jOOQ (Kotlin)
概要
jOOQ(Java Object Oriented Querying)は「SQL重視のType-safeなクエリビルダー」として開発された、Kotlinでも活用できるデータベースアクセスライブラリです。「SQLファースト」の哲学を掲げ、SQLの表現力を損なうことなく、コンパイル時型安全性を提供します。データベーススキーマからKotlinコードを自動生成し、IDEの自動補完とコンパイル時チェックにより、実行時SQLエラーを大幅に削減できます。複雑なJOIN、ウィンドウ関数、集約クエリなどのSQLの全機能をKotlinのタイプセーフなDSLで記述でき、エンタープライズレベルのデータベースアプリケーション開発における信頼性と生産性を同時に実現します。
詳細
jOOQ 2025年版は、Kotlin Multiplatform対応とCoroutinesサポートにより、現代的なKotlinアプリケーション開発に最適化されています。データベーススキーマの変更を自動検出し、対応するKotlinコードを再生成することで、スキーマとコードの整合性を常に保持します。PostgreSQL、MySQL、Oracle、SQL Server、H2など主要データベースの方言や固有機能を完全サポートし、ベンダー固有のSQL機能も型安全に利用できます。バッチ処理、トランザクション管理、接続プール統合、結果セットストリーミングなど、企業向けアプリケーションに必要な高度な機能を包括的に提供しています。
主な特徴
- SQL-First哲学: SQLの表現力を最大限活用した型安全なクエリ構築
- スキーマ同期: データベーススキーマからKotlinコードの自動生成
- 完全な型安全性: コンパイル時のSQL構文・型チェック
- データベース方言対応: 主要データベースの固有機能フルサポート
- Kotlin統合: Coroutines、拡張関数、型推論の活用
- 企業向け機能: バッチ処理、トランザクション、パフォーマンス最適化
メリット・デメリット
メリット
- SQLの表現力を損なわない高度なクエリ記述能力
- コンパイル時型チェックによる実行時エラーの大幅削減
- データベーススキーマとコードの自動同期
- IDEの強力な自動補完とリファクタリング支援
- 複雑なビジネスロジックのSQL内表現が可能
- 豊富なデータベース方言サポートとポータビリティ
デメリット
- 学習コストが高く、SQLとjOOQ両方の知識が必要
- コード生成ステップが必要で、ビルドプロセスが複雑化
- シンプルなCRUD操作には過剰で、開発初期のオーバーヘッド
- 生成されるコードが大量で、プロジェクトサイズが増大
- ライセンス費用(商用データベース使用時)が必要な場合がある
- Active Recordパターンに慣れた開発者には学習障壁
参考ページ
書き方の例
セットアップ
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.22"
id("nu.studer.jooq") version "8.2.1"
}
dependencies {
implementation("org.jooq:jooq:3.18.7")
implementation("org.jooq:jooq-kotlin:3.18.7")
implementation("org.jooq:jooq-kotlin-coroutines:3.18.7")
// データベースドライバー
implementation("org.postgresql:postgresql:42.7.1")
// または implementation("mysql:mysql-connector-java:8.0.33")
// 接続プール
implementation("com.zaxxer:HikariCP:5.1.0")
// jOOQ コード生成用
jooqGenerator("org.postgresql:postgresql:42.7.1")
}
// jOOQ 設定
jooq {
configurations {
create("main") {
generateSchemaSourceOnCompilation.set(true)
generator.apply {
name = "org.jooq.codegen.KotlinGenerator"
database.apply {
name = "org.jooq.meta.postgres.PostgresDatabase"
inputSchema = "public"
// データベース接続設定
properties.add(
org.jooq.meta.jaxb.Property().apply {
key = "dialect"
value = "POSTGRES"
}
)
}
target.apply {
packageName = "com.example.jooq.generated"
directory = "src/main/kotlin"
}
generate.apply {
isDeprecated = false
isRecords = true
isImmutablePojos = true
isFluentSetters = true
isKotlinSetterJvmNameAnnotations = true
}
}
}
}
}
-- sample_schema.sql (サンプルスキーマ)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT true
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
user_id INTEGER REFERENCES users(id),
category_id INTEGER,
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_published BOOLEAN DEFAULT false
);
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_active ON users(is_active);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(is_published, published_at);
基本的な使い方
// データベース接続設定
import org.jooq.*
import org.jooq.impl.DSL
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import com.example.jooq.generated.Tables.*
import com.example.jooq.generated.tables.records.*
class DatabaseConfig {
companion object {
fun createDataSource(): HikariDataSource {
val config = HikariConfig().apply {
jdbcUrl = "jdbc:postgresql://localhost:5432/mydb"
username = "user"
password = "password"
driverClassName = "org.postgresql.Driver"
maximumPoolSize = 20
minimumIdle = 5
connectionTimeout = 30000
idleTimeout = 600000
maxLifetime = 1800000
}
return HikariDataSource(config)
}
fun createContext(dataSource: HikariDataSource): DSLContext {
return DSL.using(dataSource, SQLDialect.POSTGRES)
}
}
}
// 基本的なCRUD操作
class UserRepository(private val create: DSLContext) {
// ユーザー作成
fun insertUser(name: String, email: String, age: Int): UsersRecord {
return create.insertInto(USERS)
.set(USERS.NAME, name)
.set(USERS.EMAIL, email)
.set(USERS.AGE, age)
.returning()
.fetchOne()!!
}
// ユーザー検索
fun findUserById(id: Int): UsersRecord? {
return create.selectFrom(USERS)
.where(USERS.ID.eq(id))
.fetchOne()
}
fun findUserByEmail(email: String): UsersRecord? {
return create.selectFrom(USERS)
.where(USERS.EMAIL.eq(email))
.fetchOne()
}
// 全ユーザー取得
fun findAllUsers(): List<UsersRecord> {
return create.selectFrom(USERS)
.where(USERS.IS_ACTIVE.isTrue)
.orderBy(USERS.CREATED_AT.desc())
.fetch()
}
// ユーザー更新
fun updateUser(id: Int, name: String?, email: String?, age: Int?): Int {
val update = create.update(USERS)
name?.let { update.set(USERS.NAME, it) }
email?.let { update.set(USERS.EMAIL, it) }
age?.let { update.set(USERS.AGE, it) }
return update.set(USERS.UPDATED_AT, DSL.currentTimestamp())
.where(USERS.ID.eq(id))
.execute()
}
// ユーザー削除(論理削除)
fun deactivateUser(id: Int): Int {
return create.update(USERS)
.set(USERS.IS_ACTIVE, false)
.set(USERS.UPDATED_AT, DSL.currentTimestamp())
.where(USERS.ID.eq(id))
.execute()
}
// ユーザー削除(物理削除)
fun deleteUser(id: Int): Int {
return create.deleteFrom(USERS)
.where(USERS.ID.eq(id))
.execute()
}
// 条件検索
fun searchUsers(
nameQuery: String? = null,
minAge: Int? = null,
maxAge: Int? = null,
isActive: Boolean = true
): List<UsersRecord> {
var condition = USERS.IS_ACTIVE.eq(isActive)
nameQuery?.let {
condition = condition.and(USERS.NAME.likeIgnoreCase("%$it%"))
}
minAge?.let {
condition = condition.and(USERS.AGE.greaterOrEqual(it))
}
maxAge?.let {
condition = condition.and(USERS.AGE.lessOrEqual(it))
}
return create.selectFrom(USERS)
.where(condition)
.orderBy(USERS.NAME.asc())
.fetch()
}
}
// 使用例
fun demonstrateBasicUsage() {
val dataSource = DatabaseConfig.createDataSource()
val create = DatabaseConfig.createContext(dataSource)
val userRepository = UserRepository(create)
// ユーザー作成
val user1 = userRepository.insertUser("田中太郎", "[email protected]", 30)
println("Created user: ${user1.id} - ${user1.name}")
// ユーザー検索
val foundUser = userRepository.findUserByEmail("[email protected]")
println("Found user: ${foundUser?.name}")
// ユーザー更新
val updated = userRepository.updateUser(
id = user1.id!!,
name = "田中太郎(更新済み)",
age = 31
)
println("Updated $updated records")
// 条件検索
val searchResults = userRepository.searchUsers(
nameQuery = "田中",
minAge = 25
)
println("Search results: ${searchResults.size} users")
}
複雑なクエリとJOIN操作
// 高度なクエリ操作
class AdvancedQueryService(private val create: DSLContext) {
// JOIN クエリ
fun getUsersWithPostCount(): List<Record> {
return create.select(
USERS.ID,
USERS.NAME,
USERS.EMAIL,
DSL.count(POSTS.ID).`as`("post_count")
)
.from(USERS)
.leftJoin(POSTS).on(USERS.ID.eq(POSTS.USER_ID))
.where(USERS.IS_ACTIVE.isTrue)
.groupBy(USERS.ID, USERS.NAME, USERS.EMAIL)
.orderBy(DSL.count(POSTS.ID).desc())
.fetch()
}
// 複雑なJOIN
fun getPublishedPostsWithUserAndCategory(): List<Record> {
return create.select(
POSTS.ID,
POSTS.TITLE,
POSTS.CONTENT,
POSTS.PUBLISHED_AT,
USERS.NAME.`as`("author_name"),
USERS.EMAIL.`as`("author_email"),
CATEGORIES.NAME.`as`("category_name")
)
.from(POSTS)
.join(USERS).on(POSTS.USER_ID.eq(USERS.ID))
.leftJoin(CATEGORIES).on(POSTS.CATEGORY_ID.eq(CATEGORIES.ID))
.where(POSTS.IS_PUBLISHED.isTrue)
.and(USERS.IS_ACTIVE.isTrue)
.orderBy(POSTS.PUBLISHED_AT.desc())
.fetch()
}
// サブクエリ
fun getActiveUsersWithRecentPosts(daysAgo: Int): List<UsersRecord> {
val recentDate = DSL.currentTimestamp().minus(daysAgo)
return create.selectFrom(USERS)
.where(USERS.IS_ACTIVE.isTrue)
.and(USERS.ID.`in`(
DSL.select(POSTS.USER_ID)
.from(POSTS)
.where(POSTS.PUBLISHED_AT.greaterThan(recentDate))
.and(POSTS.IS_PUBLISHED.isTrue)
))
.orderBy(USERS.NAME)
.fetch()
}
// ウィンドウ関数
fun getUserPostRankings(): List<Record> {
return create.select(
USERS.NAME,
POSTS.TITLE,
POSTS.PUBLISHED_AT,
DSL.rowNumber().over(
DSL.partitionBy(USERS.ID)
.orderBy(POSTS.PUBLISHED_AT.desc())
).`as`("post_rank"),
DSL.count().over(
DSL.partitionBy(USERS.ID)
).`as`("total_posts")
)
.from(POSTS)
.join(USERS).on(POSTS.USER_ID.eq(USERS.ID))
.where(POSTS.IS_PUBLISHED.isTrue)
.orderBy(USERS.NAME, POSTS.PUBLISHED_AT.desc())
.fetch()
}
// 集約クエリ
fun getUserStatistics(): List<Record> {
return create.select(
USERS.ID,
USERS.NAME,
DSL.count(POSTS.ID).`as`("total_posts"),
DSL.countDistinct(POSTS.CATEGORY_ID).`as`("categories_used"),
DSL.min(POSTS.PUBLISHED_AT).`as`("first_post_date"),
DSL.max(POSTS.PUBLISHED_AT).`as`("latest_post_date"),
DSL.avg(DSL.length(POSTS.CONTENT)).`as`("avg_content_length")
)
.from(USERS)
.leftJoin(POSTS).on(USERS.ID.eq(POSTS.USER_ID))
.where(USERS.IS_ACTIVE.isTrue)
.groupBy(USERS.ID, USERS.NAME)
.having(DSL.count(POSTS.ID).greaterThan(0))
.orderBy(DSL.count(POSTS.ID).desc())
.fetch()
}
// CTE (Common Table Expression)
fun getTopUsersWithPostDetails(): List<Record> {
val topUsers = DSL.name("top_users")
val topUsersQuery = create.select(
USERS.ID,
USERS.NAME,
DSL.count(POSTS.ID).`as`("post_count")
)
.from(USERS)
.join(POSTS).on(USERS.ID.eq(POSTS.USER_ID))
.where(POSTS.IS_PUBLISHED.isTrue)
.groupBy(USERS.ID, USERS.NAME)
.having(DSL.count(POSTS.ID).greaterThan(5))
return create.with(topUsers).`as`(topUsersQuery)
.select(
topUsers.field("name"),
topUsers.field("post_count"),
POSTS.TITLE,
POSTS.PUBLISHED_AT
)
.from(topUsers)
.join(POSTS).on(topUsers.field("id", Int::class.java).eq(POSTS.USER_ID))
.where(POSTS.IS_PUBLISHED.isTrue)
.orderBy(
topUsers.field("post_count").desc(),
POSTS.PUBLISHED_AT.desc()
)
.fetch()
}
}
// PostgreSQL特有の機能
class PostgreSQLSpecificQueries(private val create: DSLContext) {
// JSON操作
fun searchUsersByPreferences(key: String, value: String): List<Record> {
// preferencesカラムがJSONB型の場合
return create.select(USERS.ID, USERS.NAME, USERS.EMAIL)
.from(USERS)
.where(DSL.condition("preferences ->> {0} = {1}", key, value))
.fetch()
}
// 配列操作
fun findUsersWithSkill(skill: String): List<Record> {
// skillsカラムが配列型の場合
return create.select(USERS.ID, USERS.NAME)
.from(USERS)
.where(DSL.condition("{0} = ANY(skills)", skill))
.fetch()
}
// 全文検索
fun fullTextSearch(query: String): List<Record> {
return create.select(
POSTS.ID,
POSTS.TITLE,
POSTS.CONTENT,
DSL.function("ts_rank", Double::class.java,
DSL.function("to_tsvector", Any::class.java,
DSL.val("english"), POSTS.CONTENT),
DSL.function("plainto_tsquery", Any::class.java,
DSL.val("english"), DSL.val(query))
).`as`("rank")
)
.from(POSTS)
.where(DSL.condition(
"to_tsvector('english', content) @@ plainto_tsquery('english', {0})",
query
))
.orderBy(DSL.field("rank").desc())
.fetch()
}
}
トランザクションとバッチ処理
// トランザクション管理
class TransactionalService(private val create: DSLContext) {
// 基本的なトランザクション
fun createUserWithProfile(
name: String,
email: String,
age: Int,
bio: String?
): Result<UsersRecord> {
return try {
create.transactionResult { configuration ->
val transactionalCreate = DSL.using(configuration)
// ユーザー作成
val user = transactionalCreate.insertInto(USERS)
.set(USERS.NAME, name)
.set(USERS.EMAIL, email)
.set(USERS.AGE, age)
.returning()
.fetchOne()!!
// プロフィール作成(仮想的なテーブル)
bio?.let {
transactionalCreate.insertInto(DSL.table("user_profiles"))
.set(DSL.field("user_id"), user.id)
.set(DSL.field("bio"), it)
.execute()
}
user
}
} catch (e: Exception) {
Result.failure(e)
}
}
// 複雑なトランザクション処理
fun transferPosts(fromUserId: Int, toUserId: Int): Result<Int> {
return try {
create.transactionResult { configuration ->
val ctx = DSL.using(configuration)
// 移行元ユーザーの存在確認
val fromUser = ctx.selectFrom(USERS)
.where(USERS.ID.eq(fromUserId))
.fetchOne()
?: throw IllegalArgumentException("Source user not found")
// 移行先ユーザーの存在確認
val toUser = ctx.selectFrom(USERS)
.where(USERS.ID.eq(toUserId))
.fetchOne()
?: throw IllegalArgumentException("Target user not found")
// 投稿の移行
val transferredCount = ctx.update(POSTS)
.set(POSTS.USER_ID, toUserId)
.set(POSTS.UPDATED_AT, DSL.currentTimestamp())
.where(POSTS.USER_ID.eq(fromUserId))
.execute()
// 移行元ユーザーの無効化
ctx.update(USERS)
.set(USERS.IS_ACTIVE, false)
.set(USERS.UPDATED_AT, DSL.currentTimestamp())
.where(USERS.ID.eq(fromUserId))
.execute()
transferredCount
}
} catch (e: Exception) {
Result.failure(e)
}
}
// バッチ処理
fun batchInsertUsers(users: List<UserData>): Result<IntArray> {
return try {
val result = create.transactionResult { configuration ->
val ctx = DSL.using(configuration)
val batch = ctx.batch(
ctx.insertInto(USERS)
.set(USERS.NAME, DSL.param("name", String::class.java))
.set(USERS.EMAIL, DSL.param("email", String::class.java))
.set(USERS.AGE, DSL.param("age", Int::class.java))
)
users.forEach { userData ->
batch.bind(userData.name, userData.email, userData.age)
}
batch.execute()
}
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
// バッチ更新
fun batchUpdateUserStatus(userIds: List<Int>, isActive: Boolean): Result<IntArray> {
return try {
val result = create.transactionResult { configuration ->
val ctx = DSL.using(configuration)
val batch = ctx.batch(
ctx.update(USERS)
.set(USERS.IS_ACTIVE, DSL.param("isActive", Boolean::class.java))
.set(USERS.UPDATED_AT, DSL.currentTimestamp())
.where(USERS.ID.eq(DSL.param("id", Int::class.java)))
)
userIds.forEach { userId ->
batch.bind(isActive, userId)
}
batch.execute()
}
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// データクラス
data class UserData(
val name: String,
val email: String,
val age: Int
)
// 結果型
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()
companion object {
fun <T> success(value: T): Result<T> = Success(value)
fun failure(error: Throwable): Result<Nothing> = Failure(error)
}
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
is Success -> success(transform(value))
is Failure -> this
}
inline fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
is Success -> transform(value)
is Failure -> this
}
}
Kotlin Coroutinesとの統合
// Coroutines対応のリポジトリ
import kotlinx.coroutines.*
import org.jooq.kotlin.coroutines.*
class AsyncUserRepository(private val create: DSLContext) {
// 非同期ユーザー検索
suspend fun findUserByIdAsync(id: Int): UsersRecord? = withContext(Dispatchers.IO) {
create.selectFrom(USERS)
.where(USERS.ID.eq(id))
.awaitSingle()
}
// 非同期ユーザー一覧取得
suspend fun findAllUsersAsync(): List<UsersRecord> = withContext(Dispatchers.IO) {
create.selectFrom(USERS)
.where(USERS.IS_ACTIVE.isTrue)
.orderBy(USERS.CREATED_AT.desc())
.awaitAll()
}
// 非同期ユーザー作成
suspend fun insertUserAsync(name: String, email: String, age: Int): UsersRecord =
withContext(Dispatchers.IO) {
create.insertInto(USERS)
.set(USERS.NAME, name)
.set(USERS.EMAIL, email)
.set(USERS.AGE, age)
.returning()
.awaitSingle()
}
// 並行処理による一括データ取得
suspend fun getUsersWithPostsAsync(userIds: List<Int>): Map<UsersRecord, List<PostsRecord>> =
coroutineScope {
val userDeferreds = userIds.map { userId ->
async {
val user = create.selectFrom(USERS)
.where(USERS.ID.eq(userId))
.awaitSingleOrNull()
val posts = user?.let {
create.selectFrom(POSTS)
.where(POSTS.USER_ID.eq(userId))
.awaitAll()
} ?: emptyList()
user to posts
}
}
userDeferreds.awaitAll()
.mapNotNull { (user, posts) -> user?.let { it to posts } }
.toMap()
}
// ストリーミング処理
suspend fun processAllUsersStream(processor: suspend (UsersRecord) -> Unit) {
withContext(Dispatchers.IO) {
create.selectFrom(USERS)
.where(USERS.IS_ACTIVE.isTrue)
.awaitStream()
.collect { user ->
processor(user)
}
}
}
}
// 非同期サービス層
class AsyncUserService(
private val userRepository: AsyncUserRepository,
private val create: DSLContext
) {
// 非同期ユーザー作成とバリデーション
suspend fun createUserSafely(
name: String,
email: String,
age: Int
): Result<UsersRecord> = try {
// 重複チェック
val existingUser = create.selectFrom(USERS)
.where(USERS.EMAIL.eq(email))
.awaitSingleOrNull()
if (existingUser != null) {
Result.failure(IllegalArgumentException("Email already exists"))
} else {
val user = userRepository.insertUserAsync(name, email, age)
Result.success(user)
}
} catch (e: Exception) {
Result.failure(e)
}
// 非同期検索とフィルタリング
suspend fun searchUsersAsync(
nameQuery: String? = null,
minAge: Int? = null,
maxAge: Int? = null,
limit: Int = 100
): List<UsersRecord> = withContext(Dispatchers.IO) {
var condition = USERS.IS_ACTIVE.isTrue
nameQuery?.let {
condition = condition.and(USERS.NAME.likeIgnoreCase("%$it%"))
}
minAge?.let {
condition = condition.and(USERS.AGE.greaterOrEqual(it))
}
maxAge?.let {
condition = condition.and(USERS.AGE.lessOrEqual(it))
}
create.selectFrom(USERS)
.where(condition)
.limit(limit)
.orderBy(USERS.NAME.asc())
.awaitAll()
}
// 並列バッチ処理
suspend fun processUsersBatch(
batchSize: Int = 100,
processor: suspend (List<UsersRecord>) -> Unit
) = coroutineScope {
val totalUsers = create.selectCount()
.from(USERS)
.where(USERS.IS_ACTIVE.isTrue)
.awaitSingle()
val batches = (0 until totalUsers step batchSize)
batches.map { offset ->
async {
val batch = create.selectFrom(USERS)
.where(USERS.IS_ACTIVE.isTrue)
.limit(batchSize)
.offset(offset)
.awaitAll()
processor(batch)
}
}.awaitAll()
}
}
// 使用例
suspend fun demonstrateAsyncUsage() {
val dataSource = DatabaseConfig.createDataSource()
val create = DatabaseConfig.createContext(dataSource)
val asyncRepository = AsyncUserRepository(create)
val asyncService = AsyncUserService(asyncRepository, create)
// 非同期ユーザー作成
val userResult = asyncService.createUserSafely(
name = "非同期太郎",
email = "[email protected]",
age = 25
)
when (userResult) {
is Result.Success -> println("User created: ${userResult.value.name}")
is Result.Failure -> println("Error: ${userResult.error.message}")
}
// 並行検索
val searchResults = asyncService.searchUsersAsync(
nameQuery = "太郎",
minAge = 20
)
println("Found ${searchResults.size} users")
// バッチ処理
asyncService.processUsersBatch { batch ->
println("Processing batch of ${batch.size} users")
// 各バッチの処理
}
}
エラーハンドリング
// 包括的なエラーハンドリング
import org.jooq.exception.*
class RobustUserService(private val create: DSLContext) {
// 安全なユーザー作成
fun createUserSafely(name: String, email: String, age: Int): DatabaseResult<UsersRecord> {
return try {
// 入力値検証
validateUserInput(name, email, age).let { validation ->
if (validation != null) {
return DatabaseResult.ValidationError(validation)
}
}
// トランザクション内で実行
val user = create.transactionResult { configuration ->
val ctx = DSL.using(configuration)
// 重複チェック
val existingUser = ctx.selectFrom(USERS)
.where(USERS.EMAIL.eq(email))
.fetchOne()
if (existingUser != null) {
throw DuplicateEmailException("Email $email already exists")
}
// ユーザー作成
ctx.insertInto(USERS)
.set(USERS.NAME, name)
.set(USERS.EMAIL, email)
.set(USERS.AGE, age)
.returning()
.fetchOne()!!
}
DatabaseResult.Success(user)
} catch (e: DuplicateEmailException) {
DatabaseResult.BusinessError(e.message ?: "Duplicate email")
} catch (e: DataAccessException) {
when (e) {
is DataChangedException ->
DatabaseResult.ConcurrencyError("Data was modified by another transaction")
is IntegrityConstraintViolationException ->
DatabaseResult.ConstraintError("Database constraint violation: ${e.message}")
else ->
DatabaseResult.DatabaseError("Database error: ${e.message}")
}
} catch (e: Exception) {
DatabaseResult.UnknownError("Unexpected error: ${e.message}")
}
}
// 安全なユーザー更新
fun updateUserSafely(
id: Int,
name: String? = null,
email: String? = null,
age: Int? = null
): DatabaseResult<Int> {
return try {
// 入力値検証
if (name != null || email != null || age != null) {
validateUserUpdateInput(name, email, age).let { validation ->
if (validation != null) {
return DatabaseResult.ValidationError(validation)
}
}
}
val updatedRows = create.transactionResult { configuration ->
val ctx = DSL.using(configuration)
// 存在確認
val existingUser = ctx.selectFrom(USERS)
.where(USERS.ID.eq(id))
.fetchOne()
?: throw UserNotFoundException("User with ID $id not found")
// メール重複チェック
email?.let { newEmail ->
val duplicateUser = ctx.selectFrom(USERS)
.where(USERS.EMAIL.eq(newEmail))
.and(USERS.ID.ne(id))
.fetchOne()
if (duplicateUser != null) {
throw DuplicateEmailException("Email $newEmail already exists")
}
}
// 更新実行
val updateQuery = ctx.update(USERS)
name?.let { updateQuery.set(USERS.NAME, it) }
email?.let { updateQuery.set(USERS.EMAIL, it) }
age?.let { updateQuery.set(USERS.AGE, it) }
updateQuery.set(USERS.UPDATED_AT, DSL.currentTimestamp())
.where(USERS.ID.eq(id))
.execute()
}
DatabaseResult.Success(updatedRows)
} catch (e: UserNotFoundException) {
DatabaseResult.NotFoundError(e.message ?: "User not found")
} catch (e: DuplicateEmailException) {
DatabaseResult.BusinessError(e.message ?: "Duplicate email")
} catch (e: DataAccessException) {
DatabaseResult.DatabaseError("Database error: ${e.message}")
} catch (e: Exception) {
DatabaseResult.UnknownError("Unexpected error: ${e.message}")
}
}
// データベース接続の健全性チェック
fun checkDatabaseHealth(): DatabaseResult<Map<String, Any>> {
return try {
val stats = mutableMapOf<String, Any>()
// 基本接続テスト
val connectionTest = create.selectOne().fetch()
stats["connectionStatus"] = "OK"
// テーブル存在確認
val tableExists = create.meta().tables.any { it.name == "users" }
stats["tableExists"] = tableExists
// レコード数チェック
val userCount = create.selectCount().from(USERS).fetchOne(0, Int::class.java)
stats["userCount"] = userCount ?: 0
// 最新レコードの確認
val latestUser = create.select(USERS.CREATED_AT)
.from(USERS)
.orderBy(USERS.CREATED_AT.desc())
.limit(1)
.fetchOne()
stats["latestUserDate"] = latestUser?.get(USERS.CREATED_AT)?.toString() ?: "No users"
DatabaseResult.Success(stats.toMap())
} catch (e: Exception) {
DatabaseResult.DatabaseError("Health check failed: ${e.message}")
}
}
// バッチ操作の安全な実行
fun safeBatchOperation(
operations: List<BatchOperation>
): DatabaseResult<BatchResult> {
return try {
val result = create.transactionResult { configuration ->
val ctx = DSL.using(configuration)
val batchResult = BatchResult()
operations.forEach { operation ->
try {
when (operation) {
is BatchOperation.CreateUser -> {
val user = ctx.insertInto(USERS)
.set(USERS.NAME, operation.name)
.set(USERS.EMAIL, operation.email)
.set(USERS.AGE, operation.age)
.returning()
.fetchOne()
batchResult.successful.add(operation)
batchResult.results[operation] = user
}
is BatchOperation.UpdateUser -> {
val updated = ctx.update(USERS)
.set(USERS.NAME, operation.name)
.set(USERS.UPDATED_AT, DSL.currentTimestamp())
.where(USERS.ID.eq(operation.id))
.execute()
if (updated > 0) {
batchResult.successful.add(operation)
} else {
batchResult.failed.add(
FailedOperation(operation, "User not found")
)
}
}
is BatchOperation.DeleteUser -> {
val deleted = ctx.deleteFrom(USERS)
.where(USERS.ID.eq(operation.id))
.execute()
if (deleted > 0) {
batchResult.successful.add(operation)
} else {
batchResult.failed.add(
FailedOperation(operation, "User not found")
)
}
}
}
} catch (e: Exception) {
batchResult.failed.add(
FailedOperation(operation, e.message ?: "Unknown error")
)
}
}
batchResult
}
DatabaseResult.Success(result)
} catch (e: Exception) {
DatabaseResult.DatabaseError("Batch operation failed: ${e.message}")
}
}
// プライベートヘルパーメソッド
private fun validateUserInput(name: String, email: String, age: Int): String? {
if (name.isBlank()) return "Name cannot be blank"
if (name.length > 255) return "Name too long (max 255 characters)"
if (!isValidEmail(email)) return "Invalid email format"
if (age < 0 || age > 150) return "Age must be between 0 and 150"
return null
}
private fun validateUserUpdateInput(name: String?, email: String?, age: Int?): String? {
name?.let {
if (it.isBlank()) return "Name cannot be blank"
if (it.length > 255) return "Name too long (max 255 characters)"
}
email?.let {
if (!isValidEmail(it)) return "Invalid email format"
}
age?.let {
if (it < 0 || it > 150) return "Age must be between 0 and 150"
}
return null
}
private fun isValidEmail(email: String): Boolean {
return email.matches(Regex("[^@]+@[^@]+\\.[^@]+"))
}
}
// カスタム例外クラス
class UserNotFoundException(message: String) : Exception(message)
class DuplicateEmailException(message: String) : Exception(message)
// 結果型
sealed class DatabaseResult<out T> {
data class Success<T>(val data: T) : DatabaseResult<T>()
data class ValidationError(val message: String) : DatabaseResult<Nothing>()
data class BusinessError(val message: String) : DatabaseResult<Nothing>()
data class NotFoundError(val message: String) : DatabaseResult<Nothing>()
data class ConstraintError(val message: String) : DatabaseResult<Nothing>()
data class ConcurrencyError(val message: String) : DatabaseResult<Nothing>()
data class DatabaseError(val message: String) : DatabaseResult<Nothing>()
data class UnknownError(val message: String) : DatabaseResult<Nothing>()
fun isSuccess(): Boolean = this is Success
fun isError(): Boolean = this !is Success
inline fun <R> map(transform: (T) -> R): DatabaseResult<R> = when (this) {
is Success -> Success(transform(data))
else -> this as DatabaseResult<R>
}
inline fun onSuccess(action: (T) -> Unit): DatabaseResult<T> {
if (this is Success) action(data)
return this
}
inline fun onError(action: (String) -> Unit): DatabaseResult<T> {
when (this) {
is ValidationError -> action(message)
is BusinessError -> action(message)
is NotFoundError -> action(message)
is ConstraintError -> action(message)
is ConcurrencyError -> action(message)
is DatabaseError -> action(message)
is UnknownError -> action(message)
else -> {}
}
return this
}
}
// バッチ操作関連クラス
sealed class BatchOperation {
data class CreateUser(val name: String, val email: String, val age: Int) : BatchOperation()
data class UpdateUser(val id: Int, val name: String) : BatchOperation()
data class DeleteUser(val id: Int) : BatchOperation()
}
data class BatchResult(
val successful: MutableList<BatchOperation> = mutableListOf(),
val failed: MutableList<FailedOperation> = mutableListOf(),
val results: MutableMap<BatchOperation, Any?> = mutableMapOf()
)
data class FailedOperation(
val operation: BatchOperation,
val error: String
)