Jetpack之Room

2021-12-15  本文已影响0人  0246eafe46bd

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
                }
        }
    }
}

主要有以下修改

  1. 在@Database注解中,将版本号升级成了2
  2. 实现了一个Migration的匿名类,并传入了1和 2这两个参数,表示当数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑
  3. 在构建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
                }
        }
    }
}
上一篇 下一篇

猜你喜欢

热点阅读