# Android Room Database 2026完全ガイド:マイグレーション、リレーション、Coroutines連携 > Room Databaseのマイグレーション、エンティティリレーション、Kotlin CoroutinesおよびFlowとの統合について詳しく解説します。2026年の本番環境対応パターンを含む実践的なチュートリアルです。 - Published: 2026-07-10 - Updated: 2026-07-10 - Author: Anthony Fillion-Maillet - Tags: android, room, kotlin, sqlite, coroutines, database - Reading time: 12 min --- Room DatabaseはAndroidにおけるSQLiteの堅牢な抽象化レイヤーを提供し、生のSQLiteを扱う際の冗長なボイラープレートコードを排除します。Room 2.6とKotlin Coroutinesの組み合わせにより、リアクティブで型安全なデータベースレイヤーの構築が大幅に簡素化されました。 > **Room 2.6の主要機能** > > Room 2.6(2026年安定版)では、マイグレーションサポートの改善、KSP処理速度の向上、Flowとの統合強化が導入されています。このチュートリアルでは、本番環境対応のコードを作成するためにこれらの最新APIを使用します。 ## Kotlin KSPを使用したRoom Databaseのセットアップ Roomには3つのコアコンポーネントが必要です:エンティティ(テーブル)、DAO(データアクセスオブジェクト)、およびデータベースクラスです。KSPアノテーションプロセッサがコンパイル時に実装コードを生成します。 モジュールレベルの`build.gradle.kts`に依存関係を追加します。KSPは2024年以降、推奨されるアノテーションプロセッサとしてKAPTに置き換わり、より高速なビルド時間を実現しています。 ```kotlin // build.gradle.kts (Module :app) plugins { id("com.google.devtools.ksp") version "2.1.0-1.0.29" } dependencies { val roomVersion = "2.6.1" implementation("androidx.room:room-runtime:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") // Coroutines support ksp("androidx.room:room-compiler:$roomVersion") } ``` `room-ktx`アーティファクトはCoroutine拡張機能を提供し、DAOでsuspend関数の使用を可能にします。 ## プライマリキーとインデックスを含むエンティティの定義 エンティティはSQLiteテーブルに直接マッピングされます。各エンティティクラスには少なくとも1つのプライマリキーが必要です。頻繁にクエリされるカラムにインデックスを追加すると、書き込みが若干遅くなる代わりに読み取りパフォーマンスが向上します。 ```kotlin // User.kt import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey @Entity( tableName = "users", indices = [ Index(value = ["email"], unique = true), // Unique constraint + index Index(value = ["created_at"]) // Index for sorting queries ] ) data class User( @PrimaryKey(autoGenerate = true) val id: Long = 0, val email: String, val displayName: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), val isActive: Boolean = true ) ``` `@ColumnInfo`アノテーションは、Kotlinプロパティ名と目的のデータベースカラム名が異なる場合にカラム名をカスタマイズします。 ## CoroutinesとFlowを使用したDAOの構築 DAOはデータベース操作を定義します。Room 2.6は3つの非同期パターンをサポートしています:ワンショット操作用のsuspend関数、リアクティブストリーム用の`Flow`、Java相互運用用の`ListenableFuture`です。 ```kotlin // UserDao.kt import androidx.room.* import kotlinx.coroutines.flow.Flow @Dao interface UserDao { // One-shot insert - returns the generated ID @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser(user: User): Long // Batch insert - efficient for multiple records @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertUsers(users: List) // Flow emits new data whenever the table changes @Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC") fun observeActiveUsers(): Flow> // Suspend function for one-shot read @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserById(userId: Long): User? // Update returns the number of affected rows @Update suspend fun updateUser(user: User): Int // Delete by primary key @Delete suspend fun deleteUser(user: User) // Custom delete query @Query("DELETE FROM users WHERE is_active = 0") suspend fun deleteInactiveUsers(): Int } ``` Flowを返すクエリは、基礎となるデータが変更されると自動的に更新された結果を発行します。これによりViewModelでの手動更新ロジックが不要になります。 ## Roomデータベースクラスの作成 データベースクラスはエンティティとDAOを結び付けます。シングルトンパターンにより複数のデータベースインスタンスを防止し、リソースリークを回避します。 ```kotlin // AppDatabase.kt import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @Database( entities = [User::class, Post::class], version = 1, exportSchema = true // Generates schema JSON for migration validation ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun postDao(): PostDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ) .fallbackToDestructiveMigration() // Only for development .build() INSTANCE = instance instance } } } } ``` `exportSchema = true`を設定すると、`schemas/`フォルダにJSONスキーマファイルが生成され、テストでのマイグレーション検証に役立ちます。 > **本番環境でのマイグレーション戦略** > > 本番環境では絶対に`fallbackToDestructiveMigration()`を使用しないでください。これはスキーマが変更されると全てのユーザーデータを削除します。次のセクションで適切なマイグレーション処理について説明します。 ## 安全なデータベースマイグレーションの作成 マイグレーションは、アプリバージョン間でスキーマが変更された際にユーザーデータを保持します。Roomは期待されるスキーマとマイグレーション後のデータベースを比較することで、実行時にマイグレーションを検証します。 マイグレーションは、データベースをバージョンNからバージョンN+1に変換するために必要なSQL文を定義します。 ```kotlin // Migrations.kt import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase // Migration from version 1 to 2: add phone_number column val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT") } } // Migration from version 2 to 3: add posts table with foreign key val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(""" CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """.trimIndent()) db.execSQL("CREATE INDEX index_posts_user_id ON posts(user_id)") } } // Register migrations in database builder fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ) .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() .also { INSTANCE = it } } } ``` Roomはマイグレーションチェーンをサポートしています。ユーザーがバージョン1からバージョン3にアップグレードする場合、RoomはMIGRATION_1_2、次にMIGRATION_2_3を順番に実行します。 ## Room 2.6の自動マイグレーション 単純なスキーマ変更(カラム、テーブル、インデックスの追加)の場合、Roomは自動的にマイグレーションを生成できます。これにより単純な更新のボイラープレートが削減されます。 ```kotlin // AppDatabase.kt @Database( entities = [User::class, Post::class], version = 3, autoMigrations = [ AutoMigration(from = 1, to = 2), // Adding nullable column AutoMigration( from = 2, to = 3, spec = AutoMigration2To3::class // Complex changes need spec ) ], exportSchema = true ) abstract class AppDatabase : RoomDatabase() { // DAOs... } // Migration spec for changes that need hints @RenameColumn(tableName = "users", fromColumnName = "name", toColumnName = "display_name") class AutoMigration2To3 : AutoMigrationSpec ``` 自動マイグレーションはバージョン比較のために`exportSchema = true`が必要です。カラム名変更、テーブル名変更、削除の場合は、適切なアノテーションを持つ`AutoMigrationSpec`を提供します。 ## 埋め込みオブジェクトによるエンティティリレーションのモデリング Roomは3つのリレーションパターンをサポートしています:埋め込みオブジェクト、一対多、多対多です。ORMとは異なり、Roomはフラットなデータを返します—リレーションには明示的なクエリが必要です。 ### 一対多:ユーザーと投稿 ユーザーは複数の投稿を持つことができます。両方のエンティティを組み合わせたデータクラスで`@Relation`アノテーションを使用してリレーションを定義します。 ```kotlin // Post.kt @Entity( tableName = "posts", foreignKeys = [ ForeignKey( entity = User::class, parentColumns = ["id"], childColumns = ["user_id"], onDelete = ForeignKey.CASCADE // Delete posts when user deleted ) ], indices = [Index("user_id")] // Required for foreign key performance ) data class Post( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(name = "user_id") val userId: Long, val title: String, val content: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis() ) // UserWithPosts.kt - Relation container data class UserWithPosts( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "user_id" ) val posts: List ) // In UserDao.kt @Transaction // Ensures atomic read of user + posts @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserWithPosts(userId: Long): UserWithPosts? @Transaction @Query("SELECT * FROM users WHERE is_active = 1") fun observeActiveUsersWithPosts(): Flow> ``` `@Transaction`アノテーションは、内部的にリレーションが複数のクエリにまたがる場合の不整合な読み取りを防止します。 ### 多対多:ユーザーとタグ 多対多リレーションには、外部キーのペアを格納するジャンクション(クロスリファレンス)テーブルが必要です。 ```kotlin // Tag.kt @Entity(tableName = "tags") data class Tag( @PrimaryKey(autoGenerate = true) val tagId: Long = 0, val name: String ) // UserTagCrossRef.kt - Junction table @Entity( tableName = "user_tag_cross_ref", primaryKeys = ["userId", "tagId"], foreignKeys = [ ForeignKey(entity = User::class, parentColumns = ["id"], childColumns = ["userId"], onDelete = ForeignKey.CASCADE), ForeignKey(entity = Tag::class, parentColumns = ["tagId"], childColumns = ["tagId"], onDelete = ForeignKey.CASCADE) ] ) data class UserTagCrossRef( val userId: Long, val tagId: Long ) // UserWithTags.kt data class UserWithTags( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "tagId", associateBy = Junction(UserTagCrossRef::class) ) val tags: List ) ``` `associateBy`パラメータは、ユーザーとタグをリンクするジャンクションテーブルを指定します。 ## RoomとViewModelおよびRepositoryの統合 クリーンアーキテクチャはデータベースレイヤーをUIから分離します。Repositoryパターンはデータソースを抽象化し、ViewModelはCompose UIにステートを公開します。 ```kotlin // UserRepository.kt class UserRepository(private val userDao: UserDao) { val activeUsers: Flow> = userDao.observeActiveUsers() suspend fun createUser(email: String, displayName: String): Long { val user = User(email = email, displayName = displayName) return userDao.insertUser(user) } suspend fun deactivateUser(userId: Long) { userDao.getUserById(userId)?.let { user -> userDao.updateUser(user.copy(isActive = false)) } } } // UserViewModel.kt class UserViewModel( private val repository: UserRepository ) : ViewModel() { val users: StateFlow> = repository.activeUsers .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun createUser(email: String, name: String) { viewModelScope.launch { repository.createUser(email, name) // No manual refresh needed - Flow emits automatically } } } ``` `stateIn`オペレーターは、RoomからのコールドFlowをCompose収集に適したホットStateFlowに変換します。Coroutinesパターンの詳細については、[Kotlin Coroutines](/blog/android/mastering-kotlin-coroutines)のガイドを参照してください。 > **Room Databaseのテスト** > > Roomはテスト用にインメモリデータベースビルダー`Room.inMemoryDatabaseBuilder()`を提供しています。テストはより高速に実行され、実行間でデータは保持されません。マイグレーションは必ず`room-testing`アーティファクトの`MigrationTestHelper`でテストしてください。 ## 複合型のための型コンバーター Roomはプリミティブ型のみをネイティブに格納します。型コンバーターは複合型(日付、列挙型、リスト)を格納可能な形式に変換します。 ```kotlin // Converters.kt import androidx.room.TypeConverter import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset class Converters { // Instant <-> Long (milliseconds) @TypeConverter fun fromInstant(instant: Instant?): Long? = instant?.toEpochMilli() @TypeConverter fun toInstant(millis: Long?): Instant? = millis?.let { Instant.ofEpochMilli(it) } // LocalDate <-> String (ISO format) @TypeConverter fun fromLocalDate(date: LocalDate?): String? = date?.toString() @TypeConverter fun toLocalDate(dateStr: String?): LocalDate? = dateStr?.let { LocalDate.parse(it) } // List <-> JSON string @TypeConverter fun fromStringList(list: List?): String? = list?.joinToString(separator = ",") @TypeConverter fun toStringList(data: String?): List? = data?.split(",")?.filter { it.isNotBlank() } } // Register in database class @Database(...) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() ``` 複合オブジェクトのJSONシリアライゼーションには、手動の文字列操作の代わりに[kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization)の使用を検討してください。 ## Coroutinesスコープでのデータベース操作の処理 Room DAO操作はIOディスパッチャーで実行する必要があります。Roomはsuspend関数のスレッディングを内部的に処理しますが、リポジトリレベルのエラーハンドリングによりコードの堅牢性が維持されます。 ```kotlin // UserRepository.kt with error handling import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class UserRepository(private val userDao: UserDao) { suspend fun createUser(email: String, displayName: String): Result { return withContext(Dispatchers.IO) { try { val id = userDao.insertUser( User(email = email, displayName = displayName) ) Result.success(id) } catch (e: SQLiteConstraintException) { // Unique constraint violation (duplicate email) Result.failure(DuplicateEmailException(email)) } } } suspend fun deleteAllInactive(): Int = withContext(Dispatchers.IO) { userDao.deleteInactiveUsers() } } class DuplicateEmailException(email: String) : Exception("Email already exists: $email") ``` 結果を`Result`でラップすることで、ViewModelがtry-catchボイラープレートなしでエラーを適切に処理できます。 ## まとめ 本番環境対応のRoom Databaseレイヤーを構築するには、いくつかの重要なプラクティスが含まれます: - Room 2.6でより高速なコンパイルのためにKAPTの代わりにKSPを使用する - WHERE句とJOIN条件で使用されるカラムにインデックスを追加する - データ変更時の自動UI更新のためにDAOからFlowを返す - スキーマ変更には明示的なマイグレーションを作成する—本番環境では破壊的マイグレーションを使用しない - 一貫した読み取りを確保するためにリレーションクエリには`@Transaction`を使用する - ViewModelでのクリーンなエラーハンドリングのためにデータベース操作をResultでラップする - アップデートをリリースする前に`MigrationTestHelper`でマイグレーションをテストする これらのパターンを[Room Databaseの面接質問](/technologies/android/interview-questions/android-room-database)で練習して、概念を強化してください。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/android/room-database-android-2026-migrations-relations-coroutines