Room数据库框架使用

概述

room是Google官方推荐的一个数据库Sqlite框架。Room在SQLite上提供了一个抽象层,以便在充分利用Sqlite的功能的同时也能够访问数据库。

依赖引入

首先需要在gradle中引入room依赖包,示例如下:

dependencies {
  //版本
  def room_version = "2.2.5"

  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"

  // optional - Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:$room_version"

  // optional - Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
  //optional   - rxjava for room 
  implementation "androidx.room:room-rxjava2:$room_version"
}

Room结构

Room主要包含有三个组件:DataBase(数据库),Entity(数据表结构),DAO(数据表操作)。

常规的操作流程如下:

  1. APP使用DataBase来获取与该数据关联的数据表操作对象:DAO。
  2. 应用使用每个DAO从数据库中获取实体:Entity。
  3. 应用对Entity进行更改或获取数据。
  4. 应用将对Entity的更改通过DAO保存到数据库中。

具体的结构如下图所示:


room框架.png

DataBase

Database主要是包含了DAO并且提供创建和链接数据库的方法。

1. 创建数据库

创建DataBase主要包括如下几个步骤:

  1. 创建继承RoomDatabase的抽象类
  2. 在继承类前使用注解@Database
  3. 声明数据库结构的Entity并设置数据库版本号。
  4. 数据库实例最好能够定义为单例。
    示例代码如下:
@Database(entities = [TestUserEntity::class], version = 2, exportSchema = false)
abstract class TestDataBase : RoomDatabase() {

    //获取DAO数据库操作
    abstract fun getDao(): ITestUserDao

    //单例模式
    companion object {e
        private const val DB_NAME = "test_user_database"

        @Volatile
        private var INSTANCE: TestDataBase? = null

        fun getDatabase(context: Context): TestDataBase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    TestDataBase::class.java, DB_NAME
                ).build()
                INSTANCE = instance
                return instance
            }
        }

        //升级语句
        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
//                database.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
            }
        }
    }
}
数据库迁移

当升级Android应用时,有时需要更改数据库中的数据结构,要用户升级应用的时候保持原有的数据不变。此时就需要用到数据迁移Migration。

Room中通过支持Migration类进行增量迁移以满足此需求。通过设置不同的Migration对象完成版本升级中的变动。当应用更新需要升级数据库版本时,Room会从一个或者多个Migration对象中运行migrate()方法,实现数据库版本升级。

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
                "PRIMARY KEY(`id`))")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
注意事项

数据迁移必须完整的定义所有版本的迁移,否则如果没有对应版本的Migration时,Room数据库会清楚原来的数据重写创建。

DAO

DAO全称:data access object,可以通过SQL语句进行对数据库的操作并且将这些语句和JAVA中的方法关联调用,编译器会检查SQL语句并且通过注解生成对应的SQL语句及数据库操作。

在Room中,需要被设置为DAO需要注意如下几点:

  1. DAO必须是抽象类或者接口。
  2. DAO必须要使用@Dao注解进行标识。

DAO示例代码如下:

@Dao
interface ITestUserDao {
    //采用suspend关键字使用kotlin协程使得数据库的操作方法不在主线程进行
    @Query("select * from test_user")
    fun getAll(): List<TestUserEntity>

    @Query("select * from test_user where id = (:userId)")
    fun getById(userId: Long): Flowable<TestUserEntity>

    @Update
    suspend fun updateUserInfo(vararg userEntity: TestUserEntity)

    @Insert
    fun insertUser(vararg userEntity: TestUserEntity)

    @Delete
    fun deleteUser(vararg userEntity: TestUserEntity)

    @Query("delete from test_user where id = (:userId)")
    fun deleteById(userId: Long)

}

新建一个DAO主要SQL语句的设置,可以从如下几个方面进行设置:

1. 插入数据
  1. 插入数据方法需要在方法前使用@Insert注解进行标识。
  2. 当参数只有一个时,则它可以返回 long,这是插入项的新 rowId。
  3. 如果参数是数组或集合,则应返回 long[] 或 List<Long>。
  4. 查看编译文件,可以发现插入操作是在一个独立的事务中完成的,保证了插入操作的原子性。
@Dao
    interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insertUsers(vararg users: User)

        @Insert
        fun insertBothUsers(user1: User, user2: User)

        @Insert
        fun insertUsersAndFriends(user: User, friends: List<User>)
    }
2. Update
  1. Update方法需要采用注解@Update来完成
  2. Update方法使用和每个实体的主键匹配查询
  3. Update方法也可以返回Int值,表示数据库中更新的行数。
    @Dao
    interface MyDao {
        @Update
        fun updateUsers(vararg users: User)
    }
3. 删除
  1. Delete方法需要采用注解@Delete进行标识
  2. Delete方法使用的是主键查询要删除的实体。
  3. Delete方法也可以返回Int值,表示数据库中更新的行数。
@Dao
    interface MyDao {
        @Delete
        fun deleteUsers(vararg users: User)
    }
4. 查询
  1. 查询方法需要使用@Query注解进行标识
  2. @Query不仅仅可以用来标识查询方法,还可以标识其他的任意SQL语句
  3. 每个@Query方法在编译时就是检查SQL语句是否正确,如果存在错误则直接编译报错,而不是运行时失败。
@Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge")
        fun loadAllUsersOlderThan(minAge: Int): Array<User>
    }
5. 带参数查询
  1. 在查询过程通添加条件不可避免的需要添加参数,可以在SQL语句中使用:param的方式引入参数。
  2. 如果编译检查SQL语句没有找打匹配的参数,则会编译报错。
@Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

        @Query("SELECT * FROM user WHERE first_name LIKE :search " +
               "OR last_name LIKE :search")
        fun findUserWithName(search: String): List<User>
    }
6. 传递参数的集合
  1. 存在有部分查询传入的数量不定的参数,参数的确切数量要到运行时才能得到,可以采用如下的参数合集的方式进行完成。
@Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
    }
7. 查询语句返回对象类型
  1. 如果需要返回实体类的部分字段,可以通过新建一个只包含这部分字段的类,返回返回此新类的集合对象。
  2. 查询语句还支持分布其他的返回类型,例如RxJava中的Flowable<T>等。
  3. DAO里的方法不能够放在主线程执行,因此需要等待
  4. 可以对DAO里的方法添加suspend关键字,以使用kotlin协程功能使得这个方法成为异步方法
  5. LiveData:如果返回为LiveData对象,则不需要进行异步操作,因为room会自动完成异步操作。
数据库查询类型.png
8. 事务
  1. 实现自定义事务需要添加如下注解@Transaction进行标识。
@Dao
    abstract class UsersDao {
        @Transaction
        open suspend fun setLoggedInUser(loggedInUser: User) {
            deleteUser(loggedInUser)
            insertUser(loggedInUser)
        }

        @Query("DELETE FROM users")
        abstract fun deleteUser(user: User)

        @Insert
        abstract suspend fun insertUser(user: User)
    }

Entity

在Room DataBase中,Entity表示的是一张数据表的结构。举个例子来说,我们新建一个UserEntity类。

@Entity(tableName = "test_user")
class TestUserEntity(
    @PrimaryKey
    @ColumnInfo(name = "id") 
    var userId: Long,
    
    @ColumnInfo(name = "name")
    var name: String? = null
    ) {}

如上述代码所示,一个简单的user Entity类就定义好了,同时也定义了一个名为test_user的数据表,其中的列信息主要有id和name两项,Entity对象和数据表的对应关系如下:

  1. 一个Entity对象代表数据表中的一行
  2. Entity类对应一个数据表,其成员变量对应数据表中的列

新建一个Entity可以从如下几个方面进行设置

1. 新建Entity
  1. 必须要使用@Entity注解来表示此类是一个Entity类。
  2. 可以在@Entity注解上使用属性tableName =来指定此Entity对应的表的名字,如不采用则是使用默认命名。
2. 主键设置
  1. 每个Entity至少需要定义一个主键,即使此类只有一个属性,主键不能为空
  2. 对于主键字段可以使用@PrimaryKey注解来声明此字段为主键。
  3. 如果主键比较复杂,可以在@Entity注解中使用属性promaryKeys =进行声明

@Entity(primaryKeys = {"id", "name"})

3. 列设置
  1. 使用@ColumnInfo来声明列信息,可以通过设置属性name =来指定列的名称,如果不设置则会采用变量的小写形式作为列名称
  2. 如果在Entity类中存在一些变量不需要生成数据表的列,可以使用@Ignore注解注释需要被忽视的属性来不生成对应的列。
  3. 如果类继承上面有父类,并且不想父类的属性生成数据表中的列,可以采用@ignoredColumns
//父类
open class User() {
    var num: Long? = null
}

//子类定义为数据表结构,忽视
@Entity(tableName = "test_user", ignoredColumns = ["num"])
class TestUserEntity(
    @PrimaryKey @ColumnInfo(name = "id") var userId: Long,
    @ColumnInfo(name = "name") var name: String? = null
) : User() {

    fun printData(): String {
        Log.d("Message", Thread.currentThread().name)
        return "id= $userId     name= $name"
    }
}
4. 嵌套Entity

如果定义的Entity对象里面有个ChildEntity类的对象,并且希望定义的Entity中表列表字段包括包含ChildEntity类对象中的变量,可以通过注解@Embedded来实现,是来代码如下:

data class Address(
        val street: String?,
        val state: String?,
        val city: String?,
        @ColumnInfo(name = "post_code") val postCode: Int
    )

    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
        @Embedded val address: Address?
    )

参考文章

Google文档

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

推荐阅读更多精彩内容