即學(xué)即用Android Jetpack - Room

前言

即學(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ò)GreenDaoDBFlow等優(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)ER圖

我們的目標(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)缭受,這里還有如何配合LiveDataRxJava 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ù)組米者,還可以配合LiveDataRxJava 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)建AppDataBasexxxDao的實(shí)現(xiàn)類(lèi)酸钦。

第五步 簡(jiǎn)要封裝

這里有必要提醒一下怪得,在不使用LiveDataRxJava的前提下,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é)

總結(jié)

Room作為谷歌的官方數(shù)據(jù)庫(kù)框架醉箕,優(yōu)點(diǎn)和缺點(diǎn)都十分明顯钾腺。到此,Room的學(xué)習(xí)到此就結(jié)束了讥裤。本人水平有限放棒,難免有誤,歡迎指正己英。
Over~

參考文章:

《Android Room 框架學(xué)習(xí)》
《7 Steps To Room》

??如果覺(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)和教程這里全都有!》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末损肛,一起剝皮案震驚了整個(gè)濱河市厢破,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌治拿,老刑警劉巖摩泪,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異劫谅,居然都是意外死亡见坑,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)捏检,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)荞驴,“玉大人,你說(shuō)我怎么就攤上這事贯城⌒苈ィ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵能犯,是天一觀的道長(zhǎng)鲫骗。 經(jīng)常有香客問(wèn)我犬耻,道長(zhǎng),這世上最難降的妖魔是什么挎峦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任香追,我火速辦了婚禮,結(jié)果婚禮上坦胶,老公的妹妹穿的比我還像新娘透典。我一直安慰自己,他們只是感情好顿苇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布峭咒。 她就那樣靜靜地躺著,像睡著了一般纪岁。 火紅的嫁衣襯著肌膚如雪凑队。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天幔翰,我揣著相機(jī)與錄音漩氨,去河邊找鬼。 笑死遗增,一個(gè)胖子當(dāng)著我的面吹牛叫惊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播做修,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼霍狰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了饰及?” 一聲冷哼從身側(cè)響起蔗坯,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎燎含,沒(méi)想到半個(gè)月后宾濒,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屏箍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年绘梦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铣除。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鹦付,靈堂內(nèi)的尸體忽然破棺而出尚粘,到底是詐尸還是另有隱情,我是刑警寧澤敲长,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布郎嫁,位于F島的核電站秉继,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏泽铛。R本人自食惡果不足惜尚辑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盔腔。 院中可真熱鬧杠茬,春花似錦、人聲如沸弛随。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)舀透。三九已至栓票,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間愕够,已是汗流浹背走贪。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惑芭,地道東北人坠狡。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像强衡,于是被迫代替她去往敵國(guó)和親擦秽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容