Android Jetpack Room的详细教程

6,179 阅读4分钟

1. 简介

自从Google推出了Room,我们可以优雅的使用sqlite数据库。
当然用Room+Paging的组合,代码写起来飞起来 关于Room+Paging的使用方法请参考下面的教程。
Paging在Android中的应用: juejin.cn/post/684490…

在App的build.gradle的文件中添加Room库的引用。

    def room_version = "2.2.5"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-rxjava2:$room_version"
    implementation "androidx.room:room-testing:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

2. 创建Entity

首先我们要创建一个data class作为数据库的Entity class。 然后使用下面介绍的注解添加关于数据库的额外信息。

@Entity

在想创建的entity上面添加@Entity的注解。

@Entity(tableName = "users_table")
data class User(...)

还可以根据需求在@Entity后面可以添加很多参数。

  1. tableName, 可以为Table起名字。示例,tableName = "users_table"
  2. primaryKeys, 可以为Table指定主键。示例,primaryKeys = {"firstName", "lastName"}
  3. Index, 为了加快数据的查询速度吗,可以添加索引。示例,indices = {@Index("lastName")
  4. unique,在索引当中需要确保两个列中的信息的唯一性时可以添加。示例indices = {@Index(value = {"first_name", "last_name"},unique = true)}

@PrimaryKey

@PrimaryKey注解跟上面@Entity中的primaryKeys的作用是一样的。 针对自动增加的主键,还可以添加autoGenerate=true

@PrimaryKey(autoGenerate = true)
val id: Int? = null,

@ColumnInfo

为entity中的属性添加列表名字name="name"

@ColumnInfo(name = "birthday")
val birthday: String

@Ignore

如果在entity中存在不想存入数据库的属性,可以添加忽略的注解@Ignore

@Ignore
val nationality: String

@Embedded

如果在Entity类里面如果有其他的对象,可以使用@Embedded进行嵌套。当然要被嵌套的对象也需要是被@Entity加上注解的Entity类。

@Embedded
val address: Address

3. 创建Dao

@Dao

Dao的全称是Data Access Object,是数据访问对象是一个面向对象的数据库接口。在一般开发中需要单独抽象出一个Dao。 在Room中需要创建interface,并添加@Dao注解。

@Dao
interface UserDao {}

@Query

需要在实现查询的方法上面添加@Query,并加上要查询的SQL语句。

@Query("SELECT * FROM users_table where first_name = :firstName")
suspend fun getUserByFirstName(firstName:String): List<User>

如果方法中有传入的参数需要在SQL查询语句中使用,可以用:parameter来绑定参数。

@Insert

要在数据库中插入数据时需要加上@Insert的注解,但是不需要加上SQL语句。 需要把插入数据库的对象作为参数传入到函数中。

@Insert
suspend fun insertUser(users: List<User>)

@Delete

要在数据库中删除数据时需要加上@Delete的注解,但是不需要加上SQL语句。
需要把删除的对象作为参数传入到函数中。

@Delete
suspend fun deleteUser(users: List<User>)

@Update

要在数据库中更新数据时需要加上@Update的注解,但是不需要加上SQL语句。 需要把更新的对象作为参数传入到函数中。

@Update
public void updateUsers(User... user);

@Transaction

可以使用@Transaction的注解,添加SQLite的事务。

@Transaction
suspend fun deleteAllAndInsertUser(users: List<User>) {
    deleteUser(getAll())
    insertUser(users)
}

整体代码

@Dao
interface UserDao {

    @Query("SELECT * FROM users_table")
    suspend fun getAll(): List<User>

    @Insert
    suspend fun insertUser(user: User)
    
    @Update
    public void updateUsers(User... user);

    @Delete
    suspend fun deleteUser(users: List<User>)

    @Transaction
    suspend fun deleteAllAndInsertUser(users: List<User>) {
        deleteUser(getAll())
        users.forEach { insertUser(it) }
    }
}

4. 创建RoomDatabase

示例代码如下:

@Database(version = 1, entities = [User::class])  // 添加Database的注解,还需要添加版本信息和entities信息
@TypeConverters(Converter::class)  // 添加TypeConverter的注解
abstract class AppDatabase : RoomDatabase() {

    companion object {
        private const val DB_NAME = "user_database.db"

        private var INSTANCE: AppDatabase? = null
        private var lock = Any()

        fun getInstance(context: Context): AppDatabase {
            synchronized(lock) {
                if (INSTANCE == null) {
                    // 第一个参数是Context, 第二个参数是DB的class,第三个参数是DB名字
                    INSTANCE =
                        Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME).build()
                }

                return INSTANCE!!
            }
        }

        fun destroyInstance() {
            INSTANCE = null
        }
    }

    abstract fun getUserDao(): UserDao
}

数据库类需要继承RoomDatabase,同时必须声明的是抽象类。 还有需要注意的一点是,创建数据库的实例对性能的一样特别大,所以不能重复创建。针对这个问题普遍的做法是把创建实例的方法设计成Singleton模式

@Database

需要给数据库的类上面加上@Database的注解。同时还需要不上下列信息。

  1. version,指定数据库的版本。
  2. entities,需要指定所有的entity类。
@Database(version = 1, entities = [User::class])

@TypeConverter

Room是支持字符串和整型等的基本数据类型,但是遇到其他类型的数据类型时,我们就需要用@TypeConverter来进行数据类型的转换。比如User里面的生日的属性是Date时,就需要进行数据类型的转换。

class Converter {
    @TypeConverter
    fun longToDate(timeStamp: Long?): Date? {
        if (timeStamp == null) return null
        return Date(timeStamp)
    }

    @TypeConverter
    fun dateToLong(date: Date?): Long? {
        if (date == null) return null
        return date.time
    }
}

5. 数据库升级,Migration

当App已经发布以后遇到需要更新和改变数据库结构时,就需要用到数据库的数据迁移。Room为我们提供了数据库升级的功能。

    //  从版本1升级到版本2
    val MIGRATION_1_2 = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
                    "PRIMARY KEY(`id`))")
        }
    }

    // 从版本2升级到版本3
    val MIGRATION_2_3 = object : Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
        }
    }

    // 添加版本升级到Room的builder中
    Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
            .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
    

6. Github

Github: github.com/HyejeanMOON…

其他教程:
Android的属性动画(Property Animation)详细教程: juejin.cn/post/684490…
Android ConstraintLayout的易懂教程: juejin.cn/post/684490…
在RecyclerView中可以应对多个ViewType的库--Groupie: juejin.cn/post/684490…
Google的MergeAdapter的使用: juejin.cn/post/684490…
Paging在Android中的应用: juejin.cn/post/684490…
Android UI测试之Espresso: juejin.cn/post/684490…
Android WorkManager的使用: juejin.cn/post/684490…