Exposed
ExposedはJetBrains開発のKotlin優先ORMフレームワークで、「SQL DSLファーストアプローチ」による型安全なデータベースアクセスを実現します。簡潔な構文と最新Kotlin機能の完全統合により、Kotlinx datetime対応、コルーチンサポート、直感的なクエリ構築を特徴とします。2025年現在、Kotlin ORM最有力候補として開発者の60%が軽量ソリューションとして選択し、新規プロジェクトで推奨される第一選択肢となっています。
GitHub概要
JetBrains/Exposed
Kotlin SQL Framework
スター8,855
ウォッチ130
フォーク738
作成日:2013年7月30日
言語:Kotlin
ライセンス:Apache License 2.0
トピックス
daokotlinormsql
スター履歴
データ取得日時: 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?)