Jetpack Room基本用法


前言

日常开发中,总会遇到需要将数据持久化在本地,供需要的时候使用。

本地数据持久化例如:文件保存、SharedPreferences(DataStore,sp的替代者)、数据库等等。简单的键值对信息我们直接使用SP保存在xml中就可以了,但是更加复杂的信息就需要数据库(SQLite)上场了。

本文就使用Room来操作SQLite数据库。

更加详细的内容可以查看官网,链接已经放在参考中,可以自行查阅。


一、Room是什么?

Room 持久性库是在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。

Room的优势:

  1. 针对 SQL 查询的编译时验证
  2. 可最大限度减少重复和容易出错的样板代码的方便注解。
  3. 简化了数据库迁移路径。
  4. Android官方维护,不像其他开源项目出现问题无人能够解决

主要组件

1. 数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点。
2. 数据实体,用于表示应用的数据库中的表。
3. 数据访问对象 (DAO),提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法。

数据库类为应用提供与该数据库关联的 DAO 的实例,对于数据库的增删改查就是定义在Dao实例中。

引用官网的Room 库架构的示意图:

Room 库架构的示意图。

二、使用步骤

1.引入库

必选库如下(示例):

在app module的build.gradle中配置

dependencies {
    
    
    def room_version = "2.4.2"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
}

遇到的问题:
如果我们使用的是Kotlin编写的代码,此时引入room-compiler库必须使用kapt而不是annotationProcessor。否则后续Room不能生成自动构建的代码。

plugins {
    
    
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-android'
    // 需要在次数增加kapt插件
    id 'kotlin-kapt'
}

dependencies {
    
    
    def room_version = "2.4.2"
    implementation "androidx.room:room-runtime:$room_version"
    // 使用kapt来引入注解处理器
    kapt "androidx.room:room-compiler:$room_version"
}

扩展库(根据需要选择)如下(示例):

dependencies {
    
    
    def room_version = "2.4.2"
    // optional - RxJava2 support for Room
    implementation "androidx.room:room-rxjava2:$room_version"
    // optional - RxJava3 support for Room
    implementation "androidx.room:room-rxjava3:$room_version"
    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"
    // optional - Test helpers
    testImplementation "androidx.room:room-testing:$room_version"
    // optional - Paging 3 Integration
    implementation "androidx.room:room-paging:2.5.0-alpha01"
}

如果出现库拉取失败,可以将镜像换为阿里云的镜像,原文见结尾的参考文章

如下配置是在最新的Android studio(小蜜蜂),是配置在settings.gradle中,小蜜蜂之前的Android studio是配置在Project的build.gradle中。

// 需要按着顺序摆放(也可以将Google的引用给注释掉),因为编译器在使用的时候是按照先后顺序来访问的
pluginManagement {
    
    
    repositories {
    
    
        maven {
    
     url 'https://maven.aliyun.com/repository/public' }
        maven {
    
     url 'https://maven.aliyun.com/repository/google' }
        maven {
    
     url 'https://maven.aliyun.com/repository/gradle-plugin' }

        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement {
    
    
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
    
    
        maven {
    
     url 'https://maven.aliyun.com/repository/public' }
        maven {
    
     url 'https://maven.aliyun.com/repository/google' }
        maven {
    
     url 'https://maven.aliyun.com/repository/gradle-plugin' }

        google()
        mavenCentral()
    }
}
rootProject.name = "Determined"
include ':app'

2.定义数据实体

  • Room 默认将类名称用作数据库表名称。如果您希望表具有不同的名称,请设置 @Entity 注解的 tableName 属性
  • Room 默认使用字段名称作为数据库中的列名称。如果您希望列具有不同的名称,请将 @ColumnInfo 注解添加到该字段并设置 name 属性

注意:SQLite 中的表和列名称不区分大小写。

2.1 定义主键

每个 Room 实体都必须定义一个主键,用于唯一标识相应数据库表中的每一行。

示例如下:

// 主键,设置autoGenerate = true之后Room才会自动给id分配值(从1开始自增加)
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = COLUMN_ID)
var uId: Long = 0

注意:如果您需要 Room 为实体实例分配自动 ID,请将 @PrimaryKey 的 autoGenerate 属性设为 true。

复合主键,本人从未使用过

如果您需要通过多个列的组合对实体实例进行唯一标识,则可以通过列出 @Entity 的 primaryKeys 属性中的以下列定义一个复合主键:

@Entity(primaryKeys = [User.COLUMN_ID, User.COLUMN_NAME])
class User {
    
    }

2.2 忽略字段

默认情况下,Room 会为实体中定义的每个字段创建一个列。
如果某个实体中有您不想保留的字段,则可以使用 @Ignore 为这些字段添加注解

@Entity(tableName = User.TAB_NAME)
class User {
    
    
    ...
    @Ignore
    var uSex: Int = 0
}

2.3 完整代码如下(示例):

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

/**
 * @author 86351
 * @date 2022/5/3
 * @description
 */
@Entity(tableName = User.TAB_NAME)
class User {
    
    
    companion object {
    
    
        const val TAB_NAME = "user"
        const val COLUMN_ID = "id"
        const val COLUMN_NAME = "name"
        const val COLUMN_AGE = "age"
    }


    // 主键,设置autoGenerate = true之后Room才会自动给id分配值(从1开始自增加)
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = COLUMN_ID)
    var uId: Int = 0
    // 设置在数据库表中字段名称,如果不设置,默认就是当前的属性名
    @ColumnInfo(name = COLUMN_NAME)
    var uName: String? = null
    @ColumnInfo(name = COLUMN_AGE)
    var uAge: Int = 0
    
    @Ignore
    var uSex: Int = 0
}

3.使用 Room DAO 访问数据

新建UserDao接口,并且使用@Dao注解,最后将增删改查定义在接口之中,点击Make Project之后,编译器会自动帮我们生成真正的插入数据库的代码。
Make Project自动生成的代码

3.1 插入

使用 @Insert 注释插入方法,可以将该方法的参数插入到数据库对应的表中。

注意:@Insert 方法的每个参数必须是带有 @Entity 注解的 Room 数据实体类的实例数据实体类实例的集合

  • 如果 @Insert 方法接收单个参数,则会返回 long 值,这是插入项的新 rowId
  • 如果参数是数组或集合,则该方法返回由 long 值组成的数组或集合,并且每个值都作为其中一个插入项的 rowId

示例代码

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

/**
 * @author 86351
 * @date 2022/5/3
 * @description
 */
@Dao
interface UserDao {
    
    

    /**
     * 查询当前表中数据的总数
     */
    @Query("SELECT COUNT(*) FROM " + User.TAB_NAME)
    fun count(): Int

    /**
     * 插入单条数据.
     * 
     * @param user 用户实体实例
     */
    @Insert
    fun insert(user: User): Long

    /**
     * 插入多条数据.
     * 
     * @param users 用户实体实例集合
     */
    @Insert
    fun insertList(users: List<User>): List<Long>
}

3.2 更新

借助 @Update 注释,您可以定义更新数据库表中特定行的方法。

Room 使用主键将传递的实体实例与数据库中的行进行匹配。
如果没有具有相同主键的行,Room 不会进行任何更改。

@Update 方法可以选择性地返回 int 值,该值指示成功更新的行数。注意:就是具体更新成功了多少行

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update

/**
 * @author 86351
 * @date 2022/5/3
 * @description
 */
@Dao
interface UserDao {
    
    

    /**
     * 更新单条数据.
     *
     * @param user 用户实体实例
     * @return 根据主键更新成功的行数
     */
    @Update
    fun update(user: User): Int

    /**
     * 更新多条数据.
     *
     * @param users 用户实体实例集合
     * @return 根据主键更新成功的行数,就是更新成功了多少行
     */
    @Update
    fun updateList(users: List<User>): Int
}

注意:update的返回值必须是void或者整型类型int,否则编译运行会直接报如下错误。
在这里插入图片描述

3.3 查询

使用 @Query 注解,您可以编写 SQL 语句并将其作为 DAO 方法公开。

使用查询方法从应用的数据库查询数据,或者需要执行更复杂的插入、更新和删除操作。

Room 会在编译时验证 SQL 查询。这意味着,如果查询出现问题,则会出现编译错误,而不是运行时失败。

代码示例:

import androidx.room.*

/**
 * @author 86351
 * @date 2022/5/3
 * @description
 */
@Dao
interface UserDao {
    
    

    /**
     * 查询指定id的数据.
     *
     * @param id 指定需要查询数据的id
     * @return 返回查询的实体类
     */
    @Query("SELECT *FROM " + User.TAB_NAME + " WHERE " + User.COLUMN_ID + " = :id")
    fun selectById(id: Long): User

    /**
     * 查询user表中所有数据.
     *
     * @return 数据表中数据集合
     */
    @Query("SELECT *FROM " + User.TAB_NAME)
    fun selectAll(): List<User>
}

3.4 删除

借助 @Delete 注释,您可以定义从数据库表中删除特定行的方法。

Room 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。

@Delete 方法可以选择性地返回 int 值,该值指示成功删除的行数。

import androidx.room.*

/**
 * @author 86351
 * @date 2022/5/3
 * @description
 */
@Dao
interface UserDao {
    
    

    /**
     * 删除一条数据。
     *
     * @param user 用户实体实例
     * @return 根据主键删除成功的行数
     */
    @Delete
    fun delete(user: User): Int
 
    /**
     * 根据指定id删除数据.
     *
     * 跟删除单条数据是一样的。
     */
    @Query("DELETE FROM " + User.TAB_NAME + " WHERE " + User.COLUMN_ID + " = :id")
    fun deleteById(id: Long): Int

    /**
     * 删除多条数据。
     *
     * @param users 用户实体实例集合
     * @return 根据主键删除成功的行数,就是删除成功了多少行
     */
    @Delete
    fun deleteList(users: List<User>): Int
}

3.5 完整的UserDao代码

import androidx.room.*

/**
 * @author 86351
 * @date 2022/5/3
 * @description
 */
@Dao
interface UserDao {
    
    

    /**
     * 查询当前表中数据的总数
     */
    @Query("SELECT COUNT(*) FROM " + User.TAB_NAME)
    fun count(): Int

    /**
     * 插入单条数据.
     *
     * @param user 用户实体实例
     * @return rowId
     */
    @Insert
    fun insert(user: User): Long

    /**
     * 插入多条数据.
     *
     * @param users 用户实体实例集合
     * @return rowId list
     */
    @Insert
    fun insertList(users: List<User>): List<Long>

    /**
     * 更新单条数据.
     *
     * @param user 用户实体实例
     * @return 根据主键更新成功的行数
     */
    @Update
    fun update(user: User): Int

    /**
     * 更新多条数据.
     *
     * @param users 用户实体实例集合
     * @return 根据主键更新成功的行数,就是更新成功了多少行
     */
    @Update
    fun updateList(users: List<User>): Int

    /**
     * 删除一条数据。
     *
     * @param user 用户实体实例
     * @return 根据主键删除成功的行数
     */
    @Delete
    fun delete(user: User): Int

    /**
     * 根据指定id删除数据.
     *
     * 跟删除单条数据是一样的。
     */
    @Query("DELETE FROM " + User.TAB_NAME + " WHERE " + User.COLUMN_ID + " = :id")
    fun deleteById(id: Long): Int

    /**
     * 删除多条数据。
     *
     * @param users 用户实体实例集合
     * @return 根据主键删除成功的行数,就是删除成功了多少行
     */
    @Delete
    fun deleteList(users: List<User>): Int

    /**
     * 查询指定id的数据.
     *
     * @param id 指定需要查询数据的id
     * @return 返回查询的实体类
     */
    @Query("SELECT *FROM " + User.TAB_NAME + " WHERE " + User.COLUMN_ID + " = :id")
    fun selectById(id: Long): User

    /**
     * 查询user表中所有数据.
     *
     * @return 数据表中数据集合
     */
    @Query("SELECT *FROM " + User.TAB_NAME)
    fun selectAll(): List<User>
}

官网Room DAO访问数据更多的使用方式

4.操作数据库

4.1 定义操作数据库的类

定义操作数据库的DeterminedDatabase类。DeterminedDatabase定义了数据库配置,并作为应用对持久性数据的主要访问点。

数据库类必须满足以下条件:

  1. 该类必须带有 @Database 注解该注解包含列出所有与数据库关联的数据实体的 entities 数组
  2. 该类必须是一个抽象类,用于扩展 RoomDatabase
  3. 对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例。

示例代码:

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

/**
 * @author 86351
 * @date 2022/5/3
 * @description
 */
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class DeterminedDatabase : RoomDatabase() {
    
    
    companion object {
    
    
        private const val DB_NAME = "determined.db"

        @Volatile
        private var INSTANCE: DeterminedDatabase? = null

        fun getInstance(context: Context): DeterminedDatabase {
    
    
            return INSTANCE ?: synchronized(this) {
    
    
                return INSTANCE ?: buildDatabase(context).also {
    
    
                    INSTANCE = it
                }
            }
        }

        private fun buildDatabase(context: Context): DeterminedDatabase {
    
    
            return Room.databaseBuilder(context, DeterminedDatabase::class.java, DB_NAME).build()
        }
    }

    abstract fun userDao(): UserDao
}

注意:
1、 如果您的应用在单个进程中运行,在实例化 DeterminedDatabase对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。

2、如果您的应用在多个进程中运行,请在数据库构建器调用中包含 enableMultiInstanceInvalidation()。这样,如果您在每个进程中都有一个DeterminedDatabase实例,可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中 DeterminedDatabase 的实例。

注意:
Room操作数据库要放在子线程,在Room调用增删改查方法时会检测是否在主线程,如果是则会直接抛出异常,但是也可以在Room数据库创建的时候调用allowMainThreadQueries()方法允许运行在主线程

示例代码:

Room.databaseBuilder(context, DeterminedDatabase::class.java, DB_NAME).allowMainThreadQueries().build()

原因,以插入方法为例找到最终抛出异常的代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.2 使用数据库类,操作数据库

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity(), View.OnClickListener {
    
    
    companion object {
    
    
        private const val TAG = "MainActivity"
    }

    private lateinit var btInsert: Button
    private lateinit var btUpdate: Button
    private lateinit var btQuery: Button
    private lateinit var btDelete: Button

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        init()
    }

    private fun init() {
    
    
        btInsert = findViewById(R.id.btInsert)
        btUpdate = findViewById(R.id.btUpdate)
        btQuery = findViewById(R.id.btQuery)
        btDelete = findViewById(R.id.btDelete)

        btInsert.setOnClickListener(this)
        btUpdate.setOnClickListener(this)
        btQuery.setOnClickListener(this)
        btDelete.setOnClickListener(this)
    }

    override fun onClick(view: View?) {
    
    
        when (view?.id) {
    
    
            R.id.btInsert -> {
    
    
                insert()
            }
            R.id.btUpdate -> {
    
    
                update()
            }
            R.id.btQuery -> {
    
    
                query()
            }
            R.id.btDelete -> {
    
    
                delete()
            }
        }
    }

    private fun insert() {
    
    
        runData {
    
    
            val user = User()
            user.uName = "张三"
            user.uAge = 25
            val rowId = DeterminedDatabase.getInstance(this@MainActivity).userDao().insert(user)
            Log.d(TAG, "rowId:: $rowId")
        }
    }

    private fun update() {
    
    
        runData {
    
    
            val user = User()
            user.uId = 1
            user.uName = "李四"
            user.uAge = 25
            val columnNum = DeterminedDatabase.getInstance(this@MainActivity).userDao().update(user)
            Log.d(TAG, "columnNum:: $columnNum")
        }
    }

    private fun query() {
    
    
        runData {
    
    
            val list = DeterminedDatabase.getInstance(this@MainActivity).userDao().selectAll()
            for (user in list) {
    
    
                Log.d(TAG, "user:: $user")
            }
        }
    }

    private fun delete() {
    
    
        runData {
    
    
            val user = User()
            user.uId = 1
            val columnNum = DeterminedDatabase.getInstance(this@MainActivity).userDao().delete(user)
            Log.d(TAG, "columnNum:: $columnNum")
        }
    }

    private fun runData(runnable: Runnable) {
    
    
        Executors.newSingleThreadExecutor().execute(runnable)
    }
}

插入数据库内容并且查询数据库内容log
在这里插入图片描述

修改数据库内容并且查询log
在这里插入图片描述
删除数据库内容并且查询log
在这里插入图片描述


总结

以上的文章内容的讲解,只是简单的介绍了Room的使用,而room提供了很多功能如果有需要可以查看官网。
使用 Android Jetpack 的 Room 部分将数据保存到本地数据库。

还有数据库升级未补充,喜欢的可以参考官网链接
Room迁移数据库(数据库升级)

文章参考

1、android官方文档关于Room的使用
2、阿里云的镜像
3、 rowid 表的 SQLite 文档

猜你喜欢

转载自blog.csdn.net/u013855006/article/details/124556025