This article mainly refers to the official documentation, and then saves the search history as an example to operate a wave.
The preparatory work
Room provides an abstraction layer on top of SQLite for smooth database access while taking full advantage of SQLite’s power.
Rely on
To use Room in your application, add the following dependencies to your application’s build.gradle file.
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
Copy the code
The major components
The database
: contains the database holder and serves as the primary access point to the underlying connection to apply retained persistent relational data. use@Database
Annotated classes should meet the following criteria:- Is to extend
RoomDatabase
Abstract class of. - Add a list of entities associated with the database to the comment.
- Contains zero parameters and returns use
@Dao
Abstract methods of annotated classes.
At run time, you can call
Room.databaseBuilder()
或Room.inMemoryDatabaseBuilder()
To obtainDatabase
The instance.- Is to extend
Entity
: indicates a table in the database.DAO
: contains methods used to access the database.
The application uses the Room database to get the DATA access object (DAO) associated with the database. The application then uses each DAO to retrieve entities from the database, and then saves all changes to those entities back into the database. Finally, the application uses entities to get and set values corresponding to table columns in the database.
The relationship is shown as follows:
Ok, once you understand the basic concept, let’s see how it works.
Entity
@Entity(tableName = "t_history")
data class History(
/ * * *@PrimaryKeyPrimary key, autoGenerate = true increment *@ColumnInfoColumn, typeAffinity field type *@IgnoreIgnore the * /
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
val id: Int? = null.@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
valname: String? .@ColumnInfo(name = "insert_time", typeAffinity = ColumnInfo.TEXT)
valinsertTime: String? .@ColumnInfo(name = "type", typeAffinity = ColumnInfo.INTEGER)
val type: Int = 1
)
Copy the code
- The Entity object corresponds to a table and is used
@Entity
Annotate and declare your table name @PrimaryKey
The primary key,autoGenerate = true
Since the increase@ColumnInfo
Column and declare the column name,typeAffinity
The field type@Ignore
Declare objects to be ignored
A very simple table, mainly name and insertTime fields.
DAO
@Dao
interface HistoryDao {
// Query all search histories by type
@Query("SELECT * FROM t_history WHERE type=:type")
fun getAll(type: Int = 1): Flow<List<History>>
@ExperimentalCoroutinesApi
fun getAllDistinctUntilChanged(a) = getAll().distinctUntilChanged()
// Add a search history
@Insert
fun insert(history: History)
// Delete a search history
@Delete
fun delete(history: History)
// Update a search history
@Update
fun update(history: History)
// Delete a search history by id
@Query("DELETE FROM t_history WHERE id = :id")
fun deleteByID(id: Int)
// Delete all search history
@Query("DELETE FROM t_history")
fun deleteAll(a)
}
Copy the code
- @ Insert: increase
- @ Delete: Delete
- @ Update: change
- @ Query: to check
One thing to note here is that the set returned by querying all the search histories I’ve decorated with Flow.
Whenever any data in the database is updated, no matter which row of data is changed, the query operation is re-executed and the Flow is distributed again.
Similarly, if an unrelated data update occurs, the Flow will also be distributed and receive the same data as before.
This is because the SQLite database’s content update notification function is in units of Table data, not Row data, so it triggers content update notifications whenever data in the Table is updated. Room does not know which data in the table is updated, so it refires the Query operation defined in the DAO. You can use operators of Flow, such as distinctUntilChanged, to ensure that you are notified only when there is an update to the data you are interested in.
// Query all search histories by type
@Query("SELECT * FROM t_history WHERE type=:type")
fun getAll(type: Int = 1): Flow<List<History>>
@ExperimentalCoroutinesApi
fun getAllDistinctUntilChanged(a) = getAll().distinctUntilChanged()
Copy the code
The database
@Database(entities = [History::class], version = 1)
abstract class HistoryDatabase : RoomDatabase() {
abstract fun historyDao(a): HistoryDao
companion object {
private const val DATABASE_NAME = "history.db"
private lateinit var mPersonDatabase: HistoryDatabase
// Note: If your application is running in a single process, follow the singleton design pattern when instantiating AppDatabase objects.
// The cost per RoomDatabase instance is quite high, and you rarely need to access multiple instances in a single process
fun getInstance(context: Context): HistoryDatabase {
if (!this::mPersonDatabase.isInitialized) {
// Create an instance of the database
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).build()
}
return mPersonDatabase
}
}
}
Copy the code
- use
@Database
Annotation statement entities
Array that corresponds to all tables in this databaseversion
Database Version number
Note:
If your application is running in a single process, follow the singleton design pattern when instantiating AppDatabase objects. The cost of each RoomDatabase instance is quite high, and you rarely need to access multiple instances in a single process.
use
Get the database where you need it
mHistoryDao = HistoryDatabase.getInstance(this).historyDao()
Copy the code
Get search history
private fun getSearchHistory(a) {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.getAll().collect {
withContext(Dispatchers.Main){
/ / update the UI}}}}Copy the code
Collect is a way for Flow to obtain data, not the only way to view documents.
Why put it in a coroutine? Because database operations are time consuming, and coroutines can easily specify threads so that they don’t block the UI thread.
Check Flow source also found that Flow is under the coroutine package
package kotlinx.coroutines.flow
Copy the code
Collect, for example, is also modified by suspend. Since suspend is supported, it is not beautiful to cooperate with coroutines.
@InternalCoroutinesApi
public suspend fun collect(collector: FlowCollector<T>)
Copy the code
Save search records
private fun saveSearchHistory(text: String) {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.insert(History(null, text, DateUtils.longToString(System.currentTimeMillis())))
}
}
Copy the code
Clearing local History
private fun cleanHistory(a) {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.deleteAll()
}
}
Copy the code
Author: yechaoa
Database Upgrade
Database upgrade is an important operation, after all, may cause data loss, is also a very serious problem.
Room uses the Migration class to perform the upgrade, and we just have to tell the Migration class what has changed, such as adding fields or tables.
Define Migration class
/** * new updateTime column */
private val MIGRATION_1_2: Migration = object : Migration(1.2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")}}/** * database version 2->3 new label table */
private val MIGRATION_2_3: Migration = object : Migration(2.3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")}}Copy the code
Migration takes two arguments:
- StartVersion old version
- EndVersion new version
Notifying the database of updates
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
Copy the code
The complete code
@Database(entities = [History::class, Label::class], version = 3)
abstract class HistoryDatabase : RoomDatabase() {
abstract fun historyDao(a): HistoryDao
companion object {
private const val DATABASE_NAME = "history.db"
private lateinit var mPersonDatabase: HistoryDatabase
fun getInstance(context: Context): HistoryDatabase {
if (!this::mPersonDatabase.isInitialized) {
// Create an instance of the database
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
}
return mPersonDatabase
}
/** * new updateTime column */
private val MIGRATION_1_2: Migration = object : Migration(1.2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")}}/**
* 数据库版本 2->3 新增label表
*/
private val MIGRATION_2_3: Migration = object : Migration(2.3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")}}}}Copy the code
Note: a version number change in the @database annotation should also be added to the entities parameter if it is a new table.
Recommended upgrade sequence
Change the version number -> Add Migration -> Add to databaseBuilder
Configure compiler options
Room has the following annotation processor options:
room.schemaLocation
: Configures and enables the ability to export the database schema to a JSON file in a given directory. For more details, see Room Migration.room.incremental
: Enables the Gradle incremental comment processor.room.expandProjection
: Configure Room to rewrite the query so that its top star projection expands to contain only the columns defined in the DAO method return type.
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true"."room.expandProjection":"true"]}}}}Copy the code
After configuration, compile and run, the Schemas folder will be generated under the Module folder, where there is a JSON file that contains the basic information of the database.
{
"formatVersion": 1."database": {
"version": 1."identityHash": "xxx"."entities": [{"tableName": "t_history"."createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `insert_time` TEXT, `type` INTEGER NOT NULL)"."fields": [{"fieldPath": "id"."columnName": "id"."affinity": "INTEGER"."notNull": false
},
{
"fieldPath": "name"."columnName": "name"."affinity": "TEXT"."notNull": false
},
{
"fieldPath": "insertTime"."columnName": "insert_time"."affinity": "TEXT"."notNull": false
},
{
"fieldPath": "type"."columnName": "type"."affinity": "INTEGER"."notNull": true}]."primaryKey": {
"columnNames": [
"id"]."autoGenerate": true
},
"indices": []."foreignKeys": []}],"views": []."setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)"."INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'xxx')"]}}Copy the code
Ok, the basic use of the explanation is over, if it is useful to you, click a thumbs up ^ _ ^
reference
- Room Official Document
- Room Update Log
- Flow official documentation
- Actual combat | using Flow in the Room
- Coroutines Flow best practice | application based on the Android developer summit