Jetpack之Room

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
                }
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容