http://tommwq.tech/blog/room%e4%bd%bf%e7%94%a8%e7%ae%80%e4%bb%8b/
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)值、列在主鍵中的順序。