系列文章导航:
- 【译】Google官方推出的Android架构组件系列文章(一)App架构指南
- 【译】Google官方推出的Android架构组件系列文章(二)将Architecture Components引入工程
- 【译】Google官方推出的Android架构组件系列文章(三)处理生命周期
- 【译】Google官方推出的Android架构组件系列文章(四)LiveData
- 【译】Google官方推出的Android架构组件系列文章(五)ViewModel
- 【译】Google官方推出的Android架构组件系列文章(六)Room持久化库
原文地址:https://developer.android.com/topic/libraries/architecture/room.html
Room
在SQLite之上提供了一个抽象层,可以在使用SQLite的全部功能的同时流畅访问数据库。
注意:将
Room
导入工程,请参考将Architecture Components引入工程
需要处理大量结构化数据的应用能从本地持久化数据中受益匪浅。最常见的使用场景是缓存相关的数据。比如,当设备无法访问网络时,用户仍然可以在离线时浏览内容。当设备重新联网后,任何用户发起的内容更改将同步到服务器。
核心框架提供了操作原始SQL内容的内置支持。尽管这些API很强大,但它们相对较低层,需要大量的时间和精力才能使用:
- 没有对原始SQL查询语句的编译时验证。 当你的数据图变化时,你需要手动更新受影响的SQL查询语句。这个过程可能很耗时,而且容易出错。
- 你需要使用大量模板代码来进行SQL语句和Java数据对象的转换。
Room
在SQLite
之上提供一个抽象层,来帮助你处理这些问题。
Room
包含三大组件:
-
Database:利用这个组件来创建一个数据库持有者。注解定义一系列实体,类的内容定义一系列DAO。它也是底层连接的主入口点。
注解类应该是继承RoomDatabase的抽象类。在运行期间,你可以通过调用
Room.databaseBuilder()
或Room.inMemoryDatabaseBuilder()
方法获取其实例。 Entity:这个组件表示持有数据库行的类。对于每个实体,将会创建一个数据库表来持有他们。你必须通过Database类的entities数组来引用实体类。实体类的中的每个字段除了添加有@Ignore注解外的,都会存放到数据库中。
注意:Entity可以有一个空的构造函数(如果DAO类可以访问每个持久化字段),或者一个构造函数其参数包含与实体类中的字段匹配的类型和名字。
Room
还可以使用全部或部分构造函数,比如只接收部分字段的构造函数。
-
DAO: 该组件表示作为数据访问对象(
DAO
)的类或接口。DAO
是Room
的主要组件,负责定义访问数据库的方法。由@Database
注解标注的类必须包含一个无参数且返回使用@Dao
注解的类的抽象方法。当在编译生成代码时,Room
创建该类的实现。
注意:通过使用DAO类代替查询构建器或者直接查询来访问数据库,你可以分离数据库架构的不同组件。此外,DAO允许你在测试应用时轻松地模拟数据库访问。
这些组件,以及与应用程序其他部分的关系,如图所示:
以下代码片段包含一个数据库配置样例,其包含一个实体和一个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
,或为它提供getter
和setter
。如果你使用setter
和getter
,请记住,它们基于Room
的Java 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
来强制满足唯一属性。下面代码样例阻止表含有对于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;
}
关系
因为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)
作为一组REMOVE
和REPLACE
操作,而不是单个UPDATE
操作。这个替换冲突值的方法将会影响到你的外键约束。更多详细信息,请参见SQLite文档的ON_CONFLICT
语句。
嵌套对象
有时,你希望将一个实体或POJO表达作为数据库逻辑中的一个整体,即使对象包含了多个字段。在这种情况下,你可以使用@Embeded
注解来表示要在表中分为为子字段的对象。然后,你可以像其他单独的列一样查询嵌入的字段。
例如,我们的User
类可以包含一个类型为Address
的字段,其表示了一个字段组合,包含street
、city
、state
和postCode
。为了将这些组合列单独的存放到表中,将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
对象的表将包含以下名字的列:id
,firstName
,street
,state
,city
和post_code
注意:嵌入字段也可以包含其他潜入字段。
如果实体包含了多个同一类型的嵌入字段,你可以通过设置prefix
属性来保持每列的唯一性。Room
然后将提供的值添加到嵌入对象的每个列名的开头。
数据访问对象(DAO)
Room
的主要组件是Dao
类。DAO
以简洁的方式抽象了对于数据库的访问。
Dao
要么是一个接口,要么是一个抽象类。如果它是抽象类,它可以有一个使用RoomDatabase
作为唯一参数的可选构造函数。
注意:
Room
不允许在主线程中访问数据库,除非你可以builder上调用allowMainThreadQueries(),因为它可能会长时间锁住UI。异步查询(返回LiveData
或RxJava 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
的方法
@Query是DAO
类中使用的主要注解。可以让你执行数据库读/写操作。每个@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 name
和last 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_name
和last_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
还能从你定义的查询中返回RxJava2
的Publisher
和Flowable
对象。要使用此功能,请将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
允许你写任何查询,因此你还可以连接表。此外,如果响应是一个可观察的数据类型,比如Flowable
或LiveData
,Room
会监视查询中引用的所有无效的表。(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
对象,另一个则执行从Long
到Date
的逆向转换。由于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
到不同的作用域,包括单独的实体,DAO
和DAO
方法。更多信息,参见@TypeConverters的引用文档。
数据库迁移
当你添加和更改App功能时,你需要修改实体类来反映这些更改。当用户更新到你的应用最新版本时,你不想要他们丢失所有存在的数据,尤其是你无法从远端服务器恢复数据时。
Room
允许你编写Migration类来保留用户数据。每个Migration
类指明一个startVersion
和endVersion
。在运行时,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
创建旧版本的数据库以进行测试。
为了测试这些迁移,添加Room
的android.arch.persistence.room:testing
组件到测试依赖,然后添加模式位置作为一个asset文件夹,如下所示:
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.
}
}
测试数据库
当应用程序运行测试时,如果你没有测试数据库本身,则不需要创建一个完整的数据库。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
禁止实体类之间的对象引用。相反,你必须显式请求你的应用程序需要的数据。