タイトル: データベースの操作 (Android アプリケーション)
SEOタイトル: Android DB 操作完全ガイド(SQLite/Room/SQLDelight/Firestore)
| この記事の要点 |
|
Android のDB選択肢
| 方式 | 用途 | 現状 |
|---|---|---|
| SQLiteOpenHelper(生 SQL) | 古いアプリの保守 | 非推奨(直接利用は避ける) |
| Room (Jetpack) | ローカル永続化のデファクト | 推奨 |
| SQLDelight | KMP(マルチプラットフォーム) | 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 推奨。