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

系列文章导航

  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禁止实体类之间的对象引用。相反,你必须显式请求你的应用程序需要的数据。

推荐阅读更多精彩内容