Krush
Krushは「Kotlinファーストなタイプセーフ ORM」として開発された、Kotlinの言語特性を最大限活用するデータベースアクセスライブラリです。アノテーションベースのエンティティ定義とコード生成により、コンパイル時型安全性を保証し、Kotlinのデータクラス、null安全性、型推論といった現代的な言語機能との完璧な統合を実現します。シンプルで直感的なAPIでありながら、複雑なクエリ構築、関係マッピング、バッチ処理をサポートし、Kotlinらしい簡潔で読みやすいデータベースアクセス層を提供します。
GitHub概要
トピックス
スター履歴
ライブラリ
Krush
概要
Krushは「Kotlinファーストなタイプセーフ ORM」として開発された、Kotlinの言語特性を最大限活用するデータベースアクセスライブラリです。アノテーションベースのエンティティ定義とコード生成により、コンパイル時型安全性を保証し、Kotlinのデータクラス、null安全性、型推論といった現代的な言語機能との完璧な統合を実現します。シンプルで直感的なAPIでありながら、複雑なクエリ構築、関係マッピング、バッチ処理をサポートし、Kotlinらしい簡潔で読みやすいデータベースアクセス層を提供します。
詳細
Krush 2025年版は、Kotlin Multiplatform対応とCoroutines完全サポートにより、モダンなKotlinアプリケーション開発のニーズに応えています。データクラス中心の設計思想により、エンティティ定義が非常にシンプルで保守性が高く、Kotlin開発者にとって自然な開発体験を提供します。PostgreSQL、MySQL、SQLite、H2など主要データベースをサポートし、JDBC上での軽量な抽象化レイヤーとして動作。アクティブレコードパターンとリポジトリパターンの両方に対応し、プロジェクトの要件に応じて柔軟な実装が可能です。
主な特徴
- Kotlinファースト: データクラスとKotlin言語機能の完全活用
- 型安全性: コンパイル時型チェックとnull安全性
- シンプルなAPI: 直感的で学習コストの低いインターフェース
- 軽量設計: 最小限のランタイムオーバーヘッド
- 関係マッピング: one-to-one、one-to-many、many-to-many関係サポート
- マルチプラットフォーム: JVM、Android、Kotlin/Nativeでの動作
メリット・デメリット
メリット
- Kotlinの言語特性を活かした自然で簡潔なコード記述
- データクラース中心の設計で高い保守性と可読性
- コンパイル時型安全性によるバグの早期発見
- 軽量なライブラリで最小限のパフォーマンスオーバーヘッド
- Coroutinesとの優れた統合によるスムーズな非同期処理
- 学習コストが低く、既存のKotlin知識で対応可能
デメリット
- 比較的新しいライブラリで、エコシステムが発展途上
- 複雑なクエリや高度なSQLfeatureには制約がある
- ドキュメントとコミュニティリソースが限定的
- 大規模エンタープライズ向け機能の不足
- 他の言語からの移行時には学習コストが発生
- データベース固有の最適化機能が限定的
参考ページ
書き方の例
セットアップ
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.22"
kotlin("kapt") version "1.9.22"
}
dependencies {
implementation("pl.touk.krush:krush-annotation-processor:0.8.2")
implementation("pl.touk.krush:krush-runtime:0.8.2")
kapt("pl.touk.krush:krush-annotation-processor:0.8.2")
// Database drivers
implementation("org.postgresql:postgresql:42.7.1")
implementation("mysql:mysql-connector-java:8.0.33")
implementation("com.h2database:h2:2.2.224")
// Connection pool
implementation("com.zaxxer:HikariCP:5.1.0")
// Coroutines support
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
// Annotation processing configuration
kapt {
arguments {
arg("krush.generated.package", "com.example.generated")
}
}
-- Database schema example
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tags (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
color VARCHAR(7) DEFAULT '#000000'
);
CREATE TABLE post_tags (
post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE,
tag_id BIGINT REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
基本的な使い方
import pl.touk.krush.*
import java.time.LocalDateTime
// Entity definitions with Krush annotations
@Entity
@Table("users")
data class User(
@Id
@GeneratedValue
val id: Long? = null,
@Column("name")
val name: String,
@Column("email")
val email: String,
@Column("age")
val age: Int,
@Column("created_at")
val createdAt: LocalDateTime = LocalDateTime.now(),
@Column("updated_at")
val updatedAt: LocalDateTime = LocalDateTime.now()
)
@Entity
@Table("posts")
data class Post(
@Id
@GeneratedValue
val id: Long? = null,
@Column("title")
val title: String,
@Column("content")
val content: String?,
@Column("user_id")
val userId: Long,
@Column("published")
val published: Boolean = false,
@Column("created_at")
val createdAt: LocalDateTime = LocalDateTime.now()
) {
// Relationships
@ManyToOne
@JoinColumn("user_id")
lateinit var user: User
@ManyToMany
@JoinTable("post_tags", joinColumns = [JoinColumn("post_id")],
inverseJoinColumns = [JoinColumn("tag_id")])
var tags: List<Tag> = emptyList()
}
@Entity
@Table("tags")
data class Tag(
@Id
@GeneratedValue
val id: Long? = null,
@Column("name")
val name: String,
@Column("color")
val color: String = "#000000"
)
// Database configuration
class DatabaseConfig {
companion object {
fun createDataSource(): DataSource {
return HikariDataSource().apply {
jdbcUrl = "jdbc:postgresql://localhost:5432/krush_demo"
username = "postgres"
password = "password"
maximumPoolSize = 10
minimumIdle = 5
}
}
}
}
// Repository implementation
class UserRepository(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Create user
suspend fun createUser(name: String, email: String, age: Int): User? = withContext(Dispatchers.IO) {
val user = User(name = name, email = email, age = age)
krush.save(user)
}
// Find user by ID
suspend fun findUserById(id: Long): User? = withContext(Dispatchers.IO) {
krush.findById<User>(id)
}
// Find user by email
suspend fun findUserByEmail(email: String): User? = withContext(Dispatchers.IO) {
krush.findBy<User> { User::email eq email }
}
// Find all users
suspend fun findAllUsers(): List<User> = withContext(Dispatchers.IO) {
krush.findAll<User>()
}
// Search users by name pattern
suspend fun searchUsersByName(namePattern: String): List<User> = withContext(Dispatchers.IO) {
krush.findBy<User> { User::name like "%$namePattern%" }
}
// Find users by age range
suspend fun findUsersByAgeRange(minAge: Int, maxAge: Int): List<User> = withContext(Dispatchers.IO) {
krush.findBy<User> {
(User::age gte minAge) and (User::age lte maxAge)
}
}
// Update user
suspend fun updateUser(user: User): User? = withContext(Dispatchers.IO) {
val updatedUser = user.copy(updatedAt = LocalDateTime.now())
krush.update(updatedUser)
}
// Delete user
suspend fun deleteUser(id: Long): Boolean = withContext(Dispatchers.IO) {
krush.deleteById<User>(id) > 0
}
// Count users
suspend fun countUsers(): Long = withContext(Dispatchers.IO) {
krush.count<User>()
}
// Pagination
suspend fun findUsersWithPagination(page: Int, size: Int): List<User> = withContext(Dispatchers.IO) {
krush.findBy<User>(
limit = size,
offset = page * size
) { User::createdAt.desc() }
}
}
// Usage example
suspend fun demonstrateBasicOperations() {
val dataSource = DatabaseConfig.createDataSource()
val userRepository = UserRepository(dataSource)
// Create users
val user1 = userRepository.createUser("Alice Johnson", "[email protected]", 28)
val user2 = userRepository.createUser("Bob Smith", "[email protected]", 32)
val user3 = userRepository.createUser("Charlie Brown", "[email protected]", 25)
println("Created users: ${listOfNotNull(user1, user2, user3)}")
// Find user by email
val foundUser = userRepository.findUserByEmail("[email protected]")
println("Found user by email: $foundUser")
// Search users by name
val searchResults = userRepository.searchUsersByName("B")
println("Users with 'B' in name: $searchResults")
// Find users by age range
val youngUsers = userRepository.findUsersByAgeRange(25, 30)
println("Users aged 25-30: $youngUsers")
// Update user
user1?.let { user ->
val updatedUser = userRepository.updateUser(
user.copy(name = "Alice Johnson Smith", age = 29)
)
println("Updated user: $updatedUser")
}
// Count and pagination
val totalUsers = userRepository.countUsers()
println("Total users: $totalUsers")
val firstPage = userRepository.findUsersWithPagination(page = 0, size = 2)
println("First page users: $firstPage")
}
関係とクエリ
class PostRepository(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Create post with user
suspend fun createPost(title: String, content: String?, userId: Long, published: Boolean = false): Post? =
withContext(Dispatchers.IO) {
val post = Post(
title = title,
content = content,
userId = userId,
published = published
)
krush.save(post)
}
// Find posts by user
suspend fun findPostsByUser(userId: Long): List<Post> = withContext(Dispatchers.IO) {
krush.findBy<Post> { Post::userId eq userId }
}
// Find posts with user (JOIN)
suspend fun findPostsWithUser(): List<Pair<Post, User>> = withContext(Dispatchers.IO) {
krush.query<Post> {
select(Post::class, User::class)
from(Post::class)
join(User::class).on { Post::userId eq User::id }
orderBy(Post::createdAt.desc())
}.map { row ->
Pair(row.get(Post::class), row.get(User::class))
}
}
// Find published posts only
suspend fun findPublishedPosts(): List<Post> = withContext(Dispatchers.IO) {
krush.findBy<Post> { Post::published eq true }
}
// Search posts by title
suspend fun searchPostsByTitle(titlePattern: String): List<Post> = withContext(Dispatchers.IO) {
krush.findBy<Post> { Post::title like "%$titlePattern%" }
}
// Complex query: Find users with most posts
suspend fun findUsersWithMostPosts(limit: Int = 5): List<Pair<User, Long>> = withContext(Dispatchers.IO) {
krush.query<User> {
select(User::class, count(Post::id))
from(User::class)
leftJoin(Post::class).on { User::id eq Post::userId }
groupBy(User::id)
orderBy(count(Post::id).desc())
limit(limit)
}.map { row ->
Pair(row.get(User::class), row.get(1, Long::class.java))
}
}
// Update post status
suspend fun publishPost(postId: Long): Boolean = withContext(Dispatchers.IO) {
krush.updateBy<Post>({ Post::id eq postId }) {
set(Post::published, true)
} > 0
}
// Delete posts by user
suspend fun deletePostsByUser(userId: Long): Int = withContext(Dispatchers.IO) {
krush.deleteBy<Post> { Post::userId eq userId }
}
// Bulk operations
suspend fun bulkUpdatePostPublishStatus(postIds: List<Long>, published: Boolean): Int =
withContext(Dispatchers.IO) {
krush.updateBy<Post>({ Post::id `in` postIds }) {
set(Post::published, published)
}
}
}
class TagRepository(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Create tag
suspend fun createTag(name: String, color: String = "#000000"): Tag? = withContext(Dispatchers.IO) {
val tag = Tag(name = name, color = color)
krush.save(tag)
}
// Find tag by name
suspend fun findTagByName(name: String): Tag? = withContext(Dispatchers.IO) {
krush.findBy<Tag> { Tag::name eq name }
}
// Find all tags
suspend fun findAllTags(): List<Tag> = withContext(Dispatchers.IO) {
krush.findAll<Tag>()
}
// Find or create tag
suspend fun findOrCreateTag(name: String, color: String = "#000000"): Tag = withContext(Dispatchers.IO) {
findTagByName(name) ?: createTag(name, color)!!
}
// Find popular tags (by post count)
suspend fun findPopularTags(limit: Int = 10): List<Pair<Tag, Long>> = withContext(Dispatchers.IO) {
krush.query<Tag> {
select(Tag::class, count())
from(Tag::class)
join("post_tags").on { Tag::id eq column("tag_id") }
groupBy(Tag::id)
orderBy(count().desc())
limit(limit)
}.map { row ->
Pair(row.get(Tag::class), row.get(1, Long::class.java))
}
}
}
// Usage example for relationships
suspend fun demonstrateRelationshipsAndQueries() {
val dataSource = DatabaseConfig.createDataSource()
val userRepository = UserRepository(dataSource)
val postRepository = PostRepository(dataSource)
val tagRepository = TagRepository(dataSource)
// Create user and posts
val user = userRepository.createUser("Content Creator", "[email protected]", 30)
user?.let { u ->
postRepository.createPost("First Post", "This is my first post!", u.id!!, true)
postRepository.createPost("Draft Post", "This is a draft", u.id!!, false)
postRepository.createPost("Another Post", "More content here", u.id!!, true)
}
// Find posts with users
val postsWithUsers = postRepository.findPostsWithUser()
println("Posts with users: $postsWithUsers")
// Find published posts
val publishedPosts = postRepository.findPublishedPosts()
println("Published posts: $publishedPosts")
// Search posts
val searchResults = postRepository.searchPostsByTitle("Post")
println("Posts containing 'Post': $searchResults")
// Users with most posts
val topUsers = postRepository.findUsersWithMostPosts(3)
println("Top users by post count: $topUsers")
// Create tags
val tags = listOf(
tagRepository.findOrCreateTag("Technology", "#3498db"),
tagRepository.findOrCreateTag("Programming", "#e74c3c"),
tagRepository.findOrCreateTag("Kotlin", "#f39c12")
)
println("Created/found tags: $tags")
// Popular tags
val popularTags = tagRepository.findPopularTags(5)
println("Popular tags: $popularTags")
}
トランザクションとバッチ操作
class UserService(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Transaction example
suspend fun transferPostsBetweenUsers(fromUserId: Long, toUserId: Long): Boolean = withContext(Dispatchers.IO) {
try {
krush.transaction {
// Verify both users exist
val fromUser = krush.findById<User>(fromUserId)
val toUser = krush.findById<User>(toUserId)
if (fromUser == null || toUser == null) {
throw IllegalArgumentException("One or both users not found")
}
// Transfer posts
val transferCount = krush.updateBy<Post>({ Post::userId eq fromUserId }) {
set(Post::userId, toUserId)
}
println("Transferred $transferCount posts from user $fromUserId to $toUserId")
}
true
} catch (e: Exception) {
println("Transfer failed: ${e.message}")
false
}
}
// Batch user creation
suspend fun batchCreateUsers(usersData: List<Triple<String, String, Int>>): List<User> =
withContext(Dispatchers.IO) {
krush.transaction {
usersData.mapNotNull { (name, email, age) ->
krush.save(User(name = name, email = email, age = age))
}
}
}
// Complex transaction: Create user with initial posts and tags
suspend fun createUserWithContent(
userName: String,
userEmail: String,
userAge: Int,
posts: List<Pair<String, String>>,
tagNames: List<String>
): UserCreationResult = withContext(Dispatchers.IO) {
try {
krush.transaction {
// Create user
val user = krush.save(User(name = userName, email = userEmail, age = userAge))
?: throw RuntimeException("Failed to create user")
// Create or find tags
val tags = tagNames.map { tagName ->
krush.findBy<Tag> { Tag::name eq tagName }.firstOrNull()
?: krush.save(Tag(name = tagName))!!
}
// Create posts
val createdPosts = posts.map { (title, content) ->
krush.save(Post(
title = title,
content = content,
userId = user.id!!,
published = false
))!!
}
UserCreationResult.Success(user, createdPosts, tags)
}
} catch (e: Exception) {
UserCreationResult.Error(e.message ?: "Unknown error")
}
}
// Bulk update with conditions
suspend fun bulkUpdateUserAges(ageUpdates: Map<Long, Int>): Int = withContext(Dispatchers.IO) {
krush.transaction {
var totalUpdated = 0
ageUpdates.forEach { (userId, newAge) ->
val updated = krush.updateBy<User>({ User::id eq userId }) {
set(User::age, newAge)
set(User::updatedAt, LocalDateTime.now())
}
totalUpdated += updated
}
totalUpdated
}
}
// Conditional cleanup operation
suspend fun cleanupInactiveUsers(daysSinceLastPost: Int): CleanupResult = withContext(Dispatchers.IO) {
krush.transaction {
val cutoffDate = LocalDateTime.now().minusDays(daysSinceLastPost.toLong())
// Find inactive users
val inactiveUserIds = krush.query<User> {
select(User::id)
from(User::class)
where {
not(exists {
select(literal(1))
from(Post::class)
where { (Post::userId eq User::id) and (Post::createdAt gt cutoffDate) }
})
}
}.map { it.get(User::id) }
if (inactiveUserIds.isEmpty()) {
return@transaction CleanupResult(0, 0, emptyList())
}
// Delete posts first (foreign key constraint)
val deletedPosts = krush.deleteBy<Post> { Post::userId `in` inactiveUserIds }
// Delete users
val deletedUsers = krush.deleteBy<User> { User::id `in` inactiveUserIds }
CleanupResult(deletedUsers, deletedPosts, inactiveUserIds)
}
}
// Safe batch operation with validation
suspend fun safeBatchCreateUsers(usersData: List<Triple<String, String, Int>>): BatchCreationResult =
withContext(Dispatchers.IO) {
// Validation
val errors = mutableListOf<String>()
usersData.forEachIndexed { index, (name, email, age) ->
if (name.isBlank()) errors.add("Empty name at index $index")
if (!email.contains("@")) errors.add("Invalid email at index $index: $email")
if (age < 0 || age > 150) errors.add("Invalid age at index $index: $age")
}
if (errors.isNotEmpty()) {
return@withContext BatchCreationResult.ValidationError(errors)
}
try {
val createdUsers = krush.transaction {
// Check for duplicate emails
val emails = usersData.map { it.second }
val existingEmails = krush.findBy<User> { User::email `in` emails }
.map { it.email }
if (existingEmails.isNotEmpty()) {
throw RuntimeException("Duplicate emails found: ${existingEmails.joinToString()}")
}
// Batch creation
usersData.mapNotNull { (name, email, age) ->
krush.save(User(name = name, email = email, age = age))
}
}
BatchCreationResult.Success(createdUsers)
} catch (e: Exception) {
BatchCreationResult.Error(e.message ?: "Batch creation failed")
}
}
}
// Result classes
sealed class UserCreationResult {
data class Success(val user: User, val posts: List<Post>, val tags: List<Tag>) : UserCreationResult()
data class Error(val message: String) : UserCreationResult()
}
sealed class BatchCreationResult {
data class Success(val users: List<User>) : BatchCreationResult()
data class ValidationError(val errors: List<String>) : BatchCreationResult()
data class Error(val message: String) : BatchCreationResult()
}
data class CleanupResult(
val deletedUsers: Int,
val deletedPosts: Int,
val inactiveUserIds: List<Long>
)
// Usage example for transactions and batch operations
suspend fun demonstrateTransactionsAndBatch() {
val dataSource = DatabaseConfig.createDataSource()
val userService = UserService(dataSource)
// Batch user creation
val newUsersData = listOf(
Triple("Emma Wilson", "[email protected]", 26),
Triple("David Chen", "[email protected]", 31),
Triple("Sarah Johnson", "[email protected]", 29)
)
when (val result = userService.safeBatchCreateUsers(newUsersData)) {
is BatchCreationResult.Success -> {
println("✓ Batch created ${result.users.size} users: ${result.users}")
}
is BatchCreationResult.ValidationError -> {
println("✗ Validation errors: ${result.errors}")
}
is BatchCreationResult.Error -> {
println("✗ Batch creation failed: ${result.message}")
}
}
// Create user with content
when (val result = userService.createUserWithContent(
userName = "Tech Blogger",
userEmail = "[email protected]",
userAge = 32,
posts = listOf(
"Getting Started with Kotlin" to "Kotlin is a great language...",
"Database Access with Krush" to "Let's explore Krush ORM..."
),
tagNames = listOf("Kotlin", "Programming", "Tutorial")
)) {
is UserCreationResult.Success -> {
println("✓ Created user with content:")
println(" User: ${result.user}")
println(" Posts: ${result.posts}")
println(" Tags: ${result.tags}")
}
is UserCreationResult.Error -> {
println("✗ User creation failed: ${result.message}")
}
}
// Cleanup inactive users
val cleanupResult = userService.cleanupInactiveUsers(30)
println("Cleanup result: $cleanupResult")
}
エラーハンドリング
// Custom exceptions
sealed class KrushException(message: String, cause: Throwable? = null) : Exception(message, cause) {
class EntityNotFoundException(entityType: String, id: Any) : KrushException("$entityType not found with id: $id")
class DuplicateKeyException(message: String) : KrushException("Duplicate key violation: $message")
class ValidationException(message: String) : KrushException("Validation failed: $message")
class DatabaseException(message: String, cause: Throwable? = null) : KrushException("Database error: $message", cause)
class TransactionException(message: String, cause: Throwable? = null) : KrushException("Transaction failed: $message", cause)
}
// Safe result wrapper
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: KrushException) : Result<Nothing>()
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
is Success -> Success(transform(data))
is Error -> this
}
inline fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
is Success -> transform(data)
is Error -> this
}
fun getOrNull(): T? = when (this) {
is Success -> data
is Error -> null
}
}
class SafeUserRepository(private val dataSource: DataSource) {
private val krush = Krush(dataSource)
// Safe user creation with validation
suspend fun createUserSafely(name: String, email: String, age: Int): Result<User> = withContext(Dispatchers.IO) {
try {
// Input validation
if (name.isBlank()) {
return@withContext Result.Error(KrushException.ValidationException("Name cannot be empty"))
}
if (!email.contains("@") || email.length < 5) {
return@withContext Result.Error(KrushException.ValidationException("Invalid email format"))
}
if (age < 0 || age > 150) {
return@withContext Result.Error(KrushException.ValidationException("Age must be between 0 and 150"))
}
// Check for duplicate email
val existingUser = krush.findBy<User> { User::email eq email }.firstOrNull()
if (existingUser != null) {
return@withContext Result.Error(KrushException.DuplicateKeyException("Email $email already exists"))
}
// Create user
val user = krush.save(User(name = name, email = email, age = age))
if (user != null) {
Result.Success(user)
} else {
Result.Error(KrushException.DatabaseException("Failed to create user"))
}
} catch (e: Exception) {
when (e) {
is KrushException -> Result.Error(e)
else -> Result.Error(KrushException.DatabaseException("Unexpected error", e))
}
}
}
// Safe user retrieval
suspend fun getUserSafely(userId: Long): Result<User> = withContext(Dispatchers.IO) {
try {
val user = krush.findById<User>(userId)
if (user != null) {
Result.Success(user)
} else {
Result.Error(KrushException.EntityNotFoundException("User", userId))
}
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Failed to retrieve user", e))
}
}
// Safe user update
suspend fun updateUserSafely(
userId: Long,
name: String,
email: String,
age: Int
): Result<User> = withContext(Dispatchers.IO) {
try {
// Validation
if (name.isBlank()) {
return@withContext Result.Error(KrushException.ValidationException("Name cannot be empty"))
}
if (!email.contains("@")) {
return@withContext Result.Error(KrushException.ValidationException("Invalid email format"))
}
if (age < 0 || age > 150) {
return@withContext Result.Error(KrushException.ValidationException("Invalid age"))
}
val result = krush.transaction {
// Check if user exists
val existingUser = krush.findById<User>(userId)
?: throw KrushException.EntityNotFoundException("User", userId)
// Check for email conflicts (excluding current user)
val emailConflict = krush.findBy<User> {
(User::email eq email) and (User::id neq userId)
}.firstOrNull()
if (emailConflict != null) {
throw KrushException.DuplicateKeyException("Email $email is already in use")
}
// Update user
val updatedUser = existingUser.copy(
name = name,
email = email,
age = age,
updatedAt = LocalDateTime.now()
)
krush.update(updatedUser) ?: throw KrushException.DatabaseException("Failed to update user")
}
Result.Success(result)
} catch (e: KrushException) {
Result.Error(e)
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Unexpected error during update", e))
}
}
// Safe deletion with cascade check
suspend fun deleteUserSafely(userId: Long): Result<Boolean> = withContext(Dispatchers.IO) {
try {
val result = krush.transaction {
// Check if user exists
val user = krush.findById<User>(userId)
?: throw KrushException.EntityNotFoundException("User", userId)
// Check for related posts
val postCount = krush.count<Post> { Post::userId eq userId }
if (postCount > 0) {
// Delete related posts first
krush.deleteBy<Post> { Post::userId eq userId }
}
// Delete user
val deleteCount = krush.deleteById<User>(userId)
deleteCount > 0
}
Result.Success(result)
} catch (e: KrushException) {
Result.Error(e)
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Failed to delete user", e))
}
}
// Safe batch operation
suspend fun batchCreateUsersSafely(
usersData: List<Triple<String, String, Int>>
): Result<List<User>> = withContext(Dispatchers.IO) {
try {
// Validate all data first
usersData.forEachIndexed { index, (name, email, age) ->
if (name.isBlank()) {
throw KrushException.ValidationException("Name cannot be empty at index $index")
}
if (!email.contains("@")) {
throw KrushException.ValidationException("Invalid email at index $index: $email")
}
if (age < 0 || age > 150) {
throw KrushException.ValidationException("Invalid age at index $index: $age")
}
}
// Check for duplicate emails in batch
val emails = usersData.map { it.second }
if (emails.size != emails.toSet().size) {
throw KrushException.ValidationException("Duplicate emails in batch data")
}
val result = krush.transaction {
// Check for existing emails
val existingEmails = krush.findBy<User> { User::email `in` emails }
.map { it.email }
if (existingEmails.isNotEmpty()) {
throw KrushException.DuplicateKeyException("Existing emails found: ${existingEmails.joinToString()}")
}
// Batch create
usersData.mapNotNull { (name, email, age) ->
krush.save(User(name = name, email = email, age = age))
}
}
Result.Success(result)
} catch (e: KrushException) {
Result.Error(e)
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Batch creation failed", e))
}
}
// Database health check
suspend fun checkDatabaseHealth(): Result<String> = withContext(Dispatchers.IO) {
try {
val count = krush.count<User>()
Result.Success("Database connection healthy. Total users: $count")
} catch (e: Exception) {
Result.Error(KrushException.DatabaseException("Database health check failed", e))
}
}
}
// Usage example for error handling
suspend fun demonstrateErrorHandling() {
val dataSource = DatabaseConfig.createDataSource()
val safeUserRepo = SafeUserRepository(dataSource)
// Test database health
when (val healthResult = safeUserRepo.checkDatabaseHealth()) {
is Result.Success -> println("✓ ${healthResult.data}")
is Result.Error -> println("✗ Health check failed: ${healthResult.exception.message}")
}
// Safe user creation
when (val result = safeUserRepo.createUserSafely("Jane Doe", "[email protected]", 28)) {
is Result.Success -> {
println("✓ Successfully created user: ${result.data}")
// Safe update
when (val updateResult = safeUserRepo.updateUserSafely(
result.data.id!!, "Jane Smith", "[email protected]", 29
)) {
is Result.Success -> println("✓ Successfully updated user: ${updateResult.data}")
is Result.Error -> println("✗ Update failed: ${updateResult.exception.message}")
}
}
is Result.Error -> println("✗ User creation failed: ${result.exception.message}")
}
// Test invalid data
when (val invalidResult = safeUserRepo.createUserSafely("", "invalid-email", -5)) {
is Result.Success -> println("This shouldn't happen")
is Result.Error -> println("✓ Correctly caught validation error: ${invalidResult.exception.message}")
}
// Test duplicate email
when (val duplicateResult = safeUserRepo.createUserSafely("Another Jane", "[email protected]", 30)) {
is Result.Success -> println("This shouldn't happen")
is Result.Error -> println("✓ Correctly caught duplicate email: ${duplicateResult.exception.message}")
}
// Test safe batch creation
val batchData = listOf(
Triple("User 1", "[email protected]", 25),
Triple("User 2", "[email protected]", 30),
Triple("", "[email protected]", 35) // Invalid name
)
when (val batchResult = safeUserRepo.batchCreateUsersSafely(batchData)) {
is Result.Success -> println("✓ Batch creation successful: ${batchResult.data}")
is Result.Error -> println("✓ Correctly caught batch validation error: ${batchResult.exception.message}")
}
// Test non-existent user retrieval
when (val result = safeUserRepo.getUserSafely(999L)) {
is Result.Success -> println("This shouldn't happen")
is Result.Error -> println("✓ Correctly caught entity not found: ${result.exception.message}")
}
}