安卓开发玩转JetPack之Room的使用
Room数据库
安卓原生的SQLite数据库是关系型数据库,而我们进行安卓开发使用的语言是面向对象语言。将这两者之间建立一种映射关系就是ORM框架,Room就是安卓官方推出的对象映射库,它有一个强大的功能就是用面向对象的思维和数据库进行交互,绝大多数情况下不需要使用SQL语句。
核心组件
Room核心由Entity、Dao和Database3部分组成,共同协作完成数据库操作。
- Entity :用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中字段自动生成。
- Dao:Dao是数据访问对象的意思,主要对数据库的各项操作进行封装,比如insert,query这些语句,实际编程时就不需要和底层数据库打交道
- Database:用于定义数据库中的关键信息,包括数据库的版本号,包含的实体类以及提供Dao层的访问实例。
使用前的准备
使用Room前需要先在build.gradle(模块级)中添加依赖
如果是kotlin项目,需要在插件中加入kapt,并导入两条依赖
plugins {id("org.jetbrains.kotlin.kapt")//使得原本为 Java 设计的注解处理器能够处理 Kotlin 代码
}dependencies {implementation("androidx.room:room-runtime:2.6.1")kapt("androidx.room:room-compiler:2.6.1")
}
在defaultConfig中启用schema导出
defaultConfig {// Room schema 导出配置(Kotlin DSL 正确语法)javaCompileOptions {annotationProcessorOptions {arguments.putAll(mapOf("room.schemaLocation" to "${projectDir}/schemas","room.incremental" to "true"))}}
}
最后额外为kapt添加
// kapt 专用配置(Kotlin DSL 正确语法)
kapt {arguments {arg("room.schemaLocation", "${projectDir}/schemas")arg("room.incremental", "true")}
}
如果是Java项目,就添加这两条依赖
dependencies {implementation ("androidx.room:room-runtime:2.6.1")annotationProcessor ("androidx.room:room-compiler:2.6.1")
}
Entity(实体)
Entity主要就是声明实际数据的实体类,和直接声明一个类没有什么区别。定义完类后类名上加上@Entity的注解即可
@Entity
data class User(val name : String, val age : Int) {@PrimaryKey(autoGenerate = true)var id = 0
}
不过Room要求必须有一个主键,这里添加了一个id主键,如果没有这个主键编译器会报错,这里演示的话就只定义这一个实体类了
Dao
Dao是让我们对数据库可能进行的操作进行封装,Dao必须使用接口,代码如下。
@Dao
interface UserDao {@Insertfun insertUser(user: User): Long@Updatefun updateUser(newUser: User)@Query("select * from User")fun loadAllUsers(): List<User>@Query("select * from User where age > :age")fun loadUserOlderThan(age : Int): List<User>@Deletefun deleteUser(user: User)@Query("delete from User where name = :name")fun deleteUserByName(name: String) : Int}
UserDao接口的上面使用了一个@Dao注解,这样Room才能将它识别为一个Dao。我们业务需求需要哪些操作我们在接口中定义即可。数据库中有增删改查4中操作,因此Room也提供了对应的4中注解。
增删改几种操作都比较固定直接使用注解标识即可,但是如果是查询数据那我们需要根据我们对应的业务需求编写对应的SQL语句,只使用一个@Query注解Room也无法知道我们想查哪些数据。比如我们想要查询所有用户信息,定义了一个loadAllUsers()方法,那么需要在注解中编写对应的SQL语句才行。另外,入宫时使用非实体类参数进行增删改数据,那么也要编写SQL语句才行,而且这是不能用@Insert、@Delete或者@Update注解,都要使用@Query注解才行,这个参考代码中的deleteUserByName()方法。
另外注意"select * from User where age > :age"里面的 :age,这里的:是一个占位符,声明一个参数绑定点,编译器会与方法参数进行绑定
DataBase
DataBase的写法是固定的,只需要定义数据库的版本号,包含的实体类以及Dao层的访问实例。
@Database(version = 1, entities = [User::class])
abstract class MyDatabase : RoomDatabase() {abstract fun userDao(): UserDaocompanion object {private var instance: MyDatabase ?= null@Synchronizedfun getDatabase(context: Context): MyDatabase {instance?.let { return it}return Room.databaseBuilder(context.applicationContext,MyDatabase::class.java, "app_dadtabase").build().apply { instance = this }}}
}
Database依旧在头部使用@Database注解,并在注解中声明了数据库的版本号以及包含的实体类,多个实体类之间用逗号隔开即可。
该Database类必须继承自RoomDatabase类并且一定要使用abstract关键字将其声明为抽象类,提供相应的抽象方法用于获取之前编写的Dao实例,不够只需要进行方法声明即可,具体的实现是由Room在底层自动完成的。接着编写一个单例模式确保Database全局只有一个实例。这里使用instance缓存该实例,然后在getDatabase()方法中判断,如果不为空就直接返回,否则调用Room.databaseBuilder()方法构建一个Mydatabase实例,databaseBuilder()方法接收3个参数,第一个是全局环境,注意一定要用applicationContext,不能使用普通的context,否则容易出现内存泄漏。第二个参数是该数据库的class类型,第三个参数是数据库名,最后调用build并用apply返回即可。
下面是在活动中的使用,需要额外定义一个xml文件,这个就不在这里展示了,大家自己写几个button就好了。
private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {private lateinit var binding : ActivityMainBindinglateinit var sp : SharedPreferenceslateinit var viewModel : CounterViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)insets}val userDao = MyDatabase.getDatabase(this).userDao()val user1 = User("Mike", 18)val user2 = User("Bruce", 8)var oldList = listOf<User>()Log.d(TAG, "onCreate: 没查询的oldlist是" + oldList)binding.addButton.setOnClickListener {thread{user1.id = userDao.insertUser(user1)user2.id = userDao.insertUser(user2)}}binding.queryButton.setOnClickListener {thread{ oldList = userDao.loadUserOlderThan(10) }Thread.sleep(100)Log.d(TAG, "onCreate: 查询过之后的oldlist是" + oldList)}binding.updateButton.setOnClickListener {thread{user2.name = "joker"userDao.updateUser(user2)}}binding.deleteButton.setOnClickListener {thread{ userDao.deleteUser(user2) }}}
}
首先获取userDao的实例,用该实例调用在Dao中封装好的api即可。另外要注意,Room为了防止UI卡顿和ANR(应用无响应),默认禁止在主线程执行数据库操作,因此相关代码里都开了子线程。
更新数据库
Room在数据库升级方面比较繁琐。我们就在上面的基础上新建一张Book表
@Entity
data class Book(val name : String, val pages: Int) {@PrimaryKey(autoGenerate = true)var id: Long = 0
}
这是Book的实体类
@Dao
interface BookDao {@Insertfun insertBook(book: Book): Long@Query("select * from Book")fun loadAllBooks() : List<Book>
}
这是BookDao封装的api。下来看dataBase代码的改动
@Database(version =2, entities = [User::class, Book::class])
abstract class MyDatabase : RoomDatabase() {abstract fun userDao(): UserDaoabstract fun bookDao(): BookDaocompanion object {val MIGRATION1_2 = object : Migration(1, 2) {override fun migrate(db: SupportSQLiteDatabase) {db.execSQL("create table Book (id integer primary key autoincrement not null," +" name text not null," +"pages integer not null")}}private var instance: MyDatabase ?= null@Synchronizedfun getDatabase(context: Context): MyDatabase {instance?.let {return it}return Room.databaseBuilder(context.applicationContext,MyDatabase::class.java, "app_dadtabase").addMigrations(MIGRATION1_2).build().apply { instance = this }}}
}
首先是注解中的改动,版本号更新为了2,实体类中加入了Book,在类里面加入了bookDao()抽象方法,这些都很简单,关键的是下面我们实现了一个匿名类并传入了1,2参数标识数据库版本从1升级到2时就执行这个匿名类中的逻辑。由于我们新增一张表,所以要在migrate()方法中编写相关的建表语句,建表语句必须和Book实体类中声明的结构完全一致,否则会报错。最后构建数据库实例时,加入一个addMigrations()方法并把MIARAGION1_2传入即可。
上述工作结束后当我们进行数据库操作时,Room就会自动根据当前数据库版本号执行升级逻辑,不过上述代码时新增了一张表,如果是向现有的表增加新列,只需要使用alter语句即可
@Database(version =3, entities = [User::class, Book::class])
abstract class MyDatabase : RoomDatabase() {abstract fun userDao(): UserDaoabstract fun bookDao(): BookDaocompanion object {val MIGRATION1_2 = object : Migration(1, 2) {override fun migrate(db: SupportSQLiteDatabase) {db.execSQL("create table Book (id integer primary key autoincrement not null," +" name text not null," +"pages integer not null)")}}val MIGRATION2_3 = object : Migration(2, 3) {override fun migrate(db: SupportSQLiteDatabase) {db.execSQL("alter table Book add column author text not null default 'unknown'")}}private var instance: MyDatabase ?= null@Synchronizedfun getDatabase(context: Context): MyDatabase {instance?.let {return it}return Room.databaseBuilder(context.applicationContext,MyDatabase::class.java, "app_dadtabase").addMigrations(MIGRATION1_2, MIGRATION2_3).build().apply { instance = this }}}
}
代码和上面差不多,版本升级到3,再写一个升级语句,最后在addMigrations()方法中传入该语句即可。到此Room的基本用法就介绍完了。
最后,尽管Room已经尽量避免我们手写SQL语句,但是仍然不能完全避开SQL的基础语法,想要灵活地运用数据库,还是建议多掌握一些SQL语法。
