我们都知道Android数据可持久化有几种实现方式,比如存储成文件(File)、SP等。这次就要说到数据库了。
其实在以前没有Room库的时候,使用本地化数据库要不直接使用SQLite,要不就是使用ORMLite或者LitePal这种第三方库。第三方库不是不好,而是作者可能到后面就不维护了,不维护的后果就是随着Android版本更新,各式各样的bug就会出现。因此官方做ORM映射库就狠关键了。恰好,官方推出了这个名叫Room的Jetpack库。
什么是Room?
官方的介绍是这个样子的:
Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的用例是缓存相关数据。这样,当设备无法访问网络时,用户仍可在离线状态下浏览相应内容。设备重新连接到网络后,用户发起的所有内容更改都会同步到服务器。
由于 Room 负责为您处理这些问题,因此我们强烈建议您使用 Room(而不是 SQLite)。不过,如果您想直接使用 SQLite API,请参阅使用 SQLite 保存数据。
整体的结构图是这样的:
Room的使用
引入依赖
如果要使用Room,需要先在build.gradle
或build.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名称改为你想要的名称。当然这个注解还可以接受其他的变量,比如primaryKeys
、foreignKeys
、ignoredColumns
等等。
定义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
必须包含entities
和version
参数。前者告诉Room要生成哪些表,后者是告诉Room数据库版本是多少。类中定义具体的DAO。
最后简单说一下suspend的问题,Room也支持Coroutine,可以将数据库的操作变成异步操作,但是需要引入androidx.room:room-ktx
包。
数据库迁移
这里引用一下官方的介绍。
Room 持久性库支持通过
Migration
类进行增量迁移以满足此需求。每个Migration
子类通过替换Migration.migrate()
方法定义startVersion
和endVersion
之间的迁移路径。当应用更新需要升级数据库版本时,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
}