Room使用簡(jiǎn)介

http://tommwq.tech/blog/room%e4%bd%bf%e7%94%a8%e7%ae%80%e4%bb%8b/

  1. Entity
  2. Dao
  3. Database
  4. Room插件
  5. 常見問題
    1. DatabaseBuilder的callback未被調(diào)用
    2. Room檢查表結(jié)構(gòu)的方法

Room是Jetpack中的ORM組件。Room可以簡(jiǎn)化SQLite數(shù)據(jù)庫(kù)操作。Room包含3個(gè)主要的組件:

  • Entity。Entity是實(shí)體類,代表數(shù)據(jù)庫(kù)里的一張表。
  • DAO。DAO提供了訪問數(shù)據(jù)庫(kù)的接口,返回Entity或Entity集合。
  • Database。Database是Entity和DAO的集合,代表一個(gè)SQLite數(shù)據(jù)庫(kù)。Database是我們?cè)L問DAO和Entity的入口。

Entity

Entity是實(shí)體類,代表一個(gè)數(shù)據(jù)表。我們首先看一個(gè)簡(jiǎn)單的例子:

@Entity(tableName="users")
data class User (
    @PrimaryKey
    var uid: Int,
    @ColumnInfo(name = "first_name") 
    var firstName: String?,
    @ColumnInfo(name = "last_name") 
    var lastName: String?
    @Ignore
    var picture: Bitmap?
)
注解 說明
@Entity 聲明實(shí)體類。
@PrimaryKey 聲明主鍵。
@ColumnInfo 聲明字段在數(shù)據(jù)表中的屬性。
@Ignore 禁止將字段映射到數(shù)據(jù)表。

Room要求實(shí)體類必須擁有主鍵,且主鍵必須是Int或Long型。@ColumnInfo聲明了列名和域名的對(duì)照關(guān)系。如果列名和域名相同,可以省略這個(gè)注解。上面這個(gè)Entity對(duì)應(yīng)的SQL模式就是:

CREATE TABLE users (
    INT uid PRIMARY KEY,
    TEXT first_name,
    TEXT last_name
);

可以看到,從Entity到SQL的映射是非常直觀的。

實(shí)體類的域可以擁有默認(rèn)值。實(shí)體類除了作為數(shù)據(jù)容器之外,也可以具有行為。參考下面的例子:

@Entity(tableName = "plants")
data class Plant(
    @PrimaryKey @ColumnInfo(name = "id") 
    val plantId: String,
    val name: String,
    val description: String,
    val growZoneNumber: Int,
    val wateringInterval: Int = 7,
    val imageUrl: String = ""
) {
    fun shouldBeWatered(since: Calendar, lastWateringDate: Calendar) =
        since > lastWateringDate.apply { add(DAY_OF_YEAR, wateringInterval) }

    override fun toString() = name
}

Entity告訴Room如何在Java對(duì)象和SQL記錄之間進(jìn)行轉(zhuǎn)換。然而要從SQLite數(shù)據(jù)庫(kù)中得到Java對(duì)象,我們還需要Dao。

Dao

還是從例子入手。

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

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): User

    @Insert
    suspend fun insertAll(vararg users: User)

    @Delete
    suspend fun delete(user: User)
}
注解 說明
@Dao 聲明接口是DAO。
@Query 將SQL查詢語句映射為Java方法。
@Insert 將SQL插入語句映射為Java方法。
@Delete 將SQL刪除語句映射為Java方法。

從例子里可以看出,DAO和Entity有兩個(gè)區(qū)別,首先Entity是類,而DAO是接口。其次,Entity將Java對(duì)象映射為SQL記錄,將域映射為數(shù)據(jù)表中的列;DAO將SQL語句映射為Java方法。我們不需要手動(dòng)編寫這些方法,Room會(huì)自動(dòng)生成它們。

數(shù)據(jù)庫(kù)查詢會(huì)引發(fā)磁盤IO,這是一個(gè)耗時(shí)操作。為了避免ANR,需要將數(shù)據(jù)庫(kù)查詢放到后臺(tái)線程里執(zhí)行。很多時(shí)候我們需要根據(jù)查詢結(jié)果來更新界面,而界面必須在主線程中修改。那么如何將后臺(tái)線程查詢出的數(shù)據(jù)傳遞給主線程呢?你可以自己編寫Handler,更簡(jiǎn)單的辦法是讓查詢方法返回LiveData。

@Dao
interface PlantDao {
    @Query("SELECT * FROM plants ORDER BY name")
    fun getPlants(): LiveData<List<Plant>>

    @Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
    fun getPlantsWithGrowZoneNumber(growZoneNumber: Int): LiveData<List<Plant>>

    @Query("SELECT * FROM plants WHERE id = :plantId")
    fun getPlant(plantId: String): LiveData<Plant>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(plants: List<Plant>)
}

這里簡(jiǎn)單介紹一下LiveData。LiveData是一個(gè)為更新界面而定制的Observable,它將后臺(tái)線程的數(shù)據(jù)投遞到主線程。為了避免過度渲染,LiveData只在Activity或Fragment活躍的時(shí)候才投遞數(shù)據(jù)。

如果使用kotlin進(jìn)行開發(fā),可以將DAO方法聲明為suspend,配合viewModelScope使用。

Database

Database是Entity和DAO的集合,也是訪問Entity和DAO的入口。Database是一個(gè)抽象類,每個(gè)DAO由一個(gè)抽閑方法返回。

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

此外Entity必須在@Database中進(jìn)行注冊(cè)。

實(shí)際的Database類也是由Room生成的。通過Room.databaseBuilder可以構(gòu)造Database類。

val db = Room.databaseBuilder(
    applicationContext,
    AppDatabase::class.java,
    "database-name"
).build()

綜合起來,Room的用法可以總結(jié)為:

  • 用Entity封裝數(shù)據(jù)記錄,用DAO映射查詢語句。
  • 通過databaseBuilder得到Database,通過Database得到DAO,通過DAO管理Entity。

Room插件

Room會(huì)自動(dòng)生成類,這個(gè)動(dòng)作是在編譯期完成的,因?yàn)槲覀冃枰刖幾g插件。

apply plugin: 'kotlin-kapt'

dependencies {
    def room_version = "2.1.0-alpha04"
    kapt "android.arch.persistence.room:compiler:$room_version"
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0-alpha'
   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0-alpha'

   implementation 'androidx.room:room-runtime:2.1.0-alpha06'
   kapt 'androidx.room:room-compiler:2.1.0-alpha06'
   implementation 'androidx.room:room-ktx:2.1.0-alpha06'
}

常見問題

DatabaseBuilder的callback未被調(diào)用

Room底層使用了SQLiteOpenHelper,只有當(dāng)數(shù)據(jù)庫(kù)被實(shí)際使用時(shí),數(shù)據(jù)庫(kù)才會(huì)被建立,回調(diào)函數(shù)才被調(diào)用。如果要手動(dòng)調(diào)用callback,可以執(zhí)行

// and then
db.beginTransaction()
db.endTransaction()

// or query a dummy select statement
db.query("select 1", null)
return db

Room檢查表結(jié)構(gòu)的方法

Room的createFromAsset使用PRAGMA tableinfo('tbl')來得到表的結(jié)構(gòu),并生成TableInfo實(shí)例。將這個(gè)實(shí)例和由Entity類生成的TableInfo進(jìn)行比對(duì),如果不一致,拋出IllegalStateException異常。

"Migration didn't properly handle XXX

tableinfo為每個(gè)列生成一行,記錄了列的編號(hào)、名字、數(shù)據(jù)類型、是否可為NULL、默認(rèn)值、列在主鍵中的順序。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容