前言
日常开发中,总会遇到需要将数据持久化在本地,供需要的时候使用。
本地数据持久化例如:文件保存、SharedPreferences(DataStore,sp的替代者)、数据库等等。简单的键值对信息我们直接使用SP保存在xml中就可以了,但是更加复杂的信息就需要数据库(SQLite)上场了。
本文就使用Room来操作SQLite数据库。
更加详细的内容可以查看官网,链接已经放在参考中,可以自行查阅。
一、Room是什么?
Room 持久性库是在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
Room的优势:
- 针对 SQL 查询的编译时验证
- 可最大限度减少重复和容易出错的样板代码的方便注解。
- 简化了数据库迁移路径。
- Android官方维护,不像其他开源项目出现问题无人能够解决
主要组件
1. 数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点。 2. 数据实体,用于表示应用的数据库中的表。 3. 数据访问对象 (DAO),提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法。
数据库类为应用提供与该数据库关联的 DAO 的实例,对于数据库的增删改查就是定义在Dao实例中。
引用官网的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'
id 'kotlin-kapt'
}
dependencies {
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
扩展库(根据需要选择)如下(示例):
dependencies {
def room_version = "2.4.2"
implementation "androidx.room:room-rxjava2:$room_version"
implementation "androidx.room:room-rxjava3:$room_version"
implementation "androidx.room:room-guava:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
implementation "androidx.room:room-paging:2.5.0-alpha01"
}
如果出现库拉取失败,可以将镜像换为阿里云的镜像,原文见结尾的参考文章
如下配置是在最新的Android studio(小蜜蜂),是配置在settings.gradle中,小蜜蜂之前的Android studio是配置在Project的build.gradle中。
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 实体都必须定义一个主键,用于唯一标识相应数据库表中的每一行。
示例如下:
@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
@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"
}
@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之后,编译器会自动帮我们生成真正的插入数据库的代码。
3.1 插入
使用 @Insert 注释插入方法,可以将该方法的参数插入到数据库对应的表中。
注意:@Insert 方法的每个参数必须是带有 @Entity 注解的 Room 数据实体类的实例或数据实体类实例的集合。
- 如果 @Insert 方法接收单个参数,则会返回 long 值,这是插入项的新 rowId。
- 如果参数是数组或集合,则该方法返回由 long 值组成的数组或集合,并且每个值都作为其中一个插入项的 rowId。
示例代码:
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface UserDao {
@Query("SELECT COUNT(*) FROM " + User.TAB_NAME)
fun count(): Int
@Insert
fun insert(user: User): Long
@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
@Dao
interface UserDao {
@Update
fun update(user: User): Int
@Update
fun updateList(users: List<User>): Int
}
注意:update的返回值必须是void或者整型类型int,否则编译运行会直接报如下错误。
3.3 查询
使用 @Query 注解,您可以编写 SQL 语句并将其作为 DAO 方法公开。
使用查询方法从应用的数据库查询数据,或者需要执行更复杂的插入、更新和删除操作。
Room 会在编译时验证 SQL 查询。这意味着,如果查询出现问题,则会出现编译错误,而不是运行时失败。
代码示例:
import androidx.room.*
@Dao
interface UserDao {
@Query("SELECT *FROM " + User.TAB_NAME + " WHERE " + User.COLUMN_ID + " = :id")
fun selectById(id: Long): User
@Query("SELECT *FROM " + User.TAB_NAME)
fun selectAll(): List<User>
}
3.4 删除
借助 @Delete 注释,您可以定义从数据库表中删除特定行的方法。
Room 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。
@Delete 方法可以选择性地返回 int 值,该值指示成功删除的行数。
import androidx.room.*
@Dao
interface UserDao {
@Delete
fun delete(user: User): Int
@Query("DELETE FROM " + User.TAB_NAME + " WHERE " + User.COLUMN_ID + " = :id")
fun deleteById(id: Long): Int
@Delete
fun deleteList(users: List<User>): Int
}
3.5 完整的UserDao代码
import androidx.room.*
@Dao
interface UserDao {
@Query("SELECT COUNT(*) FROM " + User.TAB_NAME)
fun count(): Int
@Insert
fun insert(user: User): Long
@Insert
fun insertList(users: List<User>): List<Long>
@Update
fun update(user: User): Int
@Update
fun updateList(users: List<User>): Int
@Delete
fun delete(user: User): Int
@Query("DELETE FROM " + User.TAB_NAME + " WHERE " + User.COLUMN_ID + " = :id")
fun deleteById(id: Long): Int
@Delete
fun deleteList(users: List<User>): Int
@Query("SELECT *FROM " + User.TAB_NAME + " WHERE " + User.COLUMN_ID + " = :id")
fun selectById(id: Long): User
@Query("SELECT *FROM " + User.TAB_NAME)
fun selectAll(): List<User>
}
官网Room DAO访问数据更多的使用方式
4.操作数据库
4.1 定义操作数据库的类
定义操作数据库的DeterminedDatabase 类。DeterminedDatabase 定义了数据库配置,并作为应用对持久性数据的主要访问点。
数据库类必须满足以下条件:
- 该类必须带有
@Database 注解 ,该注解包含列出所有与数据库关联的数据实体的 entities 数组 。 - 该类必须是一个抽象类,用于扩展 RoomDatabase。
- 对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例。
示例代码:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@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 文档
|