使用Room持久库保存数据

Room持久性库在SQLite的基础上提供了一个抽象层,允许流利的数据库访问,同时利用的SQLite的全部力量。
该库可帮助在运行应用程序的设备上创建应用程序数据缓存。这个缓存作为您的应用程序的单一来源,允许用户在应用程序中查看关键信息的一致副本,而不管用户是否具有互联网连接。
要将Room导入Android项目,请参阅添加组件
有关将Room功能应用于应用程序的数据存储持久性解决方案的指南请参阅 Room培训指南

理解Room的数据迁移
示例应用程序

一.简介

处理巨大数量的结构化数据的应用程序可以从本地保存数据中获益。最常见的用例是缓存相关的数据。这样,当设备无法访问网络时,用户仍然可以在脱机状态下浏览该内容。设备重新联机后,任何用户启动的内容更改都会同步到服务器。

由于Room会为您处理这些问题,所以强烈建议您使用Room而不是SQLite。但是,如果您更喜欢使用SQLite API来处理应用程序的数据库,Android仍然支持使用SQLite直接访问数据库。
Room有三个主要部分:
Database数据库:包含数据库持有者,并作为与应用持久关系数据的底层连接的主要接入点。@Database注解的类应该满足以下条件:

Entity实体:表示数据库中的一个表。
DAO::包含用于访问数据库的方法。

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

该应用程序使用Room数据库来获取与该数据库相关联的数据访问对象或DAO。 然后,应用程序使用每个DAO从数据库中获取实体,并将对这些实体的任何更改保存回数据库。 最后,应用程序使用一个实体来获取和设置与数据库中的表列对应的值。

图1.Room架构图

以下代码片段包含具有1个Entity实体和1个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 实例都相当昂贵,而且很少需要访问多个实例。

二、具体使用

1.使用Room entities定义数据

使用 Room持久性库时,可以将相关字段集合定义为实体。对于每个实体,在关联的Database对象内创建一个表来保存这些项目。
默认情况下,Room会为实体中定义的每个字段创建一个列。如果实体具有不想保留的字段,可以使用@Ignore注释。您必须通过entities数组在Database类中的引用实体类。
以下代码片段显示了如何定义一个实体:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

去访问一个字段,Room必须有权限访问它。你可以公开一个字段,或者你可以为它提供一个getter和setter。如果您使用getter和setter方法,请记住它们基于Room中的JavaBeans约定。

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

1.1使用主键

每个实体必须至少定义一个字段作为主键。<即使只有1个字段,仍然需要使用注释对字段进行 @PrimaryKey 注释。另外,如果你想室自动分配ID的实体,您可以设置@PrimaryKeyautoGenerate 属性。如果实体具有复合主键,则可以使用@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;
}

1.2注释索引和唯一性

根据您访问数据的方式,您可能需要对数据库中的某些字段进行索引以加快查询速度。要将索引添加到实体,请在@Entity注释中包含indices属性,列出要包含在索引或组合索引中的列的名称。以下代码片段演示了这个注释过程:

@Entity(indices = {@Index("name"),
        @Index(value = {"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来确保唯一性。以下代码示例可防止表中有两列包含与firstName lastName列相同的一组值

@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;
}

1.3定义对象之间的关系

因为SQLite是一个关系数据库,所以你可以指定对象之间的关系。尽管大多数对象关系映射库允许实体对象相互引用,但Room明确禁止这样做。要了解此决定背后的技术推理,请参阅了解Room不允许使用对象引用的原因
即使您不能使用直接关系,Room仍允许您定义实体之间的外键约束。
例如,如果有另一个实体被调用,您可以使用@ForeignKey注释来定义它与实体的关系,如以下代码片段所示:

@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;
}

外键非常强大,因为它们允许您指定在引用实体更新时做什么。比如,您可以告诉SQLite删除用户所有的书,如果相应的user实例通过在 @ForeignKey注释中包含 onDelete = CASCADE 进行删除。

注:SQLite在处理 @Insert(onConflict = REPLACE) 是做一组REMOVEREPLACE操作,而不是一个单一的UPDATE 操作。这种替换冲突值的方法可能会影响您的外键约束。有关更多详细信息,请参阅该SQLite文档)的ON_CONFLICT条款。


1.4创建嵌套的对象

有时,即使对象包含多个字段,您也希望在数据库逻辑中将实体或普通Java对象(PO​​JO)表示为一个有凝聚力的整体。在这些情况下,可以使用@Embedded 注释来表示要分解到表中的子字段的对象。然后,您可以像查看其他单个列一样查询嵌入的字段。
例如,我们的User类可以包含Address类型的字段,它代表一系列字段streetcitystatepostCode的组合。要在表中单独的存储组合列,请在User类中包含一个注释为 @EmbeddedAddress字段,如以下代码片段所示:

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;
}

该表表示一个包含列名为:id firstName street state citypost_code的列的user对象

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

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


2.使用Room DAOs访问数据

要使用Room持久性库访问应用程序的数据,您需要使用数据访问对象或DAOs。这一系列的 Dao对象构成了Room的主要组件,因为每个DAO都包含提供对应用程序数据库的抽象访问的方法。通过使用DAO类而不是查询构建器或直接查询访问数据库,可以分离数据库体系结构的不同组件。此外,DAO允许您在测试应用程序时轻松模拟数据库访问

一个DAO可以是一个接口或一个抽象类。如果它是一个抽象类,它可以有一个只有RoomDatabase作为参数的构造函数。Room在编译时创建每个DAO实现

注意:除非已在构建器上调用allowMainThreadQueries(),否则会Room不支持主线程上的数据库访问,因为它可能会长时间锁定UI。异步查询以及返回 LiveData Flowable实例的查询或免于此规则,因为它们在需要时异步地在后台线程上运行查询。


  • 2.1定义方法以方便使用

您可以使用DAO类来表示多个便利查询。这个文件包括几个常见的例子。

2.1.1.插

当您创建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 方法只接收到1个参数,它可以返回一个long,这是插入的项目的新rowId。如果参数是一个数组或一个集合,它应该返回long[]List<Long>代替。有关更多详细信息,请参阅@Insert注释的参考文档以及 SQLite documentation for rowid tables

2.1.2更新

通过给定的参数Update方法方便从数据库中修改一组实体。它使用与每个实体的主键相匹配的查询。下面的代码片段演示了如何定义这个方法:

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

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

2.1.3删除

通过给定参数Delete方法很方便从数据库中删除一组实体。它使用主键来查找要删除的实体。下面的代码片段演示了如何定义这个方法:

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

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


  • 2.2查询信息

@Query是DAO类中使用的主要注释。它允许您在数据库上执行读取/写入操作。每种 @Query方法都在编译时进行验证,所以如果查询出现问题,则会发生编译错误而不是运行时失败。Room还验证查询的返回值,以便如果返回对象中的字段名称与查询响应中相应的列名称不匹配,Room会以下列两种方式之一提醒您:

  • 如果只有一些字段名称匹配,它会发出警告。
  • 如果没有字段名称匹配,则会出现错误。
2.2.1简单的查询
@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}

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

2.2.2将参数传递给查询

大多数时候,您需要将参数传递到查询中以执行过滤操作,例如只显示比特定年龄更早的用户。要完成此任务,请使用Room注释中的方法参数,如以下代码片段所示:

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

在编译时处理此查询时,Room将bind参数与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);
}
2.2.3返回列的子集

大多数情况下,你只需要得到一个实体的几个字段。例如,您的用户界面可能只显示用户的名字和姓氏,而不是每个用户的详细信息。通过只提取出现在应用的用户界面中的列,您可以节省宝贵的资源,并且查询更快完成。
Room允许您从查询中返回任何基于Java的对象,只要结果列的集合可以映射到返回的对象中即可。例如,您可以创建以下普通的基于Java的旧对象(PO​​JO)来获取用户的名字和姓氏:

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列的值,这些值可以映射到类的字段。因此,Room可以生成正确的代码。如果查询返回太多的列或类中不存在的列,则Room会显示警告。

注意:这些POJO也可以使用 @Embedded >注释。

2.2.4传递一组参数

有些查询可能需要传递可变数量的参数,直到运行时才能知道参数的确切数量。例如,您可能希望从一部分地区中检索有关所有用户的信息。当一个参数表示一个集合时,Room会理解,并根据提供的参数数量在运行时自动扩展它。

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

在执行查询时,您经常希望应用程序的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

2.2.6使用RxJava进行响应式查询

Room也可以从你定义的查询中返回RxJava2 PublisherFlowabl对象。要使用此功能,请将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);
}

有关更多详细信息,请参阅Google DevelopersRoom和RxJava文章

2.2.7直接游标访问

如果您的应用程序的逻辑需要直接访问返回行,则可以从查询中返回Cursor对象,如以下代码片段所示:

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

警告:非常不鼓励使用Cursor API,因为它不能保证行是否存在或者行包含的值。只有当您已经拥有需要游标的代码并且无法轻松重构时才使用此功能。

2.2.8查询多个表

有些查询可能需要访问多个表来计算结果。Room允许你写任何查询,你也可以加入表。此外,如果返回值是可观察的数据类型(如FlowableLiveData),Room会查看查询中引用的所有表以使失效。
以下代码片段显示如何执行表连接来合并包含借用图书的用户的表和包含当前正在借阅的图书的数据的表之间的信息:

@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;
   }
}


3. 迁移Room数据库

在您的应用中添加和更改功能时,您需要修改实体类以反映这些更改。当用户更新到最新版本的应用程序时,您不希望它们丢失所有的现有数据,特别是当您无法从远程服务器恢复数据时。
Room持久性库允许你编写 Migration 的类以这种方式保存用户数据。每个 Migration 类指定一个startVersionendVersion。在运行时,Room运行每个Migration 类的migrate() 方法,使用正确的顺序将数据库迁移到更高版本。

警告:如果您不提供必要的迁移,则会重建数据库,这意味着您将丢失数据库中的所有数据。

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");
    }
};

警告:要保持迁移逻辑按预期运行,请使用完整查询,而不是引用代表查询的常量。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">迁移过程完成后,Room会验证模式以确保正确迁移。</font><font style="vertical-align: inherit;">如果Room找到问题,则会抛出包含不匹配信息的异常。</font></font>

3.1 测试迁移

迁移不是微不足道的,写入失败可能会导致您的应用程序崩溃循环。为了保持您的应用程序的稳定性,您应该事先测试您的迁移。Room提供了一个testingMaven工件来协助这个测试过程。但是,要使这个工件正常工作,您需要导出数据库的模式。

  • 3.1.1导出模式

编译前,Room将数据库的模式信息导出到JSON文件中。要导出模式,在build.gradle文件中设置room.schemaLocation注释处理属性,如下面的代码片段所示:

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

您应该将导出的JSON文件(代表数据库的模式历史记录)存储在版本控制系统中,因为它允许Room为测试目的创建较旧版本的数据库。
要测试这些迁移,请将Room中的 android.arch.persistence.room:testingMaven工件添加到您的测试依赖项中,并将该模式​​位置作为资源文件夹添加,如以下代码片段所示:
build.gradle

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

测试包提供了一个MigrationTestHelper 类,它可以读取这些模式文件。它也实现了JUnit4 TestRule接口,所以它可以管理创建的数据库。
示例迁移测试出现在以下代码片段中:

@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.
    }
}


4.测试Room数据库

使用Room持久性库创建数据库时,验证应用数据库和用户数据的稳定性非常重要
有两种方法来测试你的数据库:

  • 在Android设备上。
  • 在您的主机开发机器上(不推荐)。

有关特定于数据库迁移的测试的信息,请参阅 测试迁移

注意:在为您的应用程序运行测试时,Room允许您创建DAO类的模拟实例。这样,如果您不测试数据库本身,则不需要创建完整的数据库。这个功能是可能的,因为你的DAO不会泄露你的数据库的任何细节。


4.1在Android设备上测试

测试数据库实现的推荐方法是编写在Android设备上运行的JUnit测试。因为这些测试不需要创建一个活动,它们应该比你的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));
    }
}

4.2 在主机上测试

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

注意:即使此设置允许您的测试运行速度非常快,也不建议这样做,因为设备上运行的SQLite版本以及您的用户设备可能与主机上的版本不匹配。



5.使用Room引用复杂数据

Room提供了原始类型和盒装类型之间转换的功能,但不允许实体之间的对象引用。本文档解释了如何使用类型转换器,以及为什么Room不支持对象引用。


5.1 使用类型转换器

有时,您的应用程序需要使用您想要存储在单个数据库列中的自定义数据类型。要为自定义类型添加这种支持,您需要提供一个 TypeConverter将自定义类转换为Room可以保留的已知类型的类。
例如,如果我们想要保存实例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

接下来,为了Room可以使用您在AppDatabaseentity
DAO中定义的转换器 ,您需要添加 @TypeConverters 注释到AppDatabase类:
AppDatabase.java

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.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 不同的范围,包括单个实体,DAO和DAO方法。有关更多详细信息,请参阅@TypeConverters 注释的参考文档


5.2了解房间不允许使用对象引用的原因

关键要点:Room不允许实体类之间的对象引用。相反,您必须明确地请求您的应用程序需要的数据。

将数据库中的关系映射到相应的对象模型是一种常见的做法,在服务器端运行良好。即使程序在访问时加载字段,服务器仍然运行良好。
但是,在客户端,这种延迟加载是不可行的,因为它通常发生在UI线程上,并且在UI线程中查询磁盘上的信息会造成显着的性能问题。UI线程通常有大约16毫秒的时间来计算和绘制一个活动的更新布局,所以即使查询只需要5毫秒,仍然可能是您的应用程序将耗尽时间画框,造成明显的视觉故障。如果有单独的事务并行运行,或者设备正在运行其他磁盘密集型任务,则查询可能需要更多时间才能完成。但是,如果您不使用延迟加载,则应用程序将获取比所需更多的数据,从而导致内存消耗问题。对象关系映射通常将这个决定留给开发人员,以便他们可以做任何最适合他们的应用程序的用例。开发人员通常决定在应用程序和用户界面之间共享模型。但是,这种解决方案并不能很好地扩展,因为随着UI的变化,共享模型会产生难以让开发人员预测和调试的问题。
例如,考虑加载Book对象列表的UI,每本书都有一个Author对象。您最初可能会设计查询以使用延迟加载,以便Book使用getAuthor()方法返回作者的实例。调用的第一个调用将getAuthor()查询数据库。一段时间后,你意识到你也需要在应用的用户界面中显示作者的名字。您可以轻松地添加方法调用,如下面的代码片段所示:

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

但是,这个看似无辜的变化使得表格在主线上被查询。
如果提前查询作者信息,如果不再需要这些数据,则很难更改数据的加载方式。例如,如果您的应用程序的用户界面不再需要显示Author信息,则您的应用程序将有效地加载不再显示的数据,浪费宝贵的内存空间。如果Author类引用另一个表(例如,Books)则应用程序的效率会进一步降低。
要使用Room同时引用多个实体,您需要创建一个包含每个实体的POJO,然后编写一个连接相应表的查询。这种结构良好的模型与Room健壮的查询验证功能相结合,可让您的应用程序在加载数据时消耗更少的资源,从而改善应用程序的性能和用户体验。

推荐阅读更多精彩内容