前言
是一个基于 SQLite 的强大数据库框架,是谷歌官方提供的给开发者使用的,Room 也是 JetPack 的一部分。Room 强大在哪里呢?
- 使用编译时注解对 sql 语句进行检查
- 与 sql 语句的使用更加贴近,能够降低学习成本
- 支持
RxJava
、LiveData
、协程
等联合使用 - 官方推荐,不需要额外引入第三方库
Room 常见注解
表实体相关注解
- @Entity:声明一个表
- @ColumnInfo:声明表中的字段名称
- @PrimaryKey:声明表中的主键字段
- @Embedded:用于表中嵌套表
- @Ignore:声明某个字段只是临时用,不存储在数据库中
数据库持有类相关注解
- @Database:声明一个数据库持有者类
注意:标有 @Database
注解的类需要具备以下特征:
1、继承
RoomDatabase
的抽象类2、在注释中包括与数据库关联的实体列表,即
@Database(entities ={ })
3、包含一个无参的抽象方法,并返回一个使用
@Dao
注解的类
表操作相关注解
Room 提供了4个注解,专门用于对表进行增删查改操作,如下:
- 插入数据:可以通过
@Insert
注解插入数据 - 更新数据:可以通过
@Update
注解更新数据,根据参数对象的主键更新数据 - 删除数据:可以通过
@Delete
注解删除数据,根据参数对象的主键删除数据 - 查找数据:可以通过
@Query
注解查找数据,根据@Query
中的 SQL 语句查找指定数据
其中,@Insert
和 @Update
可以设置数据新旧数据冲突策略,可以通过传参onConflict
设置,onConflict
的各个含义如下:
OnConflictStrategy.IGNORE
:有旧数据存在,想要插入的数据将会插入失败(默认采用的策略)OnConflictStrategy.REPLACE
:有旧数据存在,则替换掉旧数据OnConflictStrategy.ABORT
:有旧数据存在,则终止事务OnConflictStrategy.ROLLBACK
:有旧数据存在,则回滚事务(已废弃,使用 ABORT 代替)OnConflictStrategy.FAIL
:有旧数据存在,则提示插入数据失败(已废弃,使用 ABORT 代替)
表的嵌套
比如现在有一个 Movie 表,它结构如下:
id(主键) | name(名称) | year(上映年份) | score(评分) |
---|
用代码来表示是这样的:
@Entity
data class Movie(
// @PrimaryKey 声明 id 为主键,autoGenerate = true,表示自增
@PrimaryKey(autoGenerate = true) val id: Int,
// 电影名称, @ColumnInfo 定义表中的 name 字段
@ColumnInfo(name = "name") val name: String?,
// 上映年份, @ColumnInfo 定义表中的 year 字段
@ColumnInfo(name = "year") val year: Int?,
// 电影评分, @ColumnInfo 定义表中的 name 字段
@ColumnInfo(name = "score") val score: Float?
)
现在,我们给 Movie 表嵌套一个 Info 表,来记录一些其他的信息,如下:
@Entity
data class Movie(
// @PrimaryKey 声明 id 为主键,autoGenerate = true,表示自增
@PrimaryKey(autoGenerate = true) val id: Int,
// 电影名称, @ColumnInfo 定义表中的 name 字段
@ColumnInfo(name = "name") val name: String?,
// 上映年份, @ColumnInfo 定义表中的 year 字段
@ColumnInfo(name = "year") val year: Int?,
// 电影评分, @ColumnInfo 定义表中的 name 字段
@ColumnInfo(name = "score") val score: Float?,
// 嵌套一个 Info 表
@Embedded val info: Info
)
另外,Info 表的表设计代码如下,包含一个主键id,内容content,是否已被观看isWatched:
@Entity
data class Info(
// @PrimaryKey 声明 id 为主键,autoGenerate = true,表示自增
@PrimaryKey(autoGenerate = true) val id: Int,
// 是否已看过
@ColumnInfo(name = "isWatched") val isWatched: Boolean?,
// 内容
@ColumnInfo(name = "content") val content: String?,
)
Movie 表嵌套了 Info 表,那么 Movie 表将会自动添加 Info 表里面的列名字段,且 Info 的主键 id 将在 Movie 中被忽略不再被作为主键,Movie 结构变为如下:
id(主键) | name(名称) | year(上映年份) | score(评分) | content(内容) | isWatched(是否已看过) |
---|
Room Demo 演示
下面通过一个小 Demo 来演示一下 Room 的使用。(使用 Hilt + Mvvm + LiveData 的代码框架来演示)
步骤一:引入 Room 相关依赖:
// Project-build.gradle
buildscript {
ext {
roomVersion = "2.4.2"
}
}
// app-build.gradle
plugins {
id 'kotlin-kapt'
}
dependencies {
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
// // 可选 - RxJava2 support for Room
// implementation "androidx.room:room-rxjava2:rootProject.roomVersion"
// // 可选 - RxJava3 support for Room
// implementation "androidx.room:room-rxjava3:rootProject.roomVersion"
// // 可选 - Guava support for Room, including Optional and ListenableFuture
// implementation "androidx.room:room-guava:rootProject.roomVersion"
// // 可选 - Test helpers
// testImplementation "androidx.room:room-testing:rootProject.roomVersion"
// // 可选 - Paging 3 Integration
// implementation "androidx.room:room-paging:2.5.0-alpha01"
}
步骤二:建一个 Movie 表
/**
* 建一个 Movie 表
*/
@Entity
data class Movie(
// @PrimaryKey 声明 id 为主键,autoGenerate = true,表示自增
@PrimaryKey(autoGenerate = true) val id: Int,
// 电影名称, @ColumnInfo 定义表中的 name 字段
@ColumnInfo(name = "name") val name: String?,
// 上映年份, @ColumnInfo 定义表中的 year 字段
@ColumnInfo(name = "year") val year: Int?,
// 电影评分, @ColumnInfo 定义表中的 name 字段
@ColumnInfo(name = "score") val score: Float?,
)
步骤三:创建 Dao 类
@Dao
interface MovieDao {
@Insert(
entity = Movie::class, // 目标实体,即插入的表
onConflict = OnConflictStrategy.REPLACE // 有旧数据存在,则替换掉旧数据
)
suspend fun insert(vararg movies: Movie)
@Update(entity = Movie::class, onConflict = OnConflictStrategy.REPLACE)
suspend fun update(vararg movies: Movie)
@Delete(entity = Movie::class)
suspend fun delete(movie: Movie)
@Delete(entity = Movie::class)
suspend fun deleteList(movie: List<Movie>)
@Query("SELECT * FROM movie")
suspend fun loadAllMovies(): List<Movie>
@get:Query("SELECT * FROM movie")
val allMovies: Flow<List<Movie>>
@Query("SELECT * FROM movie")
fun loadAllMovies2(): LiveData<List<Movie>>
@Query("SELECT * FROM movie WHERE id IN (:ids)")
fun loadAllByIds(ids: IntArray): Flow<List<Movie>>
}
步骤四:创建数据库类,该类应遵循单例设计模式,因为每个RoomDatabase
实例都相当消耗性能,并且我们很少需要访问多个 Database 实例。
@Database(
// 传入有 @Entity 注解的 class 对象,也就是表,可用添加多张表
entities = [Movie::class],
// 数据库版本号
version = 1,
// 设置是否导出数据库 schema,默认为 true,如果需要进行数据库迁移,需要置为 true
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
// 必须有一个无参抽象方法,并返回一个使用了 @Dao 注解的类
abstract fun getMovieDao(): MovieDao
companion object {
@Volatile
private var instance: AppDatabase? = null
private const val DATA_BASE_NAME = "ycx_app.db"
@JvmStatic
fun getInstance(context: Context) = instance ?: synchronized(AppDatabase::class.java) {
instance ?: buildDatabase(context)
}
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATA_BASE_NAME)
.addCallback(
object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 首次创建数据库时调用
}
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
// 在打开数据库时调用
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
super.onDestructiveMigration(db)
// 在数据库被破坏性迁移后调用
}
}
).build()
}
}
}
以上是创建的数据库的准备工作,下面我们开始在项目中操作数据库。(使用 Hilt + Mvvm + LiveData 的代码框架来演示)
步骤五:创建 Application 类
@HiltAndroidApp
class MainApplication : Application()
步骤六:编写 依赖注入 代码,提供 AppDatabase 实例对象,以及 MovieDao 实例对象
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {
@Singleton
@Provides
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return AppDatabase.getInstance(context)
}
@Provides
fun provideMovieDao(appDatabase: AppDatabase): MovieDao {
return appDatabase.getMovieDao()
}
}
步骤七:创建 MovieRepository 数据仓库类
@Singleton
class MovieRepository @Inject constructor(private val movieDao: MovieDao) {
val allMovies = movieDao.allMovies
suspend fun loadAllMovies() = movieDao.loadAllMovies()
fun loadMoviesByIds(ids: IntArray) = movieDao.loadAllByIds(ids)
}
步骤八:编写 MainViewModel 代码,其中,使用了 LiveData 对查询到的 Movie 数据进行包装
@HiltViewModel
class MainViewModel @Inject constructor(
private val movieRepository: MovieRepository,
) : ViewModel() {
// movie 表所有数据(方式一)
val allMovies = movieRepository.allMovies.asLiveData()
// movie 表所有数据(方式二)
val moviesMediatorLiveData = MutableLiveData<List<Movie>>()
fun loadAllMovies() {
viewModelScope.launch {
moviesMediatorLiveData.postValue(movieRepository.loadAllMovies())
}
}
}
// `asLiveData()` 是 `Flow<T>` 的扩展函数,想要使用该扩展函数,需要引入以下两个依赖:
// implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
// implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
步骤九:编写 MainActivity 类,监听 MainViewModel 中的 LiveData 数据。同时对数据库进行增删查改。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mMainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initObserve()
}
private fun initObserve() {
mMainViewModel.allMovies.observe(this) {
Log.i("TestLog", "1 observe allMovies=${
Gson().toJson(it)}")
}
mMainViewModel.moviesMediatorLiveData.observe(this) {
Log.i("TestLog", "2 observe allMovies=${
Gson().toJson(it)}")
}
}
fun insert(view: View) {
// 往 Movie 表中插入一条数据
mMainViewModel.viewModelScope.launch {
AppDatabase.getInstance(applicationContext).getMovieDao()
.insert(Movie(1, "《Spider Man》", 2020, 8.9F))
}
Log.i("TestLog", "insert")
}
fun update(view: View) {
// 更新 Movie 表中 id=1 数据
mMainViewModel.viewModelScope.launch {
AppDatabase.getInstance(applicationContext).getMovieDao()
.update(Movie(1, "《Spider Man 2》", 2021, 9.9F))
Log.i("TestLog", "update")
}
}
fun delete(view: View) {
// 删除 Movie 表中 id=1 数据
mMainViewModel.viewModelScope.launch {
AppDatabase.getInstance(applicationContext).getMovieDao()
.delete(Movie(1, "", 0, 0F))
}
Log.i("TestLog", "delete")
}
fun query(view: View) {
// 查询 Movie 所有数据
mMainViewModel.loadAllMovies()
}
}
步骤十:由于项目中我们用到了 Hilt ,所以需要添加 Hilt 下相关依赖:
// Project-build.gradle
buildscript {
ext {
hiltVersion = '2.41'
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hiltVersion"
}
}
// app-build.gradle
plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
kapt "com.google.dagger:hilt-android-compiler:$rootProject.hiltVersion"
implementation "com.google.dagger:hilt-android:$rootProject.hiltVersion"
}
数据迁移
当数据库的表结构发生变化时,我们需要通过数据库迁移(Migrations)升级表结构,避免数据丢失。
Room 数据迁移只需要使用 Migration类即可,非常简单。
比如现在有一个 Movie 表,它结构如下:
id(主键) | name(名称) | year(上映年份) | score(评分) |
---|
用代码来表示是这样的:
@Entity
data class Movie(
// @PrimaryKey 声明 id 为主键,autoGenerate = true,表示自增
@PrimaryKey(autoGenerate = true) val id: Int,
// 电影名称, @ColumnInfo 定义表中的 name 字段
@ColumnInfo(name = "name") val name: String?,
// 上映年份, @ColumnInfo 定义表中的 year 字段
@ColumnInfo(name = "year") val year: Int?,
// 电影评分, @ColumnInfo 定义表中的 name 字段
@ColumnInfo(name = "score") val score: Float?
)
现在,我们想要给 Movie 表增加一个 content 字段,代码如下:
@Database(entities = [Movie::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
// 必须有一个无参抽象方法,并返回一个使用了 @Dao 注解的类
abstract fun getMovieDao(): MovieDao
companion object {
// 数据迁移
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE movie ADD COLUMN content INTEGER")
}
}
@Volatile
private var instance: AppDatabase? = null
private const val DATA_BASE_NAME = "ycx_app.db"
@JvmStatic
fun getInstance(context: Context) = instance ?: synchronized(AppDatabase::class.java) {
instance ?: buildDatabase(context)
}
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATA_BASE_NAME)
.addMigrations(MIGRATION_1_2) // 添加迁移
.build()
}
}
}