【Android】Use of JetPack Room

foreword

It is a powerful database framework based on SQLite, which is officially provided by Google for developers. Room is also a part of JetPack. Where is the strength of Room?

  • Use compile-time annotations to check sql statements
  • It is closer to the use of sql statements, which can reduce learning costs
  • Support the joint use of RxJava, LiveData, 协程etc.
  • Official recommendation, no need to introduce additional third-party libraries

Room Common Annotations

Notes about table entities

  • @Entity: Declare a table
  • @ColumnInfo: declares the field name in the table
  • @PrimaryKey: declares the primary key field in the table
  • @Embedded: for nested tables in tables
  • @Ignore: Declare that a field is only for temporary use and is not stored in the database

Notes about database holding classes

  • @Database: Declare a database holder class

Note: Classes marked with @Databaseannotations need to have the following characteristics:

1. RoomDatabaseInherited abstract class

2. Include the list of entities associated with the database in the annotation, i.e.@Database(entities ={ })

3. Contains an abstract method with no parameters and returns a @Daoclass that uses annotations

Notes on table operations

Room provides 4 annotations, which are specially used to add, delete, check and modify the table, as follows:

  • Insert data: data can be inserted through @Insertannotations
  • Update data: You can update data through @Updateannotations , and update data according to the primary key of the parameter object
  • Delete data: You can delete data through @Deleteannotations , and delete data according to the primary key of the parameter object
  • Find data: You can find data through @Queryannotations , @Queryand find specified data according to the SQL statement in

Among them, @Insertand @Updatecan set the old and new data conflict strategy, which can onConflictbe set by passing parameters. onConflictThe meanings of each are as follows:

  • OnConflictStrategy.IGNORE: Old data exists, and the data you want to insert will fail to insert ( the default strategy)
  • OnConflictStrategy.REPLACE: If old data exists, replace the old data
  • OnConflictStrategy.ABORT: If old data exists, terminate the transaction
  • OnConflictStrategy.ROLLBACK: If old data exists, roll back the transaction (obsolete, use ABORT instead)
  • OnConflictStrategy.FAIL: If there is old data, it will prompt that the data insertion failed (deprecated, use ABORT instead)

Nesting of tables

For example, now there is a Movie table, its structure is as follows:

id (primary key) name year (release year) score

Expressed in code like this:

@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?
)

Now, we nest an Info table in the Movie table to record some other information, as follows:

@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
)

In addition, the table design code of the Info table is as follows, including a primary key id, content content, whether it has been watched 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?,
)

The Movie table nests the Info table, then the Movie table will automatically add the column name field in the Info table, and the primary key id of Info will be ignored in Movie and no longer be used as the primary key. The Movie structure becomes as follows:

id (primary key) name year (release year) score content (content) isWatched (whether it has been watched)

Room Demo demo

Let's use a small demo to demonstrate the use of Room. (Use the code framework of Hilt + Mvvm + LiveData to demonstrate)

Step 1: Introduce Room-related dependencies:

// 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"
}

Step 2: Create a Movie table

/**
 * 建一个 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?,
)

Step 3: Create Dao class

@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>>
}

Step 4: Create a database class, which should follow the singleton design pattern, because each RoomDatabaseinstance consumes a lot of performance, and we rarely need to access multiple Database instances.

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

The above is the preparatory work for the created database. Next, we start to operate the database in the project. (Use the code framework of Hilt + Mvvm + LiveData to demonstrate)

Step 5: Create the Application class

@HiltAndroidApp
class MainApplication : Application()

Step 6: Write dependency injection code, provide AppDatabase instance object, and MovieDao instance object

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

Step 7: Create MovieRepository data warehouse class

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

Step 8: Write the MainViewModel code, where LiveData is used to package the queried Movie data

@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"

Step 9: Write the MainActivity class to listen to the LiveData data in the MainViewModel. At the same time, add, delete, check and modify the database.

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

Step 10: Since we use Hilt in the project, we need to add related dependencies under 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"
}

data migration

When the table structure of the database changes, we need to upgrade the table structure through database migrations (Migrations) to avoid data loss.

Room data migration only needs to use the Migration class, which is very simple.

For example, now there is a Movie table, its structure is as follows:

id (primary key) name year (release year) score

Expressed in code like this:

@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?
)

Now, we want to add a content field to the Movie table, the code is as follows:

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

Guess you like

Origin blog.csdn.net/yang553566463/article/details/125008322