前言
Android 開(kāi)發(fā)者使用數(shù)據(jù)庫(kù)的時(shí)候,最先想到的是 SQLite呢撞。如果有對(duì)外公開(kāi)的需求损姜,則需再包裝一層 ContentProvider。除此之外殊霞,也可以選擇開(kāi)源的數(shù)據(jù)庫(kù)框架摧阅,比如 GreenDao,DBFlow等绷蹲。
本文將講述 Google 推出的數(shù)據(jù)庫(kù)框架 Room棒卷,和您一起探討: 如何使用 Room、其實(shí)現(xiàn)的大致原理以及它的優(yōu)勢(shì)祝钢。
簡(jiǎn)介
Room 是房間的意思比规。房間除了能存放物品,還能帶給人溫暖和安心的感覺(jué)拦英。用 Room 給這個(gè)抽象的軟件架構(gòu)命名蜒什,增加了人文色彩,很有溫度疤估。
先來(lái)看一下 Room 框架的基本組件灾常。
使用起來(lái)大體就是這幾個(gè)步驟,很便捷做裙。
使用前需要構(gòu)筑如下依賴岗憋。
dependencies {
def room_version = "2.2.6"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
}
實(shí)戰(zhàn)
下面將通過(guò)一個(gè)展示電影列表的 demo 演示 Room 框架的使用肃晚。
組件構(gòu)建
首先構(gòu)建一個(gè)電影表 Movie锚贱,有名稱、演員关串、上映年份拧廊、評(píng)分這么幾個(gè)字段监徘。
@Entity
class Movie() : BaseObservable() {
@PrimaryKey(autoGenerate = true)
var id = 0
@ColumnInfo(name = "movie_name", defaultValue = "Harry Potter")
lateinit var name: String
@ColumnInfo(name = "actor_name", defaultValue = "Jack Daniel")
lateinit var actor: String
@ColumnInfo(name = "post_year", defaultValue = "1999")
var year = 1999
@ColumnInfo(name = "review_score", defaultValue = "8.0")
var score = 8.0
}
@Entity 表示數(shù)據(jù)庫(kù)中的表
@PrimaryKey 表示主鍵,autoGenerate 表示自增
@ColumnInfo 表示字段吧碾,name 表示字段名稱
然后構(gòu)建一個(gè)訪問(wèn) Movie 表的 DAO 接口凰盔。
interface MovieDao {
@Insert
fun insert(vararg movies: Movie?): LongArray?
@Delete
fun delete(movie: Movie?): Int
@Update
fun update(vararg movies: Movie?): Int
@get:Query("SELECT * FROM movie")
val allMovies: LiveData<List<Movie?>?>
}
@Dao 表示訪問(wèn) DB 的方法,需要聲明為接口或抽象類倦春,編譯階段將生成 _Impl 實(shí)現(xiàn)類户敬,此處則將生成 MovieDao_Impl.java 文件
@Insert、@Delete睁本、@Update 和 @Query 分別表示數(shù)據(jù)庫(kù)的增刪改查方法
最后需要構(gòu)建 Room 使用的入口 RoomDatabase尿庐。
@Database(entities = [Movie::class], version = 1)
abstract class MovieDataBase : RoomDatabase() {
abstract fun movieDao(): MovieDao
companion object {
@Volatile
private var sInstance: MovieDataBase? = null
private const val DATA_BASE_NAME = "jetpack_movie.db"
@JvmStatic
fun getInstance(context: Context): MovieDataBase? {
if (sInstance == null) {
synchronized(MovieDataBase::class.java) {
if (sInstance == null) {
sInstance = createInstance(context)
}
}
}
return sInstance
}
private fun createInstance(context: Context): MovieDataBase {
return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
...
.build()
}
}
}
@Database 表示繼承自 RoomDatabase 的抽象類,entities 指定表的實(shí)現(xiàn)類列表呢堰,version 指定了 DB 版本
必須提供獲取 DAO 接口的抽象方法,比如上面定義的 movieDao()枉疼,Room 將通過(guò)這個(gè)方法實(shí)例化 DAO 接口
RoomDatabase 實(shí)例的內(nèi)存開(kāi)銷較大皮假,建議使用單例模式管理
編譯時(shí)將生成 _Impl 實(shí)現(xiàn)類,此處將生成 MovieDataBase_Impl.java 文件
組件調(diào)用
本 demo 將結(jié)合 ViewModel 和 Room 進(jìn)行數(shù)據(jù)交互骂维,依賴 LiveData 進(jìn)行異步查詢惹资,畫(huà)面上則采用 Databinding 將數(shù)據(jù)和視圖自動(dòng)綁定。
class DemoActivity : AppCompatActivity() {
private var movieViewModel: MovieViewModel? = null
private var binding: ActivityRoomDbBinding? = null
private var movieList: List<Movie?>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRoomDbBinding.inflate(layoutInflater)
setContentView(binding!!.root)
binding!!.lifecycleOwner = this
movieViewModel = ViewModelProvider(this).get(MovieViewModel::class.java)
movieViewModel?.getMovieList(this, { movieList: List<Movie?>? ->
if (movieList == null) return@getMovieList
this.movieList = movieList
binding?.setMovieList(movieList)
})
}
}
ViewModel 通過(guò) MediatorLiveData 擔(dān)當(dāng)列表查詢的中介航闺,當(dāng) DB 初始化結(jié)束后再更新 UI布轿。
private val mediatorLiveData = MediatorLiveData<List<Movie?>?>()
private val db: MovieDataBase?
private val mContext: Context
init {
mContext = application
db = MovieDataBase.getInstance(mContext)
if (db != null) {
mediatorLiveData.addSource(db.movieDao().allMovies) { movieList ->
if (db.databaseCreated.value != null) {
mediatorLiveData.postValue(movieList)
}
}
};
}
fun getMovieList(owner: LifecycleOwner?, observer: Observer<List<Movie?>?>?) {
if (owner != null && observer != null)
mediatorLiveData.observe(owner, observer)
}
}
RoomDatabase 創(chuàng)建后異步插入初始化數(shù)據(jù),并通知 MediatorLiveData来颤。
val databaseCreated = MutableLiveData<Boolean?>()
...
companion object {
...
private fun createInstance(context: Context): MovieDataBase {
return Room.databaseBuilder(context.applicationContext, ...)
...
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Executors.newFixedThreadPool(5).execute {
val dataBase = getInstance(context)
val ids = dataBase!!.movieDao().insert(*Utils.initData)
dataBase.databaseCreated.postValue(true)
}
}
...
})
.build()
}
}
}
運(yùn)行效果
通過(guò) Database Inspector 工具可以看到 DB 數(shù)據(jù)創(chuàng)建成功了汰扭。Database Inspector 支持實(shí)時(shí)刷新,查詢和修改等 DB 操作福铅,是 DB 開(kāi)發(fā)的利器萝毛。
如果不知道如何使用Database Inspector,可參考官方文檔使用 Database Inspector 調(diào)試數(shù)據(jù)庫(kù)
DAO 的具體使用
@Insert
@Insert 支持設(shè)置沖突策略滑黔,默認(rèn)為 OnConflictStrategy.ABORT 即中止并回滾笆包。還可以指定為其他策略。
- OnConflictStrategy.REPLACE 沖突時(shí)替換為新記錄
- OnConflictStrategy.IGNORE 忽略沖突 (不推薦)
- OnConflictStrategy.ROLLBACK 已廢棄略荡,使用 ABORT 替代
- OnConflictStrategy.FAIL 同上
其聲明的方法返回值可為空庵佣,也可為插入行的 ID 或列表。
- fun insertWithOutId(movie: Movie?)
- fun insert(movie: Movie?): Long?
- fun insert(vararg movies: Movie?): LongArray?
@Delete
和 @Insert 一樣支持不返回刪除結(jié)果或返回刪除的函數(shù)汛兜,不再贅述巴粪。
@ Update
和 @Insert 一樣支持設(shè)置沖突策略和定制返回更新結(jié)果。此外需要注意的是 @Update 操作將匹配參數(shù)的主鍵 id 去更新字段。
- fun update(vararg movies: Movie?): Int
@ Query
查詢操作主要依賴 @Update 的 value肛根,指定不同的 SQL 語(yǔ)句即可獲得相應(yīng)的查詢結(jié)果辫塌。在編譯階段就將驗(yàn)證語(yǔ)句是否正確,避免錯(cuò)誤的查詢語(yǔ)句影響到運(yùn)行階段派哲。
查詢所有字段
@get:Query(“SELECT * FROM movie”)查詢指定字段
@get:Query(“SELECT id, movie_name, actor_name, post_year, review_score FROM movie”)排序查詢
@get:Query(“SELECT * FROM movie ORDER BY post_year DESC”) 比如查詢最近發(fā)行的電影列表匹配查詢
@Query(“SELECT * FROM movie WHERE id = :id”)多字段匹配查詢
@Query(“SELECT * FROM movie WHERE movie_name LIKE :keyWord " + " OR actor_name LIKE :keyWord”) 比如查詢名稱和演員中匹配關(guān)鍵字的電影模糊查詢
@Query(“SELECT * FROM movie WHERE movie_name LIKE ‘%’ || :keyWord || ‘%’ " + " OR actor_name LIKE ‘%’ || :keyWord || ‘%’”) 比如查詢名稱和演員中包含關(guān)鍵字的電影限制行數(shù)查詢
@Query(“SELECT * FROM movie WHERE movie_name LIKE :keyWord LIMIT 3”) 比如查詢名稱匹配關(guān)鍵字的前三部電影參數(shù)引用查詢
@Query(“SELECT * FROM movie WHERE review_score >= :minScore”) 比如查詢?cè)u(píng)分大于指定分?jǐn)?shù)的電影多參數(shù)查詢
@Query(“SELECT * FROM movie WHERE post_year BETWEEN :minYear AND :maxYear”) 比如查詢介于發(fā)行年份區(qū)間的電影不定參數(shù)查詢
@Query(“SELECT * FROM movie WHERE movie_name IN (:keyWords)”)Cursor 查詢
@Query(“SELECT * FROM movie WHERE movie_name LIKE ‘%’ || :keyWord || ‘%’ LIMIT :limit”)
fun searchMoveCursorByLimit(keyWord: String?, limit: Int): Cursor?
注意: Cursor 需要保證查詢到的字段和取值一一對(duì)應(yīng)臼氨,所以不推薦使用響應(yīng)式查詢
demo 采用的 LiveData 進(jìn)行的觀察式查詢,還可以配合 RxJava2芭届,Kotlin 的 Flow 進(jìn)行響應(yīng)式查詢储矩。
進(jìn)階使用
數(shù)據(jù)庫(kù)升級(jí)降級(jí)
在 Movie 類里增加了新字段后,重新運(yùn)行已創(chuàng)建過(guò) DB 的 demo 會(huì)發(fā)生崩潰褂乍。
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.
將 @Database 的 version 升級(jí)為 2 之后再次運(yùn)行仍然發(fā)生崩潰椰苟。
A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow fallback of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
提醒我們調(diào)用 fallbackToDestructiveMigration() 以允許升級(jí)失敗時(shí)破壞性地刪除 DB。
如果照做的話树叽,將能避免發(fā)生崩潰舆蝴,并且 onDestructiveMigration() 將被回調(diào)。在這個(gè)回調(diào)里可以試著重新初始化 DB题诵。
private fun createInstance(context: Context): MovieDataBase {
return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
.fallbackToDestructiveMigration()
.addCallback(object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
super.onDestructiveMigration(db)
// Init DB again after db removed.
Executors.newFixedThreadPool(5).execute {
val dataBase = getInstance(context)
val ids = dataBase!!.movieDao().insert(*Utils.initData)
dataBase.databaseCreated.postValue(true)
}
}
})
.build()
}
但是 DB 升級(jí)后洁仗,無(wú)論原有數(shù)據(jù)被刪除還是重新初始化都是用戶難以接受的。
我們可以通過(guò) addMigrations() 指定升級(jí)之后的遷移處理來(lái)達(dá)到保留舊數(shù)據(jù)和增加新字段的雙贏性锭。
比如如下展示的從版本 1 升級(jí)到版本 2赠潦,并增加一個(gè)默認(rèn)值為 8.0 的評(píng)分列的遷移處理。
private fun createInstance(context: Context): MovieDataBase {
return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
// .fallbackToDestructiveMigration()
.addMigrations(object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE movie "
+ " ADD COLUMN review_score INTEGER NOT NULL DEFAULT 8.0")
}
})
...
})
.build()
}
注意:
- 降級(jí)則調(diào)用:
fallbackToDestructiveMigrationOnDowngrade() 來(lái)指定在降級(jí)時(shí)刪除 DB草冈,也可以像上述那樣指定 drop column 來(lái)進(jìn)行數(shù)據(jù)遷移她奥。 - 如果想要遷移數(shù)據(jù),無(wú)論是升級(jí)還是降級(jí)怎棱,必須要給 @Database 的 version 指定正確的目標(biāo)版本哩俭。Migration 遷移處理的起始版本以及實(shí)際的遷移處理 migrate() 都必不可少。
事務(wù)處理
當(dāng)我們的 DB 操作需要保持一致性拳恋,或者查詢關(guān)聯(lián)性結(jié)果的時(shí)候需要保證事務(wù)處理凡资。Room 提供了 @Transaction 注解幫助我們快速實(shí)現(xiàn)這個(gè)需求,它將確保注解內(nèi)的方法運(yùn)行在同一個(gè)事務(wù)模式谬运。
@Dao
public interface MovieDao {
@Transaction
default void insetNewAndDeleteOld(Movie newMovie, Movie oldMovie) {
insert(newMovie);
delete(oldMovie);
}
}
需要注意的是隙赁,事務(wù)處理比較占用性能,避免在事務(wù)處理的方法內(nèi)執(zhí)行耗時(shí)邏輯梆暖。
另外伞访,@Inset、@Delete 和 @Update 的處理自動(dòng)在事務(wù)模式進(jìn)行處理轰驳,無(wú)需增加 @Transaction 注解厚掷。
public long[] insert(final Movie... movies) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
long[] _result = __insertionAdapterOfMovie.insertAndReturnIdsArray(movies);
__db.setTransactionSuccessful();
return _result;
} finally {
__db.endTransaction();
}
}
上面的源碼也啟發(fā)我們可以手動(dòng)執(zhí)行事務(wù)處理弟灼,一般來(lái)說(shuō)不需要,取決于具體情況蝗肪。
RoomDatabase 的 beginTransaction() 和 endTransaction() 不推薦外部使用了,可以采用封裝好的 runInTransaction() 實(shí)現(xiàn)蠕趁。
db.runInTransaction(Runnable {
val database = db.getOpenHelper().getWritableDatabase();
val contentValues = ContentValues()
contentValues.put("movie_name", newMovie.getName())
contentValues.put("actor_name", newMovie.getActor())
contentValues.put("post_year", newMovie.getYear())
contentValues.put("review_score", newMovie.getScore())
database.insert("movie", SQLiteDatabase.CONFLICT_ABORT, contentValues)
database.delete("movie", "id = " + oldMovie.getId(), null)
})
原理淺談
簡(jiǎn)要介紹下 Room 的部分實(shí)現(xiàn)原理薛闪。因篇幅有限只展示關(guān)鍵流程,感興趣者可自行探究具體代碼俺陋。
RoomDatabase 的創(chuàng)建
RoomDatabase$Builder 的 build() 調(diào)用后便通過(guò)反射創(chuàng)建了 @Databse 注解聲明的 RoomDatabase 實(shí)例 XXX_Impl豁延。
SupportSQLiteDatabase 的創(chuàng)建
SupportSQLiteDatabase 是模仿 SQLiteDatabase 作成的接口,供 Room 框架內(nèi)部對(duì) DB 進(jìn)行操作腊状。由 FrameworkSQLiteDatabase 實(shí)現(xiàn)诱咏,其將通過(guò)內(nèi)部持有的 SQLiteDatabase 實(shí)例,代理 DB 操作缴挖。
SupportSQLiteDatabase 的創(chuàng)建由增刪改查等 DB 操作觸發(fā)袋狞,需要經(jīng)歷 DB 的創(chuàng)建,表的創(chuàng)建映屋,表的初始化苟鸯,升降級(jí)以及打開(kāi)等過(guò)程。
創(chuàng)建 DB 文件
創(chuàng)建表
初始化表
升級(jí)表
DB 文件已經(jīng)存在并且版本和目標(biāo)版本不一致的話棚点,將執(zhí)行數(shù)據(jù)遷移早处。但如果遷移處理未配置或者執(zhí)行失敗了便將刪除 DB 并執(zhí)行相應(yīng)的回調(diào)。
打開(kāi)表
DB 的創(chuàng)建或升級(jí)都正常完成后將回調(diào) onOpen()瘫析。
注意
Room 框架的使用過(guò)程中遇到了些容易出錯(cuò)的地方砌梆,需要格外留意。
- RoomDatabase 的實(shí)例建議采用單例模式管理
- 不要在 UI 線程執(zhí)行 DB 操作贬循,否則發(fā)生異常:
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
通過(guò)調(diào)用:
allowMainThreadQueries() 可以回避咸包,但不推薦。 - 不要在 Callback#onCreate() 里同步執(zhí)行 insert 等 DB 處理杖虾,否則將阻塞 DB 實(shí)例的初始化并發(fā)生異常: getDatabase called recursively诉儒。
- @Entity 注解類不要提供多個(gè)構(gòu)造函數(shù),使用 @Ignore 可以回避亏掀。
- Callback#onCreate() 并非由 RoomDatabase$Builder#build() 觸發(fā)忱反,而是由具體的增刪改查操作觸發(fā),切記滤愕。
結(jié)語(yǔ)
通過(guò)上述的實(shí)戰(zhàn)和原理介紹可以看出温算,Room 的本質(zhì)是在 SQLite 的基礎(chǔ)上進(jìn)行封裝的抽象層,通過(guò)一系列注解讓用戶能夠更簡(jiǎn)便的使用 SQLite间影。正因?yàn)榇俗⒏停邆淞艘恍﹥?yōu)勢(shì),值得開(kāi)發(fā)者大膽使用。
聲明注解便能完成接口的定義巩割,易上手
編譯階段將驗(yàn)證注解里聲明的 SQL 語(yǔ)句裙顽,提高了開(kāi)發(fā)效率
支持使用 RxJava2,LiveData 以及 Flow 進(jìn)行異步查詢
相較其他數(shù)據(jù)庫(kù)框架 SQL 執(zhí)行效率更高
Demo參見(jiàn)Github-JetpackDemo