Android Room Database 2026完全ガイド:マイグレーション、リレーション、Coroutines連携

Room Databaseのマイグレーション、エンティティリレーション、Kotlin CoroutinesおよびFlowとの統合について詳しく解説します。2026年の本番環境対応パターンを含む実践的なチュートリアルです。

Room Database Android Kotlin SQLiteデータ永続化

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に置き換わり、より高速なビルド時間を実現しています。

build.gradle.kts (Module :app)kotlin
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つのプライマリキーが必要です。頻繁にクエリされるカラムにインデックスを追加すると、書き込みが若干遅くなる代わりに読み取りパフォーマンスが向上します。

User.ktkotlin
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です。

UserDao.ktkotlin
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<User>)

    // Flow emits new data whenever the table changes
    @Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC")
    fun observeActiveUsers(): Flow<List<User>>

    // 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を結び付けます。シングルトンパターンにより複数のデータベースインスタンスを防止し、リソースリークを回避します。

AppDatabase.ktkotlin
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文を定義します。

Migrations.ktkotlin
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は自動的にマイグレーションを生成できます。これにより単純な更新のボイラープレートが削減されます。

AppDatabase.ktkotlin
@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を提供します。

Androidの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

埋め込みオブジェクトによるエンティティリレーションのモデリング

Roomは3つのリレーションパターンをサポートしています:埋め込みオブジェクト、一対多、多対多です。ORMとは異なり、Roomはフラットなデータを返します—リレーションには明示的なクエリが必要です。

一対多:ユーザーと投稿

ユーザーは複数の投稿を持つことができます。両方のエンティティを組み合わせたデータクラスで@Relationアノテーションを使用してリレーションを定義します。

Post.ktkotlin
@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<Post>
)

// 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<List<UserWithPosts>>

@Transactionアノテーションは、内部的にリレーションが複数のクエリにまたがる場合の不整合な読み取りを防止します。

多対多:ユーザーとタグ

多対多リレーションには、外部キーのペアを格納するジャンクション(クロスリファレンス)テーブルが必要です。

Tag.ktkotlin
@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<Tag>
)

associateByパラメータは、ユーザーとタグをリンクするジャンクションテーブルを指定します。

RoomとViewModelおよびRepositoryの統合

クリーンアーキテクチャはデータベースレイヤーをUIから分離します。Repositoryパターンはデータソースを抽象化し、ViewModelはCompose UIにステートを公開します。

UserRepository.ktkotlin
class UserRepository(private val userDao: UserDao) {

    val activeUsers: Flow<List<User>> = 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<List<User>> = 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のガイドを参照してください。

Room Databaseのテスト

Roomはテスト用にインメモリデータベースビルダーRoom.inMemoryDatabaseBuilder()を提供しています。テストはより高速に実行され、実行間でデータは保持されません。マイグレーションは必ずroom-testingアーティファクトのMigrationTestHelperでテストしてください。

複合型のための型コンバーター

Roomはプリミティブ型のみをネイティブに格納します。型コンバーターは複合型(日付、列挙型、リスト)を格納可能な形式に変換します。

Converters.ktkotlin
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<String> <-> JSON string
    @TypeConverter
    fun fromStringList(list: List<String>?): String? =
        list?.joinToString(separator = ",")

    @TypeConverter
    fun toStringList(data: String?): List<String>? =
        data?.split(",")?.filter { it.isNotBlank() }
}

// Register in database class
@Database(...)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

複合オブジェクトのJSONシリアライゼーションには、手動の文字列操作の代わりにkotlinx.serializationの使用を検討してください。

Coroutinesスコープでのデータベース操作の処理

Room DAO操作はIOディスパッチャーで実行する必要があります。Roomはsuspend関数のスレッディングを内部的に処理しますが、リポジトリレベルのエラーハンドリングによりコードの堅牢性が維持されます。

UserRepository.kt with error handlingkotlin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class UserRepository(private val userDao: UserDao) {

    suspend fun createUser(email: String, displayName: String): Result<Long> {
        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<T>でラップすることで、ViewModelがtry-catchボイラープレートなしでエラーを適切に処理できます。

まとめ

本番環境対応のRoom Databaseレイヤーを構築するには、いくつかの重要なプラクティスが含まれます:

  • Room 2.6でより高速なコンパイルのためにKAPTの代わりにKSPを使用する
  • WHERE句とJOIN条件で使用されるカラムにインデックスを追加する
  • データ変更時の自動UI更新のためにDAOからFlowを返す
  • スキーマ変更には明示的なマイグレーションを作成する—本番環境では破壊的マイグレーションを使用しない
  • 一貫した読み取りを確保するためにリレーションクエリには@Transactionを使用する
  • ViewModelでのクリーンなエラーハンドリングのためにデータベース操作をResultでラップする
  • アップデートをリリースする前にMigrationTestHelperでマイグレーションをテストする

これらのパターンをRoom Databaseの面接質問で練習して、概念を強化してください。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

フルスタック開発者、SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年7月10日 更新

タグ

#android
#room
#kotlin
#sqlite
#coroutines
#database

共有

関連記事