Room
Room是Android官方推出了一个ORM框架,将数据库中的数据映射到对象上
使用Room进行增删改查
Room的整体结构主要由Entity、Dao和Database这3部分组成
Entity:用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的
Dao:是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可
Database:用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao层的访问实例
添加依赖
plugins {
......
id 'kotlin-kapt'
}
......
dependencies {
implementation "androidx.room:room-runtime:2.1.0"
kapt "androidx.room:room-compiler:2.1.0"
......
}
新增了一个kotlin-kapt插件,同时在dependencies闭包中添加了两个Room的依赖库。由于Room会根据我们在项目中声明的注解来动态生成代码,因此这里一定要使用kapt引入Room的编译时注解库,而启用编译时注解功能则一定要先添加kotlin-kapt插件。注意,kapt只能在Kotlin项目中使用,如果是Java项目的话,使用annotationProcessor即可
创建实体类User
// 使用@Entity注解,将User声明成了一个实体类
@Entity
data class User(var name: String, var age: Int) {
// 使用@PrimaryKey注解将它设为了主键,再把autoGenerate参数指定成true,使得主键的值是自动生成的
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
创建UserDao
Dao必须使用接口,访问数据库的操作无非就是增删改查这4种,但是业务需求却是千变万化的。而Dao要做的事情就是覆盖所有的业务需求,使得业务方永远只需要与Dao层进行交互,而不必和底层的数据库打交道
// 使用了一个@Dao注解,这样Room才能将它识别成一个Dao
@Dao
interface UserDao {
@Insert
fun insertUser(user: User): Long
@Update
fun updateUser(newUser: User): Long
@Query("select * from User")
fun loadAllUsers(): List<User>
// 将方法中传入的参数指定到SQL语句
@Query("select * from User where age > :age")
fun loadAllOlderThan(age: Int): List<User>
@Delete
fun loadAllUsers(user: User)
@Query("delete from User where name=:name")
fun deleteUserByLastName(name: String): Int
}
如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,那么必须编写SQL语句,可以将方法中传入的参数指定到SQL语句
定义Database
只需要定义好3个部分的内容:数据库的版本号、包含哪些实体类,以及提供Dao层的访问实例
// 使用@Database注解,在注解中声明数据库的版本号以及包含哪些实体类,多个实体类之间用逗号隔开
@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类必须继承自RoomDatabase类,并且一定要使用abstract关键字将它声明成抽象类,然后提供相应的抽象方法,用于获取之前编写的Dao的实例,比如这里提供的userDao()方法,只需要进行方法声明就可以了,具体的方法实现是由Room在底层自动完成的
在companion object结构体中编写了一个单例模式,因为原则上全局应该只存在一份AppDatabase的实例。databaseBuilder()方法接收3个参数,第一个参数一定要使用applicationContext,而不能使用普通的context,否则容易出现内存泄漏的情况,第二个参数是AppDatabase的Class类型,第三个参数是数据库名
使用Room
class RoomActivity : AppCompatActivity() {
companion object {
private const val TAG = "RoomActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_room)
val userDao = AppDatabase.getDatabase(this).userDao()
val user1 = User("Tom", 34)
val user2 = User("Tony", 26)
val user3 = User("Jack", 14)
add_user.setOnClickListener {
thread {
user1.id = userDao.insertUser(user1)
user2.id = userDao.insertUser(user2)
user3.id = userDao.insertUser(user3)
}
}
update_user.setOnClickListener {
thread {
user3.age = 10
userDao.updateUser(user3)
}
}
get_all_user.setOnClickListener {
thread {
val loadAllUsers = userDao.loadAllUsers()
loadAllUsers.forEach {
Log.d(TAG, "onCreate: ${it}")
}
}
}
get_user_older_than.setOnClickListener {
thread {
val loadAllOlderThan = userDao.loadAllOlderThan(20)
loadAllOlderThan.forEach {
Log.d(TAG, "onCreate: ${it}")
}
}
}
delete_user.setOnClickListener {
thread {
userDao.deleteUser(user1)
}
}
delete_user_tony.setOnClickListener {
thread {
userDao.deleteUserByName("Tony")
}
}
}
}
由于数据库操作属于耗时操作,Room默认是不允许在主线程中进行数据库操作
问题
编译是出现下面的问题
E:\AndroidStudioProject\Jetpack\app\build\tmp\kapt3\stubs\debug\com\example\jetpack\room\data\AppDatabase.java:15: ����: Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide `room.schemaLocation` annotation processor argument OR set exportSchema to false.
public abstract class AppDatabase extends androidx.room.RoomDatabase {
^
未提供架构导出目录,因此无法导出架构
在编译时,Room 会将数据库的架构信息导出为 JSON 文件(默认exportSchema = true导出架构)
要导出架构,可以在在 app/build.gradle 文件中设置 room.schemaLocation 注释处理器属性(设置将json存放的位置),如下
android {
compileSdk 30
defaultConfig {
......
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
// 这句只是打印路径,可以去掉
println("$projectDir/schemas".toString())
}
}
}
......
}
第二种解决方式就是不导出架构,如下
@Database(version = 1, entities = [User::class], exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
Room的数据库升级
开发测试阶段
如果还只是在开发测试阶段,Room提供了一个简单粗暴的方法
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).fallbackToDestructiveMigration()
.build().apply {
instance = this
}
}
在构建AppDatabase实例的时候,加入一个fallbackToDestructiveMigration()方法。这样只要数据库进行了升级,Room就会将当前的数据库销毁,然后再重新创建,随之而来的副作用就是之前数据库中的所有数据就全部丢失了
用户升级阶段
加入要在数据库中添加一张Book表
Book的实体类
@Entity
data class Book(var name: String, var pages: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
创建BookDao接口
@Dao
interface BookDao {
@Insert
fun insertBook(book: Book): Long
@Query("select * from Book")
fun loadAllBook(): List<Book>
}
数据库升级的方法
修改AppDatabase中的代码,在里面编写数据库升级的逻辑
@Database(version = 2, entities = [User::class, Book::class], exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun bookDao(): BookDao
companion object {
private var instance: AppDatabase? = null
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)")
}
}
@Synchronized
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
- 实现了一个Migration的匿名类,并传入了1和 2这两个参数,表示当数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑
- 在构建AppDatabase实例的时候,加入一个addMigrations()方法,并把MIGRATION_1_2传入
每次数据库升级并不一定都要新增一张表,也有可能是向现有的表中添加新的列。这种情况只需要使用alter语句修改表结构就可以了,例如向Book表中添加一个作者字段
Book
@Entity
data class Book(var name: String, var pages: Int,var author:String) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
AppDatabase
@Database(version = 2, entities = [User::class, Book::class], exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun bookDao(): BookDao
companion object {
private var instance: AppDatabase? = null
private 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 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'")
}
}
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build().apply {
instance = this
}
}
}
}