• Android Room使用详解


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

  • 相关阅读:
    详解Android Intent
    【JAVA EE企业级开发四步走完全攻略】
    撼动IT界的10大编程语言
    System.getProperty() 常用值
    Android List,Adapter相关
    为程序员量身定制的12个目标
    java 算法数据
    JAVA基础之理解JNI原理
    Java 工厂模式
    linux下C语言读取网卡MAC地址
  • 原文地址:https://www.cnblogs.com/littlepanpc/p/9269549.html
Copyright © 2020-2023  润新知