前言
即學(xué)即用Android Jetpack系列Blog的目的是通過(guò)學(xué)習(xí)Android Jetpack
完成一個(gè)簡(jiǎn)單的Demo,本文是即學(xué)即用Android Jetpack系列Blog的第四篇祟敛。
我們?cè)谌粘5墓ぷ髦泄萏獠涣撕蛿?shù)據(jù)打交道埠巨,因此,存儲(chǔ)數(shù)據(jù)便是一項(xiàng)很重要的工作印蔬,在此之前勋桶,我使用過(guò)GreenDao、DBFlow等優(yōu)秀的ORM數(shù)據(jù)庫(kù)框架,但是例驹,這些框架都不是谷歌官方的捐韩,現(xiàn)在,我們有了谷歌官方的Room
數(shù)據(jù)庫(kù)框架眠饮,看看它能夠給我們帶來(lái)什么?
語(yǔ)言:Kotlin
Demo地址:https://github.com/mCyp/Hoo
目錄
一奥帘、介紹
友情提示
官方文檔:Room
谷歌實(shí)驗(yàn)室:官方教程
SQL語(yǔ)法:SQLite教程
谷歌官方的介紹:
The Room persistence library provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite.
簡(jiǎn)單來(lái)說(shuō):Room是一個(gè)基于SQLite的強(qiáng)大數(shù)據(jù)庫(kù)框架。
1. Room優(yōu)點(diǎn)
可是它強(qiáng)大在哪里呢?
- 使用編譯時(shí)注解,能夠?qū)?code>@Query和
@Entity
里面的SQL語(yǔ)句等進(jìn)行驗(yàn)證。 - 與SQL語(yǔ)句的使用更加貼近檀咙,能夠降低學(xué)習(xí)成本。
- 對(duì)
RxJava 2
的支持(大部分都Android數(shù)據(jù)庫(kù)框架都支持)校套,對(duì)LiveData
的支持膳算。 -
@Embedded
能夠減少表的創(chuàng)建蜘拉。
二持寄、實(shí)戰(zhàn)
我們的目標(biāo)結(jié)構(gòu):
我們的目標(biāo)挺簡(jiǎn)單的,三張表,
用戶(hù)表
、鞋表
和收藏記錄表
湃交,用戶(hù)表
和鞋表
存在多對(duì)多的關(guān)系,確定好目標(biāo)之后温圆,正式開(kāi)始我們的實(shí)戰(zhàn)之旅了饱搏。
第一步 添加依賴(lài)
模塊層的build.gradle
添加:
apply plugin: 'kotlin-kapt'
dependencies {
// ... 省略無(wú)關(guān)
// room
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
}
項(xiàng)目下的build.gradle
添加:
ext {
roomVersion = '2.1.0-alpha06'
//... 省略無(wú)關(guān)
}
第二步 創(chuàng)建表(實(shí)體)
這里我們以用戶(hù)表
和收藏記錄表
為例恨锚,用戶(hù)表
:
/**
* 用戶(hù)表
*/
@Entity(tableName = "user")
data class User(
@ColumnInfo(name = "user_account") val account: String // 賬號(hào)
, @ColumnInfo(name = "user_pwd") val pwd: String // 密碼
, @ColumnInfo(name = "user_name") val name: String
, @Embedded val address: Address // 地址
, @Ignore val state: Int // 狀態(tài)只是臨時(shí)用雨让,所以不需要存儲(chǔ)在數(shù)據(jù)庫(kù)中
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id: Long = 0
}
收藏記錄表
:
/**
* 喜歡的球鞋
*/
@Entity(
tableName = "fav_shoe"
, foreignKeys = [ForeignKey(entity = Shoe::class, parentColumns = ["id"], childColumns = ["shoe_id"])
, ForeignKey(entity = User::class, parentColumns = ["id"], childColumns = ["user_id"])
],indices = [Index("shoe_id")]
)
data class FavouriteShoe(
@ColumnInfo(name = "shoe_id") val shoeId: Long // 外鍵 鞋子的id
, @ColumnInfo(name = "user_id") val userId: Long // 外鍵 用戶(hù)的id
, @ColumnInfo(name = "fav_date") val date: Date // 創(chuàng)建日期
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id: Long = 0
}
對(duì)于其中的一些注解狸相,你可能不是很明白岩齿,解釋如下:
注解 | 說(shuō)明 |
---|---|
@Entity |
聲明這是一個(gè)表(實(shí)體)菇用,主要參數(shù):tableName -表名耐量、foreignKeys -外鍵山叮、indices -索引往衷。 |
@ColumnInfo |
主要用來(lái)修改在數(shù)據(jù)庫(kù)中的字段名。 |
@PrimaryKey |
聲明該字段主鍵并可以聲明是否自動(dòng)創(chuàng)建。 |
@Ignore |
聲明某個(gè)字段只是臨時(shí)用,不存儲(chǔ)在數(shù)據(jù)庫(kù)中。 |
@Embedded |
用于嵌套一也,里面的字段同樣會(huì)存儲(chǔ)在數(shù)據(jù)庫(kù)中塘秦。 |
最后一個(gè)可能解釋的不明菱皆,我們直接看例子就好疲陕,如我們的用戶(hù)表
式廷,里面有一個(gè)變量address
,它是一個(gè)Address
類(lèi):
/**
* 地址
*/
data class Address(
val street:String,val state:String,val city:String,val postCode:String
)
通常情況下妻导,如果我們想這些字段存儲(chǔ)在數(shù)據(jù)庫(kù)中逛绵,有兩種方法:
- 重新創(chuàng)建一個(gè)表進(jìn)行一對(duì)一關(guān)聯(lián),但是多創(chuàng)建一個(gè)表顯得麻煩倔韭。
- 在用戶(hù)表中增加字段术浪,可是使用第二種方式映射出來(lái)的對(duì)象又顯得不那么面向?qū)ο蟆?/li>
@Embedded
解決了第二種方式中問(wèn)題,既不需要多創(chuàng)建一個(gè)表寿酌,又能將數(shù)據(jù)庫(kù)中映射的對(duì)象看上去面向?qū)ο蟆?/p>
放上Shoe
表胰苏,后面會(huì)用到:
/**
* 鞋表
*/
@Entity(tableName = "shoe")
data class Shoe(
@ColumnInfo(name = "shoe_name") val name: String // 鞋名
, @ColumnInfo(name = "shoe_description") val description: String// 描述
, @ColumnInfo(name = "shoe_price") val price: Float // 價(jià)格
, @ColumnInfo(name = "shoe_brand") val brand: String // 品牌
, @ColumnInfo(name = "shoe_imgUrl") val imageUrl: String // 圖片地址
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id: Long = 0
}
第三步 創(chuàng)建Dao
有了數(shù)據(jù)庫(kù),我們現(xiàn)在需要建立數(shù)據(jù)處理的方法醇疼,就是數(shù)據(jù)的增刪查改硕并。如果想聲明一個(gè)Dao
,只要在抽象類(lèi)或者接口加一個(gè)@Dao
注解就行秧荆。
增
@Insert
注解聲明當(dāng)前的方法為新增的方法倔毙,并且可以設(shè)置當(dāng)新增沖突的時(shí)候處理的方法。
用到增的地方有很多乙濒,Demo中本地用戶(hù)的注冊(cè)陕赃、鞋子集合的新增和收藏的新增,這里我們選擇具有代表性的shoeDao
:
/**
* 鞋子的方法
*/
@Dao
interface ShoeDao {
// 省略...
// 增加一雙鞋子
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertShoe(shoe: Shoe)
// 增加多雙鞋子
// 除了List之外颁股,也可以使用數(shù)組
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertShoes(shoes: List<Shoe>)
}
刪
@Delete
注解聲明當(dāng)前的方法是一個(gè)刪除方法么库。
用法與@Insert
類(lèi)似,同樣以ShoeDao
為例:
/**
* 鞋子的方法
*/
@Dao
interface ShoeDao {
// 省略...
// 刪除一雙鞋子
@Delete
fun deleteShoe(shoe: Shoe)
// 刪除多個(gè)鞋子
// 參數(shù)也可以使用數(shù)組
@Delete
fun deleteShoes(shoes:List<Shoe>)
}
改
@Update
注解聲明當(dāng)前方法是一個(gè)更新方法
用法同樣與@Insert
類(lèi)似:
/**
* 鞋子的方法
*/
@Dao
interface ShoeDao {
// 省略...
// 更新一雙鞋
@Update
fun updateShoe(shoe:Shoe)
// 更新多雙鞋
// 參數(shù)也可以是集合
@Update
fun updateShoes(shoes:Array<Shoe>)
}
查
增刪改是如此的簡(jiǎn)單甘有,查是否也是如此的簡(jiǎn)單呢廊散?答案是否定的,Room
的查很接近原生的SQL語(yǔ)句梧疲。@Query
注解不僅可以聲明這是一個(gè)查詢(xún)語(yǔ)句允睹,也可以用來(lái)刪除和修改运准,不可以用來(lái)新增。
簡(jiǎn)單查詢(xún)
除了簡(jiǎn)單查詢(xún)缭受,這里還有如何配合LiveData
和RxJava 2
胁澳。
@Dao
interface ShoeDao {
// 查詢(xún)一個(gè)
@Query("SELECT * FROM shoe WHERE id=:id")
fun findShoeById(id: Long): Shoe?
// 查詢(xún)多個(gè) 通過(guò)品牌查詢(xún)多款鞋
@Query("SELECT * FROM shoe WHERE shoe_brand=:brand")
fun findShoesByBrand(brand: String): List<Shoe>
// 模糊查詢(xún) 排序 同名鞋名查詢(xún)鞋
@Query("SELECT * FROM shoe WHERE shoe_name LIKE :name ORDER BY shoe_brand ASC")
fun findShoesByName(name:String):List<Shoe>
// 配合LiveData 返回所有的鞋子
@Query("SELECT * FROM shoe")
fun getAllShoesLD(): LiveData<List<Shoe>>
// 配合LiveData 通過(guò)Id查詢(xún)單款鞋子
@Query("SELECT * FROM shoe WHERE id=:id")
fun findShoeByIdLD(id: Long): LiveData<Shoe>
// 配合RxJava 通過(guò)Id查詢(xún)單款鞋子
@Query("SELECT * FROM shoe WHERE id=:id")
fun findShoeByIdRx(id: Long): Flowable<Shoe>
// 省略...
}
查詢(xún)多個(gè)的時(shí)候,可以返回List
和數(shù)組
米者,還可以配合LiveData
和RxJava 2
韭畸。當(dāng)然,更多的查詢(xún)可以參考SQL語(yǔ)法蔓搞。
復(fù)合查詢(xún)
因?yàn)楸綝emo并沒(méi)有引入RxJava 2
胰丁,所以本文基本以LiveData
為例。
@Dao
interface ShoeDao {
// 省略...
// 根據(jù)收藏結(jié)合 查詢(xún)用戶(hù)喜歡的鞋的集合 內(nèi)聯(lián)查詢(xún)
@Query(
"SELECT shoe.id,shoe.shoe_name,shoe.shoe_description,shoe.shoe_price,shoe.shoe_brand,shoe.shoe_imgUrl " +
"FROM shoe " +
"INNER JOIN fav_shoe ON fav_shoe.shoe_id = shoe.id " +
"WHERE fav_shoe.user_id = :userId"
)
fun findShoesByUserId(userId: Long): LiveData<List<Shoe>>
}
第四步 創(chuàng)建數(shù)據(jù)庫(kù)
創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)對(duì)象是一件非常消耗資源喂分,使用單例可以避免過(guò)多的資源消耗锦庸。
/**
* 數(shù)據(jù)庫(kù)文件
*/
@Database(entities = [User::class,Shoe::class,FavouriteShoe::class],version = 1,exportSchema = false)
abstract class AppDataBase:RoomDatabase() {
// 得到UserDao
abstract fun userDao():UserDao
// 得到ShoeDao
abstract fun shoeDao():ShoeDao
// 得到FavouriteShoeDao
abstract fun favouriteShoeDao():FavouriteShoeDao
companion object{
@Volatile
private var instance:AppDataBase? = null
fun getInstance(context:Context):AppDataBase{
return instance?: synchronized(this){
instance?:buildDataBase(context)
.also {
instance = it
}
}
}
private fun buildDataBase(context: Context):AppDataBase{
return Room
.databaseBuilder(context,AppDataBase::class.java,"jetPackDemo-database")
.addCallback(object :RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 讀取鞋的集合
val request = OneTimeWorkRequestBuilder<ShoeWorker>().build()
WorkManager.getInstance(context).enqueue(request)
}
})
.build()
}
}
}
@Database
注解聲明當(dāng)前是一個(gè)數(shù)據(jù)庫(kù)文件,注解中entities
變量聲明數(shù)據(jù)庫(kù)中的表(實(shí)體)蒲祈,以及其他的例如版本等變量甘萧。同時(shí),獲取的Dao
也必須在數(shù)據(jù)庫(kù)類(lèi)中梆掸。完成之后扬卷,點(diǎn)擊build
目錄下的make project
,系統(tǒng)就會(huì)自動(dòng)幫我創(chuàng)建AppDataBase
和xxxDao
的實(shí)現(xiàn)類(lèi)酸钦。
第五步 簡(jiǎn)要封裝
這里有必要提醒一下怪得,在不使用LiveData
和RxJava
的前提下,Room
的操作是不可以放在主線程中的卑硫。這里選擇比較有示范性的UserRepository
:
/**
* 用戶(hù)處理倉(cāng)庫(kù)
*/
class UserRepository private constructor(private val userDao: UserDao) {
//...
/**
* 登錄用戶(hù) 本地?cái)?shù)據(jù)庫(kù)的查詢(xún)
*/
fun login(account: String, pwd: String):LiveData<User?>
= userDao.login(account,pwd)
/**
* 注冊(cè)一個(gè)用戶(hù) 本地?cái)?shù)據(jù)庫(kù)的新增
*/
suspend fun register(email: String, account: String, pwd: String):Long {
return withContext(IO) {
userDao.insertUser(User(account, pwd, email))
}
}
companion object {
@Volatile
private var instance: UserRepository? = null
fun getInstance(userDao: UserDao): UserRepository =
// ...
}
}
register()
方法是一個(gè)普通方法汇恤,所以它需要在子線程使用,如代碼所見(jiàn)拔恰,通過(guò)協(xié)程實(shí)現(xiàn)。login()
是配合LiveData
使用的基括,不需要額外創(chuàng)建子線程颜懊,但是他的核心數(shù)據(jù)庫(kù)操作還是在子線程中實(shí)現(xiàn)的。
現(xiàn)在风皿,你就可以愉快的操作本地?cái)?shù)據(jù)庫(kù)了河爹。
三、更多
除了上面的基本使用技巧之外桐款,還有一些不常用的知識(shí)需要我們了解咸这。
1. 類(lèi)型轉(zhuǎn)換器
我們都知道,SQLite支持的類(lèi)型有:NULL魔眨、INTEGER媳维、REAL酿雪、TEXT和BLOB,對(duì)于Data
類(lèi)侄刽,SQLite還可以將其轉(zhuǎn)化為T(mén)EXT指黎、REAL或者INTEGER,如果是Calendar
類(lèi)呢州丹?Room
為你提供了解決方法醋安,使用@TypeConverter
注解,我們使用谷歌官方Demo-SunFlower例子:
class Converters {
@TypeConverter fun calendarToDatestamp(calendar: Calendar): Long = calendar.timeInMillis
@TypeConverter fun datestampToCalendar(value: Long): Calendar =
Calendar.getInstance().apply { timeInMillis = value }
}
然后在數(shù)據(jù)庫(kù)聲明的時(shí)候墓毒,加上@TypeConverters(Converters::class)
就行了:
@Database(...)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
//...
}
2. 數(shù)據(jù)庫(kù)遷移
Room
的數(shù)據(jù)庫(kù)遷移實(shí)在是麻煩吓揪,同查詢(xún)一樣,需要使用到SQL語(yǔ)句所计,但比查詢(xún)麻煩的多柠辞。感興趣的各位可以參考下面的文章:
《Understanding migrations with Room》 谷歌工程師寫(xiě)的
《Android Room 框架學(xué)習(xí)》
四、總結(jié)
Room
作為谷歌的官方數(shù)據(jù)庫(kù)框架醉箕,優(yōu)點(diǎn)和缺點(diǎn)都十分明顯钾腺。到此,Room
的學(xué)習(xí)到此就結(jié)束了讥裤。本人水平有限放棒,難免有誤,歡迎指正己英。Over~
參考文章:
??如果覺(jué)得本文不錯(cuò)间螟,可以查看Android Jetpack
系列的其他文章:
第一篇:《即學(xué)即用Android Jetpack - Navigation》
第二篇:《即學(xué)即用Android Jetpack - Data Binding》
第三篇:《即學(xué)即用Android Jetpack - ViewModel & LiveData》
第五篇:《即學(xué)即用Android Jetpack - Paging》
第六篇:《即學(xué)即用Android Jetpack - WorkManger》
第七篇:《即學(xué)即用Android Jetpack - Startup》
第八篇:《即學(xué)即用Android Jetpack - Paging 3》
項(xiàng)目總結(jié)篇:《學(xué)習(xí)Android Jetpack? 實(shí)戰(zhàn)和教程這里全都有!》