本文主要還是參考官方文件,然後以儲存搜尋歷史為例操作一波。
準備工作
Room
在SQLite
上提供了一個抽象層,以便在充分利用 SQLite 的強大功能的同時,能夠流暢地訪問資料庫。
依賴
如需在應用中使用 Room
,請將以下依賴項新增到應用的 build.gradle
檔案。
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
複製程式碼
主要元件
資料庫
:包含資料庫持有者,並作為應用已保留的持久關係型資料的底層連線的主要接入點。 使用@Database
註釋的類應滿足以下條件:- 是擴充套件
RoomDatabase
的抽象類。 - 在註釋中新增與資料庫關聯的實體列表。
- 包含具有 0 個引數且返回使用
@Dao
註釋的類的抽象方法。
Room.databaseBuilder()
或Room.inMemoryDatabaseBuilder()
獲取Database
的例項。- 是擴充套件
Entity
:表示資料庫中的表。DAO
:包含用於訪問資料庫的方法。
應用使用 Room 資料庫來獲取與該資料庫關聯的資料訪問物件 (DAO)。然後,應用使用每個 DAO 從資料庫中獲取實體,然後再將對這些實體的所有更改儲存回資料庫中。 最後,應用使用實體來獲取和設定與資料庫中的表列相對應的值。
關係如圖:
ok,基本概念瞭解之後,看一下具體是怎麼搞的。
Entity
@Entity(tableName = "t_history")
data class History(
/**
* @PrimaryKey主鍵,autoGenerate = true 自增
* @ColumnInfo 列 ,typeAffinity 欄位型別
* @Ignore 忽略
*/
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
val id: Int? = null,
@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
val name: String?,
@ColumnInfo(name = "insert_time", typeAffinity = ColumnInfo.TEXT)
val insertTime: String?,
@ColumnInfo(name = "type", typeAffinity = ColumnInfo.INTEGER)
val type: Int = 1
)
複製程式碼
- Entity物件對應一張表,使用
@Entity
註解,並宣告你的表名即可 @PrimaryKey
主鍵,autoGenerate = true
自增@ColumnInfo
列,並宣告列名 ,typeAffinity
欄位型別@Ignore
宣告忽略的物件
很簡單的一張表,主要是name
和insertTime
欄位。
DAO
@Dao
interface HistoryDao {
//按型別 查詢所有搜尋歷史
@Query("SELECT * FROM t_history WHERE type=:type")
fun getAll(type: Int = 1): Flow<List<History>>
@ExperimentalCoroutinesApi
fun getAllDistinctUntilChanged() = getAll().distinctUntilChanged()
//新增一條搜尋歷史
@Insert
fun insert(history: History)
//刪除一條搜尋歷史
@Delete
fun delete(history: History)
//更新一條搜尋歷史
@Update
fun update(history: History)
//根據id 刪除一條搜尋歷史
@Query("DELETE FROM t_history WHERE id = :id")
fun deleteByID(id: Int)
//刪除所有搜尋歷史
@Query("DELETE FROM t_history")
fun deleteAll()
}
複製程式碼
- @Insert:增
- @Delete:刪
- @Update:改
- @Query:查
這裡有一個點需要注意的,就是查詢所有搜尋歷史
返回的集合我用Flow
修飾了。
只要是資料庫中的任意一個數據有更新,無論是哪一行資料的更改,那就重新執行 query
操作並再次派發 Flow
。
同樣道理,如果一個不相關的資料更新時,Flow
也會被派發,會收到與之前相同的資料。
這是因為 SQLite 資料庫的內容更新通知功能是以表 (Table) 資料為單位,而不是以行 (Row) 資料為單位,因此只要是表中的資料有更新,它就觸發內容更新通知。Room 不知道表中有更新的資料是哪一個,因此它會重新觸發 DAO 中定義的 query 操作。您可以使用 Flow 的操作符,比如 distinctUntilChanged 來確保只有在當您關心的資料有更新時才會收到通知。
//按型別 查詢所有搜尋歷史
@Query("SELECT * FROM t_history WHERE type=:type")
fun getAll(type: Int = 1): Flow<List<History>>
@ExperimentalCoroutinesApi
fun getAllDistinctUntilChanged() = getAll().distinctUntilChanged()
複製程式碼
資料庫
@Database(entities = [History::class], version = 1)
abstract class HistoryDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
companion object {
private const val DATABASE_NAME = "history.db"
private lateinit var mPersonDatabase: HistoryDatabase
//注意:如果您的應用在單個程序中執行,在例項化 AppDatabase 物件時應遵循單例設計模式。
//每個 RoomDatabase 例項的成本相當高,而您幾乎不需要在單個程序中訪問多個例項
fun getInstance(context: Context): HistoryDatabase {
if (!this::mPersonDatabase.isInitialized) {
//建立的資料庫的例項
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).build()
}
return mPersonDatabase
}
}
}
複製程式碼
- 使用
@Database
註解宣告 entities
陣列,對應此資料庫中的所有表version
資料庫版本號
注意:
如果您的應用在單個程序中執行,在例項化 AppDatabase 物件時應遵循
單例設計模式
。 每個 RoomDatabase 例項的成本相當高,而您幾乎不需要在單個程序中訪問多個例項。
使用
在需要的地方獲取資料庫
mHistoryDao = HistoryDatabase.getInstance(this).historyDao()
複製程式碼
獲取搜尋歷史
private fun getSearchHistory() {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.getAll().collect {
withContext(Dispatchers.Main){
//更新ui
}
}
}
}
複製程式碼
collect
是Flow
獲取資料的方式,並不是唯一方式,可以檢視文件。
為什麼放在協程
裡面呢,因為資料庫的操作是費時的,而協程可以輕鬆的指定執行緒,這樣不阻塞UI
執行緒。
檢視Flow原始碼也發現,Flow是協程包下的
package kotlinx.coroutines.flow
複製程式碼
以collect為例,也是被suspend
修飾的,既然支援掛起
,那配合協程
豈不美哉。
@InternalCoroutinesApi
public suspend fun collect(collector: FlowCollector<T>)
複製程式碼
儲存搜尋記錄
private fun saveSearchHistory(text: String) {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.insert(History(null, text, DateUtils.longToString(System.currentTimeMillis())))
}
}
複製程式碼
清空本地歷史
private fun cleanHistory() {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.deleteAll()
}
}
複製程式碼
作者:yechaoa
資料庫升級
資料庫升級是一個重要的操作,畢竟可能會造成資料丟失,也是很嚴重的問題。
Room通過Migration
類來執行升級的操作,我們只要告訴Migration
類改了什麼就行,比如新增
欄位或表。
定義Migration類
/**
* 資料庫版本 1->2 t_history表格新增了updateTime列
*/
private val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")
}
}
/**
* 資料庫版本 2->3 新增label表
*/
private val MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")
}
}
複製程式碼
Migration
接收兩個引數:
- startVersion 舊版本
- endVersion 新版本
通知資料庫更新
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
複製程式碼
完整程式碼
@Database(entities = [History::class, Label::class], version = 3)
abstract class HistoryDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
companion object {
private const val DATABASE_NAME = "history.db"
private lateinit var mPersonDatabase: HistoryDatabase
fun getInstance(context: Context): HistoryDatabase {
if (!this::mPersonDatabase.isInitialized) {
//建立的資料庫的例項
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
}
return mPersonDatabase
}
/**
* 資料庫版本 1->2 t_history表格新增了updateTime列
*/
private val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")
}
}
/**
* 資料庫版本 2->3 新增label表
*/
private val MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")
}
}
}
}
複製程式碼
注意:
@Database
註解中版本號的更改,如果是新增表的話,entities
引數裡也要新增上。
建議升級操作順序
修改版本號 -> 新增Migration -> 新增給databaseBuilder
配置編譯器選項
Room 具有以下註解處理器選項:
room.schemaLocation
:配置並啟用將資料庫架構匯出到給定目錄中的 JSON 檔案的功能。如需瞭解詳情,請參閱 Room 遷移。room.incremental
:啟用 Gradle 增量註釋處理器。room.expandProjection
:配置 Room 以重寫查詢,使其頂部星形投影在展開後僅包含 DAO 方法返回型別中定義的列。
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true"]
}
}
}
}
複製程式碼
配置好之後,編譯執行,module資料夾下會生成一個schemas
資料夾,其下有一個json
檔案,裡面包含資料庫的基本資訊。
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "xxx",
"entities": [
{
"tableName": "t_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `insert_time` TEXT, `type` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "insertTime",
"columnName": "insert_time",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'xxx')"
]
}
}
複製程式碼
ok,基本使用講解完了,如果對你有用,點個讚唄 ^ _ ^