Room的初衷
提起SQLite,作为Android开发者还是比较幸福的的,Android核心框架已为处理SQL提供了相当大的支持,API也非常强大,省起来很大的力气。但是其模板化处理方式,导致开发者花费大量的时间和精力去维护数据库:
- 在编译时,没有对原始SQL查询语句验证。随着表结构的更改,需要手动更新SQL查询语句。这个过程不仅耗时耗精力,而且很容易出错。
- 需要使用大量的样板代码执行SQL操作和Java数据对象之间的转换。
正因为这些原因,一批大神造轮子,开源了很多优秀的开源框架,比如GreenDao、OrmLite、Active Android等等,给我们带来了十分的便利。
这里给大家推荐另外一个开源框架 - Room,其作者是Android的爹Google。既然是Google出品,我想有必要学习一下。
Room给我们带来了什么样的惊喜呢?
- 避免了样板间似的代码块。
- 能轻松的将SQLite表数据转换为Java对象
- Room提供了编译SQLite语句时检查,避免了SQL语句在执行时,才发现错误
- 可以返回RxJava的Flowable和LiveData的可观察实例,对SQLite的异步操作提供强力支持。
Gradle配置
在buildl.gradle里面添加依赖即可:
- compile “android.arch.persistence.room:runtime:1.0.0-alpha1”
- annotationProcessor “android.arch.persistence.room:compiler:1.0.0-alpha1”
Room对RxJava 2是完美支持, 如果习惯了RxJava异步操作的,可以添加RxJava支持库:
- compile “android.arch.persistence.room:rxjava2:1.0.0-alpha1”
Room的三大组成部分
- Database(数据库):使用此组件创建数据库Holder。
- 通过注解实体类定义表结构,该实体类的实例即为数据库中的数据访问对象(在Dao内操作)。
- 它是链接SQL底层的主要接入点。
- 注解的类必须是继承于RoomDatabase的抽象类
- 在运行时,可以通过调用Room.databaseBuilder()或Room.inMemoryDatabaseBuilder()获取其实例
Entity(实体类):该组件表示持有一个表的字段(即数据库一行的数据)的实体类。
- 对于每个实体,创建一个数据库表来保存它们。
- 必须通过Database类中的Entity数组引用实体类
- 实体类的每个字段都会保存数据库的表中。如果不想保存某字段,该字段需使用@Ignore注解
如果Dao类可以访问每个持久化的字段(即表中的字段),实体可以有一个空构造函数。当然,该实体类还可以有一个构造函数,其参数应包含与实体中的字段相匹配的类型和名称。Room可以使用含有全部字段或者部分字段的构造函数,例如只含有部分字段的构造函数??????
DAO(抽象类/接口):该组件表示作为数据访问对象(DAO即为Data Access Object的简写)的类或者接口。
- DAO是Room的主要组件,负责定义访问数据库的方法。
- 该抽象类或接口使用@Database注解,同时必须含有一个无参数的抽象方法,并返回@Dao注解的实际操作类。
- 在编译时,Room创建这个抽象类或接口的实现。
注意:通过使用DAO类访问数据库,而不是使用查询构建器或直接查询,可以将数据库体系结构的不同组件分离。另外,在测试应用程序时,DAO可以轻松的模拟数据库访问。
Room与应用程序的架构体系
简单使用
现在已经对Room库有了初步的认识,下面我们来看看Room库在应用程序中,怎么应用的呢?
创建实体类-UserEntity,使用@Entity注解,将其作为数据库中的一个表的实体
@Entity(tableName = "user") data class UserEntity (@PrimaryKey @ColumnInfo(name = "id")val id: Int, @ColumnInfo(name = "name")val name: String, @ColumnInfo(name = "is_brrowed")val isBrrowed: Int)
声明抽象类AppDatabase,继承于RoomDatabase,该类使用@Database注解,用于为应用程序创建一个数据库
@Database(entities = arrayOf(UserEntity::class), version = 1) abstract class AppDatabase: RoomDatabase() { }
请注意:- 在Kotlin中,entities注解参数为vararg参数传递时,必须将参数的显示的的声明为arrayOf()。
- 在编译时,由Room库对AppDatabase实现,我们不需多做处理。
创建数据库访问对象(DAO), 用于访问数据库
@Dao interface UserDao { // 向表中插入一系列 @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertUser(vararg user: UserEntity) @Insert fun insertUser(users: List<UserEntity>) *** }
在AppDatabase引用UserDao,就是在AppDatabase中声明一个抽象方法,返回UserDao实例。
@Database(entities = arrayOf(UserEntity::class), version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
创建数据库,像数据库添加数据后,并查询。
class RoomActivity : BaseActivity() { *** @SuppressLint("StaticFieldLeak") override fun setListener() { acb_create.setOnClickListener { doAsync { applicationContext.deleteDatabase(DATABASE_NAME) mDataBase = Room.databaseBuilder(applicationContext, AppDatabase::class.java, DATABASE_NAME).build() } } acb_insert.setOnClickListener { doAsync { val users: MutableList<UserEntity> = mutableListOf() (0..10).mapTo(users) { val user = UserEntity(id, "Test - it", id % 2) id++ user } mDataBase?.beginTransaction() try { mDataBase?.userDao()?.insertUser(users) mDataBase?.setTransactionSuccessful() } finally { mDataBase?.endTransaction() } } } *** acb_query.setOnClickListener { var users: List<UserEntity>? = null doAsync { mDataBase?.beginTransaction() try { users = mDataBase?.userDao()?.queryAll() Log.i("123", users?.toString()) mDataBase?.setTransactionSuccessful() } finally { mDataBase?.endTransaction() } runOnUiThread { mList.clear() mList.addAll(mList.size, users!!.toList()) mAdapterUser.notifyDataSetChanged() } } } } }
请注意:在Kotlin中,Room库对数据库的操作必须在后台线程中执行。如果在UI主线程中操作数据库,将会报错。因为Room认为操作数据库是耗时操作,所以为了操作数据库而阻塞UI线程,Room默认这种行为是禁止的,故而报错。如果想开启在UI线程中访问数据库,可以再构建AppDatabase实例是设置禁用Room检查在主线程查询数据库。,但是不推荐,也就是这样,:
mDataBase = Room.databaseBuilder(applicationContext, AppDatabase::class.java, DATABASE_NAME) .allowMainThreadQueries() .build()
操作数据库
前面,我们已经了解到了Room的基本使用,下面我们来看看对数据库操作的细节。对于数据库的操作,莫过于“增删改查”,那我们从这些操作来深入了解Room的应用。
增
向数据库中添加数据,使用到了@Insert注解:
- 将@Dao注解类中的方法标记为插入方法。
- 该方法的实现将其参数插入到数据库中
- @Insert注解方法的所有参数必须是使用@Entity注解的类或其集合/数组
例如:
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(vararg user: UserEntity)
@Insert
fun insertUser(users: List<UserEntity>)
示例中,我们可以通过调用insertUser方法向数据添加一条数据和一个集合。
对数据库设计时,不允许重复数据的出现。否则,必然造成大量的冗余数据。实际上,难免会碰到这个问题:冲突。当我们像数据库插入数据时,该数据已经存在了,必然造成了冲突。该冲突该怎么处理呢?在@Insert注解中有conflict用于解决插入数据冲突的问题,其默认值为OnConflictStrategy.ABORT。对于OnConflictStrategy而言,它封装了Room解决冲突的相关策略。对于冲突不熟悉的,可以参考SQL As Understood By SQLite.
- OnConflictStrategy.REPLACE:冲突策略是取代旧数据同时继续事务
- OnConflictStrategy.ROLLBACK:冲突策略是回滚事务
- OnConflictStrategy.ABORT:冲突策略是终止事务
- OnConflictStrategy.FAIL:冲突策略是事务失败
- OnConflictStrategy.IGNORE:冲突策略是忽略冲突
现在,我们已经知道了,当遇到冲突时,Room的默认的处理方式为终止事务。有这样一个需求,User表中的name索引是唯一的。将User实例插入表时,如果出现冲突,用新数据取代旧数据。那么,我们需要做以下工作:
修改UserEntity类,把name字段标记为唯一的索引;
@Entity(tableName = "user", indices = arrayOf(Index(value = *arrayOf("name"), unique = true))) data class UserEntity (val name: String,val isBrrowed: Int) { @PrimaryKey(autoGenerate = true) var id: Int = 0 }
修改插入方法的@Insert注解,将其onConflict属性设置为OnConflictStrategy.REPLACE
@Dao interface UserDao { // 向表中插入一系列 @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertUser(vararg user: UserEntity) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertUser(users: List<UserEntity>) }
至于,如何设置数据库表中的字段或字段组的唯一性,后续再做讲解。
其他的冲突解决策略,这里不多做说明,有兴趣的可以尝试设计场景实现。
删
@Delete注解的方法,将从数据库表中删除与所接收的实体参数所对应的数据,以每个实体的主键作为查询对应关系的依据。 如下所示:
@Dao
interface UserDao {
// 删除表中的数据
@Delete
fun deleteUser(vararg user: UserEntity): Int
@Delete
fun deleteUser(users: List<UserEntity>): Int
}
在此方法中, 可以返回一个int值, 表示数据库中删除的行数。
改
@Update注解的方法, 以接收到一组实体更新数据库表中的数据, 它使用每个实体的主键作为查询的依据。 如下所示
@Dao
interface UserDao {
// 更新表中一系列数据
@Update
fun updateUser(vararg user: UserEntity): Int
@Update
fun updateUser(users: List<UserEntity>): Int
}
在此方法中, 可以返回一个int值, 表示数据库中更新的行数。
查
后续介绍….
那些坑
在尝试使用Kotlin编写Room时,碰到了不知道多少坑,一度的想放弃,总是不如觉得使用Java得心应手。默默的在焦躁中,到了现在,细数碰到的坑吧。
- 在Kotlin中,使用Room创建数据库时,对于Entity而言,并不能以官方文档的例子来创建,虽然有get()和set(),但是没有提到Room的实体类有且只有一个构造函数。如果实体类有多个构造函数时,在编译时,提示构造函数过多,从而Room不知道选择哪一个构造函数创建实体类,导致编译不通过。也就是意味着,在Kotlin中,声明实体类时,不能在构造函数中初始化成员。因为在构造函数中初始化参数,必然创建了多个构造函数,从而导致编译不通过。在官方例子的User类有一个无参的构造函数。
值得注意的是,实体的类名即为数据库中的表名。在查看文档时,都能够清楚地知道这一点。可是,我们习惯了使用Entity作为实体类名的后缀,也就意味着表名称的后缀为Entity。比如创建了UserEntity实体类时,其默认的表名应该为userentity。如果想重命名表名,应该设置@Database注解的tableName属性。比如,把UserEntity表名重命名为user,可以这么做:
@Entity(tableName = "user") data class UserEntity (@PrimaryKey @ColumnInfo(name = "id")val id: Int, @ColumnInfo(name = "name")val name: String, @ColumnInfo(name = "is_brrowed")val isBrrowed: Int)
关于数据库操作问题,Room默认禁止在主线程中访问数据库,因为它可能会长时间阻塞UI线程,而导致ANR。Room会检测该操作是否在主线程操作,如果在主线程中操作数据库,Room会抛出异常:
如果想取消掉Room检测在主线程中操作数据库,可以这么做:mDataBase = Room.databaseBuilder(applicationContext, AppDatabase::class.java, DATABASE_NAME) .allowMainThreadQueries() .build()
调用allowMainThreadQueries()方法,将禁用Room检查在主线程查询数据库。这时,就算你在主线程中操作数据库,也不会再报异常,但是不推荐这么用。
到这里,Room的基本使用算是结束了,接下来了解 @Entity到底有多能?