この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:3
ページ更新者:guest
更新日時:2026-06-11 07:07:02

タイトル: データベースの操作 (Android アプリケーション)
SEOタイトル: Android DB 操作完全ガイド(SQLite/Room/SQLDelight/Firestore)

この記事の要点
  • 昔: SQLiteOpenHelper を継承して生 SQL を書く方式
  • 今: Room (Jetpack) がデファクト。@Entity / @Dao / @Database
  • Coroutine / Flow と統合され、suspend fun / Flow> 返却が標準
  • KMP では SQLDelight がコンパイル時 SQL 検査で人気
  • クラウド同期は Firebase Realtime DB / Firestore。オフライン対応 + リアルタイム購読

Android のDB選択肢

方式用途現状
SQLiteOpenHelper(生 SQL)古いアプリの保守非推奨(直接利用は避ける)
Room (Jetpack)ローカル永続化のデファクト推奨
SQLDelightKMP(マルチプラットフォーム)KMP プロジェクト推奨
SharedPreferences / DataStoreキー・バリュー設定軽量設定用
Firebase Realtime DB / Firestoreクラウド同期 / リアルタイム同期が必要なときに

古いやり方: SQLiteOpenHelper

API レベル 1 から存在する旧来の方式。直接使うことはもう推奨されませんが、既存コードの保守で出会います。

class DBHelper(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,
                age INTEGER
            )
        """.trimIndent())
    }
    override fun onUpgrade(db: SQLiteDatabase, oldVer: Int, newVer: Int) {
        db.execSQL("DROP TABLE IF EXISTS users")
        onCreate(db)
    }
}

// 利用
val helper = DBHelper(context)
val db = helper.writableDatabase
val cv = ContentValues().apply {
    put("name", "taro")
    put("age", 30)
}
db.insert("users", null, cv)

val cursor = db.rawQuery("SELECT id, name, age FROM users", null)
while (cursor.moveToNext()) {
    val id = cursor.getInt(0)
    val name = cursor.getString(1)
}
cursor.close()

問題点: 文字列SQL、手動Cursor処理、メインスレッドブロックリスク。

Room の基本(推奨)

Gradle 依存追加

// build.gradle.kts (app)
dependencies {
    val roomVer = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVer")
    implementation("androidx.room:room-ktx:$roomVer")    // Coroutine 拡張
    ksp("androidx.room:room-compiler:$roomVer")           // または kapt
}

plugins {
    id("com.google.devtools.ksp")
}

Entity / DAO / Database

// Entity = テーブル
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String,
    val age: Int,
    val createdAt: Long = System.currentTimeMillis()
)

// DAO = データアクセス
@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY id DESC")
    fun observeAll(): Flow>      // Flow でリアクティブ購読

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun findById(id: Long): UserEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(user: UserEntity): Long

    @Update suspend fun update(user: UserEntity)
    @Delete suspend fun delete(user: UserEntity)

    @Query("DELETE FROM users")
    suspend fun clear()
}

// Database
@Database(entities = [UserEntity::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

// インスタンス化(DI 使うのが良い)
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
val dao = db.userDao()

Coroutine / Flow で使う

class UserRepository(private val dao: UserDao) {
    val users: Flow> = dao.observeAll()

    suspend fun add(name: String, age: Int) {
        dao.upsert(UserEntity(name = name, age = age))
    }
}

// ViewModel
class UserViewModel(repo: UserRepository) : ViewModel() {
    val users = repo.users.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    fun addUser(name: String, age: Int) = viewModelScope.launch {
        repo.add(name, age)
    }
}

// Compose 側
@Composable
fun UserScreen(vm: UserViewModel = hiltViewModel()) {
    val users by vm.users.collectAsState()
    LazyColumn { items(users) { Text(it.name) } }
}

マイグレーション

// version 1 → 2 で age を nullable に
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE users ADD COLUMN bio TEXT")
    }
}

Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db")
    .addMigrations(MIGRATION_1_2)
    .fallbackToDestructiveMigration()    // 開発中の最終手段(既存データ消去)
    .build()

TypeConverters(複合型のシリアライズ)

class Converters {
    @TypeConverter fun fromDate(d: Date?): Long? = d?.time
    @TypeConverter fun toDate(t: Long?): Date? = t?.let(::Date)

    @TypeConverter fun fromList(xs: List?): String? = xs?.joinToString("|")
    @TypeConverter fun toList(s: String?): List? = s?.split("|")
}

@Database(entities = [UserEntity::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }

初期データの投入(コールバック)

Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db")
    .addCallback(object : RoomDatabase.Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            db.execSQL("INSERT INTO users(name, age) VALUES ('admin', 30)")
            db.execSQL("INSERT INTO users(name, age) VALUES ('guest', 20)")
        }
    })
    .build()

SQLDelight(KMP)

iOS と共通コードでDB操作したい場合、SQLDelight が有力です。.sq ファイルに SQL を書き、コンパイル時に型安全なKotlin APIを生成します。

-- src/commonMain/sqldelight/com/example/User.sq
CREATE TABLE userEntity (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    age INTEGER NOT NULL
);

selectAll:
SELECT * FROM userEntity ORDER BY id DESC;

insertUser:
INSERT INTO userEntity(name, age) VALUES (?, ?);
// 生成された型安全API
val users: List = database.userQueries.selectAll().executeAsList()
database.userQueries.insertUser("taro", 30)

Firebase: Realtime Database / Firestore

// Firestore(NoSQL ドキュメントDB)
val db = Firebase.firestore

// 書き込み
db.collection("users").document("taro").set(mapOf(
    "name" to "taro",
    "age" to 30
))

// リアルタイム購読
db.collection("users")
    .addSnapshotListener { snap, err ->
        snap?.documents?.forEach { doc ->
            Log.d("Firestore", "${doc.id} => ${doc.data}")
        }
    }

// オフライン対応はデフォルト ON(端末キャッシュ)
選択肢強み弱み
Realtime DBシンプル、リアルタイム強いJSONツリーで設計難
Firestoreクエリ柔軟、スケール、サブコレクション料金体系が複雑
Room(ローカルのみ)同期不要なら最強クラウド同期は別途

FAQ

Q: SQLiteOpenHelper から Room へ移行したい
A: 既存スキーマを Room の Entity で再現し、createFromAsset や手動 Migration で取り込みます。バージョン継承に注意。

Q: メインスレッドで Room を呼ぶとクラッシュする
A: Room はメインスレッド呼び出しを禁止しています。suspend fun + viewModelScope or allowMainThreadQueries()(テストのみ)を使ってください。

Q: 暗号化したい
A: SQLCipher for Room 拡張で透過的に暗号化可能。鍵管理は AndroidKeyStore 推奨。