美文网首页Android架构组件AndroidAndroid面试知识
【译】Google官方推出的Android架构组件系列文章(六)

【译】Google官方推出的Android架构组件系列文章(六)

作者: 清风流苏 | 来源:发表于2017-07-25 15:29 被阅读1339次

    系列文章导航

    1. 【译】Google官方推出的Android架构组件系列文章(一)App架构指南
    2. 【译】Google官方推出的Android架构组件系列文章(二)将Architecture Components引入工程
    3. 【译】Google官方推出的Android架构组件系列文章(三)处理生命周期
    4. 【译】Google官方推出的Android架构组件系列文章(四)LiveData
    5. 【译】Google官方推出的Android架构组件系列文章(五)ViewModel
    6. 【译】Google官方推出的Android架构组件系列文章(六)Room持久化库

    原文地址:https://developer.android.com/topic/libraries/architecture/room.html

    Room在SQLite之上提供了一个抽象层,可以在使用SQLite的全部功能的同时流畅访问数据库。

    注意:将Room导入工程,请参考将Architecture Components引入工程

    需要处理大量结构化数据的应用能从本地持久化数据中受益匪浅。最常见的使用场景是缓存相关的数据。比如,当设备无法访问网络时,用户仍然可以在离线时浏览内容。当设备重新联网后,任何用户发起的内容更改将同步到服务器。

    核心框架提供了操作原始SQL内容的内置支持。尽管这些API很强大,但它们相对较低层,需要大量的时间和精力才能使用:

    • 没有对原始SQL查询语句的编译时验证。 当你的数据图变化时,你需要手动更新受影响的SQL查询语句。这个过程可能很耗时,而且容易出错。
    • 你需要使用大量模板代码来进行SQL语句和Java数据对象的转换。

    RoomSQLite之上提供一个抽象层,来帮助你处理这些问题。

    Room包含三大组件:

    • Database:利用这个组件来创建一个数据库持有者。注解定义一系列实体,类的内容定义一系列DAO。它也是底层连接的主入口点。

      注解类应该是继承RoomDatabase的抽象类。在运行期间,你可以通过调用Room.databaseBuilder()Room.inMemoryDatabaseBuilder()方法获取其实例。

    • Entity:这个组件表示持有数据库行的类。对于每个实体,将会创建一个数据库表来持有他们。你必须通过Database类的entities数组来引用实体类。实体类的中的每个字段除了添加有@Ignore注解外的,都会存放到数据库中。

    注意:Entity可以有一个空的构造函数(如果DAO类可以访问每个持久化字段),或者一个构造函数其参数包含与实体类中的字段匹配的类型和名字。Room还可以使用全部或部分构造函数,比如只接收部分字段的构造函数。

    • DAO: 该组件表示作为数据访问对象(DAO)的类或接口。DAORoom的主要组件,负责定义访问数据库的方法。由@Database注解标注的类必须包含一个无参数且返回使用@Dao注解的类的抽象方法。当在编译生成代码时,Room创建该类的实现。

    注意:通过使用DAO类代替查询构建器或者直接查询来访问数据库,你可以分离数据库架构的不同组件。此外,DAO允许你在测试应用时轻松地模拟数据库访问。

    这些组件,以及与应用程序其他部分的关系,如图所示:

    room_architecture.png

    以下代码片段包含一个数据库配置样例,其包含一个实体和一个DAO。

    User.java

    @Entity
    public class User {
        @PrimaryKey
        private int uid;
    
        @ColumnInfo(name = "first_name")
        private String firstName;
    
        @ColumnInfo(name = "last_name")
        private String lastName;
    
        // Getters and setters are ignored for brevity,
        // but they're required for Room to work.
    }
    

    UserDao.java

    @Dao
    public interface UserDao {
        @Query("SELECT * FROM user")
        List<User> getAll();
    
        @Query("SELECT * FROM user WHERE uid IN (:userIds)")
        List<User> loadAllByIds(int[] userIds);
    
        @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
               + "last_name LIKE :last LIMIT 1")
        User findByName(String first, String last);
    
        @Insert
        void insertAll(User... users);
    
        @Delete
        void delete(User user);
    }
    

    AppDatabase.java

    @Database(entities = {User.class}, version = 1)
    public abstract class AppDatabase extends RoomDatabase {
        public abstract UserDao userDao();
    }
    

    在创建以上文件之后,你可以通过下面代码获取创建的数据库的实例:

    AppDatabase db = Room.databaseBuilder(getApplicationContext(),
            AppDatabase.class, "database-name").build();
    

    注意:实例化AppDatabase对象时,应该遵循单例模式,因为每个RoomDatabase实例都相当昂贵,而且很少需要访问多个实例。

    实体

    当一个类由@Entity注解,并且由@Database注解的entities属性引用,Room将在数据库中为其创建一张数据库表。

    默认,Room会为实体类中的每个字段创建一列。如果实体类中包含你不想保存的字段,你可以给他们加上@Ignore注解,如下面代码片段所示:

    @Entity
    class User {
        @PrimaryKey
        public int id;
    
        public String firstName;
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }
    

    要持久化一个字段,Room必须能够访问它。你可以将字段设置为public,或为它提供gettersetter。如果你使用settergetter,请记住,它们基于RoomJava Bean约定。

    主键

    每个实体必须定义至少一个字段作为主键。甚至当仅仅只有一个字段时,你仍然需要为该字段加上@PrimaryKey注解。另外,如果你想让Room为实体分配自增ID,你可以设置@PrimaryKey注解的autoGenerate属性。如果实体包含组合主键,你可以使用@Entity注解的primaryKeys属性,如下面的代码片段所示:

    @Entity(primaryKeys = {"firstName", "lastName"})
    class User {
        public String firstName;
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }
    

    默认,Room使用类名作为数据库表名。如果你想让表采用不同的名字,设置@Entity注解的tableName属性,如下面的代码片段所示:

    @Entity(tableName = "users")
    class User {
        ...
    }
    

    警告:SQLite中的表名不区分大小写。

    tableName属性类似,Room使用字段名作为数据库中的列名。如果你想要一列采用不同的名字,添加@ColumnInfo注解到字段,如下面代码片段所示:

    @Entity(tableName = "users")
    class User {
        @PrimaryKey
        public int id;
    
        @ColumnInfo(name = "first_name")
        public String firstName;
    
        @ColumnInfo(name = "last_name")
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }
    

    索引和唯一约束

    根据访问数据的方式,你可能希望对数据库中的某些字段进行索引,以加快查询速度。要向实体添加索引,请在@Entity注解中包含indices属性,列出要包含在索引或组合索引中的列的名字。

    以下代码片段演示此注解过程:

    @Entity(indices = {@Index("name"), @Index("last_name", "address")})
    class User {
        @PrimaryKey
        public int id;
    
        public String firstName;
        public String address;
    
        @ColumnInfo(name = "last_name")
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }
    

    有时,数据库中的某些字段或字段组合必须是唯一的。你可以通过设置@Index注解的unique属性为true来强制满足唯一属性。下面代码样例阻止表含有对于firstNamelastName列包含同样的值的两条记录:

    @Entity(indices = {@Index(value = {"first_name", "last_name"},
            unique = true)})
    class User {
        @PrimaryKey
        public int id;
    
        @ColumnInfo(name = "first_name")
        public String firstName;
    
        @ColumnInfo(name = "last_name")
        public String lastName;
    
        @Ignore
        Bitmap picture;
    }
    

    关系

    因为SQLite是关系型数据库,你可以指定对象间的关系。尽管大部分的ORM库允许实体对象互相引用,但是Room明确禁止此操作。更多详细信息,请参考附录:实体间无对象引用

    尽管你无法直接使用关系,Room仍然允许你定义实体间的外键约束。

    例如,假如有另外一个叫做Book的实体,你可以使用@ForeignKey注解来定义它和User实体的关系,如下面代码所示:

    @Entity(foreignKeys = @ForeignKey(entity = User.class,
                                      parentColumns = "id",
                                      childColumns = "user_id"))
    class Book {
        @PrimaryKey
        public int bookId;
    
        public String title;
    
        @ColumnInfo(name = "user_id")
        public int userId;
    }
    

    外键是很强大的,因为它允许你指明当引用的实体更新时应该怎么处理。比如,你可以通过在@ForeignKey注解中包含onDelete=CASCADE,来告诉SQLite如果某个User实例被删除,则删除该用户的所有书。

    注意SQLite处理@Insert(onConfilict=REPLACE)作为一组REMOVEREPLACE操作,而不是单个UPDATE操作。这个替换冲突值的方法将会影响到你的外键约束。更多详细信息,请参见SQLite文档ON_CONFLICT语句。

    嵌套对象

    有时,你希望将一个实体或POJO表达作为数据库逻辑中的一个整体,即使对象包含了多个字段。在这种情况下,你可以使用@Embeded注解来表示要在表中分为为子字段的对象。然后,你可以像其他单独的列一样查询嵌入的字段。

    例如,我们的User类可以包含一个类型为Address的字段,其表示了一个字段组合,包含streetcitystatepostCode。为了将这些组合列单独的存放到表中,将Address字段加上@Embedde注解,如下代码片段所示:

    class Address {
        public String street;
        public String state;
        public String city;
    
        @ColumnInfo(name = "post_code")
        public int postCode;
    }
    
    @Entity
    class User {
        @PrimaryKey
        public int id;
    
        public String firstName;
    
        @Embedded
        public Address address;
    }
    

    这张表示User对象的表将包含以下名字的列:idfirstNamestreetstatecitypost_code

    注意:嵌入字段也可以包含其他潜入字段。

    如果实体包含了多个同一类型的嵌入字段,你可以通过设置prefix属性来保持每列的唯一性。Room然后将提供的值添加到嵌入对象的每个列名的开头。

    数据访问对象(DAO)

    Room的主要组件是Dao类。DAO以简洁的方式抽象了对于数据库的访问。

    Dao要么是一个接口,要么是一个抽象类。如果它是抽象类,它可以有一个使用RoomDatabase作为唯一参数的可选构造函数。

    注意Room不允许在主线程中访问数据库,除非你可以builder上调用allowMainThreadQueries(),因为它可能会长时间锁住UI。异步查询(返回LiveDataRxJava Flowable的查询)则不受此影响,因为它们在有需要时异步运行在后台线程上。

    方便的方法

    可以使用DAO类来表示多个方便的查询。这篇文章包含几个常用的例子。

    插入

    当你创建一个DAO方法并用@Insert注解时,Room会生成一个在在单独事务中将所有参数插入到数据库中的实现。

    下面代码展示几个插入样例:

    @Dao
    public interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        public void insertUsers(User... users);
    
        @Insert
        public void insertBothUsers(User user1, User user2);
    
        @Insert
        public void insertUsersAndFriends(User user, List<User> friends);
    }
    

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

    更多详情,参见@Insert注解的引用文档,以及SQLite文档的rowId表

    更新

    Update是一个方便的方法,用于更新数据库中以参数给出的一组实体。它使用与每个实体主键匹配的查询。下面代码片段演示如何定义该方法:

    @Dao
    public interface MyDao {
        @Update
        public void updateUsers(User... users);
    }
    

    虽然通常不是必须的,但你可以让此方法返回一个int值,指示数据库中更新的行数。

    删除

    Delete是一个方便的方法,用于删除数据库中作为参数给出的实体集。使用主键来查找要删除的实体。下面代码演示如何定义此方法:

    @Dao
    public interface MyDao {
        @Delete
        public void deleteUsers(User... users);
    }
    

    虽然通常不是必须的,但你可以让此方法返回一个int值,指示数据库中删除的行数。

    使用@Query的方法

    @QueryDAO类中使用的主要注解。可以让你执行数据库读/写操作。每个@Query方法会在编译时验证,因此如果查询有问题,则会发生编译错误而不是运行时故障。

    Room还会验证查询的返回值,以便如果返回对象中的字段名与查询相应中的相应列名不匹配,Room则会以下面两种方式的一种提醒你:

    • 如果仅仅某些字段名匹配,则给出警告
    • 如果没有字段匹配,则给出错误。

    简单查询

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

    这是一条非常简单的用于加载所有用户的查询。在编译时,Room知道它是查询user表的所有列。如果查询包含语法错误,或者如果user表不存在于数据库,Room会在应用编译时,展示相应的错误消息。

    给查询传递参数

    大部分情况,你需要给查询传递参数以便执行过滤操作,比如仅仅展示年龄大于某个值的用户。为了完成这个任务,在Room注解中使用方法参数,如下面代码所示:

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

    当查询在编译时处理时,Room匹配:minAge绑定参数和:minAge方法参数。Room采用参数名进行匹配。如果没有匹配成功,在应用编译时则发生错误。

    你还可以在查询中传递多个参数或引用她们多次,如下面代码所示:

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
    
        @Query("SELECT * FROM user WHERE first_name LIKE :search "
               + "OR last_name LIKE :search")
        public List<User> findUserWithName(String search);
    }
    
    

    返回列的子集

    大部分时间,你仅仅需要获取实体的几个字段。比如,你的UI可能展示仅仅是用户的first name和last name,而不是用户的每个详细信息。通过仅获取应用UI上显示的几列,你可以节省宝贵的资源,并且更快完成查询。

    Room允许你从查询中返回任意的java对象,只要结果列集能被映射到返回的对象。比如,你可以创建下面的POJO来拉取用户的first namelast name

    public class NameTuple {
        @ColumnInfo(name="first_name")
        public String firstName;
    
        @ColumnInfo(name="last_name")
        public String lastName;
    }
    

    现在,你可以在你的查询方法中使用这个POJO

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user")
        public List<NameTuple> loadFullName();
    }
    

    Room理解这个查询是要返回first_namelast_name列的值,并且这些值可以映射成NameTuple类的字段。因此,Room可以生成正确的代码。如果查询返回太多列,或者有列不存在NameTuple类,Room则显示一个警告。

    注意:这些POJO也可以使用@Embedded注解

    传递参数集合

    一些查询可能要求传递一组个数变化的参数,指导运行时才知道确切的参数个数。比如,你可能想要获取关于一个区域集里面所有用户的信息。Room理解当参数表示为集合时,会在运行时基于提供的参数个数自动进行展开。

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        public List<NameTuple> loadUsersFromRegions(List<String> regions);
    }
    

    可观察的查询

    当执行查询时,你经常希望应用程序的UI在数据更改时自动更新。为达到这个目的,在查询方法描述中使用返回LiveData类型的值。Room生成所有必要的代码,来达到当数据更新时更新LiveData

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
    }
    

    注意:作为1.0版本,Room使用查询中访问的表列表来决定是否更新LiveData对象。

    RxJava

    Room还能从你定义的查询中返回RxJava2PublisherFlowable对象。要使用此功能,请将Room组中的android.arch.persistence.room:rxjava2添加到构建Gradle依赖中。然后,你可以返回RxJava2中定义的类型,如下面代码所示:

    @Dao
    public interface MyDao {
        @Query("SELECT * from user where id = :id LIMIT 1")
        public Flowable<User> loadUserById(int id);
    }
    

    直接光标访问

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

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
        public Cursor loadRawUsersOlderThan(int minAge);
    }
    

    警告:非常不鼓励使用Cursor API,因为它无法保证是否行存在,或者行包含什么值。仅当你已经具有期望使用Cursor的代码,并且不能轻易重构时使用。

    查询多张表

    一些查询可能要求查询多张表来计算结果。Room允许你写任何查询,因此你还可以连接表。此外,如果响应是一个可观察的数据类型,比如FlowableLiveDataRoom会监视查询中引用的所有无效的表。(Furthermore, if the response is an observable data type, such as Flowable or LiveData, Room watches all tables referenced in the query for invalidation)

    以下代码片段显示了如何执行表连接,以整合包含借书用户的表和包含目前借出的书信息的表之间的信息。

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM book "
               + "INNER JOIN loan ON loan.book_id = book.id "
               + "INNER JOIN user ON user.id = loan.user_id "
               + "WHERE user.name LIKE :userName")
       public List<Book> findBooksBorrowedByNameSync(String userName);
    }
    

    你也可以从这些查询中返回POJO。比如,你可以写一条加载用户和他们的宠物名字的查询,如下:

    @Dao
    public interface MyDao {
       @Query("SELECT user.name AS userName, pet.name AS petName "
              + "FROM user, pet "
              + "WHERE user.id = pet.user_id")
       public LiveData<List<UserPet>> loadUserAndPetNames();
    
       // You can also define this class in a separate file, as long as you add the
       // "public" access modifier.
       static class UserPet {
           public String userName;
           public String petName;
       }
    }
    

    使用类型转换器

    Room提供对于基本类型和其包装类的内置支持。然后,你有时候使用打算以单一列存放到数据库中的自定义数据类型。为了添加对于这种自定义类型的支持,你可以提供一个TypeConverter,它将负责处理自定义类和Romm可以保存的已知类型之间的转换。

    比如,如果我们想要保存Date实例,我们可以写下面的TypeConverter来将等价的Unix时间戳存放到数据库中:

    public class Converters {
        @TypeConverter
        public static Date fromTimestamp(Long value) {
            return value == null ? null : new Date(value);
        }
    
        @TypeConverter
        public static Long dateToTimestamp(Date date) {
            return date == null ? null : date.getTime();
        }
    }
    

    上述实例定义了两个函数,一个将Date对象转换成Long对象,另一个则执行从LongDate的逆向转换。由于Room已经知道了如何持久化Long对象,因此它可以使用这个转换器来持久化保存Date类型的值。

    接下来,你将@TypeConverters注解添加到AppDatabase类,以便Room可以使用你在AppDatabase中为每个实体和DAO定义的转换器。

    AppDatabase.java

    @Database(entities = {User.java}, version = 1)
    @TypeConverters({Converter.class})
    public abstract class AppDatabase extends RoomDatabase {
        public abstract UserDao userDao();
    }
    

    使用这些转换器,你之后就可以在其他查询中使用你的自定义类型,就像使用基本类型一样,如以下代码所示:

    User.java

    @Entity
    public class User {
        ...
        private Date birthday;
    }
    

    UserDao.java

    @Dao
    public interface UserDao {
        ...
        @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
        List<User> findUsersBornBetweenDates(Date from, Date to);
    }
    

    你还可以限制@TypeConverters到不同的作用域,包括单独的实体,DAODAO方法。更多信息,参见@TypeConverters的引用文档。

    数据库迁移

    当你添加和更改App功能时,你需要修改实体类来反映这些更改。当用户更新到你的应用最新版本时,你不想要他们丢失所有存在的数据,尤其是你无法从远端服务器恢复数据时。

    Room允许你编写Migration类来保留用户数据。每个Migration类指明一个startVersionendVersion。在运行时,Room运行每个Migration类的migrate()方法,使用正确的顺序来迁移数据库到最新版本。

    警告:如果你没有提供需要的迁移类,Room将会重建数据库,也就意味着你会丢掉数据库中的所有数据。

    Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
            .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
    
    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
                    + "`name` TEXT, PRIMARY KEY(`id`))");
        }
    };
    
    static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE Book "
                    + " ADD COLUMN pub_year INTEGER");
        }
    };
    

    警告:为了使迁移逻辑正常运行,请使用完整查询,而不是引用代表查询的常量。

    在迁移过程完成后,Room会验证模式以确保迁移正确。如果Room发现问题,将还会抛出包含不匹配信息的异常。

    测试迁移

    迁移并不是简单的写入,并且一旦无法正确写入,可能导致应用程序循环崩溃。为了保持应用程序的稳定性,你应该事先测试迁移。Room提供了一个测试Maven组件来辅助测试过程。然而,要使这个组件工作,你需要导出数据库的模式。

    导出数据库模式

    汇编后,Room将你的数据库模式信息导出到一个JSON文件中。为了导出模式,在build.gradle文件中设置room.schemaLocation注解处理器属性,如下所示:

    build.gradle

    android {
        ...
        defaultConfig {
            ...
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = ["room.schemaLocation":
                                 "$projectDir/schemas".toString()]
                }
            }
        }
    }
    

    你可以将导出的JSON文件(代表了你的数据库模式历史)保存到你的版本控制系统中,因为它可以让Room创建旧版本的数据库以进行测试。

    为了测试这些迁移,添加Roomandroid.arch.persistence.room:testing组件到测试依赖,然后添加模式位置作为一个asset文件夹,如下所示:

    build.gradle

    android {
        ...
        sourceSets {
            androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
        }
    }
    

    测试包提供一个MigrationTestHelper类,该类可以读取这些模式文件。它也是一个JUnit4TestRule类,因此它可以管理创建的数据库。

    迁移测试示例如下所示:

    @RunWith(AndroidJUnit4.class)
    public class MigrationTest {
        private static final String TEST_DB = "migration-test";
    
        @Rule
        public MigrationTestHelper helper;
    
        public MigrationTest() {
            helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                    MigrationDb.class.getCanonicalName(),
                    new FrameworkSQLiteOpenHelperFactory());
        }
    
        @Test
        public void migrate1To2() throws IOException {
            SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
    
            // db has schema version 1. insert some data using SQL queries.
            // You cannot use DAO classes because they expect the latest schema.
            db.execSQL(...);
    
            // Prepare for the next version.
            db.close();
    
            // Re-open the database with version 2 and provide
            // MIGRATION_1_2 as the migration process.
            db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
    
            // MigrationTestHelper automatically verifies the schema changes,
            // but you need to validate that the data was migrated properly.
        }
    }
    

    测试数据库

    当应用程序运行测试时,如果你没有测试数据库本身,则不需要创建一个完整的数据库。Room可以让你在测试过程中轻松模拟数据访问层。这个过程是可能的,因为你的DAO不会泄漏任何数据库的细节。当测试应用的其余部分时,你应该创建DAO类的模拟或假的实例。

    有两种方式测试数据库:

    • 在你的宿主开发机上
    • 在一台Android设备上

    在宿主机上测试

    Room使用SQLite支持库,它提供了与Android Framework类相匹配的接口。该支持允许你传递自定义的支持库实现来测试数据库查询。

    即使这些设置能让你的测试运行非常快,也不推荐。因为运行在你的设备上的SQLite版本以及用户设备上的,可能和你宿主机上的版本并不匹配。

    在Android设备上测试

    推荐的测试数据库实现的方法是编写运行在Android设备上的JUnit测试。因为这些测试并不需要创建activity,它们相比UI测试应该是更快执行。

    设置测试时,你应该创建数据库的内存版本以使测试更加密封,如以下示例所示:

    @RunWith(AndroidJUnit4.class)
    public class SimpleEntityReadWriteTest {
        private UserDao mUserDao;
        private TestDatabase mDb;
    
        @Before
        public void createDb() {
            Context context = InstrumentationRegistry.getTargetContext();
            mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
            mUserDao = mDb.getUserDao();
        }
    
        @After
        public void closeDb() throws IOException {
            mDb.close();
        }
    
        @Test
        public void writeUserAndReadInList() throws Exception {
            User user = TestUtil.createUser(3);
            user.setName("george");
            mUserDao.insert(user);
            List<User> byName = mUserDao.findUsersByName("george");
            assertThat(byName.get(0), equalTo(user));
        }
    }
    

    更多信息关于测试数据库迁移,参见测试迁移

    附录:实体间无对象引用

    将数据库的关系映射到相应的对象模型是一种常见的做法,在服务端可以很好地运行。在服务端当访问时,使用高性能的延迟加载字段。

    然而,在客户端,延迟加载是不可行的,因为它可能发生在UI线程上,并且在UI线程上查询磁盘信息会产生显著的性能问题。UI线程有大约16ms的时间来计算以及绘制activity的更新布局,因此即使一个查询仅仅耗费5ms,仍然有可能你的应用会没有时间绘制帧,引发可见的卡顿。更糟糕的是,如果并行运行一个单独的事务,或者设备忙于其他磁盘重任务,则查询可能需要更多时间完成。但是,如果你不使用延迟加载,应用获取比其需要的更多数据,从而造成内存消耗问题。

    ORM通常将此决定留给开发人员,以便他们可以基于应用的使用场景来做最好的事情。不幸的是,开发人员通常最终在他们的应用和UI之间共享模型,随着UI随着时间的推移而变化,难以预料和调试的问题出现。

    举个例子,使用加载Book对象列表的UI,每个Book对象都有一个Author对象。你可能最初设计你的查询使用延迟加载,这样的话Book实例使用getAuthor()方法来返回作者。第一次调用getAuthor()会调用数据库查询。一段时间后,你会意识到你需要在应用UI上显示作者名字,你可以轻松添加方法调用,如以下代码片段所示:

    authorNameTextView.setText(user.getAuthor().getName());
    

    然而,这个看起来无害的修改,会导致Author表在主线程被查询。

    如果你频繁的查询作者信息,如果你不再需要数据,后续将会很难更改数据的加载方式,比如你的应用UI不再需要展示有关特定作者的信息的情况。因此,你的应用必须继续加载并不需要显示的数据。如果作者类引用另一个表,例如使用getBooks()方法,这种情况会更糟。

    由于这些原因,Room禁止实体类之间的对象引用。相反,你必须显式请求你的应用程序需要的数据。

    相关文章

      网友评论

        本文标题:【译】Google官方推出的Android架构组件系列文章(六)

        本文链接:https://www.haomeiwen.com/subject/fogwkxtx.html