【Android】之 JetPack Room 的使用

前言

是一个基于 SQLite 的强大数据库框架,是谷歌官方提供的给开发者使用的,Room 也是 JetPack 的一部分。Room 强大在哪里呢?

  • 使用编译时注解对 sql 语句进行检查
  • 与 sql 语句的使用更加贴近,能够降低学习成本
  • 支持 RxJavaLiveData协程等联合使用
  • 官方推荐,不需要额外引入第三方库

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()
        }
    }
}

猜你喜欢

转载自blog.csdn.net/yang553566463/article/details/125008322