Exposed

ExposedはJetBrains開発のKotlin優先ORMフレームワークで、「SQL DSLファーストアプローチ」による型安全なデータベースアクセスを実現します。簡潔な構文と最新Kotlin機能の完全統合により、Kotlinx datetime対応、コルーチンサポート、直感的なクエリ構築を特徴とします。2025年現在、Kotlin ORM最有力候補として開発者の60%が軽量ソリューションとして選択し、新規プロジェクトで推奨される第一選択肢となっています。

ORMKotlinSQL DSLJetBrains型安全データベースJVM

GitHub概要

JetBrains/Exposed

Kotlin SQL Framework

スター8,855
ウォッチ130
フォーク738
作成日:2013年7月30日
言語:Kotlin
ライセンス:Apache License 2.0

トピックス

daokotlinormsql

スター履歴

JetBrains/Exposed Star History
データ取得日時: 2025/7/19 09:31

ライブラリ

Exposed

概要

ExposedはJetBrains開発のKotlin優先ORMフレームワークで、「SQL DSLファーストアプローチ」による型安全なデータベースアクセスを実現します。簡潔な構文と最新Kotlin機能の完全統合により、Kotlinx datetime対応、コルーチンサポート、直感的なクエリ構築を特徴とします。2025年現在、Kotlin ORM最有力候補として開発者の60%が軽量ソリューションとして選択し、新規プロジェクトで推奨される第一選択肢となっています。

詳細

Exposed 2025年版は、Kotlin言語の特性を最大限活用したDSL設計により、型安全性とコード可読性を両立したモダンなデータベースアクセス層を提供します。JetBrainsによる継続的な開発とKotlinエコシステムとの深い統合により、Spring Boot、Ktor等の主要フレームワークとシームレスに連携。DSLとDAOの2つのAPIアプローチを提供し、プロジェクトの要件に応じた柔軟な実装スタイルを選択可能です。

主な特徴

  • Kotlin DSL設計: 自然で読みやすいSQL構築とクエリ記述
  • 型安全性保証: コンパイル時のSQL検証と型チェック
  • 二重API提供: DSL(関数型)とDAO(オブジェクト指向)の選択可能
  • JetBrainsサポート: 公式メンテナンスによる安定性と継続性
  • 多データベース対応: H2、MySQL、PostgreSQL、SQLite、Oracle、SQL Server対応
  • コルーチンサポート: 非同期処理とSuspend関数の完全統合

メリット・デメリット

メリット

  • JetBrains公式開発による信頼性とKotlin最適化設計
  • 学習コストが低く直感的なDSL構文による高い開発効率
  • 型安全性によるコンパイル時エラー検出とランタイム安全性
  • Spring Boot Starterによる既存プロジェクトへの簡単統合
  • 豊富なサンプルコードと公式ドキュメントの充実
  • Kotlin Multiplatformプロジェクトでの活用可能性

デメリット

  • Kotlin専用のため他言語プロジェクトでは使用不可
  • 複雑なクエリ構築時にDSLの制限による冗長性
  • JPA/Hibernateと比較したエンタープライズ機能の不足
  • マイグレーション機能の限定的サポート
  • 大規模データセットでのパフォーマンス最適化課題
  • レガシーJavaコードベースとの統合コスト

参考ページ

書き方の例

プロジェクトセットアップと依存関係

// build.gradle.kts
dependencies {
    implementation("org.jetbrains.exposed:exposed-core:1.0.0-beta-2")
    implementation("org.jetbrains.exposed:exposed-dao:1.0.0-beta-2")
    implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0-beta-2")
    
    // 日付時刻サポート
    implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0-beta-2")
    
    // Spring Boot統合
    implementation("org.jetbrains.exposed:exposed-spring-boot-starter:1.0.0-beta-2")
    
    // データベースドライバー(必要に応じて選択)
    implementation("com.h2database:h2:2.2.224")                    // H2
    implementation("mysql:mysql-connector-java:8.0.33")           // MySQL
    implementation("org.postgresql:postgresql:42.7.1")            // PostgreSQL
    implementation("org.xerial:sqlite-jdbc:3.44.1.0")            // SQLite
}

// メインクラスでの基本インポート
import org.jetbrains.exposed.v1.*
import org.jetbrains.exposed.v1.transactions.transaction

データベース接続設定

import org.jetbrains.exposed.v1.core.Database
import org.jetbrains.exposed.v1.core.DatabaseConfig

// H2インメモリデータベース
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")

// MySQL接続
Database.connect(
    "jdbc:mysql://localhost:3306/testdb",
    driver = "com.mysql.cj.jdbc.Driver",
    user = "root",
    password = "password"
)

// PostgreSQL接続
Database.connect(
    "jdbc:postgresql://localhost:5432/testdb",
    driver = "org.postgresql.Driver",
    user = "postgres",
    password = "password"
)

// SQLite接続
Database.connect("jdbc:sqlite:data.db", "org.xerial.sqlite-jdbc.Driver")

// 高度な設定例(HikariCP使用)
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource

val config = HikariConfig().apply {
    jdbcUrl = "jdbc:mysql://localhost:3306/testdb"
    driverClassName = "com.mysql.cj.jdbc.Driver"
    username = "root"
    password = "password"
    maximumPoolSize = 10
}

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

// DatabaseConfig使用例
val database = Database.connect(
    datasource = dataSource,
    databaseConfig = DatabaseConfig {
        sqlLogger = Slf4jSqlDebugLogger
        useNestedTransactions = true
    }
)

テーブル定義(DSLアプローチ)

import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.Column
import kotlinx.datetime.LocalDateTime

// 基本的なテーブル定義
object Users : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 50)
    val email = varchar("email", 100).uniqueIndex()
    val age = integer("age").nullable()
    val isActive = bool("is_active").default(true)
    
    override val primaryKey = PrimaryKey(id)
}

object Cities : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 50)
    val country = varchar("country", 50)
    
    override val primaryKey = PrimaryKey(id)
}

object Posts : Table() {
    val id = integer("id").autoIncrement()
    val title = varchar("title", 200)
    val content = text("content")
    val authorId = integer("author_id") references Users.id
    val cityId = integer("city_id") references Cities.id
    val createdAt = varchar("created_at", 50) // kotlinx-datetime使用時はdatetime()
    val publishedAt = varchar("published_at", 50).nullable()
    
    override val primaryKey = PrimaryKey(id)
}

// 複合主キーの例
object UserRoles : Table() {
    val userId = integer("user_id") references Users.id
    val roleId = integer("role_id")
    val assignedAt = varchar("assigned_at", 50)
    
    override val primaryKey = PrimaryKey(userId, roleId)
}

// インデックスとユニーク制約
object Products : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 100)
    val sku = varchar("sku", 50).uniqueIndex()
    val categoryId = integer("category_id")
    val price = decimal("price", 10, 2)
    
    override val primaryKey = PrimaryKey(id)
    
    init {
        // 複合インデックス
        index(false, categoryId, name)
    }
}

基本的なCRUD操作(DSL)

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

fun basicCrudOperations() {
    transaction {
        // テーブル作成
        SchemaUtils.create(Users, Cities, Posts)
        
        // データ挿入
        val tokyoId = Cities.insert {
            it[name] = "東京"
            it[country] = "日本"
        } get Cities.id
        
        val osakaId = Cities.insert {
            it[name] = "大阪"
            it[country] = "日本"
        } get Cities.id
        
        // ユーザー挿入
        val userId1 = Users.insert {
            it[name] = "田中太郎"
            it[email] = "[email protected]"
            it[age] = 30
            it[isActive] = true
        } get Users.id
        
        val userId2 = Users.insert {
            it[name] = "佐藤花子"
            it[email] = "[email protected]"
            it[age] = 25
            it[isActive] = true
        } get Users.id
        
        // 投稿作成
        Posts.insert {
            it[title] = "Kotlin Exposed入門"
            it[content] = "Kotlin Exposedでデータベースアクセスをしてみましょう"
            it[authorId] = userId1
            it[cityId] = tokyoId
            it[createdAt] = "2025-06-24T10:00:00"
            it[publishedAt] = "2025-06-24T12:00:00"
        }
        
        // バッチ挿入
        Users.batchInsert(listOf(
            Triple("山田次郎", "[email protected]", 28),
            Triple("鈴木三郎", "[email protected]", 32),
            Triple("高橋四郎", "[email protected]", 27)
        )) { (name, email, age) ->
            this[Users.name] = name
            this[Users.email] = email
            this[Users.age] = age
            this[Users.isActive] = true
        }
        
        // データ選択
        println("=== 全ユーザー ===")
        Users.selectAll().forEach { row ->
            println("${row[Users.id]}: ${row[Users.name]} (${row[Users.email]})")
        }
        
        // 条件付き選択
        println("\n=== アクティブユーザー(年齢25歳以上) ===")
        Users.select { (Users.isActive eq true) and (Users.age greaterEq 25) }
            .forEach { row ->
                println("${row[Users.name]} - 年齢: ${row[Users.age]}")
            }
        
        // JOIN操作
        println("\n=== ユーザーと投稿のJOIN ===")
        (Users innerJoin Posts innerJoin Cities)
            .select(Users.name, Posts.title, Cities.name)
            .forEach { row ->
                println("${row[Users.name]}${row[Cities.name]}から投稿: ${row[Posts.title]}")
            }
        
        // データ更新
        Users.update({ Users.id eq userId1 }) {
            it[age] = 31
            it[email] = "[email protected]"
        }
        
        // データ削除
        Posts.deleteWhere { Posts.authorId eq userId2 }
        
        // 集計クエリ
        val userCount = Users.select { Users.isActive eq true }.count()
        println("\nアクティブユーザー数: $userCount")
        
        val avgAge = Users.slice(Users.age.avg()).select { Users.age.isNotNull() }
            .single()[Users.age.avg()]
        println("平均年齢: $avgAge")
    }
}

DAOアプローチ(オブジェクト指向)

import org.jetbrains.exposed.v1.dao.*
import org.jetbrains.exposed.v1.dao.id.*

// DAOテーブル定義
object UsersTable : IntIdTable("users") {
    val name = varchar("name", 50)
    val email = varchar("email", 100).uniqueIndex()
    val age = integer("age").nullable()
    val isActive = bool("is_active").default(true)
}

object CitiesTable : IntIdTable("cities") {
    val name = varchar("name", 50)
    val country = varchar("country", 50)
}

object PostsTable : IntIdTable("posts") {
    val title = varchar("title", 200)
    val content = text("content")
    val author = reference("author_id", UsersTable)
    val city = reference("city_id", CitiesTable)
    val createdAt = varchar("created_at", 50)
    val publishedAt = varchar("published_at", 50).nullable()
}

// エンティティクラス定義
class User(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<User>(UsersTable)
    
    var name by UsersTable.name
    var email by UsersTable.email
    var age by UsersTable.age
    var isActive by UsersTable.isActive
    
    // 関連エンティティ
    val posts by Post referrersOn PostsTable.author
    
    // カスタムメソッド
    fun getActivePostsCount(): Long = posts.filter { it.publishedAt != null }.count()
    
    override fun toString(): String = "User(id=$id, name='$name', email='$email')"
}

class City(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<City>(CitiesTable)
    
    var name by CitiesTable.name
    var country by CitiesTable.country
    
    val posts by Post referrersOn PostsTable.city
    val users by User via PostsTable
    
    override fun toString(): String = "City(id=$id, name='$name', country='$country')"
}

class Post(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<Post>(PostsTable)
    
    var title by PostsTable.title
    var content by PostsTable.content
    var author by User referencedOn PostsTable.author
    var city by City referencedOn PostsTable.city
    var createdAt by PostsTable.createdAt
    var publishedAt by PostsTable.publishedAt
    
    val isPublished: Boolean get() = publishedAt != null
    
    override fun toString(): String = "Post(id=$id, title='$title', author='${author.name}')"
}

// DAO操作例
fun daoOperations() {
    transaction {
        // テーブル作成
        SchemaUtils.create(UsersTable, CitiesTable, PostsTable)
        
        // エンティティ作成
        val tokyo = City.new {
            name = "東京"
            country = "日本"
        }
        
        val osaka = City.new {
            name = "大阪"
            country = "日本"
        }
        
        val user1 = User.new {
            name = "田中太郎"
            email = "[email protected]"
            age = 30
            isActive = true
        }
        
        val user2 = User.new {
            name = "佐藤花子"
            email = "[email protected]"
            age = 25
            isActive = true
        }
        
        // 投稿作成
        val post1 = Post.new {
            title = "Kotlin Exposed DAO入門"
            content = "DAOパターンでデータベースアクセスを学びましょう"
            author = user1
            city = tokyo
            createdAt = "2025-06-24T10:00:00"
            publishedAt = "2025-06-24T12:00:00"
        }
        
        val post2 = Post.new {
            title = "関西でのKotlin勉強会"
            content = "大阪でKotlinの勉強会を開催します"
            author = user2
            city = osaka
            createdAt = "2025-06-24T14:00:00"
            publishedAt = null
        }
        
        // データ検索
        println("=== 全ユーザー ===")
        User.all().forEach { user ->
            println("$user - 投稿数: ${user.posts.count()}")
        }
        
        // 条件付き検索
        println("\n=== アクティブユーザー ===")
        User.find { UsersTable.isActive eq true }.forEach { user ->
            println("${user.name}: ${user.email}")
        }
        
        // 関連データアクセス
        println("\n=== ユーザーの投稿 ===")
        user1.posts.forEach { post ->
            println("${post.title} (${if (post.isPublished) "公開済み" else "下書き"})")
        }
        
        println("\n=== 都市別投稿数 ===")
        City.all().forEach { city ->
            println("${city.name}: ${city.posts.count()}件の投稿")
        }
        
        // データ更新
        user1.age = 31
        user1.email = "[email protected]"
        
        post2.publishedAt = "2025-06-24T16:00:00"
        
        // データ削除
        post2.delete()
        
        // 集計操作
        val activeUserCount = User.find { UsersTable.isActive eq true }.count()
        println("\nアクティブユーザー数: $activeUserCount")
        
        val publishedPostCount = Post.find { PostsTable.publishedAt.isNotNull() }.count()
        println("公開済み投稿数: $publishedPostCount")
    }
}

高度なクエリとトランザクション

import org.jetbrains.exposed.v1.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder
import kotlinx.coroutines.runBlocking

// 複雑なクエリ例
fun advancedQueries() {
    transaction {
        // サブクエリ使用例
        val activeUsers = Users.select { Users.isActive eq true }
        val postsFromActiveUsers = Posts.select { 
            Posts.authorId inSubQuery activeUsers.slice(Users.id)
        }
        
        println("アクティブユーザーからの投稿:")
        postsFromActiveUsers.forEach { row ->
            println("${row[Posts.title]}")
        }
        
        // 複数テーブルJOINと集計
        val userPostStats = (Users innerJoin Posts)
            .slice(Users.name, Posts.id.count(), Posts.publishedAt.count())
            .select { Users.isActive eq true }
            .groupBy(Users.id, Users.name)
            .orderBy(Posts.id.count() to SortOrder.DESC)
        
        println("\nユーザー別投稿統計:")
        userPostStats.forEach { row ->
            println("${row[Users.name]}: 総投稿${row[Posts.id.count()]}件, 公開${row[Posts.publishedAt.count()]}件")
        }
        
        // CASE文使用例
        val userStatusQuery = Users
            .slice(Users.name, Users.age.case()
                .When(Users.age.less(25), stringLiteral("若手"))
                .When(Users.age.between(25, 35), stringLiteral("中堅"))
                .Else(stringLiteral("ベテラン")).alias("status"))
            .selectAll()
        
        println("\nユーザー年齢分類:")
        userStatusQuery.forEach { row ->
            println("${row[Users.name]}: ${row[userStatusQuery.slice.last()]}")
        }
        
        // ウィンドウ関数使用例(PostgreSQL等で利用可能)
        val rankedPosts = Posts
            .slice(Posts.title, Posts.createdAt, Posts.id.rank().over().orderBy(Posts.createdAt to SortOrder.DESC))
            .select { Posts.publishedAt.isNotNull() }
            .orderBy(Posts.createdAt to SortOrder.DESC)
        
        println("\n投稿ランキング:")
        rankedPosts.forEach { row ->
            val rank = row[rankedPosts.slice.last()]
            println("$rank位: ${row[Posts.title]}")
        }
    }
}

// 非同期トランザクション(コルーチンサポート)
suspend fun suspendedTransactionExample() = newSuspendedTransaction {
    // 非同期でのデータベース操作
    val users = User.all().toList()
    
    users.forEach { user ->
        // 非同期処理内でのデータベースアクセス
        val postCount = user.posts.count()
        println("${user.name}: $postCount 件の投稿")
    }
}

// トランザクション管理
fun transactionManagement() {
    // 基本トランザクション
    transaction {
        val user = User.new {
            name = "テストユーザー"
            email = "[email protected]"
            age = 30
        }
        
        Post.new {
            title = "テスト投稿"
            content = "これはテストです"
            author = user
            city = City.all().first()
            createdAt = "2025-06-24T10:00:00"
        }
    }
    
    // ネストしたトランザクション
    transaction {
        val user = User.new {
            name = "メインユーザー"
            email = "[email protected]"
            age = 35
        }
        
        try {
            transaction {
                // 内部トランザクション
                Post.new {
                    title = "内部トランザクション投稿"
                    content = "ネストしたトランザクション内で作成"
                    author = user
                    city = City.all().first()
                    createdAt = "2025-06-24T11:00:00"
                }
                
                // エラーが発生した場合、内部トランザクションはロールバック
                if (false) { // 条件によってエラーを発生
                    throw Exception("内部トランザクションエラー")
                }
            }
        } catch (e: Exception) {
            println("内部トランザクションでエラー: ${e.message}")
            // 外部トランザクションは継続
        }
        
        // 外部トランザクションの処理は継続
        println("ユーザー ${user.name} が作成されました")
    }
}

// 使用例
fun main() {
    Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
    
    basicCrudOperations()
    daoOperations()
    advancedQueries()
    transactionManagement()
    
    // 非同期例
    runBlocking {
        suspendedTransactionExample()
    }
}

Spring Boot統合

// Application.kt
@SpringBootApplication
class ExposedDemoApplication

fun main(args: Array<String>) {
    runApplication<ExposedDemoApplication>(*args)
}

// application.yml
/*
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
exposed:
  generate-ddl: true
*/

// UserService.kt
@Service
@Transactional
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[isActive] = true
            } get Users.id
        }
    }
    
    fun findUserById(id: Int): User? {
        return transaction {
            User.findById(id)
        }
    }
    
    fun getAllActiveUsers(): List<User> {
        return transaction {
            User.find { UsersTable.isActive eq true }.toList()
        }
    }
    
    fun updateUser(id: Int, name: String?, email: String?): Boolean {
        return transaction {
            val updated = Users.update({ Users.id eq id }) {
                name?.let { newName -> it[Users.name] = newName }
                email?.let { newEmail -> it[Users.email] = newEmail }
            }
            updated > 0
        }
    }
    
    fun deleteUser(id: Int): Boolean {
        return transaction {
            val deleted = Users.deleteWhere { Users.id eq id }
            deleted > 0
        }
    }
}

// UserController.kt
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
    
    @PostMapping
    fun createUser(@RequestBody request: CreateUserRequest): ResponseEntity<Map<String, Any>> {
        val userId = userService.createUser(request.name, request.email, request.age)
        return ResponseEntity.ok(mapOf("id" to userId, "message" to "User created successfully"))
    }
    
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Int): ResponseEntity<User?> {
        val user = userService.findUserById(id)
        return if (user != null) {
            ResponseEntity.ok(user)
        } else {
            ResponseEntity.notFound().build()
        }
    }
    
    @GetMapping
    fun getAllUsers(): List<User> {
        return userService.getAllActiveUsers()
    }
    
    @PutMapping("/{id}")
    fun updateUser(
        @PathVariable id: Int,
        @RequestBody request: UpdateUserRequest
    ): ResponseEntity<Map<String, String>> {
        val updated = userService.updateUser(id, request.name, request.email)
        return if (updated) {
            ResponseEntity.ok(mapOf("message" to "User updated successfully"))
        } else {
            ResponseEntity.notFound().build()
        }
    }
    
    @DeleteMapping("/{id}")
    fun deleteUser(@PathVariable id: Int): ResponseEntity<Map<String, String>> {
        val deleted = userService.deleteUser(id)
        return if (deleted) {
            ResponseEntity.ok(mapOf("message" to "User deleted successfully"))
        } else {
            ResponseEntity.notFound().build()
        }
    }
}

data class CreateUserRequest(val name: String, val email: String, val age: Int?)
data class UpdateUserRequest(val name: String?, val email: String?)