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架構師進階知識學習;希望各位都能成為架構師,頂峰相見!