Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

初探Architecture Components之Room篇(一)

$
0
0

Room的初衷

提起SQLite,作为Android开发者还是比较幸福的的,Android核心框架已为处理SQL提供了相当大的支持,API也非常强大,省起来很大的力气。但是其模板化处理方式,导致开发者花费大量的时间和精力去维护数据库:

  • 在编译时,没有对原始SQL查询语句验证。随着表结构的更改,需要手动更新SQL查询语句。这个过程不仅耗时耗精力,而且很容易出错。
  • 需要使用大量的样板代码执行SQL操作和Java数据对象之间的转换。

正因为这些原因,一批大神造轮子,开源了很多优秀的开源框架,比如GreenDao、OrmLite、Active Android等等,给我们带来了十分的便利。
这里给大家推荐另外一个开源框架 - Room,其作者是Android的爹Google。既然是Google出品,我想有必要学习一下。

Room给我们带来了什么样的惊喜呢?

  1. 避免了样板间似的代码块。
  2. 能轻松的将SQLite表数据转换为Java对象
  3. Room提供了编译SQLite语句时检查,避免了SQL语句在执行时,才发现错误
  4. 可以返回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库在应用程序中,怎么应用的呢?

    1. 创建实体类-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)
      
    2. 声明抽象类AppDatabase,继承于RoomDatabase,该类使用@Database注解,用于为应用程序创建一个数据库

      @Database(entities = arrayOf(UserEntity::class), version = 1)
      abstract class AppDatabase: RoomDatabase() {
      
      }
      


      请注意:

      • 在Kotlin中,entities注解参数为vararg参数传递时,必须将参数的显示的的声明为arrayOf()。
      • 在编译时,由Room库对AppDatabase实现,我们不需多做处理。
    3. 创建数据库访问对象(DAO), 用于访问数据库

      @Dao
      interface UserDao {
          // 向表中插入一系列
          @Insert(onConflict = OnConflictStrategy.REPLACE)
          fun insertUser(vararg user: UserEntity)
      
          @Insert
          fun insertUser(users: List<UserEntity>)
      
          ***
      }
      
    4. 在AppDatabase引用UserDao,就是在AppDatabase中声明一个抽象方法,返回UserDao实例。

      @Database(entities = arrayOf(UserEntity::class), version = 1)
      abstract class AppDatabase : RoomDatabase() {
      
          abstract fun userDao(): UserDao
      }
      
    5. 创建数据库,像数据库添加数据后,并查询。

      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注解:

    1. 将@Dao注解类中的方法标记为插入方法。
    2. 该方法的实现将其参数插入到数据库中
    3. @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.

    1. OnConflictStrategy.REPLACE:冲突策略是取代旧数据同时继续事务
    2. OnConflictStrategy.ROLLBACK:冲突策略是回滚事务
    3. OnConflictStrategy.ABORT:冲突策略是终止事务
    4. OnConflictStrategy.FAIL:冲突策略是事务失败
    5. OnConflictStrategy.IGNORE:冲突策略是忽略冲突

    现在,我们已经知道了,当遇到冲突时,Room的默认的处理方式为终止事务。有这样一个需求,User表中的name索引是唯一的。将User实例插入表时,如果出现冲突,用新数据取代旧数据。那么,我们需要做以下工作:

    1. 修改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
      }
      
    2. 修改插入方法的@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得心应手。默默的在焦躁中,到了现在,细数碰到的坑吧。

    1. 在Kotlin中,使用Room创建数据库时,对于Entity而言,并不能以官方文档的例子来创建,虽然有get()和set(),但是没有提到Room的实体类有且只有一个构造函数。如果实体类有多个构造函数时,在编译时,提示构造函数过多,从而Room不知道选择哪一个构造函数创建实体类,导致编译不通过。也就是意味着,在Kotlin中,声明实体类时,不能在构造函数中初始化成员。因为在构造函数中初始化参数,必然创建了多个构造函数,从而导致编译不通过。在官方例子的User类有一个无参的构造函数。
    2. 值得注意的是,实体的类名即为数据库中的表名。在查看文档时,都能够清楚地知道这一点。可是,我们习惯了使用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)
      
    3. 关于数据库操作问题,Room默认禁止在主线程中访问数据库,因为它可能会长时间阻塞UI线程,而导致ANR。Room会检测该操作是否在主线程操作,如果在主线程中操作数据库,Room会抛出异常:

      如果想取消掉Room检测在主线程中操作数据库,可以这么做:

      mDataBase = Room.databaseBuilder(applicationContext,
                              AppDatabase::class.java, DATABASE_NAME)
                              .allowMainThreadQueries()
                              .build()
      

      调用allowMainThreadQueries()方法,将禁用Room检查在主线程查询数据库。这时,就算你在主线程中操作数据库,也不会再报异常,但是不推荐这么用。

    到这里,Room的基本使用算是结束了,接下来了解 @Entity到底有多能?

    作者:IO_Field 发表于2017/6/19 20:54:06 原文链接
    阅读:126 评论:0 查看评论

    Viewing all articles
    Browse latest Browse all 5930

    Trending Articles



    <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>