Android Jetpack:Room

官方网址:Room

官方介绍:

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. 使用编译时注解,能够对@Query@Entity里面的SQL语句进行验证
  2. 与SQL语句的使用更加贴近,能够降低学习成本
  3. 对RxJava2支持,对LiveData支持
  4. @Embedded能减少表的创建

2. Demo

目标结构:

三张表:用户表、鞋表和收藏记录表,用户表和鞋表存在多对多的关系。

2.1 步骤1:添加依赖

在model的build.gradel添加:

1
2
3
4
5
6
7
8
9
10
11
apply plugin: 'kotlin-kapt'

dependencies {
......


implementation "androidx.room:room-runtime:2.2.0-alpha01"
implementation "androidx.room:room-ktx:2.2.0-alpha01"
kapt "androidx.room:room-compiler:2.2.0-alpha01"
androidTestImplementation "androidx.room:room-testing:2.2.0-alpha01"
}

2.2 步骤2:创建表(实体)

用户表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.ly.allendemojetpack.db.data

import android.location.Address
import androidx.room.*

/**
* 用户表
*
* @author Liuyang
* @date 2019/8/3
*/

data class (
@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
}

收藏记录表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.ly.allendemojetpack.db.data

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.util.*

/**
* 喜欢的鞋 表
*
* @author Liuyang
* @date 2019/8/3
*/
@Entity(
tableName = "fav_shoe",
foreignKeys = [ForeignKey(entity = Shoe::class, parentColumns = ["id"], childColumns = ["shoe_id"]),
ForeignKey(entity = User::class, parentColumns = ["id"], childColumns = ["user_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:用于嵌套,里面的字段同样会存储在数据库中

最后一个,在User表中有一个变量address,它是一个Address类:

1
2
3
4
5
6
7
8
package com.ly.allendemojetpack.db.data

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

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

  1. 重新创建一个表,进行一对一的关联,但是多创建一个表显得麻烦
  2. 在用户表中增加字段,但是这样映射出来的对象显得 不面向对象

@Embedded就是为了解决上面你的第2个问题,即不多创建一个表,又能将数据库中映射的对象看上去面向对象。

Shoe表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.ly.allendemojetpack.db.data

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

/**
* 鞋子表
*
* @author Liuyang
* @date 2019/8/3
*/
@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 imgUrl: String
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id: Long = 0
}

2.3 步骤3:创建Dao

数据处理的方法,就是数据的增删改查。在抽象类或接口加一个@Dao注解即可。

2.3.1 增

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 鞋子表的方法
*
* @author Liuyang
* @date 2019/8/3
*/
@Dao
interface ShoeDao {
/**
* 选择所有的鞋
*/
@Query("SELECT * FROM shoe")
fun getAllShoes(): LiveData<List<Shoe>>

/**
* 通过id查找鞋子
*/
@Query("SELECT * FROM shoe WHERE id = :id")
fun findShoeById(id: Long): LiveData<Shoe>

/**
* 通过品牌找鞋子
*/
@Query("SELECT * FROM shoe WHERE shoe_brand = :brand")
fun findShoeByBrand(brand: String): LiveData<List<Shoe>>

/**
* 插入一种鞋子
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertShoe(shoe: Shoe)

/**
* 插入多种鞋子
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertShoe(shoes: List<Shoe>)
}

2.3.2 删

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

2.3.3 改

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

2.3.4 查

@Query注解,不仅可以声明这是一个查询语句,也能用来删除和修改,不能用来新增。

  1. 简单查询
    除了简单查询,还能配合LiveDataRxJava。这里使用的是implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    package com.ly.allendemojetpack.db.dao

    import androidx.lifecycle.LiveData
    import androidx.room.Dao
    import androidx.room.Insert
    import androidx.room.OnConflictStrategy
    import androidx.room.Query
    import com.ly.allendemojetpack.db.data.Shoe
    import io.reactivex.Flowable

    /**
    * 鞋子表的方法
    *
    * @author Liuyang
    * @date 2019/8/3
    */
    @Dao
    interface ShoeDao {

    /**
    * 通过id查找鞋子
    */
    @Query("SELECT * FROM shoe WHERE id = :id")
    fun findShoeById(id: Long): Shoe?

    /**
    * 通过品牌找鞋子
    */
    @Query("SELECT * FROM shoe WHERE shoe_brand = :brand")
    fun findShoeByBrand(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

  1. 复合查询
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 根据收藏结合,查询用户喜欢的鞋的集合
    */
    @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>>

2.4 步骤4:创建数据库

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.ly.allendemojetpack.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.ly.allendemojetpack.db.dao.FavouriteShoeDao
import com.ly.allendemojetpack.db.dao.ShoeDao
import com.ly.allendemojetpack.db.dao.UserDao
import com.ly.allendemojetpack.db.data.Shoe
import com.ly.allendemojetpack.db.data.User
import com.ly.allendemojetpack.utils.ShoeWorker

/**
* 数据库文件
*
* @author Liuyang
* @date 2019/8/2
*/
@Database(entities = [User::class, Shoe::class], version = 1, exportSchema = false)
abstract class AppDataBase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun shoeDao(): ShoeDao
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, "jetpack_db")
.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也必须在数据库类中。完成之后,make project一下工程,系统后自动创建AppDataBasexxxDao的实现类。

2.5 步骤5:简单封装

在不使用LiveDataRxJava前提下,Room的操作不能放在主线程中。这里看看UserRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.ly.allendemojetpack.db.repository

import com.ly.allendemojetpack.db.dao.UserDao
import com.ly.allendemojetpack.db.data.User
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext

class UserRepository private constructor(private val userDao: UserDao) {
/**
*用户登录
*/
fun login(account: String, pwd: String) = userDao.login(account, pwd)

/**
* 用户注册
*/
suspend fun register(email: String, account: String, pwd: String): Long {
return withContext(IO) {
userDao.insertUser(User(account, pwd, email))
}
}

/**
* 获取所有用户
*/
fun getAllUsers() = userDao.getAllUsers()

/**
* 通过id获取用户
*/
fun findUserById(id: Long) = userDao.findUserById(id)

companion object {
@Volatile
private var instance: UserRepository? = null

fun getInstance(userDao: UserDao): UserRepository =
instance ?: synchronized(this) {
instance
?: UserRepository(userDao).also {
instance = it
}
}
}
}

register()是一个普通方法,所以需要在子线程中使用,这里通过协程实现。login()是配合LiveData使用的, 不需要额外创建子线程,但是其核心数据库操作还在子线程中实现的。

这时,就可以操作本地数据库了。

3. 其他

3.1 类型转换器

SQLite支持的类型有:NULL、INTEGER、REAL、TEXT和BLOB,对于Data类,SQLite还可以将其转化为TEXT、REAL或者INTEGER,如果是Calendar类呢?Room提供了这一解决方法,使用@TypeConverter注解,谷歌官方示例:android-sunflower

1
2
3
4
5
6
class Converters {
@TypeConverter fun calendarToDatestamp(calendar: Calendar): Long = calendar.timeInMillis

@TypeConverter fun datestampToCalendar(value: Long): Calendar =
Calendar.getInstance().apply { timeInMillis = value }
}

然后在数据库声明的时候,加上@TypeConverter(COnverter::class)即可:

1
2
3
4
5
@Database(...)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
//...
}

3.2 数据库迁移:这个还需要查找资料

Android Room 框架学习