Android架構師必學——Jetpack Room數據庫應用與源碼解析
開啟掘金成長之旅!這是我參與「掘金日新計劃 · 12 月更文挑戰」的第24天,點擊查看活動詳情
Room是Google官方在SQLite基礎上封裝的一款數據持久庫,是Jetpack全家桶的一員,和Jetpack其他庫有着可以高度搭配協調的天然優勢。Room使用APT技術,大大簡化了使用SQLite的代碼量,只需使用註解配合少量代碼即可實現高效的數據庫操作。
Room介紹
- Room是一個OM(Object Mapping對象映射)數據庫,可以方便地在Android應用程序上訪問數據庫。
- Room抽象了SQLite,通過提供方便的api來查詢數據庫,並在編譯時驗證。並且可以使用SQLite的全部功能,同時擁有Java SQL查詢生成器提供的類型安全。
Room的構成
- Database:數據庫擴展了RoomDatabase的抽象類。可以通過Room獲得它的一個實例。databaseBuilder或Room.inMemoryDatabaseBuilder。
- Entity:代表一個表結構。
- Dao:數據訪問對象是Room的主要組件,負責定義訪問數據庫的方法。
優點:
- Google官方庫,和Jetpack其他庫(比如Lifecycle,LiveData)等有天然的融合搭配使用優勢。
- 在編譯器可以對SQL語法進行檢查。
- 使用APT,簡單地幾個註解搭配少量代碼即可使用,大量減少模板代碼。
- 查詢代碼自定義,可以實現更復雜的查詢功能,也可以對SQL語句進行優化。
- 簡化數據庫的遷移路徑。
不足:
- 查詢時必須手動寫SQL語句,不提供默認的查詢配置。
- 效率比其他數據庫框架(GreenDao等)並沒有多少提高。
- 數據庫版本升級稍顯複雜。
一、基本介紹
框架特點
相對於SQLiteOpenHelper
等傳統方法,使用Room操作SQLite有以下優勢:
- 編譯期的SQL語法檢查
- 開發高效,避免大量模板代碼
- API設計友好,容易理解
- 可以與
RxJava
、LiveData
、Kotlin 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方法,還可以方便地與RxJava
、LiveData
等進行集成。
我們可以使用接口或者抽象類定一個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通過把內嵌對象的屬性解包到被宿主中,建立了實體的連接。此外還可以通過@Relation
和 foreignkeys
來描述實體之間更加複雜的關係。
我們至少可以描述三種實體關係
- 一對一
- 一對多或多對一
- 多對多
一對一
主表(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_Impl
和UserDao_Impl
。kapt生成的代碼在 build/generated/source/kapt/
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
@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() { //實現見後文 } ```
- createOpenHelper:
Room.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
private final EntityDeletionOrUpdateAdapter
public UserDao_Impl(RoomDatabase __db) {
this.__db = __db;
this.__insertionAdapterOfUser = new EntityInsertionAdapter
@Override public void insertAll(final User... users) { //Implementation }
@Override public void delete(final User user) { //Implementation }
@Override
public List
@Override
public List
@Override public User findByName(final String first, final String last) { //Implementation } } ```
UserDao_Impl 主要有三個屬性:
- __db:RoomDatabase的實例
- __insertionAdapterOfUser :
EntityInsertionAdapterd
實例,用於數據insert。上例中,將在installAll()
中調用 - __deletionAdapterOfUser:
EntityDeletionOrUpdateAdapter
實例,用於數據的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通過startVersion
和endVersion
表明當前是哪個版本間的遷移,然後在運行時,按照版本順序調用各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
@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
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架構師進階知識學習;希望各位都能成為架構師,頂峯相見!