即学即用Android Jetpack - Room

前言

即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第四篇。

我们在日常的工作中,免不了和数据打交道,因此,存储数据便是一项很重要的工作,在此之前,我使用过GreenDaoDBFlow等优秀的ORM数据库框架,但是,这些框架都不是谷歌官方的,现在,我们有了谷歌官方的Room数据库框架,看看它能够给我们带来什么?

语言:Kotlin
Demo地址:https://github.com/mCyp/Hoo

目录

目录

一、介绍

友情提示
官方文档:Room
谷歌实验室:官方教程
SQL语法: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.

简单来说:Room是一个基于SQLite的强大数据库框架。

1. Room优点

可是它强大在哪里呢?

  • 使用编译时注解,能够对@Query@Entity里面的SQL语句等进行验证。
  • 与SQL语句的使用更加贴近,能够降低学习成本。
  • RxJava 2的支持(大部分都Android数据库框架都支持),对LiveData的支持。
  • @Embedded能够减少表的创建。

二、实战

我们的目标结构:

目标ER图

我们的目标挺简单的,三张表,用户表鞋表收藏记录表用户表鞋表存在多对多的关系,确定好目标之后,正式开始我们的实战之旅了。

第一步 添加依赖

模块层的build.gradle添加:

apply plugin: 'kotlin-kapt'

dependencies {
    // ... 省略无关

    // 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"
}

项目下的build.gradle添加:

ext {
   roomVersion = '2.1.0-alpha06'
   //... 省略无关
}

第二步 创建表(实体)

这里我们以用户表收藏记录表为例,用户表

/**
 * 用户表
 */
@Entity(tableName = "user")
data class User(
    @ColumnInfo(name = "user_account") val account: String // 账号
    , @ColumnInfo(name = "user_pwd") val pwd: String // 密码
    , @ColumnInfo(name = "user_name") val name: String
    , @Embedded val address: Address // 地址
    , @Ignore val state: Int // 状态只是临时用,所以不需要存储在数据库中
) {
    @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 // 外键 用户的id
    , @ColumnInfo(name = "fav_date") val date: Date // 创建日期

) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id: Long = 0
}

对于其中的一些注解,你可能不是很明白,解释如下:

注解 说明
@Entity 声明这是一个表(实体),主要参数:tableName-表名、foreignKeys-外键、indices-索引。
@ColumnInfo 主要用来修改在数据库中的字段名。
@PrimaryKey 声明该字段主键并可以声明是否自动创建。
@Ignore 声明某个字段只是临时用,不存储在数据库中。
@Embedded 用于嵌套,里面的字段同样会存储在数据库中。

最后一个可能解释的不明,我们直接看例子就好,如我们的用户表,里面有一个变量address,它是一个Address类:

/**
 * 地址
 */
data class Address(
    val street:String,val state:String,val city:String,val postCode:String
)

通常情况下,如果我们想这些字段存储在数据库中,有两种方法:

  • 重新创建一个表进行一对一关联,但是多创建一个表显得麻烦。
  • 在用户表中增加字段,可是使用第二种方式映射出来的对象又显得不那么面向对象。

@Embedded解决了第二种方式中问题,既不需要多创建一个表,又能将数据库中映射的对象看上去面向对象。

放上Shoe表,后面会用到:

/**
 * 鞋表
 */
@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 // 价格
    , @ColumnInfo(name = "shoe_brand") val brand: String // 品牌
    , @ColumnInfo(name = "shoe_imgUrl") val imageUrl: String // 图片地址
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id: Long = 0
}

第三步 创建Dao

有了数据库,我们现在需要建立数据处理的方法,就是数据的增删查改。如果想声明一个Dao,只要在抽象类或者接口加一个@Dao注解就行。

@Insert注解声明当前的方法为新增的方法,并且可以设置当新增冲突的时候处理的方法。

用到增的地方有很多,Demo中本地用户的注册、鞋子集合的新增和收藏的新增,这里我们选择具有代表性的shoeDao

/**
 * 鞋子的方法
 */
@Dao
interface ShoeDao {
    // 省略...
    // 增加一双鞋子
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertShoe(shoe: Shoe)

    // 增加多双鞋子
    // 除了List之外,也可以使用数组
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertShoes(shoes: List<Shoe>)
}

@Delete注解声明当前的方法是一个删除方法。

用法与@Insert类似,同样以ShoeDao为例:

/**
 * 鞋子的方法
 */
@Dao
interface ShoeDao {
    // 省略...
    // 删除一双鞋子
    @Delete
    fun deleteShoe(shoe: Shoe)

    // 删除多个鞋子
    // 参数也可以使用数组
    @Delete
    fun deleteShoes(shoes:List<Shoe>)
}

@Update注解声明当前方法是一个更新方法

用法同样与@Insert类似:

/**
 * 鞋子的方法
 */
@Dao
interface ShoeDao {
    // 省略...
    // 更新一双鞋
    @Update
    fun updateShoe(shoe:Shoe)

    // 更新多双鞋
    // 参数也可以是集合
    @Update
    fun updateShoes(shoes:Array<Shoe>)
}

增删改是如此的简单,查是否也是如此的简单呢?答案是否定的,Room的查很接近原生的SQL语句。@Query注解不仅可以声明这是一个查询语句,也可以用来删除和修改,不可以用来新增。

简单查询
除了简单查询,这里还有如何配合LiveDataRxJava 2

@Dao
interface ShoeDao {

    // 查询一个
    @Query("SELECT * FROM shoe WHERE id=:id")
    fun findShoeById(id: Long): Shoe?

    // 查询多个 通过品牌查询多款鞋 
    @Query("SELECT * FROM shoe WHERE shoe_brand=:brand")
    fun findShoesByBrand(brand: String): List<Shoe>

    // 模糊查询 排序 同名鞋名查询鞋
    @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 通过Id查询单款鞋子
    @Query("SELECT * FROM shoe WHERE id=:id")
    fun findShoeByIdLD(id: Long): LiveData<Shoe>

    // 配合RxJava 通过Id查询单款鞋子
    @Query("SELECT * FROM shoe WHERE id=:id")
    fun findShoeByIdRx(id: Long): Flowable<Shoe>

    // 省略...
}

查询多个的时候,可以返回List数组,还可以配合LiveDataRxJava 2。当然,更多的查询可以参考SQL语法。

复合查询
因为本Demo并没有引入RxJava 2,所以本文基本以LiveData为例。

@Dao
interface ShoeDao {
    // 省略...
    // 根据收藏结合 查询用户喜欢的鞋的集合 内联查询
    @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>>
}

第四步 创建数据库

创建一个数据库对象是一件非常消耗资源,使用单例可以避免过多的资源消耗。

/**
 * 数据库文件
 */
@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注解声明当前是一个数据库文件,注解中entities变量声明数据库中的表(实体),以及其他的例如版本等变量。同时,获取的Dao也必须在数据库类中。完成之后,点击build目录下的make project,系统就会自动帮我创建AppDataBasexxxDao的实现类。

第五步 简要封装

这里有必要提醒一下,在不使用LiveDataRxJava的前提下,Room的操作是不可以放在主线程中的。这里选择比较有示范性的UserRepository

/**
 * 用户处理仓库
 */
class UserRepository private constructor(private val userDao: UserDao) {
    //...

    /**
     * 登录用户 本地数据库的查询
     */
    fun login(account: String, pwd: String):LiveData<User?>
            = userDao.login(account,pwd)

    /**
     * 注册一个用户 本地数据库的新增
     */
    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()方法是一个普通方法,所以它需要在子线程使用,如代码所见,通过协程实现。login()是配合LiveData使用的,不需要额外创建子线程,但是他的核心数据库操作还是在子线程中实现的。

现在,你就可以愉快的操作本地数据库了。

三、更多

除了上面的基本使用技巧之外,还有一些不常用的知识需要我们了解。

1. 类型转换器

我们都知道,SQLite支持的类型有:NULL、INTEGER、REAL、TEXT和BLOB,对于Data类,SQLite还可以将其转化为TEXT、REAL或者INTEGER,如果是Calendar类呢?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 }
}

然后在数据库声明的时候,加上@TypeConverters(Converters::class)就行了:

@Database(...)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    //...
}

2. 数据库迁移

Room的数据库迁移实在是麻烦,同查询一样,需要使用到SQL语句,但比查询麻烦的多。感兴趣的各位可以参考下面的文章:

《Understanding migrations with Room》 谷歌工程师写的
《Android Room 框架学习》

四、总结

总结

Room作为谷歌的官方数据库框架,优点和缺点都十分明显。到此,Room的学习到此就结束了。本人水平有限,难免有误,欢迎指正。
Over~

参考文章:

《Android Room 框架学习》
《7 Steps To Room》

🚀如果觉得本文不错,可以查看Android Jetpack系列的其他文章:

第一篇:《即学即用Android Jetpack - Navigation》
第二篇:《即学即用Android Jetpack - Data Binding》
第三篇:《即学即用Android Jetpack - ViewModel & LiveData》
第五篇:《即学即用Android Jetpack - Paging》
第六篇:《即学即用Android Jetpack - WorkManger》
第七篇:《即学即用Android Jetpack - Startup》
第八篇:《即学即用Android Jetpack - Paging 3》
项目总结篇:《学习Android Jetpack? 实战和教程这里全都有!》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容