Android架構師必學——Jetpack Room數據庫應用與源碼解析

語言: CN / TW / HK

開啟掘金成長之旅!這是我參與「掘金日新計劃 · 12 月更文挑戰」的第24天,點擊查看活動詳情

Room是Google官方在SQLite基礎上封裝的一款數據持久庫,是Jetpack全家桶的一員,和Jetpack其他庫有着可以高度搭配協調的天然優勢。Room使用APT技術,大大簡化了使用SQLite的代碼量,只需使用註解配合少量代碼即可實現高效的數據庫操作。

Room介紹

  1. Room是一個OM(Object Mapping對象映射)數據庫,可以方便地在Android應用程序上訪問數據庫。
  2. Room抽象了SQLite,通過提供方便的api來查詢數據庫,並在編譯時驗證。並且可以使用SQLite的全部功能,同時擁有Java SQL查詢生成器提供的類型安全。

Room的構成

  1. Database:數據庫擴展了RoomDatabase的抽象類。可以通過Room獲得它的一個實例。databaseBuilder或Room.inMemoryDatabaseBuilder。
  2. Entity:代表一個表結構。
  3. Dao:數據訪問對象是Room的主要組件,負責定義訪問數據庫的方法。

優點:

  • Google官方庫,和Jetpack其他庫(比如Lifecycle,LiveData)等有天然的融合搭配使用優勢。
  • 在編譯器可以對SQL語法進行檢查。
  • 使用APT,簡單地幾個註解搭配少量代碼即可使用,大量減少模板代碼。
  • 查詢代碼自定義,可以實現更復雜的查詢功能,也可以對SQL語句進行優化。
  • 簡化數據庫的遷移路徑。

不足:

  • 查詢時必須手動寫SQL語句,不提供默認的查詢配置。
  • 效率比其他數據庫框架(GreenDao等)並沒有多少提高。
  • 數據庫版本升級稍顯複雜。

一、基本介紹

框架特點

相對於SQLiteOpenHelper等傳統方法,使用Room操作SQLite有以下優勢:

  • 編譯期的SQL語法檢查
  • 開發高效,避免大量模板代碼
  • API設計友好,容易理解
  • 可以與RxJavaLiveDataKotlin Coroutines等進行橋接

添加依賴

dependencies { implementation "androidx.room:room-runtime:2.2.5" kapt "androidx.room:room-compiler:2.2.5" }

基本組件

Room的使用,主要涉及以下3個組件

  • Database: 訪問底層數據庫的入口
  • Entity: 代表數據庫中的表(table),一般用註解
  • Data Access Object (DAO): 數據庫訪問者

這三個組件的概念也出現在其他ORM框架中,有過使用經驗的同學理解起來並不困難: 通過Database獲取DAO,然後通過DAO查詢並獲取entities,最終通過entities對數據庫table中數據進行讀寫

在這裏插入圖片描述

Database

Database是我們訪問底層數據庫的入口,管理着真正的數據庫文件。我們使用@Database定義一個Database類:

  • 派生自RoomDatabase
  • 關聯其內部數據庫table對應的entities
  • 提供獲取DAO的抽象方法,且不能有參數

@Database(entities = arrayOf(User::class), version = 1) abstract class UserDatabase : RoomDatabase() { abstract fun userDao(): UserDao }

運行時,我們可以通過Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder()獲取Database實例

val db = Room.databaseBuilder( applicationContext, UserDatabase::class.java, "users-db" ).build()

創建Databsse的成本較高,推薦使用單例的Database,避免反覆創建實例帶來的開銷

Entity

一個Entity代表數據庫中的一張表(table)。我們使用@Entity定義一個Entiry類,類中的屬性對應表中的Column

@Entity data class User( @PrimaryKey val uid: Int, @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String? )

  • 所有的屬性必須是public、或者有get、set方法
  • 屬性中至少有一個主鍵,使用@PrimaryKey表示單個主鍵,也可以像下面這樣定義多主鍵

@Entity(primaryKeys = arrayOf("firstName", "lastName"))

  • 當主鍵值為null時,autoGenerate可以幫助自動生成鍵值

@PrimaryKey(autoGenerate = true) val uid : Int

  • 默認情況下使用類名作為數據庫table名,也可使用tableName指定

@Entity(tableName = "users")

  • Entity中的所有屬性都會被持久化到數據庫,除非使用@Ignore

@Ignore val picture: Bitmap?

  • 可以使用indices指定數據庫索引,unique設置其為唯一索引

``` @Entity(indices = arrayOf(Index(value = ["last_name", "address"])))

@Entity(indices = arrayOf(Index(value = ["first_name", "last_name"], unique = true))) ```

Data Access Object (DAO)

DAO提供了訪問DB的API,我們使用@Dao定義DAO類,使用@Query@Insert@Delete定義CRUD方法

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

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

@Insert fun insertAll(vararg users: User)

@Delete fun delete(user: User) } ```

DAO的方法調用都在當前線程進行,所以要避免在UI線程直接訪問

Type Converters

有時,需要將自定義類型的數據持久化到DB,此時需要藉助Converters進行轉換

``` class Converters { @TypeConverter fun fromTimestamp(value: Long?): Date? { return value?.let { Date(it) } }

@TypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time?.toLong() } } ```

在聲明Database時,指定此Converters

@Database(entities = arrayOf(User::class), version = 1) @TypeConverters(Converters::class) abstract class UserDatabase : RoomDatabase() { abstract fun userDao(): UserDao }

2. Data Access Objects(DAO)

Room中使用Data Access Objects(DAO)對數據庫進行讀寫,相對於SQL語句直接查詢,DAO可以定義更加友好的API。DAO中可以自定義CURD方法,還可以方便地與RxJavaLiveData等進行集成。

我們可以使用接口或者抽象類定一個DAO,如果使用抽象類,可以選擇性的為其定義構造函數,並接受Database作為唯一參數。

Room在編譯期會基於定義的DAO生成具體實現類,實現具體CURD方法。

@Insert 插入

@Insert註解插入操作,編譯期生成的代碼會將所有的參數以單獨的事務更新到DB。

@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertUsers(vararg users: User) @Insert fun insertBothUsers(user1: User, user2: User) @Insert fun insertUsersAndFriends(user: User, friends: List<User>) }

onConflict設置當事務中遇到衝突時的策略

  • OnConflictStrategy.REPLACE : 替換舊值,繼續當前事務
  • OnConflictStrategy.ROLLBACK : 回滾當前事務
  • OnConflictStrategy.ABORT : 結束當前事務、回滾
  • OnConflictStrategy.FAIL : 當前事務失敗、回滾
  • OnConflictStrategy.NONE : 忽略衝突,繼續當前事務

最新代碼中ROLLBACK 和 FAIL 已經deprecated了,使用ABORT替代

@Update 更新

@Update註解定義更新操作,根據參數對象的主鍵更新指定row的數據

@Dao interface UserDao { @Update(onConflict = OnConflictStrategy.REPLACE) fun updateUsers(vararg users: User) @Update fun update(user: User) }

@Delete 刪除

@Delete定義刪除操作,根據主鍵刪除指定row

@Dao interface UserDao { @Delete fun deleteUsers(vararg users: User) }

@Query 查詢

@Query註解定義查詢操作。@Query中的SQL語句以及返回值類型等會在編譯期進行檢查,更早的暴露問題

@Dao interface UserDao { @Query("SELECT * FROM users") fun loadAllUsers(): Array<User> }

指定參數

可以用參數指定@Query中的where條件:

``` @Dao interface UserDao { @Query("SELECT * FROM users WHERE age BETWEEN :minAge AND :maxAge") fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array

@Query("SELECT * FROM users WHERE first_name LIKE :search " +
       "OR last_name LIKE :search")
fun findUserWithName(search: String): List<User>

} ```

返回子集

返回的結果可以是所有column的子集:

data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String? )@Dao interface UserDao { @Query("SELECT first_name, last_name FROM users") fun loadFullName(): List<NameTuple> }

返回Cursor

返回Cursor,可以基於Cursor進行進一步操作

@Dao interface UserDao { @Query("SELECT * FROM users") fun loadAllUsers(): Cursor }

多表查詢

@Dao interface BookDao { @Query( "SELECT * FROM book " + "INNER JOIN loan ON loan.book_id = book.id " + "INNER JOIN user ON user.id = loan.user_id " + "WHERE users.name LIKE :userName" ) fun findBooksBorrowedByNameSync(userName: String): List<Book> }

SQL可以寫任何語句,包括多表連接等

返回類型

Room可以返回Coroutine、RxJava等多個常用庫的類型結果,便於在異步、響應式開發中使用 在這裏插入圖片描述

3.實體與數據表關係

對於關係型數據庫來説,最重要的是如何將數據拆分為有相關關係的多個數據表。SQLite作為關係型數據庫,允許entits之間可以有多種關係,Room提供了多種方式表達這種關係。

@Embedded內嵌對象

@Embedded註解可以將一個Entity作為屬性內嵌到另一Entity,我們可以像訪問Column一樣訪問內嵌Entity

內嵌實體本身也可以包括其他內嵌對象

``` data class Address( val street: String?, val state: String?, val city: String?, val postCode: Int )

@Entity data class User( @PrimaryKey val id: Int, val firstName: String?, @Embedded val address: Address? ) ```

如上,等價於User表包含了 id, firstName, street, state, city, postCode等column

如果內嵌對象中存在同名字段,可以使用prefix指定前綴加以區分

@Embedded通過把內嵌對象的屬性解包到被宿主中,建立了實體的連接。此外還可以通過@Relationforeignkeys來描述實體之間更加複雜的關係。

我們至少可以描述三種實體關係

  • 一對一
  • 一對多或多對一
  • 多對多

一對一

主表(Parent Entity)中的每條記錄與從表(Child Entity)中的每條記錄一一對應。

設想一個音樂app的場景,用户(User)和曲庫(Library)有如下關係:

  • 一個User只有一個Library
  • 一個Library只屬於唯一User

``` @Entity data class User( @PrimaryKey val userId: Long, val name: String, val age: Int )

@Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "userId", childColumns = "userOwnerId", onDelete = CASCADE)) data class Library( @PrimaryKey val libraryId: Long, val title: String, val userOwnerId: Long )

data class UserAndLibrary( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userOwnerId" ) val library: Library ) ```

如上,User和Library之間屬於一對一的關係。

foreignkeys

foreignkeys作為@Relation的屬性用來定義外鍵約束。外鍵只能在從表上,從表需要有字段對應到主表的主鍵(Library的userOwnerId對應到User的userId)。

外鍵約束屬性:當有刪除或者更新操作的時候發出這個約束

通過外鍵約束,對主表的操作會受到從表的影響。例如當在主表(即外鍵的來源表)中刪除對應記錄時,首先檢查該記錄是否有對應外鍵,如果有則不允許刪除。

@Relation

為了能夠對User以及關聯的Library進行查詢,需要為兩者之間建立一對一關係:

  • 通過UserAndLibrary定義這種關係,包含兩個成員分別是主表和從表的實體
  • 為從表添加@Relation註解
  • parentColumn:主表主鍵
  • entityColumn:從表外鍵約束的字段

然後,可以通過UserAndLibrary進行查詢

@Transaction @Query("SELECT * FROM User") fun getUsersAndLibraries(): List<UserAndLibrary>

此方法要從兩個表中分別進行兩次查詢,所以@Transaction確保方法中的多次查詢的原子性

一對多

主表中的一條記錄對應從表中的零到多條記錄。

在前面音樂APP的例子中,有如下一對多關係:

  • 一個User可以創建多個播放列表(Playlist)
  • 每個Playlist只能有唯一的創作者(User)

``` @Entity data class User( @PrimaryKey val userId: Long, val name: String, val age: Int )

@Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "userId", childColumns = "userCreatorId", onDelete = CASCADE)) data class Playlist( @PrimaryKey val playlistId: Long, val userCreatorId: Long, val playlistName: String )

data class UserWithPlaylists( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userCreatorId" ) val playlists: List ) ```

可以看到,一對多關係的UserWithPlaylists與一對一類似, 只是playlists需要是一個List表示從表中的記錄不止一個。

查詢方法如下:

@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylists(): List<UserWithPlaylists>

多對多

主表中的一條記錄對應從表中的零活多個,反之亦然

  • 每個Playlist中可以有很多首歌曲(Song)
  • 每個Song可以歸屬不同的Playlist

因此,Playlist與Song之間是多對多的關係

``` @Entity data class Playlist( @PrimaryKey val id: Long, val playlistName: String )

@Entity data class Song( @PrimaryKey val id: Long, val songName: String, val artist: String )

@Entity(primaryKeys = ["playlistId", "songId"], foreignKeys = { @ForeignKey(entity = Playlist.class, parentColumns = "id", childColumns = "playlistId"), @ForeignKey(entity = Song.class, parentColumns = "id", childColumns = "songId") }))

data class PlaylistSongCrossRef( val playlistId: Long, val songId: Long ) ```

多對多關係中,Song和Playlist之間沒有明確的外鍵約束關係,需要定義一個 associative entity(又或者稱作交叉連接表):PlaylistSongCrossRef,然後分別與Song和Playlist建立外鍵約束。交叉連接的結果是Song與Playlist的笛卡爾積,即兩個表中所有記錄的組合。

基於交叉連接表,我們可以獲取一首Song與其包含它的所有Playlist,又或者一個Playlist與其包含的所有Song。

如果使用SQL獲取指定Playlist與其包含的Song,需要兩條查詢:

```

查詢playlist信息

SELECT * FROM Playlist

查詢Song信息

SELECT Song.id AS songId, Song.name AS songName, _junction.playlistId FROM PlaylistSongCrossRef AS _junction INNER JOIN Song ON (_junction.songId = Song.id)

WHERE _junction.playlistId IN (playlistId1, playlistId2, …)

```

如果使用Room,則需要定義PlaylistWithSongs類,並告訴其使用PlaylistSongCrossRef作為連接:

data class PlaylistWithSongs( @Embedded val playlist: Playlist, @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = @Junction(PlaylistSongCrossRef::class) ) val songs: List<Song> )

同理,也可定義SongWithPlaylists

data class SongWithPlaylists( @Embedded val song: Song, @Relation( parentColumn = "songId", entityColumn = "playlistId", associateBy = @Junction(PlaylistSongCrossRef::class) ) val playlists: List<Playlist> )

查詢與前面類似,很簡單:

``` @Transaction @Query("SELECT * FROM Playlist") fun getPlaylistsWithSongs(): List

@Transaction @Query("SELECT * FROM Song") fun getSongsWithPlaylists(): List ```

二、Room使用

Room 的使用可以分為三步:

創建 Entity 類:也就是實體類,每個實體類都會生成一個對應的表,每個字段都會生成對應的一列。

創建 Dao 類:Dao 是指 Data Access Object,即數據訪問對象,通常我們會在這裏封裝對數據庫的增刪改查操作,這樣的話,邏輯層就不需要和數據庫打交道了,只需要使用 Dao 類即可。

創建 Database 類:定義數據庫的版本,數據庫中包含的表、包含的 Dao 類,以及數據庫升級邏輯。

創建 Entity 類

新建一個 User 類,並添加 @Entity 註解,使 Room 為此類自動創建一個表。在主鍵上添加 @PrimaryKey(autoGenerate = true) 註解,使得 id 自增,不妨將這裏的主鍵 id 記作固定寫法。

`@Entity``data class User(var firstName: String, var lastName: String, var age: Int) {`` ``@PrimaryKey(autoGenerate = true)`` ``var id: Long = 0``}`

創建 Dao 類

創建一個接口類 UserDao,並在此類上添加 @Dao 註解。增刪改查方法分別添加 @Insert@Delete@Update@Query 註解,其中,@Query 需要編寫 SQL 語句才能實現查詢。Room 會自動為我們生成這些數據庫操作方法。

`@Dao``interface UserDao {`` ``@Insert`` ``fun insertUser(user: User): Long`` ``@Update`` ``fun updateUser(newUser: User)`` ``@Query("select * from user")`` ``fun loadAllUsers(): List`` ``@Query("select * from User where age > :age")`` ``fun loadUsersOlderThan(age: Int): List`` ``@Delete`` ``fun deleteUser(user: User)`` ``@Query("delete from User where lastName = :lastName")`` ``fun deleteUserByLastName(lastName: String): Int``}`

@Query 方法不僅限於查找,還可以編寫我們自定義的 SQL 語句,所以可以用它來執行特殊的 SQL 操作,如上例中的 deleteUserByLastName 方法所示。

創建 Database 抽象類

新建 AppDatabase 類,繼承自 RoomDatabase 類,添加 @Database 註解,在其中聲明版本號,包含的實體類。並在抽象類中聲明獲取 Dao 類的抽象方法。

`@Database(version = 1, entities = [User::class])``abstract class AppDatabase : RoomDatabase() {`` ``abstract fun userDao(): UserDao`` ``companion object {`` ``private var instance: AppDatabase? = null`` ``@Synchronized`` ``fun getDatabase(context: Context): AppDatabase {`` ``return instance?.let { it }`` ``?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.build()`` ``.apply { instance = this }`` ``}`` ``}``}`

在 getDatabase 方法中,第一個參數一定要使用 applicationContext,以防止內存泄漏,第三個參數表示數據庫的名字。

測試

佈局中只有四個 id 為 btnAdd,btnDelete,btnUpdate,btnQuery 的按鈕,故不再給出佈局代碼。

MainActivity 代碼如下:

`class MainActivity : AppCompatActivity() {`` ``override fun onCreate(savedInstanceState: Bundle?) {`` ``super.onCreate(savedInstanceState)`` ``setContentView(R.layout.activity_main)`` ``val userDao = AppDatabase.getDatabase(this).userDao()`` ``val teacher = User("lin", "guo", 66)`` ``val student = User("alpinist", "wang", 3)`` ``btnAdd.setOnClickListener {`` ``thread {`` ``teacher.id = userDao.insertUser(teacher)`` ``student.id = userDao.insertUser(student)`` ``}`` ``}`` ``btnDelete.setOnClickListener {`` ``thread {`` ``userDao.deleteUser(student)`` ``}`` ``}`` ``btnUpdate.setOnClickListener {`` ``thread {`` ``teacher.age = 666`` ``userDao.updateUser(teacher)`` ``}`` ``}`` ``btnQuery.setOnClickListener {`` ``thread {`` ``Log.d("~~~", "${userDao.loadAllUsers()}")`` ``}`` ``}`` ``}``}`

每一步操作我們都開啟了一個新線程來操作,這是由於數據庫操作涉及到 IO,所以不推薦在主線程執行。在開發環境中,我們也可以通過 allowMainThreadQueries() 方法允許主線程操作數據庫,但一定不要在正式環境使用此方法。

`Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.allowMainThreadQueries()`` ``.build()`

點擊 btnAdd,再點擊 btnQuery,Log 如下:

`~~~: [User(firstName=lin, lastName=guo, age=66), User(firstName=alpinist, lastName=wang, age=3)]`

點擊 btnDelete,再點擊 btnQuery,Log 如下:

`~~~: [User(firstName=lin, lastName=guo, age=66)]`

點擊 btnUpdate,再點擊 btnQuery,Log 如下:

`~~~: [User(firstName=lin, lastName=guo, age=666)]`

由此可見,我們的增刪改查操作都成功了。

數據庫升級

簡單升級

使用 fallbackToDestructiveMigration() 可以簡單粗暴的升級,也就是直接丟棄舊版本數據庫,然後創建最新的數據庫

`Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.fallbackToDestructiveMigration()`` ``.build()`

注:此方法過於暴力,開發階段可使用,不可在正式環境中使用,因為會導致舊版本數據庫丟失。

規範升級

新增一張表

創建 Entity 類

`@Entity``data class Book(var name: String, var pages: Int) {`` ``@PrimaryKey(autoGenerate = true)`` ``var id: Long = 0``}`

創建 Dao 類

`@Dao``interface BookDao {`` ``@Insert`` ``fun insertBook(book: Book)`` ``@Query("select * from Book")`` ``fun loadAllBooks(): List``}`

修改 Database 類:

`@Database(version = 2, entities = [User::class, Book::class])``abstract class AppDatabase : RoomDatabase() {`` ``abstract fun userDao(): UserDao`` ``abstract fun bookDao(): BookDao`` ``companion object {`` ``private var instance: AppDatabase? = null`` ``private val MIGRATION_1_2 = object : Migration(1, 2) {`` ``override fun migrate(database: SupportSQLiteDatabase) {`` ``database.execSQL(`` ``"""`` ``create table Book (`` ``id integer primary key autoincrement not null,`` ``name text not null,`` ``pages integer not null)`` ``""".trimIndent()`` ``)`` ``}`` ``}`` ``@Synchronized`` ``fun getDatabase(context: Context): AppDatabase {`` ``return instance?.let { it }`` ``?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.addMigrations(MIGRATION_1_2)`` ``.build()`` ``.apply { instance = this }`` ``}`` ``}``}`

注:這裏的修改有:

  • version 升級
  • 將 Book 類添加到 entities 中
  • 新增抽象方法 bookDao
  • 創建 Migration 對象,並將其添加到 getDatabase 的 builder 中

現在如果再操作數據庫,就會新增一張 Book 表了。

修改一張表

比如在 Book 中新增 author 字段

`@Entity``data class Book(var name: String, var pages: Int, var author: String) {` ` ``@PrimaryKey(autoGenerate = true)`` ``var id: Long = 0``}`

修改 Database,增加版本 2 到 3 的遷移邏輯:

`@Database(version = 3, entities = [User::class, Book::class])``abstract class AppDatabase : RoomDatabase() {`` ``abstract fun userDao(): UserDao`` ``abstract fun bookDao(): BookDao`` ``companion object {`` ``private var instance: AppDatabase? = null`` ``private val MIGRATION_1_2 = object : Migration(1, 2) {`` ``override fun migrate(database: SupportSQLiteDatabase) {`` ``database.execSQL(`` ``"""`` ``create table Book (`` ``id integer primary key autoincrement not null,`` ``name text not null,`` ``pages integer not null)`` ``""".trimIndent()`` ``)`` ``}`` ``}`` ``private val MIGRATION_2_3 = object : Migration(2, 3) {`` ``override fun migrate(database: SupportSQLiteDatabase) {`` ``database.execSQL(`` ``"""`` ``alter table Book add column author text not null default "unknown"`` ``""".trimIndent()`` ``)`` ``}`` ``}`` ``@Synchronized`` ``fun getDatabase(context: Context): AppDatabase {`` ``return instance?.let { it }`` ``?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.addMigrations(MIGRATION_1_2, MIGRATION_2_3)`` ``.build()`` ``.apply { instance = this }`` ``}`` ``}``}`

注:這裏的修改有:

version 升級創建 Migration 對象,並將其添加到 getDatabase 的 builder 中

測試

修改 MainActivity:

`class MainActivity : AppCompatActivity() {`` ``override fun onCreate(savedInstanceState: Bundle?) {`` ``super.onCreate(savedInstanceState)`` ``setContentView(R.layout.activity_main)`` ``val bookDao = AppDatabase.getDatabase(this).bookDao()`` ``btnAdd.setOnClickListener {`` ``thread {`` ``bookDao.insertBook(Book("第一行代碼", 666, "guolin"))`` ``}`` ``}`` ``btnQuery.setOnClickListener {`` ``thread {`` ``Log.d("~~~", "${bookDao.loadAllBooks()}")`` ``}`` ``}`` ``}``}`

點擊 btnAdd,再點擊 btnQuery,Log 如下:

`~~~: [Book(name=第一行代碼, pages=666, author=guolin)]`

這就説明我們對數據庫的兩次升級都成功了。

三、源碼分析

Room在編譯期通過kapt處理@Dao和@Database註解,並生成DAO和Database的實現類,UserDatabase_ImplUserDao_Impl。kapt生成的代碼在 build/generated/source/kapt/

133f44c167534fb49e6aaf0df1148800~tplv-k3u1fbpfcp-zoom-1.image

UserDatabase_Impl

``` public final class UserDatabase_Impl extends UserDatabase { private volatile UserDao _userDao;

//RoomDataBase的init中調用 @Override protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) { final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1) { @Override public void createAllTables(SupportSQLiteDatabase _db) { //Implementation }

  @Override
  protected void onCreate(SupportSQLiteDatabase _db) {
     //Implementation
  }

});

final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
    .name(configuration.name)
    .callback(_openCallback)
    .build();

final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);

return _helper;

}

@Override protected InvalidationTracker createInvalidationTracker() { final HashMap _shadowTablesMap = new HashMap(0); HashMap> _viewTables = new HashMap>(0); return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "users"); }

@Override public void clearAllTables() { super.assertNotMainThread(); final SupportSQLiteDatabase _db = super.getOpenHelper().getWritableDatabase(); try { super.beginTransaction(); _db.execSQL("DELETE FROM users"); super.setTransactionSuccessful(); } finally { super.endTransaction(); _db.query("PRAGMA wal_checkpoint(FULL)").close(); if (!_db.inTransaction()) { _db.execSQL("VACUUM"); } } }

@Override public UserDao userDao() { //實現見後文 } ```

  • createOpenHelperRoom.databaseBuilder().build()創建Database時,會調用實現類的createOpenHelper()創建SupportSQLiteOpenHelper,此Helper用來創建DB以及管理版本
  • createInvalidationTracker :創建跟蹤器,確保table的記錄修改時能通知到相關回調方
  • clearAllTables:清空table的實現
  • userDao:創建UserDao_Impl

UserDao_Impl

``` public final class UserDao_Impl implements UserDao { private final RoomDatabase __db;

private final EntityInsertionAdapter __insertionAdapterOfUser;

private final EntityDeletionOrUpdateAdapter __deletionAdapterOfUser;

public UserDao_Impl(RoomDatabase __db) { this.__db = __db; this.__insertionAdapterOfUser = new EntityInsertionAdapter(__db) { //Implementation }; this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter(__db) { //Implementation }; }

@Override public void insertAll(final User... users) { //Implementation }

@Override public void delete(final User user) { //Implementation }

@Override public List getAll() { //Implementation }

@Override public List loadAllByIds(final int[] userIds) { //Implementation }

@Override public User findByName(final String first, final String last) { //Implementation } } ```

UserDao_Impl 主要有三個屬性:

  • __db:RoomDatabase的實例
  • __insertionAdapterOfUserEntityInsertionAdapterd實例,用於數據insert。上例中,將在installAll()中調用
  • __deletionAdapterOfUserEntityDeletionOrUpdateAdapter實例,用於數據的update/delete。 上例中,在delete()中調用

RoomDatabase.Builder

Room通過Build模式創建Database實例

val userDatabase = Room.databaseBuilder( applicationContext, UserDatabase::class.java, "users-db" ).build()

Builder的好處時便於對Database進行配置

  • createFromAsset()/createFromFile() :從SD卡或者Asset的db文件創建RoomDatabase實例
  • addMigrations() :添加一個數據庫遷移(migration),當進行數據版本升級時需要
  • allowMainThreadQueries() :允許在UI線程進行數據庫查詢,默認是不允許的
  • fallbackToDestructiveMigration() :如果找不到migration則重建數據庫表(會造成數據丟失)

除上面以外,還有其他很多配置。調用build()後,創建UserDatabase_Impl,並調用init(),內部會調用createOpenHelper()

userDao()

@Override public UserDao userDao() { if (_userDao != null) { return _userDao; } else { synchronized(this) { if(_userDao == null) { _userDao = new UserDao_Impl(this); } return _userDao; } } }

通過構造參數,向UserDao_Impl傳入RoomDatabase

insertAll()

@Override public void insertAll(final User... users) { __db.assertNotSuspendingTransaction(); __db.beginTransaction(); try { __insertionAdapterOfUser.insert(users); __db.setTransactionSuccessful(); } finally { __db.endTransaction(); } }

使用__db開啟事務,使用__insertionAdapterOfUser執行插入操作

delete()

@Override public void delete(final User user) { __db.assertNotSuspendingTransaction(); __db.beginTransaction(); try { __deletionAdapterOfUser.handle(user); __db.setTransactionSuccessful(); } finally { __db.endTransaction(); } }

同insertAll()

getAll()

@Override public List<User> getAll() { final String _sql = "SELECT * FROM users"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0); __db.assertNotSuspendingTransaction(); final Cursor _cursor = DBUtil.query(__db, _statement, false, null); try { final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid"); final int _cursorIndexOfFirstName = CursorUtil.getColumnIndexOrThrow(_cursor, "first_name"); final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "last_name"); final List<User> _result = new ArrayList<User>(_cursor.getCount()); while(_cursor.moveToNext()) { final User _item; final int _tmpUid; _tmpUid = _cursor.getInt(_cursorIndexOfUid); final String _tmpFirstName; _tmpFirstName = _cursor.getString(_cursorIndexOfFirstName); final String _tmpLastName; _tmpLastName = _cursor.getString(_cursorIndexOfLastName); _item = new User(_tmpUid,_tmpFirstName,_tmpLastName); _result.add(_item); } return _result; } finally { _cursor.close(); _statement.release(); } }

基於@Query註解的sql語句創建RoomSQLiteQuery,然後創建cursor進行後續操作

數據庫升級

當數據庫的表結構發生變化時,我們需要通過數據庫遷移(Migrations)升級表結構,避免數據丟失。

例如,我們想要為User表增加age字段

| uid | first_name | last_name |

↓↓

| uid | first_name | last_name | age |

數據遷移需要使用Migration類:

val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER") } }

Migration通過startVersionendVersion表明當前是哪個版本間的遷移,然後在運行時,按照版本順序調用各Migration,最終遷移到最新的Version

創建Database時設置Migration:

Room.databaseBuilder( applicationContext, UserDatabase::class.java, "users-db" ).addMigrations(MIGRATION_1_2) .build()

遷移失效

遷移中如果找不到對應版的Migration,會拋出IllegalStateException

java.lang.IllegalStateException: A migration from 1 to 2 is necessary. Please provide a Migration in the builder or call fallbackToDestructiveMigration in the builder in which case Room will re-create all of the tables.

可以添加降級處理,避免crash:

Room.databaseBuilder( applicationContext, UserDatabase::class.java, "users-db" ).fallbackToDestructiveMigration() .build()

  • fallbackToDestructiveMigration:遷移失敗時,重建數據庫表
  • fallbackToDestructiveMigrationFrom:遷移失敗時,基於某版本重建數據庫表
  • fallbackToDestructiveMigrationOnDowngrade:遷移失敗,數據庫表降級到上一個正常版本

集成三方庫(LiveData、RxJava等)

作為Jetpack生態的成員,Room可以很好地兼容Jetpack的其他組件以及ACC推薦的三方庫,例如LiveData、RxJava等。

使用LiveData

DAO可以定義LiveData類型的結果,Room內部兼容了LiveData的響應式邏輯。

可觀察的查詢

通常的Query需要命令式的獲取結果,LiveData可以讓結果的更新可被觀察(Observable Queries)。

@Dao interface UserDao { @Query("SELECT * FROM users") fun getAllLiveData(): LiveData<List<User>> }

當DB的數據發生變化時,Room會更新LiveData:

``` @Override public LiveData> getAllLiveData() { final String _sql = "SELECT * FROM users"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0); return __db.getInvalidationTracker().createLiveData(new String[]{"users"}, false, new Callable>() { @Override public List call() throws Exception { final Cursor _cursor = DBUtil.query(__db, _statement, false, null); try { final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid"); final int _cursorIndexOfFirstName = CursorUtil.getColumnIndexOrThrow(_cursor, "first_name"); final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "last_name"); final List _result = new ArrayList(_cursor.getCount()); while(_cursor.moveToNext()) { final User _item; final int _tmpUid; _tmpUid = _cursor.getInt(_cursorIndexOfUid); final String _tmpFirstName; _tmpFirstName = _cursor.getString(_cursorIndexOfFirstName); final String _tmpLastName; _tmpLastName = _cursor.getString(_cursorIndexOfLastName); _item = new User(_tmpUid,_tmpFirstName,_tmpLastName); _result.add(_item); } return _result; } finally { _cursor.close(); } }

@Override
protected void finalize() {
  _statement.release();
}

}); } ```

__db.getInvalidationTracker().createLiveData() 接受3個參數

  • tableNames:被觀察的表
  • inTransaction:查詢是否基於事務
  • computeFunction:表記錄變化時的回調

computeFunction的call中執行真正的sql查詢。當Observer首次訂閲LiveData時,或者表數據發生變化時,便會執行到這裏。

使用RxJava

添加依賴

使用RxJava需要添加以下依賴

``` dependencies { def room_version = "2.2.5"

implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version"

// RxJava support for Room implementation "androidx.room:room-rxjava2:$room_version" } ```

響應式的查詢

DAO的返回值類型可以是RxJava2的各種類型:

  • @Query註解的方法:返回 Flowable 或 Observable.
  • @Insert/@Update/@Delete註解的方法: 返回Completable, Single, and Maybe(Room 2.1.0以上)

``` @Dao interface UserDao { @Query("SELECT * from users where uid = :id LIMIT 1") fun loadUserById(id: Int): Flowable

@Insert
fun insertUsers(vararg users: User): Completable

@Delete
fun deleteAllUsers(users: List<User>): Single<Int>

} @Override public Completable insertLargeNumberOfUsers(final User... users) { return Completable.fromCallable(new Callable() { @Override public Void call() throws Exception { __db.beginTransaction(); try { __insertionAdapterOfUser.insert(users); __db.setTransactionSuccessful(); return null; } finally { __db.endTransaction(); } } }); }@Override public Single deleteAllUsers(final List users) { return Single.fromCallable(new Callable() { @Override public Integer call() throws Exception { int _total = 0; __db.beginTransaction(); try { _total +=__deletionAdapterOfUser.handleMultiple(users); __db.setTransactionSuccessful(); return _total; } finally { __db.endTransaction(); } } }); }@Override public Flowable loadUserById(final int id) { final String _sql = "SELECT * from users where uid = ? LIMIT 1"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1); int _argIndex = 1; _statement.bindLong(_argIndex, id); return RxRoom.createFlowable(__db, false, new String[]{"users"}, new Callable() { @Override public User call() throws Exception { //Implementation }

@Override
protected void finalize() {
  _statement.release();
}

}); } ```

如上,使用fromCallable{...}創建Completable與Single; RxRoom.createFlowable{...}創建Flowable。call()裏執行真正的sql操作

使用協程Coroutine

添加依賴

使用Coroutine需要添加額外依賴:

``` dependencies { def room_version = "2.2.5"

implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" // Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version" } ```

掛起函數定義DAO

為UserDao中的CURD方法添加suspend

@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers(vararg users: User) @Update suspend fun updateUsers(vararg users: User) @Delete suspend fun deleteUsers(vararg users: User) @Query("SELECT * FROM users") suspend fun loadAllUsers(): Array<User> }

CoroutinesRoom.execute 中進行真正的sql語句,並通過Continuation將callback變為Coroutine的同步調用

@Override public Object insertUsers(final User[] users, final Continuation<? super Unit> p1) { return CoroutinesRoom.execute(__db, true, new Callable<Unit>() { @Override public Unit call() throws Exception { __db.beginTransaction(); try { __insertionAdapterOfUser.insert(users); __db.setTransactionSuccessful(); return Unit.INSTANCE; } finally { __db.endTransaction(); } } }, p1); }

可以對比一下普通版本的insertUsers:

@Override public void insertUsers(final User... users) { __db.assertNotSuspendingTransaction(); __db.beginTransaction(); try { __insertionAdapterOfUser.insert(users); __db.setTransactionSuccessful(); } finally { __db.endTransaction(); } }

區別很明顯,添加了suspend後,生成代碼中會使用CoroutinesRoom.execute封裝協程。

文末

文章總結了Jetpack Room的基本介紹,以及使用方法。最後源碼解析。Android架構師的學習需要很深的學習積累;Android架構師進階知識學習;希望各位都能成為架構師,頂峯相見!