简单易用的ORM数据库包:Room

我们都知道Android数据可持久化有几种实现方式,比如存储成文件(File)、SP等。这次就要说到数据库了。

其实在以前没有Room库的时候,使用本地化数据库要不直接使用SQLite,要不就是使用ORMLite或者LitePal这种第三方库。第三方库不是不好,而是作者可能到后面就不维护了,不维护的后果就是随着Android版本更新,各式各样的bug就会出现。因此官方做ORM映射库就狠关键了。恰好,官方推出了这个名叫Room的Jetpack库。

什么是Room?

官方的介绍是这个样子的:

Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。

处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的用例是缓存相关数据。这样,当设备无法访问网络时,用户仍可在离线状态下浏览相应内容。设备重新连接到网络后,用户发起的所有内容更改都会同步到服务器。

由于 Room 负责为您处理这些问题,因此我们强烈建议您使用 Room(而不是 SQLite)。不过,如果您想直接使用 SQLite API,请参阅使用 SQLite 保存数据

整体的结构图是这样的:

Room Architecture

Room的使用

引入依赖

如果要使用Room,需要先在build.gradlebuild.gradle.kts中引入Room。

// build.gradle
dependencies {
    def room_version = "2.3.0"

    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"

    // optional - RxJava2 support for Room
    implementation "androidx.room:room-rxjava2:$room_version"

    // optional - RxJava3 support for Room
    implementation "androidx.room:room-rxjava3:$room_version"

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // optional - Test helpers
    testImplementation "androidx.room:room-testing:$room_version"

    // optional - Paging 3 Integration
    implementation "androidx.room:room-paging:2.4.0-alpha05"
}
// build.gradle.kts
dependencies {
    def room_version = "2.3.0"

    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor "androidx.room:room-compiler:$room_version"

    // To use Kotlin annotation processing tool (kapt)
    kapt("androidx.room:room-compiler:$room_version")
    // To use Kotlin Symbolic Processing (KSP)
    ksp("androidx.room:room-compiler:$room_version")

    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$room_version")

    // optional - RxJava2 support for Room
    implementation "androidx.room:room-rxjava2:$room_version"

    // optional - RxJava3 support for Room
    implementation "androidx.room:room-rxjava3:$room_version"

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")

    // optional - Paging 3 Integration
    implementation("androidx.room:room-paging:2.4.0-alpha05")
}

针对Kotlin环境,我强烈建议使用KSP,而不是KAPT!

针对Kotlin环境,我强烈建议使用KSP,而不是KAPT!

针对Kotlin环境,我强烈建议使用KSP,而不是KAPT!

Room主要包括三个组成部分:

  • 数据库:由注解@Database注解的继承自RoomDatabase的抽象类。这个抽象类需要在注解出定义数据库版本号、用到过的数据实体(Entities),在抽象类内部需要定义返回值为Dao接口的函数;
  • 实体:由注解@Entity注解的data class(Java则是JavaBean)类,内部成员必须有一个由@PrimaryKey注解的主键;
  • 数据访问对象:英文全名为Database Access Object,简称DAO,由注解@Dao注解的接口,内部实现访问数据库的方法,每条方法需要用对应的操作进行注解。

定义Entity

那么继续来使用一下吧,首先先来说明一下需求:登录程序需要多用户,而每个用户的用户名、密码、套餐详情需要存储到数据库中。

因此这里先定义Entity——User。


@Entity(tableName = "user")
data class User(
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0,
    var name: String = "",
    var password: String = "",
    var pack: Int = 30
)

其中,注解@PrimaryKey接受一个autoGenerate的参数,当插入到数据库中的时候数据库会自己给这个被注解的id变量进行覆盖并赋值,所以不需要担心id会不会重复和自己要手动自定义这个id的值。

@Entity可以接受一个tableName的String变量,可以在SQLite数据库中将Table名称改为你想要的名称。当然这个注解还可以接受其他的变量,比如primaryKeysforeignKeysignoredColumns等等。

定义DAO

其次就要定义DAO了。DAO这里就需要些许的SQL知识(主要是怎么写SQL语句)。

@Dao
interface UserDao {
    @Query("select * from user order by id")
    fun allLiveUser(): LiveData<List<User>>

    @Query("select * from user order by id")
    fun allUsers(): List<User>

    @Query("select * from user where id = :id")
    suspend fun find(id: Int): User?

    @Insert
    suspend fun insert(user: User)

    @Update
    suspend fun update(user: User)

    @Delete
    suspend fun delete(user: User)
}

Room的一个好处在于插入、修改和删除不需要写大量的SQL语句,直接使用注解即可。

如果是要做一些其他的操作,那么这时候就需要使用@Query注解了。注解@Query可以接受一个名为value的String变量,而里面是即将要执行的SQL语句。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Query {
    /**
     * The SQLite query to be run.
     * @return The query to be run.
     */
    String value();
}

如果SQL语句中需要用到函数中的一些值(例如示例中的id),那么就在其前面加上冒号:就可以了,整个语句就变成了select * from user where id = :id

allLiveUser()函数和allUsers()函数的区别在于返回值不一样,DAO支持返回普通对象(例如List、Entity等),也支持返回一个“活数据”LiveData,在调用allLiveUser()函数的时候就可以添加观察者,如果数据库中的值发生了变化,那么就会通知所有观察者该执行动作了。

定义Database

定义Database比之前的要简单,只需要

@Database(entities = [(User::class)], version = 1, exportSchema = false)
abstract class AppDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
}

其中,注解@Database必须包含entitiesversion参数。前者告诉Room要生成哪些表,后者是告诉Room数据库版本是多少。类中定义具体的DAO。

最后简单说一下suspend的问题,Room也支持Coroutine,可以将数据库的操作变成异步操作,但是需要引入androidx.room:room-ktx包。

数据库迁移

这里引用一下官方的介绍。

Room 持久性库支持通过Migration类进行增量迁移以满足此需求。每个Migration子类通过替换Migration.migrate()方法定义startVersionendVersion之间的迁移路径。当应用更新需要升级数据库版本时,Room 会从一个或多个Migration子类运行migrate()方法,以在运行时将数据库迁移到最新版本。

比如说我现在要多一个需要将自己定义的Log传入数据库以便后续发现bug提供方便,那么就需要进行迁移数据库的操作:

val migration1To2 = migration(1, 2) {
    it.execSQL("""alter table user add column secret text""")
//    it.execSQL("""create table info(id integer primary key, time long, place text, message text, level text)""")
}

fun migration(startVersion: Int, endVersion: Int, migrationFunc: (SupportSQLiteDatabase) -> Unit): Migration =
        object : Migration(startVersion, endVersion) {
            override fun migrate(database: SupportSQLiteDatabase) {
                migrationFunc(database)
            }
        }

这里面定义了一个migration()的函数,之后可以省着写那么一大长串的东西。

Room 2.2版本之后为@ColumnInfo添加了一个参数叫做defaultValue。如果你在2.1及以下创建的数据库中用SQL语句定义了默认值,那么在这里就会出现迁移错误。解决办法就是:升级一个数据库版本号,对涉及这个问题的Column进行手动修改。

使用类型转换器

有时,你需要使用自定义数据类型,其中包含您想要存储到单个数据库列中的值。如需为自定义类型添加此类支持,你需要提供一个TypeConverter,它可以在自定义类与 Room 可以保留的已知类型之间来回转换。

例如,你想存储一个Date的实例,可以编写一个TypeConverter把这个对象转换成SQLite能够使用的对象:

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time?.toLong()
    }
}

接下来,将@TypeConverters注释添加到AppDatabase类中,以便Room可以使用你为该AppDatabase中的每个实体和DAO定义的转换器:

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}