SilentKnight 阅读(23) 评论(0)

 

使用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 ListenableFuture
11     implementation "android.arch.persistence.room:guava:$room_version"
12 
13     // Test helpers
14     testImplementation "android.arch.persistence.room:testing:$room_version"
15 }

 

Room有3个主要构件:

  • Database: 包含了数据库持有者, 并对于连接应用上持久化的相关数据, 作为一个主要的访问点, 来服务. 注解了@Database的类应该满足以下条件:
  1. 继承了RoomDatabase的抽象类;
  2. 包含实体列表, 而这些实体与该注解之下数据库关联;
  3. 包含一个抽象方法, 无参且返回一个注解了@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     @Insert
14     void insertAll(User... users);
15 
16     @Delete
17     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     @Ignore
10     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     @Ignore
7     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     @Ignore
13     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     @Ignore
14     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     @Ignore
14     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 @Entity
11 public class User {
12     @PrimaryKey
13     public int id;
14 
15     public String firstName;
16 
17     @Embedded
18     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     @Insert
10     public void insertUsersAndFriends(User user, List<User> friends);
11 }

如果@Insert方法只接收了一个参数, 它可以返回一个long, 表示新插入项的rowId; 如果参数是数组或者集合, 同时地, 它应该返回long[]或者List<Long>.

 

更新

 

按照惯例, 在数据库中, Update方法修改了作为参数传递的实体集合. 它使用查询来匹配每一个实体的主键.
下面的代码片断展示了如何定义这个方法:

1 @Dao
2 public interface MyDao {
3     @Update
4     public void updateUsers(User... users);
5 }

尽管通常情况下并不需要, 但是依然可以将这个方法返回int值, 表示在数据库中被修改的行数.

 

删除

 

按照惯例, Delete方法从数据库中删除了作为参数传递的实体集合. 它使用主键找到要删除的实体.
下面的代码片断展示了如何定义这个方法:

1 @Dao
2 public interface MyDao {
3     @Delete
4     public void deleteUsers(User... users);
5 }

尽管通常情况下并不需要, 但是依然可以将这个方法返回int值, 表示从数据库中删除的行数.

 

查询

 

@Query是在DAO类中使用的主要的注解. 它允许你在数据库中执行读写操作. 每一个@Query方法都在编译时被证实, 因为, 如果查询有问题出现的话, 会出现编译错误而非运行失败.
Room也证实查询的返回值, 以确定返回对象的域的名字是否跟查询响应中对应列的名字匹配, Room使用如下两种方式提醒你:

  • 如果只有一些域匹配, 它会给予警告;
  • 如果没有域匹配, 它会给予错误;

 

简单查询

 

1 @Dao
2 public interface MyDao {
3     @Query("SELECT * FROM user")
4     public User[] loadAllUsers();
5 }

这是一个非常简单的查询, 加载了所有User. 在编译时, Room知晓这是在查询user表中所有列.

如果查询语句包含语法错误, 或者user表在数据库中并不存在, Room会在编译时展示恰当的错误信息.

 

查询语句中传参

 

大多数时候, 你需要向查询语句中传参, 以执行过滤操作, 比如, 只展示大于某个年龄的user.

要完成这个任务, 在Room注解中使用方法参数, 如下所示:

1 @Dao
2 public interface MyDao {
3     @Query("SELECT * FROM user WHERE age > :minAge")
4     public User[] loadAllUsersOlderThan(int minAge);
5 }

当这个查询在编译时处理的时候, Room匹配到 :minAge, 并将它跟方法参数minAge绑定. Room使用参数名来执行匹配操作. 如果不匹配的话, app编译时会发生错误.

你也可以在查询中传递多个参数, 或者将参数引用多次, 如下所示:

1 @Dao
2 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 @Dao
2 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 @Dao
2 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 @Dao
2 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 @Dao
2 public interface MyDao {
3     @Query("SELECT * from user where id = :id LIMIT 1")
4     public Flowable<User> loadUserById(int id);
5 }


游标直接访问

 

如果你的应用逻辑要求直接访问返回的行, 你可以从查询中返回Cursor对象, 如下所示:

1 @Dao
2 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 @Dao
2 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 the
10    // "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     @Override
14     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     @Test
15     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 provide
26         // 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     @After
14     public void closeDb() throws IOException {
15         mDb.close();
16     }
17 
18     @Test
19     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 @Entity
2 public class User {
3     ...
4     private Date birthday;
5 }

UserDao.java

1 @Dao
2 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鲁棒的查询证实能力, 允许应用在加载资源时消耗更少的资源, 提升了应用的性能和用户体验.