您现在的位置是:首页 >技术杂谈 >探究Jetpack(三)之ROOM网站首页技术杂谈

探究Jetpack(三)之ROOM

jemo也怕检查 2024-09-25 12:01:12
简介探究Jetpack(三)之ROOM


ORM(Object Relational Mapping)也叫对象关系映射。简单来讲,Kotlin使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了
ORM框架赋予了一个强大的功能,就是可以用面向对象的思维来和数据库进行交互,绝大多数情况下不用再和SQL语句打交道了,同时也不用担心操作数据库的逻辑会让项目的整体代码变得混乱
由于许多大型项目中会用到数据库的功能,为了编写出更好的代码,Android官方推出了一个ORM框架,并将它加入了Jetpack当中,这就是Room

使用Room进行增删改查

先来看一下Room的整体结构。它主要由Entity、Dao和Database这3部分组成,每个部分都有明确的职责,详细说明如下

  • Entity:用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
  • Dao:Dao是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可。
  • Database:用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao层的访问实例

继续在JetpackTest项目上进行改造。首先要使用Room,需要在app/build.gradle文件中添加如下的依赖:

plugins {
	···
	id("org.jetbrains.kotlin.kapt")
}
···

dependencies {
    val room_version = "2.5.0"

    implementation("androidx.room:room-runtime:$room_version")
    implementation("androidx.room:room-ktx:$room_version")
    kapt("androidx.room:room-compiler:$room_version")
    ···
}

下面就按照刚才介绍的Room的3个组成部分一一来进行实现,首先是定义Entity,也就是实体类
好消息是JetpackTest项目中已经存在一个实体类了,就是在了解LiveData时创建的User类。然而User类目前只包含firstName、lastName和age这3个字段,但是一个良好的数据库编程建议是,给每个实体类都添加一个id字段,并将这个字段设为主键
于是对User类进行如下改造,并完成实体类的声明:

@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {

	 @PrimaryKey(autoGenerate = true)
	 var id: Long = 0
}

可以看到,这里在User的类名上使用@Entity注解,将它声明成了一个实体类,然后在User类中添加了一个id字段,并使用@PrimaryKey注解将它设为了主键,再把autoGenerate参数指定成true,使得主键的值是自动生成的,这样实体类部分就定义好了

接下来开始定义Dao,这部分也是Room用法中最关键的地方,因为所有访问数据库的操作都是在这里封装的
访问数据库的操作无非就是增删改查这4种,但是业务需求却是千变万化的。而Dao要做的事情就是覆盖所有的业务需求,使得业务方永远只需要与Dao层进行交互,而不必和底层的数据库打交道

新建一个UserDao接口,注意必须使用接口,这点和Retrofit是类似的,然后在接口中编写如下代码:

@Dao
interface UserDao {

	 @Insert
	 fun insertUser(user: User): Long
	 
	 @Update
	 fun updateUser(newUser: User)
	 
	 @Query("select * from User")
	 fun loadAllUsers(): List<User>
	 
	 @Query("select * from User where age > :age")
	 fun loadUsersOlderThan(age: Int): List<User>
	 
	 @Delete
	 fun deleteUser(user: User)
	 
	 @Query("delete from User where lastName = :lastName")
	 fun deleteUserByLastName(lastName: String): Int
}

UserDao接口的上面使用了一个@Dao注解,这样Room才能将它识别成一个Dao。UserDao的内部就是根据业务需求对各种数据库操作进行的封装。数据库操作通常有增删改查这4种,因此Room也提供了@Insert、@Delete、@Update和@Query这4种相应的注解
insertUser()方法上面使用了@Insert注解,表示会将参数中传入的User对象插入数据库中,插入完成后还会将自动生成的主键id值返回
updateUser()方法上面使用了@Update注解,表示会将参数中传入的User对象更新到数据库当中
deleteUser()方法上面使用了@Delete注解,表示会将参数传入的User对象从数据库中删除
以上几种数据库操作都是直接使用注解标识即可,不用编写SQL语句

但是如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,那么就必须编写SQL语句了。比如在UserDao接口中定义了一个loadAllUsers()方法,用于从数据库中查询所有的用户,如果只使用一个@Query注解,Room将无法知道我们想要查询哪些数据,因此必须在@Query注解中编写具体的SQL语句才行
还可以将方法中传入的参数指定到SQL语句当中,比如loadUsersOlderThan()方法就可以查询所有年龄大于指定参数的用户
另外,如果是使用非实体类参数来增删改数据,那么也要编写SQL语句才行,而且这个时候不能使用@Insert、@Delete或@Update注解,而是都要使用@Query注解才行

接下来进入最后一个部分:定义Database。这部分内容的写法是非常固定的,只需要定义好3个部分的内容:数据库的版本号、包含哪些实体类,以及提供Dao层的访问实例
新建一个AppDatabase.kt文件,代码如下所示:

@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {

	 abstract fun userDao(): UserDao
	 
	 companion object {
	 
		 private var instance: AppDatabase? = null
		 
		 @Synchronized
		 fun getDatabase(context: Context): AppDatabase {
			 instance?.let {
			 	return it
		 	 }
			 return Room.databaseBuilder(context.applicationContext,AppDatabase::class.java, "app_database")
				 .build().apply {
				 instance = this
			 }
		 }
	 }
}

这里在AppDatabase类的头部使用了@Database注解,并在注解中声明了数据库的版本号以及包含哪些实体类,多个实体类之间用逗号隔开即可
AppDatabase类必须继承自RoomDatabase类,并且一定要使用abstract关键字将它声明成抽象类,然后提供相应的抽象方法,用于获取之前编写的Dao的实例,比如这里提供的userDao()方法。不过只需要进行方法声明就可以了,具体的方法实现是由Room在底层自动完成的
在companion object结构体中编写了一个单例模式,因为原则上全局应该只存在一份AppDatabase的实例。这里使用了instance变量来缓存AppDatabase的实例,然后在getDatabase()方法中判断:

  • 如果instance变量不为空就直接返回
  • 否则就调用Room.databaseBuilder()方法来构建一个AppDatabase的实例

databaseBuilder()方法接收3个参数,注意第一个参数一定要使用applicationContext,而不能使用普通的context,否则容易出现内存泄漏的情况。第二个参数是AppDatabase的Class类型,第三个参数是数据库名
最后调用build()方法完成构建,并将创建出来的实例赋值给instance变量,然后返回当前实例即可

修改activity_main.xml中的代码,在里面加入用于增删改查的4个按钮
然后修改MainActivity中的代码,分别在这4个按钮的点击事件中实现增删改查的逻辑,如下所示:

class MainActivity : AppCompatActivity() {
	 ...
	 override fun onCreate(savedInstanceState: Bundle?) {
		 ...
		 val userDao = AppDatabase.getDatabase(this).userDao()
		 val user1 = User("Tom", "Brady", 40)
		 val user2 = User("Tom", "Hanks", 63)
		 
		 addDataBtn.setOnClickListener {
			 thread {
				 user1.id = userDao.insertUser(user1)
				 user2.id = userDao.insertUser(user2)
			 }
		 }
		 
		 updateDataBtn.setOnClickListener {
			 thread {
				 user1.age = 42
				 userDao.updateUser(user1)
			 }
		 }
		 
		 deleteDataBtn.setOnClickListener {
			 thread {
			 	userDao.deleteUserByLastName("Hanks")
			 }
		 }
		 
		 queryDataBtn.setOnClickListener {
			 thread {
				 for (user in userDao.loadAllUsers()) {
				 	Log.d("MainActivity", user.toString())
				 }
			 }
		 }
	 }
	 ...
}

这段代码的逻辑还是很简单的。首先获取了UserDao的实例,并创建两个User对象
在“Add Data”按钮的点击事件中,调用了UserDao的insertUser()方法,将这两个User对象插入数据库中,并将insertUser()方法返回的主键id值赋值给原来的User对象
在“Update Data”按钮的点击事件中,将user1的年龄修改成了42岁,并调用UserDao的updateUser()方法来更新数据库中的数据
在“Delete Data”按钮的点击事件中,调用了UserDao的deleteUserByLastName()方法,删除所有lastName是Hanks的用户
在“Query Data”按钮的点击事件中,调用了UserDao的loadAllUsers()方法,查询并打印数据库中所有的用户

运行程序,点击各种按钮,可以在日志里看到对于的内容

Room的数据库升级

数据库结构不可能在设计好了之后就永远一成不变,随着需求和版本的变更,数据库也是需要升级的。不过遗憾的是,Room在数据库升级方面设计得非常烦琐,基本上没有比使用原生的SQLiteDatabase简单到哪儿去,每一次升级都需要手动编写升级逻辑才行

假如随着业务逻辑的升级,现在打算在数据库中添加一张Book表,那么首先要做的就是创建一个Book的实体类,如下所示:

@Entity
data class Book(var name: String, var pages: Int) {

	 @PrimaryKey(autoGenerate = true)
	 var id: Long = 0
}

Book类中包含了主键id、书名、页数这几个字段,并且还使用@Entity注解将它声明成了一个实体类

然后创建一个BookDao接口,并在其中随意定义一些API:

@Dao
interface BookDao {

	 @Insert
	 fun insertBook(book: Book): Long
	 
	 @Query("select * from Book")
	 fun loadAllBooks(): List<Book>
}

接下来修改AppDatabase中的代码,在里面编写数据库升级的逻辑,如下所示:

@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {

	 abstract fun userDao(): UserDao
	 
	 abstract fun bookDao(): BookDao
	 
	 companion object {
		 val MIGRATION_1_2 = object : Migration(1, 2) {
			 override fun migrate(database: SupportSQLiteDatabase) {
			 	database.execSQL("create table Book (id integer primary key autoincrement not null, name text not null, pages integer not null)")
			 }
		 }
		 
		 private var instance: AppDatabase? = null
		 
		 fun getDatabase(context: Context): AppDatabase {
			 instance?.let {
			 	return it
			 }
			 return Room.databaseBuilder(context.applicationContext,AppDatabase::class.java, "app_database")
				 .addMigrations(MIGRATION_1_2)
				 .build().apply {
				 instance = this
			 }
		 }
	 }
}

首先在@Database注解中,将版本号升级成了2,并将Book类添加到了实体类声明中,然后又提供了一个bookDao()方法用于获取BookDao的实例
接下来就是关键的地方了,在companion object结构体中,实现了一个Migration的匿名类,并传入了1和 2这两个参数,表示当数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑。匿名类实例的变量命名也比较有讲究,这里命名成MIGRATION_1_2,可读性更高
由于要新增一张Book表,所以需要在migrate()方法中编写相应的建表语句。另外必须注意的是,Book表的建表语句必须和Book实体类中声明的结构完全一致,否则Room就会抛出异常
最后在构建AppDatabase实例的时候,加入一个addMigrations()方法,并把MIGRATION_1_2传入即可

现在进行任何数据库操作时,Room就会自动根据当前数据库的版本号执行这些升级逻辑,从而让数据库始终保证是最新的版本

每次数据库升级并不一定都要新增一张表,也有可能是向现有的表中添加新的列。这种情况只需要使用alter语句修改表结构就可以了
现在Book的实体类中只有id、书名、页数这几个字段,想要再添加一个作者字段,代码如下所示:

@Entity
data class Book(var name: String, var pages: Int, var author: String) {

	 @PrimaryKey(autoGenerate = true)
	 var id: Long = 0
}

修改AppDatabase中的代码,如下所示:

@Database(version = 3, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
	 ...
	 companion object {
		 ...
		 val MIGRATION_2_3 = object : Migration(2, 3) {
			 override fun migrate(database: SupportSQLiteDatabase) {
			 	database.execSQL("alter table Book add column author text not null default 'unknown'")
			 }
		 }
		 
		 private var instance: AppDatabase? = null
		 
		 fun getDatabase(context: Context): AppDatabase {
			 ...
			 return Room.databaseBuilder(context.applicationContext,AppDatabase::class.java, "app_database")
				 .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
				 .build().apply {
				 instance = this
			 }
		 }
	 }
}

升级步骤和之前是差不多的,这里先将版本号升级成了3,然后编写一个MIGRATION_2_3的升级逻辑并添加到addMigrations()方法中即可
比较有难度的地方就是每次在migrate()方法中编写的SQL语句,不过即使写错了也没关系,因为程序运行之后在你首次操作数据库的时候就会直接触发崩溃,并且告诉你具体的错误原因,对照着错误原因来改正你的SQL语句即可

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。