使用Room将数据保存在本地数据库
Room提供了SQLite之上的一层抽象, 既允许流畅地访问数据库, 也充分利用了SQLite.
处理大量结构化数据的应用, 能从在本地持久化数据中极大受益. 最常见的用例是缓存有关联的数据碎片. 以这种方式, 在设备不能访问网络的时候, 用户依然能够浏览离线内容. 任何用户发起的改变, 都应该在设备重新在线之后同步到服务器.
因为Room为你充分消除了这些顾虑, 使用Room而非SQLite是高度推荐的.
添加依赖
Room的依赖添加方式如下:
1 dependencies { 2 def room_version = "1.1.1" 3 4 implementation "android.arch.persistence.room:runtime:$room_version" 5 annotationProcessor "android.arch.persistence.room:compiler:$room_version" 6 7 // optional - RxJava support for Room 8 implementation "android.arch.persistence.room:rxjava2:$room_version" 9 10 // optional - Guava support for Room, including Optional and ListenableFuture11 implementation "android.arch.persistence.room:guava:$room_version"12 13 // Test helpers14 testImplementation "android.arch.persistence.room:testing:$room_version"15 }
Room有3个主要构件:
Database: 包含了数据库持有者, 并对于连接应用上持久化的相关数据, 作为一个主要的访问点, 来服务. 注解了@Database的类应该满足以下条件:
继承了RoomDatabase的抽象类;
包含实体列表, 而这些实体与该注解之下数据库关联;
包含一个抽象方法, 无参且返回一个注解了@Dao的类;
在运行时, 你可以通过调用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()方法请求Database实例.
Entity: 表示数据库内的表.
DAO: 包含用于访问数据库的方法.
这些构件, 以及它们与app余下内容的关系, 如下图:
下面的代码片断, 包含了一个数据库配置示例, 有一个实体和一个DAO:
User.java
1 @Entity 2 public class User { 3 @PrimaryKey 4 private int uid; 5 6 @ColumnInfo(name = "first_name") 7 private String firstName; 8 9 @ColumnInfo(name = "last_name")10 private String lastName;11 12 // Getters and setters are ignored for brevity,13 // but they're required for Room to work.14 }
UserDao.java
1 @Dao 2 public interface UserDao { 3 @Query("SELECT * FROM user") 4 List<User> getAll(); 5 6 @Query("SELECT * FROM user WHERE uid IN (:userIds)") 7 List<User> loadAllByIds(int[] userIds); 8 9 @Query("SELECT * FROM user WHERE first_name LIKE :first AND "10 + "last_name LIKE :last LIMIT 1")11 User findByName(String first, String last);12 13 @Insert14 void insertAll(User... users);15 16 @Delete17 void delete(User user);18 }
AppDatabase.java
1 @Database(entities = {User.class}, version = 1)2 public abstract class AppDatabase extends RoomDatabase {3 public abstract UserDao userDao();4 }
在创建了以上文件之后, 你能够使用以下代码来创建一个database实例:
1 AppDatabase db = Room.databaseBuilder(getApplicationContext(), 2 AppDatabase.class, "database-name").build();
备注: 在实例化AppDatabase对象的时候, 你应该使用单例模式, 因为每一个RoomDatabase实例都是非常耗时的, 而且你也应该很少访问多个实例.
使用Room实体定义数据
在使用Room持久化库的时候, 把相关联的域的集合定义为实体. 对于每一个实体, 数据库都会创建一个表, 该表来持有数据项.
默认情况下, Room会为实体中定义的每个域创建一个列. 如果实体中有你不想持久化的域, 可以使用@Ignore来注解掉. 在Database类中, 你必须通过entities数据来引用实体类.
下面的代码片断展示了如何定义一个实体:
1 @Entity 2 public class User { 3 @PrimaryKey 4 public int id; 5 6 public String firstName; 7 public String lastName; 8 9 @Ignore10 Bitmap picture;11 }
在持久化一个域, Room必须能够访问它. 你可以将域设置为public, 或者你可以提供该的getter/setter. 如果你使用了getter/setter的方式, 一定要记住: 在Room里面, 它们是基于JavaBeans转换的.
备注: 实体要么有个空的构造器(如果相应的DAO类能够访问每一个持久化域的话), 要有构造器里面的参数, 数据类型和名字跟实体里面定义的域相匹配. Room也能够使用包含全部或者部分域的构造器, 例如, 一个构造器只能获取所有域中的几个.
使用主键
每一个实体必须定义至少1个主键. 即使只有一个域, 你依然需要使用@PrimaryKey来注解它. 而且, 如果你想Room分配自动ID给实体的话, 你需要设置@PrimaryKey的autoGenerate属性. 如果实体有一个复合主键的话, 你需要使用注解@Entity的primaryKeys属性, 示例代码如下:
1 @Entity(primaryKeys = {"firstName", "lastName"})2 public class User {3 public String firstName;4 public String lastName;5 6 @Ignore7 Bitmap picture;8 }
默认情况下, Room使用实体类的名字作为数据库表的名字. 如果你想要表拥有一个不同的名字, 设置@Entity注解的tableName属性, 示例代码如下:
1 @Entity(tableName = "users") 2 public class User { 3 ... 4 }
注意: SQLite中表名是大小写敏感的.
跟tableName属性相似的是, Room使用域的名字作为数据库中列的名字. 如果你想要列有一个不同的名字的话, 给域添加@ColumnInfo注解, 示例代码如下:
1 @Entity(tableName = "users") 2 public class User { 3 @PrimaryKey 4 public int id; 5 6 @ColumnInfo(name = "first_name") 7 public String firstName; 8 9 @ColumnInfo(name = "last_name")10 public String lastName;11 12 @Ignore13 Bitmap picture;14 }
注解索引和唯一性
依赖于你如何访问数据, 你也许想要在数据库中建立某些域的索引, 以加速查询速度. 要给实体添加索引, 需要在@Entity中引入indices属性, 并列出你想要在索引或者复合索引中引入的列的名字. 下列代码说明了注解的处理过程:
1 @Entity(indices = {@Index("name"), 2 @Index(value = {"last_name", "address"})}) 3 public class User { 4 @PrimaryKey 5 public int id; 6 7 public String firstName; 8 public String address; 9 10 @ColumnInfo(name = "last_name")11 public String lastName;12 13 @Ignore14 Bitmap picture;15 }
有些时候, 数据库中的某些域或几组域必须是唯一的. 你可以通过将注解@Index的unique属性设置为true, 强制完成唯一的属性.
下面的代码示例防止表有两行数据在列firstName和lastName拥有相同值:
1 @Entity(indices = {@Index(value = {"first_name", "last_name"}, 2 unique = true)}) 3 public class User { 4 @PrimaryKey 5 public int id; 6 7 @ColumnInfo(name = "first_name") 8 public String firstName; 9 10 @ColumnInfo(name = "last_name")11 public String lastName;12 13 @Ignore14 Bitmap picture;15 }
定义对象之间的关系
因为SQLite是关系型数据库, 你可以指定对象之间的关系. 尽管大多数对象关系的映射允许实体对象引用彼此, 而Room却显式地禁止了这个特性. 要想了解这个讨论背后的原因, 请查看这篇文章. //todo
尽管你不能使用直接的对象关系, Room仍然允许你在实体之间定义外键约束.
比如, 如果有一个实体类Book, 你可以使用@ForeignKey注解定义它和实体User的关系, 示例代码如下:
1 @Entity(foreignKeys = @ForeignKey(entity = User.class, 2 parentColumns = "id", 3 childColumns = "user_id")) 4 public class Book { 5 @PrimaryKey 6 public int bookId; 7 8 public String title; 9 10 @ColumnInfo(name = "user_id")11 public int userId;12 }
外键非常强大, 因为它允许你指定做什么操作, 在引用实体更新的时候. 比如, 你可以告诉SQLite为用户删除所有的书, 在相应的User实例被删除时, 而该User被Book通过在@ForeignKey注解里面声明onDelete = CASCADE而关联.
备注: SQLite将@Insert(onConflict = REPLACE)作为REMOVE和REPLACE的集合来操作, 而非单独的UPDATE操作. 这个取代冲突值的方法能够影响你的外键约束.
创建嵌套对象
有些时候, 在数据库逻辑中, 你想将一个实体或者POJO表示为一个紧密联系的整体, 即使这个对象包含几个域. 在这些情况下, 你能够使用@Embedded注解来表示一个对象, 而你想将这个对象分解为表内的子域. 然后你可以查询这些嵌套域, 就像你查询其它的独立列一样.
举个例子, User类包含一个Address类的域, 这个域表示的是street, city, state, postCode这几个域的复合. 为了在表中单独存储复合的列, 在User类里面, 引入一个注解了@Embedded的Address域, 就像如下代码片断展示的一样:
1 public class Address { 2 public String street; 3 public String state; 4 public String city; 5 6 @ColumnInfo(name = "post_code") 7 public int postCode; 8 } 9 10 @Entity11 public class User {12 @PrimaryKey13 public int id;14 15 public String firstName;16 17 @Embedded18 public Address address;19 }
这个表表示User对象包含如下几列: id, firstName, street, state, city和post_code.
备注: 嵌套的域同样可以包含其它的嵌套域.
如果实体拥有多个相同类型的嵌套域, 你可以通过设置prefix属性保留每一列唯一. 然后Room给嵌套对象的每一个列名的起始处添加prefix设置的给定值.
通过Room DAO访问数据
要通过Room持久化库访问应用的数据, 你需要使用数据访问对象(data access objects, 即DAOs). Dao对象集形成了Room的主要构成, 因为每一个DAO对象都引入了提供了抽象访问数据库的方法.
使用DAO对象而非查询构造器或者直接查询来访问数据库, 你可以分开不同的数据库架构组成. 此外, DAO允许你轻易地模拟数据库访问.
DAO要么是接口, 要么是抽象类. 如果DAO是抽象类的话, 它可以随意地拥有一个将RoomDatabase作为唯一参数的构造器. Room在运行时创建DAO的实现.
备注: Room并不支持在主线程访问数据库, 除非在Builder调用allowMainThreadQueries()方法, 因为它很可能将UI锁上较长一段时间. 但是, 异步查询--返回LiveData/Flowable实例的查询--则从此规则中免除, 因为它们在需要的时候会在后台线程异步地运行查询.
方便地定义方法
使用DAO类, 可以非常方便地表示查询.
插入
当你创建了一个DAO方法并注解了@Insert的时候, Room生成了一个实现, 在单个事务中将所有的参数插入数据库.
下面的代码片断展示了几个示例查询:
1 @Dao 2 public interface MyDao { 3 @Insert(onConflict = OnConflictStrategy.REPLACE) 4 public void insertUsers(User... users); 5 6 @Insert 7 public void insertBothUsers(User user1, User user2); 8 9 @Insert10 public void insertUsersAndFriends(User user, List<User> friends);11 }
如果@Insert方法只接收了一个参数, 它可以返回一个long, 表示新插入项的rowId; 如果参数是数组或者集合, 同时地, 它应该返回long[]或者List<Long>.
更新
按照惯例, 在数据库中, Update方法修改了作为参数传递的实体集合. 它使用查询来匹配每一个实体的主键.
下面的代码片断展示了如何定义这个方法:
1 @Dao2 public interface MyDao {3 @Update4 public void updateUsers(User... users);5 }
尽管通常情况下并不需要, 但是依然可以将这个方法返回int值, 表示在数据库中被修改的行数.
删除
按照惯例, Delete方法从数据库中删除了作为参数传递的实体集合. 它使用主键找到要删除的实体.
下面的代码片断展示了如何定义这个方法:
1 @Dao2 public interface MyDao {3 @Delete4 public void deleteUsers(User... users);5 }
尽管通常情况下并不需要, 但是依然可以将这个方法返回int值, 表示从数据库中删除的行数.
查询
@Query是在DAO类中使用的主要的注解. 它允许你在数据库中执行读写操作. 每一个@Query方法都在编译时被证实, 因为, 如果查询有问题出现的话, 会出现编译错误而非运行失败.
Room也证实查询的返回值, 以确定返回对象的域的名字是否跟查询响应中对应列的名字匹配, Room使用如下两种方式提醒你:
如果只有一些域匹配, 它会给予警告;
如果没有域匹配, 它会给予错误;
简单查询
1 @Dao2 public interface MyDao {3 @Query("SELECT * FROM user")4 public User[] loadAllUsers();5 }
这是一个非常简单的查询, 加载了所有User. 在编译时, Room知晓这是在查询user表中所有列.
如果查询语句包含语法错误, 或者user表在数据库中并不存在, Room会在编译时展示恰当的错误信息.
查询语句中传参
大多数时候, 你需要向查询语句中传参, 以执行过滤操作, 比如, 只展示大于某个年龄的user.
要完成这个任务, 在Room注解中使用方法参数, 如下所示:
1 @Dao2 public interface MyDao {3 @Query("SELECT * FROM user WHERE age > :minAge")4 public User[] loadAllUsersOlderThan(int minAge);5 }
当这个查询在编译时处理的时候, Room匹配到 :minAge, 并将它跟方法参数minAge绑定. Room使用参数名来执行匹配操作. 如果不匹配的话, app编译时会发生错误.
你也可以在查询中传递多个参数, 或者将参数引用多次, 如下所示:
1 @Dao2 public interface MyDao {3 @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")4 public User[] loadAllUsersBetweenAges(int minAge, int maxAge);5 6 @Query("SELECT * FROM user WHERE first_name LIKE :search "7 + "OR last_name LIKE :search")8 public List<User> findUserWithName(String search);9 }
返回列的子集
大多数情况下, 你只需要实体中的几个域. 比如, UI中只需要展示用户的姓和名, 而非用户的每一个细节. 通过只查询UI中展示的列, 将节省宝贵的资源, 查询也更快.
Room允许从查询中返回基于Java的对象, 只要结果列集合能够映射成返回对象. 比如, 你创建了一个POJO来获取用户的名和姓:
1 public class NameTuple {2 @ColumnInfo(name="first_name")3 public String firstName;4 5 @ColumnInfo(name="last_name")6 public String lastName;7 }
现在, 你可以在查询方法中使用这个POJO了:
1 @Dao2 public interface MyDao {3 @Query("SELECT first_name, last_name FROM user")4 public List<NameTuple> loadFullName();5 }
Room明白: 查询返回了列first_name和last_name, 这些值能够映射到NameTuple为的域中.
由此, Room能够产生适当的代码. 如果查询返回了太多列, 或者返回了NameTuple类中并不存在的列, Room将展示警告信息.
备注: POJO也可以使用@Embedded注解.
传递参数集
一些查询可能要求你传入可变数目的参数, 直到运行时才知道精确的参数数量.
比如, 你可能想要搜索地区子集下的所有用户. Room明白参数表示集合的时机, 并在运行时自动地基于提供了参数数目展开它.
1 @Dao2 public interface MyDao {3 @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")4 public List<NameTuple> loadUsersFromRegions(List<String> regions);5 }
可观察查询
在执行查询的时候, 经常想要在数据发生改变的时候自动更新UI. 要达到这个目的, 需要在查询方法描述中返回LiveData类型的值. 在数据库更新的时候, Room生成所有必要的代码以更新LiveData.
1 @Dao2 public interface MyDao {3 @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")4 public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);5 }
备注: 在1.0版本的时候, Room使用查询中访问的表的列表来决定是否更新LiveData实例.
RxJava响应式查询
Room也可以从定义的查询中返回RxJava2中的Publisher和Flowable.
要使用这个功能, 在build.gradle文件中添加依赖: android.arch.persistence.room:rxjava2. 之后, 你可以返回在RxJava2中定义的数据类型, 如下所示:
1 @Dao2 public interface MyDao {3 @Query("SELECT * from user where id = :id LIMIT 1")4 public Flowable<User> loadUserById(int id);5 }
游标直接访问
如果你的应用逻辑要求直接访问返回的行, 你可以从查询中返回Cursor对象, 如下所示:
1 @Dao2 public interface MyDao {3 @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")4 public Cursor loadRawUsersOlderThan(int minAge);5 }
注意: 十分不推荐使用Cursor API. 因为它并不保证行是否存在以及行包含什么值.
除非你有需要Cursor的代码并且并不轻易的修改它的时候, 你才可以使用这个功能.
查询多表
有些查询可能要求访问多个表以计算结果. Room允许你写任何查询, 所以你也可以联接表. 此外, 如果响应是可观测数据类型, 诸如Flowable/LiveData, Room观察并证实查询中引用的所有表.
下面的代码片段展示了如何执行表联接, 以合并包含借书用户的表和包含在借书数据的表的信息:
1 @Dao2 public interface MyDao {3 @Query("SELECT * FROM book "4 + "INNER JOIN loan ON loan.book_id = book.id "5 + "INNER JOIN user ON user.id = loan.user_id "6 + "WHERE user.name LIKE :userName")7 public List<Book> findBooksBorrowedByNameSync(String userName);8 }
你也可以从这些查询中返回POJO. 比如, 你可以写查询加载用户和它的宠物名:
1 @Dao 2 public interface MyDao { 3 @Query("SELECT user.name AS userName, pet.name AS petName " 4 + "FROM user, pet " 5 + "WHERE user.id = pet.user_id") 6 public LiveData<List<UserPet>> loadUserAndPetNames(); 7 8 9 // You can also define this class in a separate file, as long as you add the10 // "public" access modifier.11 static class UserPet {12 public String userName;13 public String petName;14 }15 }
迁移Room数据库
当应用中添加或者改变特性的时候, 需要修改实体类以反映出这些改变. 当用户升级到最新版本的时候, 你不想用户失去所有数据, 尤其是如果你还不能从远程服务器恢复这些数据的时候.
Room持久化库允许写Migration类来保留用户数据. 每一个Migration类指定了startVersion和endVersion. 在运行时, Room运行每一个Migration类的migrate()方法, 使用正确的顺序迁移数据库到最新版本.
注意: 如果你不提供必要的迁移, Room会重建数据库, 这意味着你会失去原有数据库中的所有数据.
1 Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") 2 .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build(); 3 4 static final Migration MIGRATION_1_2 = new Migration(1, 2) { 5 @Override 6 public void migrate(SupportSQLiteDatabase database) { 7 database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, " 8 + "`name` TEXT, PRIMARY KEY(`id`))"); 9 }10 };11 12 static final Migration MIGRATION_2_3 = new Migration(2, 3) {13 @Override14 public void migrate(SupportSQLiteDatabase database) {15 database.execSQL("ALTER TABLE Book "16 + " ADD COLUMN pub_year INTEGER");17 }18 };
注意: 要保证迁移逻辑按照预期进行, 需要使用全查询而非引用表示查询的常量.
在迁移完成之后, Room会证实这个计划, 以确保迁移正确在发生了. 如果Room发现了问题, 它会抛出包含不匹配信息的异常.
迁移测试
写Migration并不是没有价值的, 不能恰当的写Migration会在应用中引起崩溃. 在保持应用的稳定性, 你应该事先测试Migration. Room提供了一个Maven测试工具. 但是, 如果要使这个工具工作, 你需要导出数据库schema.
导出schema
在编译的时候, Room会导出数据库schem信息, 形成一个Json文件. 要导出schema, 需要在build.gradle文件中设置room.schemaLocation注解处理器属性, 如下所示:
build.gradle:
1 android { 2 ... 3 defaultConfig { 4 ... 5 javaCompileOptions { 6 annotationProcessorOptions { 7 arguments = ["room.schemaLocation": 8 "$projectDir/schemas".toString()] 9 }10 }11 }12 }
你应该保存导出的Json文件--这些文件表示了数据库schema的历史--在你的版本控制体系中, 因为它允许Room创建老版本数据库用于测试.
要测试这些Migration, 需要在测试需要的依赖中添加 anroid.arch.persistence.room:testing , 并在资产文件夹下添加schema地址, 如下所示:
build.gradle:
1 android {2 ...3 sourceSets {4 androidTest.assets.srcDirs += files("$projectDir/schemas".toString())5 }6 }
测试包提供了MigrationTestHelper类, 它能够读取这些schema文件. 它也实现了JUnit4 TestRule接口, 所有它能够管理已创建的数据库.
示例Migration测试如下:
1 @RunWith(AndroidJUnit4.class) 2 public class MigrationTest { 3 private static final String TEST_DB = "migration-test"; 4 5 @Rule 6 public MigrationTestHelper helper; 7 8 public MigrationTest() { 9 helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),10 MigrationDb.class.getCanonicalName(),11 new FrameworkSQLiteOpenHelperFactory());12 }13 14 @Test15 public void migrate1To2() throws IOException {16 SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);17 18 // db has schema version 1. insert some data using SQL queries.19 // You cannot use DAO classes because they expect the latest schema.20 db.execSQL(...);21 22 // Prepare for the next version.23 db.close();24 25 // Re-open the database with version 2 and provide26 // MIGRATION_1_2 as the migration process.27 db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);28 29 // MigrationTestHelper automatically verifies the schema changes,30 // but you need to validate that the data was migrated properly.31 }32 }
测试数据库
在使用Room持久化库创建数据库的时候, 证实应用数据库和用户数据的稳定性非常重要.
有两种方式测试你的数据库:
在真机上;
在虚拟机上(不推荐);
备注: 在运行应用的测试的时候, Room允许你创建模拟DAO类的实例. 使用这种方式的话, 如果不是在测试数据库本身的话, 你不必创建完成的数据库. 这个功能是可能的, 因为DAO并不泄露任何数据库细节.
真机测试
测试数据库实现的推荐途径是在真机上运行JUnit测试. 因为这些测试并不创建Activity, 它们应该比UI测试执行地更快.
在设置测试的时候, 你应该创建内存版本数据库, 以确保测试更加地密封. 如下所示:
1 @RunWith(AndroidJUnit4.class) 2 public class SimpleEntityReadWriteTest { 3 private UserDao mUserDao; 4 private TestDatabase mDb; 5 6 @Before 7 public void createDb() { 8 Context context = InstrumentationRegistry.getTargetContext(); 9 mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();10 mUserDao = mDb.getUserDao();11 }12 13 @After14 public void closeDb() throws IOException {15 mDb.close();16 }17 18 @Test19 public void writeUserAndReadInList() throws Exception {20 User user = TestUtil.createUser(3);21 user.setName("george");22 mUserDao.insert(user);23 List<User> byName = mUserDao.findUsersByName("george");24 assertThat(byName.get(0), equalTo(user));25 }26 }
虚拟机测试
Room使用了SQLite支持库, 后者提供了在Android Framework类里面匹配的接口. 这个支持允许你传递自定义的支持库实现来测试数据库查询.
备注: 尽管这个设置允许测试运行地很快, 但它并不是值得推荐的, 因为运行在自己以及用户真机上面的SQLite版本, 可能并不匹配你的虚拟机上面的SQLite版本.
使用Room引用复杂数据
Room提供了功能支持基数数据类型和包装类型之间的转变, 但是并不允许实体间的对象引用.
使用类型转换器
有时候, 应用需要使用自定义数据类型, 该数据类型的值将保存在数据库列中. 要添加这种自定义类型的支持, 你需要提供TypeConverter, 用来将自定义类型跟Room能够持久化的已知类型相互转换.
比如, 如果我们想要持久化Date类型, 我们需要写下面的TypeConverter来在数据库中保存等价的Unix时间戳:
1 public class Converters { 2 @TypeConverter 3 public static Date fromTimestamp(Long value) { 4 return value == null ? null : new Date(value); 5 } 6 7 @TypeConverter 8 public static Long dateToTimestamp(Date date) { 9 return date == null ? null : date.getTime();10 }11 }
上述示例定义了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})3 public abstract class AppDatabase extends RoomDatabase {4 public abstract UserDao userDao();5 }
使用这些转换器, 你之后就能够在其它的查询中使用自定义的类型, 就像你使用基本数据类型一样, 如下所示:
User.java
1 @Entity2 public class User {3 ...4 private Date birthday;5 }
UserDao.java
1 @Dao2 public interface UserDao {3 ...4 @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")5 List findUsersBornBetweenDates(Date from, Date to);6 }
你也可以限制@TypeConverters的使用范围, 包括单个实体, DAO和DAO方法.
理解为什么Room不允许对象引用
要点: Room不允许实体类间的对象引用. 相反, 你必须显式地请求应用需要的数据.
从数据库到对应对象模型的映射关系是通用最佳实践, 在服务器端也运行良好. 即使是在程序加载它们正在访问的域的时候, 服务器依然执行良好.
然而在客户端, 这种类型的懒加载并不可行, 因为它通常发生在UI线程, 而在UI线程止查询硬盘信息产生了显著的性能问题. UI线程只有16ms计算和绘制Activity更新的布局, 所以, 即使查询花费了仅仅5ms, 看起来依然是应用绘制超时, 引起显著的视觉差错. 如果有另外的事件并行运行, 或者, 设备正在运行其它的硬盘密集型任务, 查询要完成就要花费更多的时间. 然而, 如果不使用懒加载, 应用获取超过需要的数据, 也会引起内存消耗问题.
对象关系型映射通常将这个决定留给开发者, 让他们做出应用用例最佳的选择. 开发者通常决定在应用和UI之间共享模型. 然后, 这个解决方案并不权衡地很好, 因为UI随着时间改变, 共享模型会产生对于开发者而言难以参与和debug的问题.
比如, UI加载Book对象列表, 同时每一本书有个Author对象. 最初你可能设计查询使用懒加载, 之后Book对象使用getAuthor()方法返回作者. getAuthor()方法的首次调用查询了数据库. 之后一段时间, 你发现同样需要展示作者姓名. 你轻易地添加如下这样的方法调用:
1 authorNameTextView.setText(book.getAuthor().getName());
然后, 这个貌似无辜的改变引起Author表在主线程被查询.
如果你提前查询作者信息, 而在你不再需要这个数据之后, 将很难改变加载的方式. 比如, UI不再需要展示Author信息, 而应用依然高效地加载不同展示的数据, 浪费了宝贵的内存空间. 应用的效率将会降级, 如果Author类引用了其它的表, 如Books.
要使用Room同时引用多个实体, 需要创建包含每个实体的POJO类, 之后写联接了相应表的查询语句. 这个结构良好的模型, 结合了Room鲁棒的查询证实能力, 允许应用在加载资源时消耗更少的资源, 提升了应用的性能和用户体验.