官方文档翻译

  • 简介
  • 导入库
  • 使用Room保存本地数据到数据库中
  • 使用Room实体定义数据
    • 使用主键
    • 注解声明与唯一性
    • 定义对象之间的关系
    • 创建嵌套对象
    • 使用Room DAOs访问数据
    • 定义查询方法
      • 插入
      • 更新
      • 删除
      • 信息查询
        • 简单查询
        • 将参数传递到查询中
        • 返回列的子集
        • 传递参数集合
        • 可观察的查询
        • RXJava的响应式查询
        • 直接Cursor访问
        • 多表查询
  • Room数据库迁移
    • 测试迁移
    • 导出schemas
  • 测试数据库
    • Android设备测试
    • 主机测试
  • 使用Room引用复杂数据
    • 使用类型转换器
    • 理解为什么Room不允许对象引用

简介

Room持久库提供了一个SQLite抽象层,让你访问数据库更加稳健,提升数据库性能。

该库帮助您在运行应用程序的设备上创建应用程序的数据缓存。这个缓存是你的应用程序唯一的真实来源,允许用户查看应用程序中关键信息的一致副本,而不管用户是否有Internet连接。

导入库

dependencies {def room_version = "1.1.1"implementation "android.arch.persistence.room:runtime:$room_version"annotationProcessor "android.arch.persistence.room:compiler:$room_version" // use kapt for Kotlin// optional - RxJava support for Roomimplementation "android.arch.persistence.room:rxjava2:$room_version"// optional - Guava support for Room, including Optional and ListenableFutureimplementation "android.arch.persistence.room:guava:$room_version"// Test helperstestImplementation "android.arch.persistence.room:testing:$room_version"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

AndroidX

dependencies {def room_version = "2.0.0-beta01"implementation "androidx.room:room-runtime:$room_version"annotationProcessor "androidx.room:room-compiler:$room_version" // use kapt for Kotlin// optional - RxJava support for Roomimplementation "androidx.room:room-rxjava2:$room_version"// optional - Guava support for Room, including Optional and ListenableFutureimplementation "androidx.room:room-guava:$room_version"// Test helperstestImplementation "androidx.room:room-testing:$room_version"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

使用Room保存本地数据到数据库中

Room在SQLite上提供了一个抽象层,以便在发挥SQLite能力的同时允许流畅的数据库访问。

处理复杂的结构化数据的应用程序可以极大地受益于本地数据的持久化。最常见的用例是缓存相关的数据片段。这样,当设备无法访问网络时,用户仍然可以在离线时浏览该内容。在设备返回联机之后,任何用户发起的内容更改都会同步到服务器。

由于Room负责这些问题,我们强烈建议使用Room而不是SQLite。但是,如果您更习惯于直接使用SQLite API,请使用SQLite读取保存数据。

Room有3个主要的组件:

Database:包含数据库持有者,并充当与应用程序持久化的、关系型的数据的底层连接的主要访问点。

用@Database注解的类应满足以下条件:

  • 是一个继承RoomDatabase的抽象类。

  • 在注释中包含与数据库相关联的实体列表。

  • 包含一个具有0个参数的抽象方法,并返回用@Dao注释的类。

在运行时,您可以通过调用Room.databaseBuilder()或Room.inMemoryDatabaseBuilder()获取数据库实例。

Entity:表示数据库内的表。

DAO: 包含用于访问数据库的方法。

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

image

图1. Room架构图

下面的代码片段示例了包含一个entity和一个DAO的数据库配置:

User.java

@Entity
public class User {@PrimaryKeyprivate 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.
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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);@Insertvoid insertAll(User... users);@Deletevoid delete(User user);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

AppDatabase.java

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

在创建上面的文件之后,使用以下代码获得创建数据库的实例:

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

注意:在实例化AppDatabase对象时,应遵循单例设计模式,因为每个Roomdatabase实例都相当消耗性能,并且您很少需要访问多个实例。

为体验Room,尝试 Android Room with a View和Android持久性 codelabs。若要浏览房间代码示例,请参阅 Android Architecture Components samples。

使用Room实体定义数据

默认情况下,Room为实体中定义的每个字段创建一个列。如果实体有不想持久的字段,则可以使用@Ignore来注解它们。必须通过Database类中的entities数组引用实体类。

下面的代码片段显示了如何定义实体:

@Entity
public class User {@PrimaryKeypublic int id;public String firstName;public String lastName;@IgnoreBitmap picture;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

要持久化一个字段,Room必须有机会进入它。你可以把一个字段公开,或者你可以为它提供一个getter和setter方法。如果使用getter和setter方法,请记住它们是基于Room中的JavaBeans约定。

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

使用主键

每个实体必须定义至少1个字段作为主键。即使只有1个字段,仍然需要用@PrimaryKey注解字段。此外,如果您想Room自动分配IDs给实体,则可以设置@ PrimaryKey的autoGenerate属性。如果实体具有复合主键,则可以使用@Entity注解的primaryKeys属性,如下面的代码片段所示:

@Entity(primaryKeys = {"firstName", "lastName"})
public class User {public String firstName;public String lastName;@IgnoreBitmap picture;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

By default, Room uses the class name as the database table name. If you want the table to have a different name, set the tableName property of the @Entity annotation, as shown in the following code snippet:
默认情况下,Room使用类名作为数据库表名。如果希望表具有不同的名称,请设置@Entity注解的tableName属性,如下面的代码片段所示:

@Entity(tableName = "users")
public class User {...
}
  • 1
  • 2
  • 3
  • 4

注意:SQLite中的表名不区分大小写。

与tableName属性类似,Room使用字段名称作为数据库中的列名。如果希望列具有不同的名称,请将@ColumnInfo注解添加到字段中,如下面的代码片段所示:

@Entity(tableName = "users")
public class User {@PrimaryKeypublic int id;@ColumnInfo(name = "first_name")public String firstName;@ColumnInfo(name = "last_name")public String lastName;@IgnoreBitmap picture;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

注解声明与唯一性

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

@Entity(indices = {@Index("name"),@Index(value = {"last_name", "address"})})
public class User {@PrimaryKeypublic int id;public String firstName;public String address;@ColumnInfo(name = "last_name")public String lastName;@IgnoreBitmap picture;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

有时,数据库中的某些字段或字段组必须是唯一的。可以通过将@Index注解的唯一属性设置为true来强制执行此唯一性属性。下面的代码示例防止表中包含两个行,它们包含firstName和lastName列的相同值集:

@Entity(indices = {@Index(value = {"first_name", "last_name"},unique = true)})
public class User {@PrimaryKeypublic int id;@ColumnInfo(name = "first_name")public String firstName;@ColumnInfo(name = "last_name")public String lastName;@IgnoreBitmap picture;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

定义对象之间的关系

因为SQLite是关系数据库,所以可以指定对象之间的关系。尽管大多数对象关系映射库允许实体对象相互引用,但Room明确禁止这一点。要了解这一决定背后的技术原理,请了解为什么Room不允许对象引用。

即使您不能使用直接关系,Room仍然允许您定义实体之间的外键约束。

例如,如果有另一个实体称为Book,则可以使用@ForeignKey 注解定义其与用户实体的关系,如下面的代码片段所示:

@Entity(foreignKeys = @ForeignKey(entity = User.class,parentColumns = "id",childColumns = "user_id"))
public class Book {@PrimaryKeypublic int bookId;public String title;@ColumnInfo(name = "user_id")public int userId;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

外键非常强大,因为它们允许您指定被引用实体更新时发生的事情。例如,如果在@ForeignKey注解中包含onDelete = CASCADE,则可以告诉SQLite删除用户的所有图书,如果用户的相应实例被删除。

注意:SQLite处理@Insert( onconflict=REPLACE)作为一套REMOVE和REPLACE操作而不是单一的更新操作。本替换冲突值的方法有可能影响您的外键约束。更多的细节,请看SQLite的文档ON_CONFLICT的条款。

创建嵌套对象

有时,即使对象包含多个字段,您也希望在数据库逻辑中将实体或普通Java对象(POJO)表示为一个有粘性的整体。在这些情况下,可以使用@Embedded注解来表示要分解成表中的子字段的对象。然后,可以像其他单个列一样查询嵌入式字段。

例如,我们的用户类可以包括一个类型地址字段,该字段表示名为“街道”、“城市”、“状态”和“邮政编码”的字段的组合。若要将组合列单独存储在表中,请在用户类中使用@Embedded注解的地址字段,如下面的代码片段所示:

public class Address {public String street;public String state;public String city;@ColumnInfo(name = "post_code")public int postCode;
}@Entity
public class User {@PrimaryKeypublic int id;public String firstName;@Embeddedpublic Address address;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

表示用户对象的表包含包含以下名称的列:id, firstName, street, state, city, and post_code。

注意:嵌入式字段还可以包括其他嵌入式字段。

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

使用Room DAOs访问数据

使用“Room persistence library”访问应用程序的数据,可以使用数据访问对象或DAOs。这组DAO对象构成了Room的主要组件,因为每个DAO都包含了对应用程序数据库访问的抽象方法。

通过使用DAO类访问数据库而不是查询构造器或直接查询,可以分离数据库体系结构的不同组件。此外,DAOs允许您在测试应用程序时轻松模拟数据库访问。

注意:在向应用程序添加DAO类之前,将架构组件库添加到应用程序的build.gradle中。

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

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

定义查询方法

有多种查询方法,可以使用DAO类来表示。这个文档包括几个常见的例子。

插入

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

下面的代码片段显示了几个示例查询:

@Dao
public interface MyDao {@Insert(onConflict = OnConflictStrategy.REPLACE)public void insertUsers(User... users);@Insertpublic void insertBothUsers(User user1, User user2);@Insertpublic void insertUsersAndFriends(User user, List<User> friends);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

如果@Insert方法只接收1个参数,则可以返回一个Long型的值,这是插入项的新rowId。如果参数是数组或集合,则应该返回long[] 或者 List类型的值。

有关详细信息,请参阅@Insert注解的参考文档,以及SQLite documentation for rowid tables。

更新

Update方法在数据库中用于修改一组实体的字段。它使用每个实体的主键来匹配查询。

下面的代码片段演示如何定义此方法:

@Dao
public interface MyDao {@Updatepublic void updateUsers(User... users);
}
  • 1
  • 2
  • 3
  • 4
  • 5

虽然通常不需要,但可以使此方法返回int型值,以指示数据库中更新的行数。

删除

删除

Delete方法用于从数据库中删除给定参数的一系列实体,它使用主键匹配数据库中相应的行。

下面的代码片段演示如何定义此方法:

@Dao
public interface MyDao {@Deletepublic void deleteUsers(User... users);
}
  • 1
  • 2
  • 3
  • 4
  • 5

虽然通常不需要,但可以使用此方法返回int值,以指示从数据库中删除的行数。

信息查询

@Query是DAO类中使用的主要注解。它允许您在数据库上执行读/写操作。每个@Query方法在编译时被验证,因此,如果存在查询问题,则会发生编译错误而不是运行时故障。

Room还验证查询的返回值,这样如果返回对象中字段的名称与查询响应中的相应列名不匹配,则Room将以以下两种方式之一提醒您:

  • 如果只有一些字段名匹配,则发出警告。

  • 如果没有字段名匹配,则会出错。

简单查询

@Dao
public interface MyDao {@Query("SELECT * FROM user")public User[] loadAllUsers();
}
  • 1
  • 2
  • 3
  • 4
  • 5

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

将参数传递到查询中

大多数情况下,需要将参数传递到查询中以执行筛选操作,例如只显示年龄大于某一年龄的用户。要完成此任务,请在您的Room注解中使用方法参数,如下面的代码片段所示:

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

当在编译时处理此查询时,Room绑定参数与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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

返回列的子集

大多数时候,你只需要得到一个实体的几个字段。例如,您的UI可能只显示用户的first name和last name,而不显示用户的每一个细节。通过只获取应用程序UI中出现的列,可以节省宝贵的资源,并且查询完成得更快。

只要可以将结果列映射到返回的对象中,就可以让您从查询中返回任何基于Java的对象。例如,您可以创建以下普通的基于Java的对象(POJO)来获取用户的first name和last name:

public class NameTuple {@ColumnInfo(name="first_name")public String firstName;@ColumnInfo(name="last_name")public String lastName;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

现在,您可以在查询方法中使用此POJO:

@Dao
public interface MyDao {@Query("SELECT first_name, last_name FROM user")public List<NameTuple> loadFullName();
}
  • 1
  • 2
  • 3
  • 4
  • 5

Room知道查询返回first_name和last_name列的值,并且这些值可以映射到NameTuple类的字段中。因此,Room可以生成适当的代码。如果查询返回太多列,或一列在NameTuple类中不存在,则Room将显示警告。

注意:这些POJOs也可以使用@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);
}
  • 1
  • 2
  • 3
  • 4
  • 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
  • 2
  • 3
  • 4
  • 5

注意:在版本1中,Room根据查询列表来决定是否更新LiveData的实例。

RXJava的响应式查询

Room还可以从您定义的查询中返回RXJava2 Publisher和Flowable动对象。若要使用此功能,请将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);
}
  • 1
  • 2
  • 3
  • 4
  • 5

有关详细信息,请参阅谷歌开发人员Room和RXJava文章。

直接Cursor访问

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

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

注意:使用Cursor API是非常令人沮丧的,因为它不能保证行是否存在或行包含哪些值。只有当你已经有了期望Cursor的代码,并且代码不容易重构时,才使用这个功能。

多表查询

有些查询可能需要访问多个表来计算结果。Room允许您编写任何查询,因此您也可以多表联合查询。此外,如果返回的是可观察的数据类型,例如Flowable或LiveData,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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

还可以从这些查询中返回POJOs。例如,您可以编写一个查询,加载用户和他们的宠物的名字如下:

@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;}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Room数据库迁移

在应用程序中添加和更改特性时,你需要修改实体类以反映这些更改。当用户更新应用程序到最新版本时,不希望它们丢失所有现有数据,尤其是如果无法从远程服务器恢复数据。

“Room persistence library“库允许您编写Migration类来保存用户数据。每个迁移类指定起始版本和终结版本。在运行时,Room运行每个迁移类的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) {@Overridepublic 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) {@Overridepublic void migrate(SupportSQLiteDatabase database) {database.execSQL("ALTER TABLE Book "+ " ADD COLUMN pub_year INTEGER");}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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

迁移过程结束后,Room验证schema以确保迁移正确地发生。如果Room发现问题,它会抛出包含不匹配信息的异常。

测试迁移

迁移不是微不足道的,无法正确写入,可能会导致应用程序崩溃。为了保持应用程序的稳定性,您应该事先测试迁移。Room提供了测试的Maven组件来帮助这个测试过程。但是,要使这个组件生效,您需要导出数据库的schema。

导出schemas

编译后,Room将数据库的schemas信息导出到JSON文件中。若要导出schema,请在build.gradle文件中设置room.schemaLocation注解处理器属性,如下面的代码片段所示:

build.gradle

android {...defaultConfig {...javaCompileOptions {annotationProcessorOptions {arguments = ["room.schemaLocation":"$projectDir/schemas".toString()]}}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

您应该在您的版本控制系统中存储表示数据库的schema历史的导出JSON文件,因为它允许Room创建用于测试目的的数据库的旧版本。

为了测试这些迁移,添加android.arch.persistence.room:testing 的Maven依赖,将并schema添加到asset文件夹,如下面的代码片段所示:

build.gradle

android {...sourceSets {androidTest.assets.srcDirs += files("$projectDir/schemas".toString())}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

测试包提供了MigrationTestHelper类,可以读取这些模式文件。它还实现了JUnit4 TestRule接口,因此它可以管理创建的数据库。

在以下代码段中出现一个示例迁移测试:

@RunWith(AndroidJUnit4.class)
public class MigrationTest {private static final String TEST_DB = "migration-test";@Rulepublic MigrationTestHelper helper;public MigrationTest() {helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),MigrationDb.class.getCanonicalName(),new FrameworkSQLiteOpenHelperFactory());}@Testpublic 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.}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

测试数据库

在使用Room库创建数据库时,验证应用程序的数据库和用户的数据的稳定性是很重要的。

测试数据库有2种方法:

  • 在Android设备上。

  • 在主机开发机器上(不推荐)。

有关数据库迁移的specific,请参见迁移测试。

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

Android设备测试

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

在设置测试时,应创建数据库的in-memory版本,以使测试更加封闭,如以下示例所示:

@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {private UserDao mUserDao;private TestDatabase mDb;@Beforepublic void createDb() {Context context = InstrumentationRegistry.getTargetContext();mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();mUserDao = mDb.getUserDao();}@Afterpublic void closeDb() throws IOException {mDb.close();}@Testpublic 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));}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

主机测试

Room 使用SQLite支持库,它提供与Android框架类中的那些接口匹配的接口。此支持允许您通过支持库的自定义实现来测试数据库查询。

注意:即使这个设置允许您的测试运行得很快,但不推荐使用,因为在您的设备和用户设备上运行的SQLite版本可能与主机上的版本不匹配。

使用Room引用复杂数据

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

使用类型转换器

有时,应用程序需要使用自定义数据类型,其值要存储在单个数据库列中。为了向自定义类型添加这种支持,您提供了一个TypeConverter,它将自定义类转换为一个已知的类型,该类型可以持久。

例如,如果我们想保存Date实例,可以编写以下TypeConverter来在数据库中存储等效UNIX时间戳:

public class Converters {@TypeConverterpublic static Date fromTimestamp(Long value) {return value == null ? null : new Date(value);}@TypeConverterpublic static Long dateToTimestamp(Date date) {return date == null ? null : date.getTime();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

前面的示例定义了2个函数,一个将Date对象转换为Long对象,另一个函数执行从Long到Date的逆转换。由于Room已经知道如何保存Long对象,所以它可以使用这个转换器来保存类型为Date的值。

接下来,向AppDatabase类添加@TypeConverters注释,以便AppDatabase可以使用您为每个entity定义的转换器和DAO:

AppDatabase.java


@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {public abstract UserDao userDao();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用这些转换器,您就可以在其他查询中使用您的自定义类型,就像使用原始类型一样,如下面的代码片段所示:

User.java


@Entity
public class User {...private Date birthday;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

UserDao.java

@Dao
public interface UserDao {...@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")List findUsersBornBetweenDates(Date from, Date to);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

您还可以将@TypeConverters限制为不同的范围,包括单个实体、DAOs和DAO方法。有关详细信息,请参阅@TypeConverters注解的参考文档。

理解为什么Room不允许对象引用

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

从数据库到相应对象模型的映射关系是一种常见的操作,在服务器端工作得很好。即使程序在访问时加载字段,服务器仍然执行得很好。

但是,在客户端,这种类型的懒加载是不可行的,因为它通常发生在UI线程上,并且在UI线程上查询disk上的信息会产生显著的性能问题。UI线程通常具有大约16ms来计算并绘制活动的更新布局,因此即使查询仅占用5毫秒,您的应用程序仍然可能耗尽绘制帧的时间,从而引起明显的视觉延迟。如果有一个单独的事务并行运行,或者如果设备正在运行其他磁盘密集型任务,则查询可能需要更多的时间来完成。但是,如果你不使用延迟加载,你的应用程序会获取比它需要更多的数据,从而造成内存消耗问题。

对象关系映射通常把这个决定留给开发人员,这样他们就可以为他们的应用程序用例做任何最好的事情。开发人员通常决定在应用程序和UI之间共享模型。然而,这个解决方案规模不大,因为随着时间的推移UI改变,共享模型产生了开发人员难以预料和调试的问题。

例如,考虑一个UI,它加载一个书本对象列表,每一本书都有一个作者对象。您可能最初设计查询使用惰性加载,以便使用getAuthor()方法来返回作者。getAuthor()调用的第一个调用查询数据库。一段时间后,您意识到需要在应用程序的UI中显示作者姓名。您可以很容易地添加方法调用,如下面的代码片段所示:

authorNameTextView.setText(book.getAuthor().getName());
  • 1

然而,这种看似无害的更改导致在主线程上查询作者表。

如果您提前查询作者信息,那么如果不再需要该数据,则难以更改加载数据的方式。例如,如果你的应用程序的UI不再需要显示作者信息,你的应用程序就有效地加载不再显示的数据,浪费宝贵的内存空间。如果作者类引用另一个表(如书籍),则应用程序的效率会进一步降低。

在使用Room同时引用多个实体时,您需要创建一个包含每个实体的POJO,然后编写一个连接相应表的查询(多表查询,必须先查出需要的数据)。这种结构良好的模型,结合Room强大的查询验证能力,可以让应用程序在加载数据时消耗更少的资源,提高应用程序的性能和用户体验。

官方Sample:https://github.com/googlesamples/android-architecture-components/tree/master/PersistenceContentProviderSample

数据库迁移Sample:https://github.com/googlesamples/android-architecture-components/tree/master/PersistenceMigrationsSample

Android Room 官方指南相关推荐

  1. android获取蓝牙信号强度,2、安卓获取ble蓝牙信号强度rssi

    毕业设计需要,需要用到蓝牙,检测蓝牙强度,当蓝牙强度减弱到一定的程度时,将停止扫描,调用其它函数 下面将实现这样的功能. 环境:Android Studio 官方指南:Ble|Android Deve ...

  2. 使用 Room 将数据保存到本地数据库学习日志+demo

    Android中使用 Room 并结合LiveData+ViewModel+RecyclerView将数据保存到本地数据库 Room理论学习 前期准备 导入库 Room三大组件之一:Entity Ro ...

  3. 最纯粹的直播技术实战01-FFmpeg的编译与运行

    最纯粹的Android直播技术实战01-FFmpeg的编译与运行 最新实战教程,Android自动化刷量.作弊与防作弊,案例:刷友盟统计.批量注册苹果帐号 这个系列的文章将会研究最纯粹的Android ...

  4. Android O 迁移应用官方指南

    Android O 引入了若干新的功能和 API,并加入了即便您未对应用做任何更改仍可能对其行为产生影响的一些变动.为帮助您做好准备,本文将说明如何执行兼容性测试,以及如何更新应用以便利用 Andro ...

  5. Instant App 常见问题官方指南 | Android 开发者 FAQ Vol.6

    我们被大家的热情惊到了 -- 事实上我们发出上一篇 Instant App 的文章没几天就收到了一大堆问题.由于涉及到的类目太多,我们这里简单归纳了一下,方便大家查看.如果还有更多问题也请随时通过留言 ...

  6. Android开发中应避免的重大错误

    by Varun Barad 由Varun Barad Android开发中应避免的重大错误 (Critical mistakes to avoid in Android development) A ...

  7. 上传Android或Java库到Maven central repository(转载)

    主要介绍利用Sonatype将jar或aar提交到Maven的中央仓库. 是不是希望将自己的jar或是aar传到maven官方库中,在The Central Repository中可以被其他人搜索使用 ...

  8. Android 8.0 Oreo 国内可用测试平台上线

    Android 8.0 Oreo 已经发布两个月了,无数开发者已经跃跃欲试,想在这个全新版本的 Android 系统上让自己的应用一展身手,我们很清楚,Android 8.0 为了更流畅的系统,以及更 ...

  9. android中编译和使用luajit开发应用,Android 嵌入 LuaJIT 的曲折道路

    相关链接:Windows 下编译 LuaJIT 懒人与伸手党可以直接看最底部. 为什么使用 LuaJIT Lua 官方版的编译嵌入相对简单,但是为什么要用 LuaJIT 呢?我所了解到的优势有: 更高 ...

  10. android 子module混淆_Android 矢量图详解

    官方文档 关于 Vector,在官方开发指南中介绍.本文章是由个人翻译官方指南然后添加个人理解完成. 由于个人精力有限,多个渠道发布,排版上可能会有问题,如果影响查看,请移步 Android 开发者家 ...

最新文章

  1. python中计算1到_如何在Python中计算-1 /(-343)^(1/3)为1/7?
  2. Spring Cloud开发实践 - 04 - Docker部署
  3. 安卓自定义时间选择器_微信小程序拾色器(颜色选择器)组件
  4. MySQL学习笔记(二)—— MySQL的安装
  5. Javascript调试之console对象,它的一些小技巧你都知道吗?
  6. Educational Codeforces Round 1(D. Igor In the Museum) (BFS+离线访问)
  7. mds算法python函数_分享python mds,sha256加密算法,c#对应sha256加密算法
  8. 游戏计算机软著登记证书,“VR沙盘游戏心理疗法软件”取得计算机软件著作权登记证书...
  9. ASCII、ANSI、UNICODE及UTF-8编码
  10. MySQL数据库接口的VC具体实现与应用
  11. linux日志按日期生成器,Cron表达式生成器
  12. Ifc 人工解析——拿墙IfcWallStandardCase来举例说明
  13. low memory killer
  14. 第02章 HTML基本标签
  15. 计算机c盘空间满了应该怎么办,C盘空间满了怎么办?我来教你你如何解决
  16. 元学习兴起,卷积“落幕” | NeurIPS2020研究趋势总结
  17. (最优化理论与方法)第二章最优化所需基础知识-第七节:保凸的运算和共轭函数
  18. mysql 备份 恢复
  19. 小型便携式AIS接收机dAI01
  20. Bat调用/弹出文件或文件夹选择对话框

热门文章

  1. UE4设置场景摄像机视角
  2. 模模搭升级,场景自动同步ThingJS平台,搭建开发无缝衔接!
  3. 2020 Bioinformatics | GraphDTA: predicting drug target binding affinity with graph neural networks
  4. 牛客网项目——项目开发(六):点赞,关注和取关,优化登录
  5. appbase_构建一个Chrome扩展程序,以使用Appbase进行实时价格跟踪
  6. 挣脱注意力经济:为什么应该练习数字极简主义?
  7. mysql 删除数据 分页_使用limit分页查询时,做delete操作,会导致丢失数据
  8. wincc7.0显示无法访问服务器,WinCC 7.0 SP3 安装时提示网络连接不可用,无法安装...
  9. 关联分析:Apriori算法
  10. pythonturtle魔法阵_开启NLP的大魔法阵——一步一步教上手Bert的Fine Tuning