【Android】Room入門|SQLiteをシンプルに扱うデータベースライブラリ

開発
Screenshot

アプリにデータを保存したい。でもSQLiteを直接扱うのは面倒…。

そんな悩みを解決してくれるのが「Room」です。

RoomはGoogleが提供するJetpackライブラリの一つで、SQLiteをラップして、より簡単にデータベースを扱えるようにしてくれます。

この記事では、Roomの基本から実践的な使い方まで、初心者にもわかりやすく解説します。

Roomとは

RoomはAndroid Jetpackの一部で、SQLiteデータベースの抽象化レイヤーを提供するライブラリです。

従来のSQLiteの問題点

// ❌ 従来のSQLite:ボイラープレートが多い
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, "app.db", null, 1) {
    
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT NOT NULL
            )
        """)
    }
    
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.execSQL("DROP TABLE IF EXISTS users")
        onCreate(db)
    }
    
    fun insertUser(name: String, email: String): Long {
        val db = writableDatabase
        val values = ContentValues().apply {
            put("name", name)
            put("email", email)
        }
        return db.insert("users", null, values)
    }
    
    fun getAllUsers(): List<User> {
        val db = readableDatabase
        val cursor = db.query("users", null, null, null, null, null, null)
        val users = mutableListOf<User>()
        
        while (cursor.moveToNext()) {
            users.add(User(
                id = cursor.getLong(cursor.getColumnIndexOrThrow("id")),
                name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
                email = cursor.getString(cursor.getColumnIndexOrThrow("email"))
            ))
        }
        cursor.close()
        return users
    }
}

コードが長く、型安全でもありません。

Roomならシンプル

// ✅ Room:アノテーションでシンプルに
@Entity
data class User(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String,
    val email: String
)

@Dao
interface UserDao {
    @Insert
    suspend fun insert(user: User)
    
    @Query("SELECT * FROM User")
    suspend fun getAll(): List<User>
}

コード量が大幅に減り、型安全になりました。

Roomの3つのコンポーネント

コンポーネント役割
Entityテーブルを定義するデータクラス
DAOデータアクセスメソッドを定義するインターフェース
Databaseデータベース本体を定義する抽象クラス

それぞれ詳しく見ていきましょう。

1. Entity(エンティティ)

Entityはデータベースのテーブルを表すデータクラスです。

基本的なEntity

@Entity
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val email: String
)

これだけで、以下のテーブルが作成されます:

CREATE TABLE User (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL
)

テーブル名・カラム名をカスタマイズ

@Entity(tableName = "users")  // テーブル名を指定
data class User(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "user_id")  // カラム名を指定
    val id: Long = 0,
    
    @ColumnInfo(name = "user_name")
    val name: String,
    
    val email: String  // カラム名はそのまま "email"
)

NULLを許可する

@Entity
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val email: String?,  // Nullableにするとカラムもnull許可
    val age: Int? = null
)

インデックスを追加

@Entity(
    indices = [
        Index(value = ["email"], unique = true),  // ユニークインデックス
        Index(value = ["name"])  // 通常のインデックス
    ]
)
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val email: String
)

複合主キー

@Entity(primaryKeys = ["firstName", "lastName"])
data class User(
    val firstName: String,
    val lastName: String,
    val email: String
)

2. DAO(Data Access Object)

DAOはデータベース操作を定義するインターフェースです。

基本的なCRUD操作

@Dao
interface UserDao {
    
    // INSERT
    @Insert
    suspend fun insert(user: User)
    
    @Insert
    suspend fun insertAll(users: List<User>)
    
    // UPDATE
    @Update
    suspend fun update(user: User)
    
    // DELETE
    @Delete
    suspend fun delete(user: User)
    
    // SELECT(全件取得)
    @Query("SELECT * FROM User")
    suspend fun getAll(): List<User>
    
    // SELECT(条件付き)
    @Query("SELECT * FROM User WHERE id = :userId")
    suspend fun findById(userId: Long): User?
    
    // SELECT(複数条件)
    @Query("SELECT * FROM User WHERE name LIKE :name AND age > :minAge")
    suspend fun findByNameAndAge(name: String, minAge: Int): List<User>
    
    // DELETE(全件削除)
    @Query("DELETE FROM User")
    suspend fun deleteAll()
    
    // COUNT
    @Query("SELECT COUNT(*) FROM User")
    suspend fun getCount(): Int
}

Insert時のコンフリクト処理

@Dao
interface UserDao {
    
    // 既存データがあれば置き換え
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOrReplace(user: User)
    
    // 既存データがあれば無視
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertOrIgnore(user: User)
    
    // 既存データがあれば例外(デフォルト)
    @Insert(onConflict = OnConflictStrategy.ABORT)
    suspend fun insert(user: User)
}

Flowでリアルタイム監視

@Dao
interface UserDao {
    
    // データが更新されると自動で通知される
    @Query("SELECT * FROM User")
    fun observeAll(): Flow<List<User>>
    
    @Query("SELECT * FROM User WHERE id = :userId")
    fun observeById(userId: Long): Flow<User?>
}

ViewModelでの使用例:

class UserViewModel(private val userDao: UserDao) : ViewModel() {
    
    // Flowを収集してUIに反映
    val users: StateFlow<List<User>> = userDao.observeAll()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

トランザクション

@Dao
interface UserDao {
    
    @Insert
    suspend fun insert(user: User)
    
    @Query("DELETE FROM User")
    suspend fun deleteAll()
    
    // 複数の操作をトランザクションで実行
    @Transaction
    suspend fun replaceAll(users: List<User>) {
        deleteAll()
        users.forEach { insert(it) }
    }
}

3. Database(データベース)

Databaseはデータベース本体を定義する抽象クラスです。

基本的なDatabase定義

@Database(
    entities = [User::class],  // Entityクラスを指定
    version = 1                 // データベースバージョン
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao  // DAOを取得するメソッド
}

シングルトンで提供

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    
    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"  // データベースファイル名
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

複数のEntityを持つDatabase

@Database(
    entities = [
        User::class,
        Post::class,
        Comment::class
    ],
    version = 1
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun postDao(): PostDao
    abstract fun commentDao(): CommentDao
}

実践:Todoアプリを作る

実際にTodoアプリのデータ層を実装してみましょう。

1. Entity

@Entity(tableName = "todos")
data class Todo(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val title: String,
    val description: String? = null,
    val isCompleted: Boolean = false,
    val createdAt: Long = System.currentTimeMillis()
)

2. DAO

@Dao
interface TodoDao {
    
    @Query("SELECT * FROM todos ORDER BY createdAt DESC")
    fun observeAll(): Flow<List<Todo>>
    
    @Query("SELECT * FROM todos WHERE isCompleted = 0 ORDER BY createdAt DESC")
    fun observeIncomplete(): Flow<List<Todo>>
    
    @Query("SELECT * FROM todos WHERE id = :id")
    suspend fun findById(id: Long): Todo?
    
    @Insert
    suspend fun insert(todo: Todo): Long
    
    @Update
    suspend fun update(todo: Todo)
    
    @Delete
    suspend fun delete(todo: Todo)
    
    @Query("UPDATE todos SET isCompleted = :isCompleted WHERE id = :id")
    suspend fun updateCompletion(id: Long, isCompleted: Boolean)
    
    @Query("DELETE FROM todos WHERE isCompleted = 1")
    suspend fun deleteCompleted()
}

3. Database

@Database(entities = [Todo::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDao
    
    companion object {
        @Volatile
        private var INSTANCE: TodoDatabase? = null
        
        fun getInstance(context: Context): TodoDatabase {
            return INSTANCE ?: synchronized(this) {
                Room.databaseBuilder(
                    context.applicationContext,
                    TodoDatabase::class.java,
                    "todo_database"
                ).build().also { INSTANCE = it }
            }
        }
    }
}

4. Repository

class TodoRepository(private val todoDao: TodoDao) {
    
    val allTodos: Flow<List<Todo>> = todoDao.observeAll()
    
    val incompleteTodos: Flow<List<Todo>> = todoDao.observeIncomplete()
    
    suspend fun addTodo(title: String, description: String? = null): Long {
        val todo = Todo(title = title, description = description)
        return todoDao.insert(todo)
    }
    
    suspend fun toggleCompletion(todo: Todo) {
        todoDao.updateCompletion(todo.id, !todo.isCompleted)
    }
    
    suspend fun deleteTodo(todo: Todo) {
        todoDao.delete(todo)
    }
    
    suspend fun clearCompleted() {
        todoDao.deleteCompleted()
    }
}

5. ViewModel

class TodoViewModel(private val repository: TodoRepository) : ViewModel() {
    
    val todos: StateFlow<List<Todo>> = repository.allTodos
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
    
    fun addTodo(title: String) {
        viewModelScope.launch {
            repository.addTodo(title)
        }
    }
    
    fun toggleCompletion(todo: Todo) {
        viewModelScope.launch {
            repository.toggleCompletion(todo)
        }
    }
    
    fun deleteTodo(todo: Todo) {
        viewModelScope.launch {
            repository.deleteTodo(todo)
        }
    }
}

マイグレーション(スキーマ変更)

アプリをアップデートしてテーブル構造を変更する場合、マイグレーションが必要です。

カラムを追加する例

// Version 1
@Entity
data class User(
    @PrimaryKey val id: Long,
    val name: String
)

// Version 2: ageカラムを追加
@Entity
data class User(
    @PrimaryKey val id: Long,
    val name: String,
    val age: Int? = null  // 新しいカラム
)
// マイグレーションを定義
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER")
    }
}

// Databaseに適用
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .addMigrations(MIGRATION_1_2)
    .build()

開発中はfallbackToDestructiveMigration

開発中はマイグレーションを書くのが面倒なので、データを消して再作成することもできます。

Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .fallbackToDestructiveMigration()  // マイグレーション失敗時にDBを再作成
    .build()

⚠️ 本番環境では使わないでください! ユーザーのデータが消えます。

TypeConverter(カスタム型の保存)

RoomはプリミティブとString以外の型を直接保存できません。DateやListを保存するにはTypeConverterを使います。

Dateを保存する

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }
    
    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)  // Converterを登録
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Listを保存する(JSON変換)

class Converters {
    private val gson = Gson()
    
    @TypeConverter
    fun fromStringList(value: List<String>?): String? {
        return value?.let { gson.toJson(it) }
    }
    
    @TypeConverter
    fun toStringList(value: String?): List<String>? {
        return value?.let {
            gson.fromJson(it, object : TypeToken<List<String>>() {}.type)
        }
    }
}

Roomを使うための設定

build.gradle.kts

plugins {
    id("com.google.devtools.ksp") version "1.9.0-1.0.13"  // KSPを追加
}

dependencies {
    val roomVersion = "2.6.1"
    
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")  // Coroutines対応
    ksp("androidx.room:room-compiler:$roomVersion")  // KSPでコード生成
}

build.gradle(Groovy)

plugins {
    id 'com.google.devtools.ksp' version '1.9.0-1.0.13'
}

dependencies {
    def room_version = "2.6.1"
    
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    ksp "androidx.room:room-compiler:$room_version"
}

まとめ

Roomの基本をまとめると:

コンポーネント役割アノテーション
Entityテーブル定義@Entity, @PrimaryKey
DAOデータアクセス@Dao, @Query, @Insert, @Update, @Delete
DatabaseDB本体@Database

基本パターン

// 1. Entity
@Entity
data class User(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String
)

// 2. DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM User")
    fun observeAll(): Flow<List<User>>
    
    @Insert
    suspend fun insert(user: User)
}

// 3. Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Roomを使えば、SQLiteの煩雑な処理から解放され、型安全でメンテナンスしやすいコードが書けます。

ぜひ実際のプロジェクトで使ってみてください!


次回は「Flow入門」として、CoroutinesのFlowを使ったリアクティブプログラミングについて解説する予定です。

タイトルとURLをコピーしました