处理大量结构化数据的应用,能从在本地持久化数据中极大受益.最常见的用例是缓存有关联的数据碎片.以这种方式,在设备不能访问网络的时候,用户依然能够浏览离线内容.任何用户发起的改变,都应该在设备重新在线之后同步到服务器.
因为Room为你充分消除了这些顾虑,使用Room而非SQLite是高度推荐的.
添加依赖
Room的依赖添加方式如下:
Room有3个主要构件:
在运行时,你可以通过调用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()方法请求Database实例.
这些构件,以及它们与app余下内容的关系,如下图:
UserDao.java
AppDatabase.java
1@Database(entities={User.class},version=1)2publicabstractclassAppDatabaseextendsRoomDatabase{3publicabstractUserDaouserDao();4}
在创建了以上文件之后,你能够使用以下代码来创建一个database实例:
1AppDatabasedb=Room.databaseBuilder(getApplicationContext(),2AppDatabase.class,"database-name").build();
备注:在实例化AppDatabase对象的时候,你应该使用单例模式,因为每一个RoomDatabase实例都是非常耗时的,而且你也应该很少访问多个实例.
使用Room实体定义数据
默认情况下,Room会为实体中定义的每个域创建一个列.如果实体中有你不想持久化的域,可以使用@Ignore来注解掉.在Database类中,你必须通过entities数据来引用实体类.
下面的代码片断展示了如何定义一个实体:
在持久化一个域,Room必须能够访问它.你可以将域设置为public,或者你可以提供该的getter/setter.如果你使用了getter/setter的方式,一定要记住:在Room里面,它们是基于JavaBeans转换的.
备注:实体要么有个空的构造器(如果相应的DAO类能够访问每一个持久化域的话),要有构造器里面的参数,数据类型和名字跟实体里面定义的域相匹配.Room也能够使用包含全部或者部分域的构造器,例如,一个构造器只能获取所有域中的几个.
使用主键
每一个实体必须定义至少1个主键.即使只有一个域,你依然需要使用@PrimaryKey来注解它.而且,如果你想Room分配自动ID给实体的话,你需要设置@PrimaryKey的autoGenerate属性.如果实体有一个复合主键的话,你需要使用注解@Entity的primaryKeys属性,示例代码如下:
默认情况下,Room使用实体类的名字作为数据库表的名字.如果你想要表拥有一个不同的名字,设置@Entity注解的tableName属性,示例代码如下:
1@Entity(tableName="users")2publicclassUser{3...4}
注意:SQLite中表名是大小写敏感的.
跟tableName属性相似的是,Room使用域的名字作为数据库中列的名字.如果你想要列有一个不同的名字的话,给域添加@ColumnInfo注解,示例代码如下:
注解索引和唯一性
依赖于你如何访问数据,你也许想要在数据库中建立某些域的索引,以加速查询速度.要给实体添加索引,需要在@Entity中引入indices属性,并列出你想要在索引或者复合索引中引入的列的名字.下列代码说明了注解的处理过程:
有些时候,数据库中的某些域或几组域必须是唯一的.你可以通过将注解@Index的unique属性设置为true,强制完成唯一的属性.下面的代码示例防止表有两行数据在列firstName和lastName拥有相同值:
定义对象之间的关系
尽管你不能使用直接的对象关系,Room仍然允许你在实体之间定义外键约束.
比如,如果有一个实体类Book,你可以使用@ForeignKey注解定义它和实体User的关系,示例代码如下:
备注:SQLite将@Insert(onConflict=REPLACE)作为REMOVE和REPLACE的集合来操作,而非单独的UPDATE操作.这个取代冲突值的方法能够影响你的外键约束.
创建嵌套对象
有些时候,在数据库逻辑中,你想将一个实体或者POJO表示为一个紧密联系的整体,即使这个对象包含几个域.在这些情况下,你能够使用@Embedded注解来表示一个对象,而你想将这个对象分解为表内的子域.然后你可以查询这些嵌套域,就像你查询其它的独立列一样.
举个例子,User类包含一个Address类的域,这个域表示的是street,city,state,postCode这几个域的复合.为了在表中单独存储复合的列,在User类里面,引入一个注解了@Embedded的Address域,就像如下代码片断展示的一样:
这个表表示User对象包含如下几列:id,firstName,street,state,city和post_code.
备注:嵌套的域同样可以包含其它的嵌套域.
如果实体拥有多个相同类型的嵌套域,你可以通过设置prefix属性保留每一列唯一.然后Room给嵌套对象的每一个列名的起始处添加prefix设置的给定值.
通过RoomDAO访问数据
要通过Room持久化库访问应用的数据,你需要使用数据访问对象(dataaccessobjects,即DAOs).Dao对象集形成了Room的主要构成,因为每一个DAO对象都引入了提供了抽象访问数据库的方法.
使用DAO对象而非查询构造器或者直接查询来访问数据库,你可以分开不同的数据库架构组成.此外,DAO允许你轻易地模拟数据库访问.
DAO要么是接口,要么是抽象类.如果DAO是抽象类的话,它可以随意地拥有一个将RoomDatabase作为唯一参数的构造器.Room在运行时创建DAO的实现.
方便地定义方法
使用DAO类,可以非常方便地表示查询.
插入
当你创建了一个DAO方法并注解了@Insert的时候,Room生成了一个实现,在单个事务中将所有的参数插入数据库.下面的代码片断展示了几个示例查询:
如果@Insert方法只接收了一个参数,它可以返回一个long,表示新插入项的rowId;如果参数是数组或者集合,同时地,它应该返回long[]或者List
更新
按照惯例,在数据库中,Update方法修改了作为参数传递的实体集合.它使用查询来匹配每一个实体的主键.下面的代码片断展示了如何定义这个方法:
1@Dao2publicinterfaceMyDao{3@Update4publicvoidupdateUsers(User...users);5}尽管通常情况下并不需要,但是依然可以将这个方法返回int值,表示在数据库中被修改的行数.
删除
按照惯例,Delete方法从数据库中删除了作为参数传递的实体集合.它使用主键找到要删除的实体.下面的代码片断展示了如何定义这个方法:
1@Dao2publicinterfaceMyDao{3@Delete4publicvoiddeleteUsers(User...users);5}尽管通常情况下并不需要,但是依然可以将这个方法返回int值,表示从数据库中删除的行数.
查询
@Query是在DAO类中使用的主要的注解.它允许你在数据库中执行读写操作.每一个@Query方法都在编译时被证实,因为,如果查询有问题出现的话,会出现编译错误而非运行失败.Room也证实查询的返回值,以确定返回对象的域的名字是否跟查询响应中对应列的名字匹配,Room使用如下两种方式提醒你:
简单查询
1@Dao2publicinterfaceMyDao{3@Query("SELECT*FROMuser")4publicUser[]loadAllUsers();5}这是一个非常简单的查询,加载了所有User.在编译时,Room知晓这是在查询user表中所有列.
如果查询语句包含语法错误,或者user表在数据库中并不存在,Room会在编译时展示恰当的错误信息.
查询语句中传参
大多数时候,你需要向查询语句中传参,以执行过滤操作,比如,只展示大于某个年龄的user.
要完成这个任务,在Room注解中使用方法参数,如下所示:
1@Dao2publicinterfaceMyDao{3@Query("SELECT*FROMuserWHEREage>:minAge")4publicUser[]loadAllUsersOlderThan(intminAge);5}当这个查询在编译时处理的时候,Room匹配到:minAge,并将它跟方法参数minAge绑定.Room使用参数名来执行匹配操作.如果不匹配的话,app编译时会发生错误.
你也可以在查询中传递多个参数,或者将参数引用多次,如下所示:
返回列的子集
大多数情况下,你只需要实体中的几个域.比如,UI中只需要展示用户的姓和名,而非用户的每一个细节.通过只查询UI中展示的列,将节省宝贵的资源,查询也更快.
Room允许从查询中返回基于Java的对象,只要结果列集合能够映射成返回对象.比如,你创建了一个POJO来获取用户的名和姓:
现在,你可以在查询方法中使用这个POJO了:
1@Dao2publicinterfaceMyDao{3@Query("SELECTfirst_name,last_nameFROMuser")4publicList
由此,Room能够产生适当的代码.如果查询返回了太多列,或者返回了NameTuple类中并不存在的列,Room将展示警告信息.备注:POJO也可以使用@Embedded注解.
传递参数集
一些查询可能要求你传入可变数目的参数,直到运行时才知道精确的参数数量.
比如,你可能想要搜索地区子集下的所有用户.Room明白参数表示集合的时机,并在运行时自动地基于提供了参数数目展开它.
1@Dao2publicinterfaceMyDao{3@Query("SELECTfirst_name,last_nameFROMuserWHEREregionIN(:regions)")4publicList
可观察查询
在执行查询的时候,经常想要在数据发生改变的时候自动更新UI.要达到这个目的,需要在查询方法描述中返回LiveData类型的值.在数据库更新的时候,Room生成所有必要的代码以更新LiveData.
备注:在1.0版本的时候,Room使用查询中访问的表的列表来决定是否更新LiveData实例.
RxJava响应式查询
Room也可以从定义的查询中返回RxJava2中的Publisher和Flowable.
要使用这个功能,在build.gradle文件中添加依赖:android.arch.persistence.room:rxjava2.之后,你可以返回在RxJava2中定义的数据类型,如下所示:
1@Dao2publicinterfaceMyDao{3@Query("SELECT*fromuserwhereid=:idLIMIT1")4publicFlowable
如果你的应用逻辑要求直接访问返回的行,你可以从查询中返回Cursor对象,如下所示:
1@Dao2publicinterfaceMyDao{3@Query("SELECT*FROMuserWHEREage>:minAgeLIMIT5")4publicCursorloadRawUsersOlderThan(intminAge);5}注意:十分不推荐使用CursorAPI.因为它并不保证行是否存在以及行包含什么值.
除非你有需要Cursor的代码并且并不轻易的修改它的时候,你才可以使用这个功能.
查询多表
有些查询可能要求访问多个表以计算结果.Room允许你写任何查询,所以你也可以联接表.此外,如果响应是可观测数据类型,诸如Flowable/LiveData,Room观察并证实查询中引用的所有表.
下面的代码片段展示了如何执行表联接,以合并包含借书用户的表和包含在借书数据的表的信息:
你也可以从这些查询中返回POJO.比如,你可以写查询加载用户和它的宠物名:
迁移Room数据库
当应用中添加或者改变特性的时候,需要修改实体类以反映出这些改变.当用户升级到最新版本的时候,你不想用户失去所有数据,尤其是如果你还不能从远程服务器恢复这些数据的时候.
Room持久化库允许写Migration类来保留用户数据.每一个Migration类指定了startVersion和endVersion.在运行时,Room运行每一个Migration类的migrate()方法,使用正确的顺序迁移数据库到最新版本.
注意:如果你不提供必要的迁移,Room会重建数据库,这意味着你会失去原有数据库中的所有数据.
注意:要保证迁移逻辑按照预期进行,需要使用全查询而非引用表示查询的常量.在迁移完成之后,Room会证实这个计划,以确保迁移正确在发生了.如果Room发现了问题,它会抛出包含不匹配信息的异常.
迁移测试
写Migration并不是没有价值的,不能恰当的写Migration会在应用中引起崩溃.在保持应用的稳定性,你应该事先测试Migration.Room提供了一个Maven测试工具.但是,如果要使这个工具工作,你需要导出数据库schema.
导出schema
在编译的时候,Room会导出数据库schem信息,形成一个Json文件.要导出schema,需要在build.gradle文件中设置room.schemaLocation注解处理器属性,如下所示:build.gradle:
你应该保存导出的Json文件--这些文件表示了数据库schema的历史--在你的版本控制体系中,因为它允许Room创建老版本数据库用于测试.
要测试这些Migration,需要在测试需要的依赖中添加anroid.arch.persistence.room:testing,并在资产文件夹下添加schema地址,如下所示:build.gradle:
测试包提供了MigrationTestHelper类,它能够读取这些schema文件.它也实现了JUnit4TestRule接口,所有它能够管理已创建的数据库.
示例Migration测试如下:
测试数据库
在使用Room持久化库创建数据库的时候,证实应用数据库和用户数据的稳定性非常重要.
有两种方式测试你的数据库:
备注:在运行应用的测试的时候,Room允许你创建模拟DAO类的实例.使用这种方式的话,如果不是在测试数据库本身的话,你不必创建完成的数据库.这个功能是可能的,因为DAO并不泄露任何数据库细节.
真机测试
测试数据库实现的推荐途径是在真机上运行JUnit测试.因为这些测试并不创建Activity,它们应该比UI测试执行地更快.
在设置测试的时候,你应该创建内存版本数据库,以确保测试更加地密封.如下所示:
虚拟机测试
Room使用了SQLite支持库,后者提供了在AndroidFramework类里面匹配的接口.这个支持允许你传递自定义的支持库实现来测试数据库查询.备注:尽管这个设置允许测试运行地很快,但它并不是值得推荐的,因为运行在自己以及用户真机上面的SQLite版本,可能并不匹配你的虚拟机上面的SQLite版本.
使用Room引用复杂数据
Room提供了功能支持基数数据类型和包装类型之间的转变,但是并不允许实体间的对象引用.
使用类型转换器
有时候,应用需要使用自定义数据类型,该数据类型的值将保存在数据库列中.要添加这种自定义类型的支持,你需要提供TypeConverter,用来将自定义类型跟Room能够持久化的已知类型相互转换.
上述示例定义了2个方法,一个把Date转变成Long,一个把Long转变成Date.因为Room已经知道如何持久化Long对象,它将使用这个转换器持久化Date类型的值.
接下来,添加@TypeConverters注解到AppDatabbase类上,之后Room就能够在AppDatabase中定义的每一个实体和DAO上使用这个转换器.AppDatabase.java
1@Database(entities={User.class},version=1)2@TypeConverters({Converters.class})3publicabstractclassAppDatabaseextendsRoomDatabase{4publicabstractUserDaouserDao();5}使用这些转换器,你之后就能够在其它的查询中使用自定义的类型,就像你使用基本数据类型一样,如下所示:User.java
1@Entity2publicclassUser{3...4privateDatebirthday;5}UserDao.java
1@Dao2publicinterfaceUserDao{3...4@Query("SELECT*FROMuserWHEREbirthdayBETWEEN:fromAND:to")5ListfindUsersBornBetweenDates(Datefrom,Dateto);6}你也可以限制@TypeConverters的使用范围,包括单个实体,DAO和DAO方法.
理解为什么Room不允许对象引用
要点:Room不允许实体类间的对象引用.相反,你必须显式地请求应用需要的数据.
从数据库到对应对象模型的映射关系是通用最佳实践,在服务器端也运行良好.即使是在程序加载它们正在访问的域的时候,服务器依然执行良好.
1authorNameTextView.setText(book.getAuthor().getName());
然后,这个貌似无辜的改变引起Author表在主线程被查询.
如果你提前查询作者信息,而在你不再需要这个数据之后,将很难改变加载的方式.比如,UI不再需要展示Author信息,而应用依然高效地加载不同展示的数据,浪费了宝贵的内存空间.应用的效率将会降级,如果Author类引用了其它的表,如Books.
要使用Room同时引用多个实体,需要创建包含每个实体的POJO类,之后写联接了相应表的查询语句.这个结构良好的模型,结合了Room鲁棒的查询证实能力,允许应用在加载资源时消耗更少的资源,提升了应用的性能和用户体验.