Android Room Local Database (本地資料庫)
在建構現代化的 Android 應用程式時,結構化本地儲存 (Ordered Local Storage) 是提供離線功能、快取資料以及提升使用者體驗的關鍵。SQLite 雖然是 Android 內建的資料庫解決方案,但直接撰寫 SQL 語法不僅繁瑣,且缺乏編譯時期的安全性檢查。
Room 是 Google 官方提供的 ORM (Object Relational Mapping) 函式庫,它在 SQLite 之上建構了一個抽象層,讓開發者能以 Kotlin 物件的開發思維來操作資料庫,並在編譯階段自動檢查 SQL 語法。
Room 三大核心組件
- Entity (實體):定義資料表 (Table) 的結構,每個類別代表一個 Table,每個實例代表一筆 Record。
- DAO (Data Access Object):負責提供讀取與操作資料的方法,如查詢、新增、刪除。
- Database:資料庫持有者,作為管理中心連接 Entity 與 DAO 並建立資料庫實例。
基礎實作範例
1. 定義 Entity
我們使用 @Entity 標記類別,並指定主鍵 (Primary Key)。
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "full_name")
val name: String,
val age: Int,
// 記錄這筆資料的建立日期
val createdAt: Long = System.currentTimeMillis()
)
2. 定義 DAO
DAO 可以定義成 Interface。Room 支持回傳 Flow,這意味著當資料庫內容變動時,UI 會自動接收到最新資料。
@Dao
interface UserDao {
// 取得所有使用者,回傳 Flow 以監聽即時更新
@Query("SELECT * FROM users ORDER BY full_name ASC")
fun getAllUsers(): Flow<List<UserEntity>>
// 根據 ID 查詢,使用 suspend 進行單次異步查詢
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): UserEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: UserEntity)
@Delete
suspend fun deleteUser(user: UserEntity)
}
3. 定義 Database
// 指定包含的 Entity、版本號以及是否需要自動遷移
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
TypeConverters 處理複雜資料
Room 預設僅支援基礎型別。如果你需要儲存 Date 或自定義的 Object,則需要轉換器。
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
// 在 Database 類別上加上標註
@Database(...)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
進階資料表關聯 (Relationships)
Room 不允許實體之間的直接引用(避免遞迴查詢問題),而是透過 @Relation 註解來連結。
一對一 (One-to-One)
假設一個「使用者」擁有一個「個人檔案」。
data class UserAndLibrary(
@Embedded val user: UserEntity,
@Relation(
parentColumn = "id", // User 的 PK
entityColumn = "ownerId" // Profile 的 FK
)
val profile: ProfileEntity
)
一對多 (One-to-Many)
一個「使用者」擁有多個「播放清單」。
data class UserWithPlaylists(
@Embedded val user: UserEntity,
@Relation(
parentColumn = "id",
entityColumn = "userOwnerId"
)
val playlists: List<PlaylistEntity>
)
多對多 (Many-to-Many)
「歌曲」與「播放清單」的關係,通常需要一個中間表 (Cross-reference table)。
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long
)
data class PlaylistWithSongs(
@Embedded val playlist: PlaylistEntity,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<SongEntity>
)
資料庫遷移 (Migrations)
當你修改 Entity(例如新增欄位)時,必須升級資料庫版本。
自動遷移 (Auto Migration - 推薦)
適用於簡單的新增欄位。
@Database(
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
],
...
)
手動遷移 (Manual Migration)
適用於複雜變更(如 Table 重新命名、資料型態轉換)。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 使用原生 SQL 執行結構變更
database.execSQL("ALTER TABLE users ADD COLUMN email TEXT DEFAULT '' NOT NULL")
}
}
與 Hilt 整合 (Dependency Injection)
在專案中使用 Hilt 注入資料庫與 DAO 是目前 Android 開發的最佳實踐。
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"my_app.db"
)
// 將手動遷移加入建置流程
.addMigrations(MIGRATION_1_2)
.build()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}
資料庫測試 (Testing Room)
測試資料庫時,為了避免污染真實數據,我們通常使用 In-memory database,這會在進程結束後自動清除。
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var db: AppDatabase
private lateinit var dao: UserDao
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
// 建立內存資料庫
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
dao = db.userDao()
}
@After
fun closeDb() {
db.close()
}
@Test
fun writeUserAndReadInList() = runTest {
val user = UserEntity(id = 1, name = "Mike", age = 25)
dao.insertUser(user)
val result = dao.getUserById(1)
assertEquals(result?.name, "Mike")
}
}
透過 Room,我們可以大幅減少處理資料持久化時的程式碼量,並透過強大的編譯檢查與關聯支援,打造更穩健的 Android 應用程式。