likes
comments
collection
share

Android使用Jetpack Room管理数据库 - 第一弹

作者站长头像
站长
· 阅读数 16

Jetpack Room是Android官方提供的一个持久化库,旨在简化Android应用程序中的数据库操作。它提供了一个抽象层,使开发人员能够以面向对象的方式处理数据库操作,而无需编写复杂的SQL查询语句。通过使用Jetpack Room,开发人员可以更快速、更高效地构建稳健的数据库驱动应用程序。

Room 最新版本为2.5.2

Android Studio版本为 Android Studio Flamingo | 2022.2.1 Patch 2

配置Room

在使用Room之前,我们需要添加它的依赖进入到工程,首先在app模块的build.gradle中添加依赖项

dependencies {
  	...
    def room_version = "2.5.2"
    implementation "androidx.room:room-runtime:$room_version"
  	// kotlin需要kapt,java则是annotationProcessor
    kapt "androidx.room:room-compiler:$room_version"
}

因为Room依赖需要kapt插件,所以我们还需要添加kapt

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

同步下工程之后,我们就可以在工程中使用Room来管理数据库了。

简单操作Room

在介绍初始化之前,我们先了解下几个相关的概念

  • Entity用来定义数据库中的某个表和表中的数据结构;
  • Dao用来管理和操作数据库中的表,包括常见的增、删、改、查等操作;
  • Database继承自RoomDatabase类,它是抽象类,AS会自动为我们生成它的实现,用于管理数据库的名称、版本和升级等操作,并且可以从它获取Dao的实现类。

了解了上面的概念之后,下面我们直接进入使用Room的环节,一起来看看Room是如何帮我们简化数据库的操作。

第一步先定义一个实体类,用于表示数据库中某个表的结构

@Entity(tableName = "user_entity")
data class UserEntity(
    val name: String,
    @PrimaryKey
    val id: Int
)

我们定义了一个UserEntity数据类,类的注解采用@Entity修饰,表示它是数据库中的一张表,设置了表名为user_entity,并且给表的主键设置为id字段,这个主键可以帮助我们在插入相同id时给予冲突策略(后面会详细介绍)。

第二步定义我们user_entityDao类,将增删改查方法先定义好

@Dao
interface UserDao {
    // 设置主键冲突之后的策略,这里选择直接覆盖原数据
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUser(userEntity: UserEntity)

    // 删除某条数据
    @Delete
    fun deleteUser(userEntity: UserEntity)

    // 更新某条数据
    @Update
    fun updateUser(userEntity: UserEntity)

    // 根据id查找数据
    @Query("select * from user_entity where id=:id")
    fun findUser(id: Int): List<UserEntity>
}

这里我们定义了一个UserDao接口,注意是接口类哦,并且使用@Dao注解进行修饰,内部定义了四个方法,分别为增删改查操作。注意看insert方法的注解,其中onConflict参数就是用来处理主键冲突的,当我们插入一个数据时,表中已经有此主键数据,这时候Room就会根据我们设置的策略来处理这条新增的数据:

  • OnConflictStrategy.REPLACE如果发生冲突,直接覆盖已有数据,将表中现存的数据替换成插入的这条;
  • OnConflictStrategy.IGNORE如果发生冲突,直接忽略此次插入操作
  • OnConflictStrategy.NONE这个是默认的策略,它和ABORT作用是一致的,都是终止此次插入操作,并且抛出SQLiteConstraintException
  • OnConflictStrategy.ROLLBACK这个表示如果发生冲突,终止插入操作,并且将事务回滚到最初的状态,在最新版本已经被标记@Deprecated推荐使用ABORT
  • OnConflictStrategy.ABORTNONE作用一致,这里就不过多介绍
  • OnConflictStrategy.FAIL这个表示如果发生冲突,终止插入操作,并且抛出SQLiteConstraintException异常,在最新版本也是被标记@Deprecated,也是推荐使用ABORT

以上就是在插入操作过程中,主键冲突时,所有的策略模式,大家可以按需求采用。

定义好Dao之后,我们就可以配置RoomDatabase了,少了它我们还不能使用Room来操作数据库呢。

@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class RoomDb : RoomDatabase() {

    abstract fun getUserDao(): UserDao
}

定义一个RoomDatabase子类,并且它是抽象方法,采用@Database注解修饰,注解中带了三个参数:

  • entities表示所有的实体类,也是就Room要创建的表结构,它是一个数组类型,可以创建多个表;
  • version表示的是数据库的版本,这个在后面的数据库升级中会详细介绍,重要信息之一;
  • exportSchema这个参数仅仅代表是否可以在编译的时候导出数据库的配置文件,如果你需要看配置可以设置为true,默认的配置文件会在app/build/schema文件夹中。

内部有一个抽象方法,是用于获取UserDao实例,这里AS会默认为我们生成实现类,具体生成的类在build/generated/source/kapt/com/...,生成之后的类名是我们定义的类名加上_Impl

最后我们需要创建RoomDb单例对象,这里我们采用的是Koin库帮助我们简化操作,具体Koin的使用前几篇文章有详细介绍,小伙伴们可以去了解下。

val module = module {
  	// 创建RoomDb的单例对象
    single {
        Room.databaseBuilder(androidContext(), RoomDb::class.java, "room_db")
            .build()
    }
  	// 创建UserDao的单例对象
    single { get<RoomDb>().getUserDao() }
}

在创建数据库RoomDb对象时,采用的是build模式,传入了ContextRoomDb和数据库名称,这里还是比较简单的,在后面涉及数据库升级时,我们还是会回到此处,添加升级操作。

到这里数据库的准备工作已经完成了,接下来就可以实践一下最常用的增删改查操作了,顺便提一下,AS现在可以直接查看和调试App的数据库了,在App inspection工具栏里面就可以体验。

class MainActivity : AppCompatActivity() {
  	// 获取UserDao单例对象
    private val userDao by inject<UserDao>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<TextView>(R.id.tv).setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO) {
              	// 插入一条数据
                userDao.insertUser(UserEntity(1, "taonce"))
            }
        }
        findViewById<TextView>(R.id.textView).setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO) {
                // 将name更新为taonce_update
                userDao.updateUser(UserEntity(1, "taonce_update"))
            }
        }
        findViewById<TextView>(R.id.textView2).setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO) {
                // 查找表中id为1的数据
                val entityList = userDao.findUser(1)
                entityList.forEach { Log.d(TAG, "find user: $it") }
            }
        }
        findViewById<TextView>(R.id.textView3).setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO) {
                // 删除此UserEntity数据
                userDao.deleteUser(UserEntity(1, "taonce_update"))
            }
        }
    }
}

当我们点击插入数据后,我们可以在App Inspection中实时查看到数据的变化。

Android使用Jetpack Room管理数据库 - 第一弹

如果你想实时的观察表中数据变化,记住勾选Live updates框,未勾选的情况需要手动点击前面的刷新图标。App Inspection还可以直接使用SQL语句来操作表,你可以在调试过程中修改或者模拟一些数据。

Android使用Jetpack Room管理数据库 - 第一弹

左边红框就是新建SQL语句的入口。

Entity的高阶用法

上面提到了在数据类采用@Entity注解之后,表结构就会根据数据类的字段来生成对应表字段,如果有个数据类的字段很多,但是我们又不想全部存入数据库,或者这个数据类引入了别的数据类此时对应的表结构会是怎样呢?

忽略字段

当我们不想表中存入全部字段时,我们可以采用忽略某些字段的形式来解决这种问题。

@Entity
data class ArticleEntity(
    @PrimaryKey()
    var articleId: Long = 0L,
    var title: String = "",
    var url: String = "",
    var author: String = "",
    @Ignore
    var authorAlisa: String = ""
)

模拟了一个文章实体类,它内部包含很多信息,但是authorAlisa这个字段我们并不想存入到表中,这个就可以采用@Ignore注解来忽略此字段,最后的表结构通过App Inspection看下,它是不包含authorAlisa字段的。

Android使用Jetpack Room管理数据库 - 第一弹

嵌套对象

当我们定义的数据类中嵌套了另外一个或者多个数据类时,如果不做任何操作Room是无法为我们创建对应的表结构,我们需要显示的通过@Embedded注解告诉Room此字段为嵌入对象,需要将嵌入对象的字段也加入到表中。

@Entity
data class ArticleEntity(
    @PrimaryKey()
    var articleId: Long = 0L,
    var title: String = "",
    var url: String = "",
    var author: String = "",
    @Ignore
    var authorAlisa: String = "",
  	// 嵌入了AuthorDetail对象
    @Embedded
    var authorDetail: AuthorDetail = AuthorDetail()
)

data class AuthorDetail(
    val authorId: Long = 0L,
    val authorName: String = "",
    val joinTime: String = "",
    val updateTime: String = ""
)

上面我们在ArticleEntity数据类中嵌入了AuthorDetail对象,Room会将被嵌入对象的字段也一并加入到表中,还是通过App Inspection来观察下表结构。

Android使用Jetpack Room管理数据库 - 第一弹

从上面的图片就可以看出被嵌入对象的字段也一起加入到AuthorEntity表中了。

嵌入List对象

当我们定义的实体类中含有List字段时,并且在不忽略此字段的情况下,无法通过@Embedded嵌入对象的方式来引入其内部字段,这时候就需要通过TypeConverter的形式来操作List字段了。

首先我们模拟带有List字段的实体类,并且在类上通过@TypeConverters注解指定类型转换

@TypeConverters(AuthorDetailConvert::class)
@Entity
data class ArticleEntity(
    @PrimaryKey()
    var articleId: Long = 0L,
    var title: String = "",
    var url: String = "",
    var author: String = "",
    @Ignore
    var authorAlisa: String = "",
    var authorDetail: List<AuthorDetail> = listOf()
)

data class AuthorDetail(
    val authorId: Long = 0L,
    val authorName: String = "",
    val joinTime: String = "",
    val updateTime: String = ""
)

然后再看看我们定义的类型转换具体实现

@ProvidedTypeConverter
class AuthorDetailConvert {
    @TypeConverter
    fun string2AuthorDetailList(detailList: String): List<AuthorDetail> {
        return Gson().fromJson(detailList, object : TypeToken<List<AuthorDetail>>() {}.type)
    }

    @TypeConverter
    fun authorDetailList2String(list: List<AuthorDetail>): String {
        return Gson().toJson(list)
    }
}

此类必须通过@ProvidedTypeConverter注解修饰,表示它提供某种具体的类型转换,内部定义两个方法,方法也必须通过@TypeConverter注解修饰。

  • authorDetailList2String方法具体含义就是将List<AuthorDetail>通过Gson转换成字符串的形式,这个是用来插入数据时调用的方法;
  • string2AuthorDetailList方法则是相反,它是将字符串通过Gson转换成我们需要的List<AuthorDetail>对象,这个是用来从数据库中取出数据时调用的方法。

总得来说也就是我们在存数据到库中的时候,会将List<T>对象转换成字符串存入到表中,它在表中是一个字段,然后再取数据时直接将字符串转换成对应List<T>对象,这样在开发者的角度就不需要额外的转换逻辑。

下面我们模拟一条数据插入到表中,看看表中保存的数据呈现的是何种样式,先模拟插入操作:

val authorList = listOf<AuthorDetail>(
    AuthorDetail(1, "taonce", "今天", "今天"),
    AuthorDetail(2, "taonce2", "今天", "今天"),
)
val articleEntity =
    ArticleEntity(1, "article", "android.com", "taonce", "taonce", authorList)
authorDao.insertAuthor(articleEntity)

此时通过App Inspection来看下表的数据

Android使用Jetpack Room管理数据库 - 第一弹

和我们预期的是一致的,它在表中的具体形式就是一个Gson字符串。

文章结尾

本次篇幅暂时就介绍以上内容,篇幅过长阅读起来会产生疲倦感,后面的数据库升级操作和技巧会另起一篇文章详细介绍,这次就到这了~

转载自:https://juejin.cn/post/7253581299058524219
评论
请登录