This article mainly refers to official documents, and then takes the example of saving search history.
Ready to work
Room
In theSQLite
last it provides an abstraction layer, so that at the same time take advantage of the power of SQLite, and can smoothly access the database.
rely
For use in the application Room
, add the following dependency to the application 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"
}
Main components
数据库
: Contains the database holder and serves as the main access point for the underlying connection of the persistent relational data that the application has retained.
Use@Database
the comment class should meet the following criteria:- It is an extension
RoomDatabase
of an abstract class. - Add a list of entities associated with the database in the comment.
- Contains an
@Dao
abstract method that has 0 parameters and returns an annotated class.
At runtime, you can callRoom.databaseBuilder()
orRoom.inMemoryDatabaseBuilder()
getDatabase
instance.
- It is an extension
Entity
: Represents a table in the database.DAO
: Contains methods for accessing the database.
The application uses the Room database to obtain the Data Access Object (DAO) associated with the database. Then, the application 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 the values corresponding to the table columns in the database.
The relationship is shown in the figure:
Ok, after understanding the basic concepts, let's take a look at how to do it.
Entity
@Entity(tableName = "t_history")
data class History(
/**
* @PrimaryKey主键,autoGenerate = true 自增
* @ColumnInfo 列 ,typeAffinity 字段类型
* @Ignore 忽略
*/
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
val id: Int? = null,
@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
val name: String?,
@ColumnInfo(name = "insert_time", typeAffinity = ColumnInfo.TEXT)
val insertTime: String?,
@ColumnInfo(name = "type", typeAffinity = ColumnInfo.INTEGER)
val type: Int = 1
)
- Entity object corresponds to a table, use
@Entity
annotations, and declare your table name @PrimaryKey
Primary key,autoGenerate = true
increment@ColumnInfo
Column, and declare the column name,typeAffinity
field type@Ignore
Declare ignored objects
A very simple table, mainly name
and insertTime
fields.
DAO
@Dao
interface HistoryDao {
//按类型 查询所有搜索历史
@Query("SELECT * FROM t_history WHERE type=:type")
fun getAll(type: Int = 1): Flow<List<History>>
@ExperimentalCoroutinesApi
fun getAllDistinctUntilChanged() = getAll().distinctUntilChanged()
//添加一条搜索历史
@Insert
fun insert(history: History)
//删除一条搜索历史
@Delete
fun delete(history: History)
//更新一条搜索历史
@Update
fun update(history: History)
//根据id 删除一条搜索历史
@Query("DELETE FROM t_history WHERE id = :id")
fun deleteByID(id: Int)
//删除所有搜索历史
@Query("DELETE FROM t_history")
fun deleteAll()
}
- @Insert: Increase
- @Delete: 删
- @Update:改
- @Query: Check
One thing to note here is 查询所有搜索历史
that I Flow
modified the returned collection .
As long as any of the data in the database is updated, no matter which line to change the data, then re-execute the query
operation and distribute again Flow
.
In the same way, if an irrelevant data is updated, it Flow
will also be distributed and will receive the same data as before.
This is because the content update notification function of the SQLite database is based on table (Table) data as a unit, rather than row (Row) data as a unit, so as long as the data in the table is updated, it will trigger the content update notification. Room doesn't know which data is updated in the table, so it will trigger the query operation defined in DAO again. You can use Flow's operators, such as distinctUntilChanged to ensure that you will only be notified when the data you care about is updated.
//按类型 查询所有搜索历史
@Query("SELECT * FROM t_history WHERE type=:type")
fun getAll(type: Int = 1): Flow<List<History>>
@ExperimentalCoroutinesApi
fun getAllDistinctUntilChanged() = getAll().distinctUntilChanged()
database
@Database(entities = [History::class], version = 1)
abstract class HistoryDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
companion object {
private const val DATABASE_NAME = "history.db"
private lateinit var mPersonDatabase: HistoryDatabase
//注意:如果您的应用在单个进程中运行,在实例化 AppDatabase 对象时应遵循单例设计模式。
//每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例
fun getInstance(context: Context): HistoryDatabase {
if (!this::mPersonDatabase.isInitialized) {
//创建的数据库的实例
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).build()
}
return mPersonDatabase
}
}
}
- Use
@Database
annotation statement entities
Array, corresponding to all tables in this databaseversion
Database version number
note:
If your application runs in a single process, you should follow it when instantiating the AppDatabase object
单例设计模式
.
The cost of each RoomDatabase instance is quite high, and you hardly need to access multiple instances in a single process.
use
Get the database where you need it
mHistoryDao = HistoryDatabase.getInstance(this).historyDao()
Get search history
private fun getSearchHistory() {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.getAll().collect {
withContext(Dispatchers.Main){
//更新ui
}
}
}
}
collect
This is Flow
the way to get data, not the only way to view the document .
Why put 协程
it inside, because the database operation is time-consuming, and the coroutine can easily specify the thread, so that it does not block the UI
thread.
Looking at the Flow source code, I found that Flow is under the coroutine package
package kotlinx.coroutines.flow
Take collect as an example. It is also suspend
modified. Since it supports it 挂起
, 协程
wouldn't it be wonderful to cooperate .
@InternalCoroutinesApi
public suspend fun collect(collector: FlowCollector<T>)
Save search history
private fun saveSearchHistory(text: String) {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.insert(History(null, text, DateUtils.longToString(System.currentTimeMillis())))
}
}
Clear local history
private fun cleanHistory() {
MainScope().launch(Dispatchers.IO) {
mHistoryDao.deleteAll()
}
}
Author: https://blog.csdn.net/yechaoa
Database upgrade
Database upgrade is an important operation. After all, it may cause data loss, which is also a serious problem.
Room Migration
performs upgrade operations through classes. We only need to tell the Migration
class what has changed, such as 新增
fields or tables.
Define the migration class
/**
* 数据库版本 1->2 t_history表格新增了updateTime列
*/
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)")
}
}
Migration
Two parameters are received:
- startVersion old version
- endVersion new version
Notify database updates
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
Complete code
@Database(entities = [History::class, Label::class], version = 3)
abstract class HistoryDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
companion object {
private const val DATABASE_NAME = "history.db"
private lateinit var mPersonDatabase: HistoryDatabase
fun getInstance(context: Context): HistoryDatabase {
if (!this::mPersonDatabase.isInitialized) {
//创建的数据库的实例
mPersonDatabase = Room.databaseBuilder(
context.applicationContext,
HistoryDatabase::class.java,
DATABASE_NAME
).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
}
return mPersonDatabase
}
/**
* 数据库版本 1->2 t_history表格新增了updateTime列
*/
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)")
}
}
}
}
Note: The
@Database
revision of the version number in the comment, if it is a new table entities
, should also be added to the parameter.
Recommended upgrade sequence
Modify the version number -> Add Migration -> Add to databaseBuilder
Configure compiler options
Room has the following annotation processor options:
room.schemaLocation
: Configure and enable the ability to export the database schema to a JSON file in a given directory. For details, see Room migration.room.incremental
: Enable Gradle incremental annotation processor.room.expandProjection
: Configure Room to rewrite the query so that the top star projection only contains the columns defined in the DAO method return type after expansion.
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true"]
}
}
}
}
After configuration, compile and run, a folder will be generated under the module schemas
folder, and there is a json
file under it , which 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')"
]
}
}
ok, the basic usage explanation is over, if it is useful to you, please give me a thumbs up ^ _ ^