这是一篇自己从Android开发文档中翻译来的关于
Room
的文档。哪里不对,欢迎纠错
Room持久性库提供了SQLite的抽象层,以便在充分利用SQLite的同时允许流畅的数据库访问。 该库可帮助你在设备上创建应用程序的缓存数据,这样不管设备是否联网都能看到数据。
Room
在本地保存数据原文地址 https://developer.android.com/training/data-storage/room/index.html
对于不重要的数据可以存储在本地,最常见的就是缓存相关的数据。这样,在设备没有网络的时候就可以浏览离线数据。当设备联网后,将用户改动的数据同步至服务端。
Room
有三个重要组件
包含数据库持有者,并作为与应用持久关联数据的底层连接的主要接入点。
使用@Database
注解,并满足以下条件
RoomDatabase
表示数据库中的表格
包含用户访问数据库的方法
这些组件以及组件与APP其他部分的关系 如图所示
下面的代码片段是一个数据库实例配置包含了一个Entity
和一个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();
数据库实例最好是单例以节省内存开销
Room
实体定义数据原文地址 https://developer.android.com/training/data-storage/room/defining-data.html
我们定义的每一个实体,Room
都会对应的在数据库中创建一个表。
默认 Room 会为 每个字段在表中创建对应的字段;如果其中一些属性不想被创建在表中怎么办,那就是使用 @Ignore
注解此属性。完成实体的创建之后必须在 Database
引用。
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
类中的每个字段都必须让Room能够访问到。否则Room无法管理。 [^] 注意 :要遵循 JavaBean 规约;否则 管杀不管埋;[^]
每个实体必须定义最少一个主键,就算类中只有一个字段,也要保证使用 @PrimaryKey
;
如果想让Room自动分配ID,可以设置 autoGenerate
为true;
如果是联合主键,可以在@Entity
中设置 primaryKeys
属性。
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
默认Room会使用类名当作数据库表名,如果你想设置其他名字,可以设置 tableName
属性
@Entity(tableName = "users")
class User {
...
}
[^]Sqlite中表名不区分大小写[^]
就像表名一样,字段的名字默认的也是类中属性的名字如果想设置其他名字,可使用 @ColumnInfo
的 name
属性
@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(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
属性设置 唯一约束。
下面代码 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
明确禁止这样做。至于为什么明确禁止,文章最后会说。原文链接:https://developer.android.com/training/data-storage/room/referencing-data.html#understand-no-object-references
虽然不能直接定义对象间引用,但是可以使用外键建立关系。
例如:有一个 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将@Insert(onConflict = REPLACE)作为一组REMOVE和REPLACE操作处理,而不是单个UPDATE操作。这种替换冲突值的方法可能会影响外键约束。有关更多详细信息,请参阅ON_CONFLICT子句的SQLite文档。[^]
Room 支持在数据实体中嵌套其他对象来组合相关字段。例如 User
中嵌套一个 Address
这个地址对象中有三个字段:街道,城市,邮编。在数据表中这个三个字段是在用户表中的,就像其他字段一样。
通过在 User
使用 ` 注解 属性
address` 即可。
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
加前缀的方式保证字段名不重复。
如果在 User
中使用下面的代码,那么嵌套字段就会是 address_street
,address_state
,address_city
和address_post_code
@Embedded(prefix = "address_")
public Address address;
Room DAO
访问数据原文地址:https://developer.android.com/training/data-storage/room/accessing-data.html
Room 使用数据对象和 DAO 访问数据库。
DAO 是 Room 的重要组件,他包含了操作数据的抽象方法;
DAO可以是一个接口或者抽象类,如果是抽象类的话,它可以有一个构造函数,它将RoomDatabase作为其唯一参数。Room会在编译时创建实现。
DAO不能在主线程的时候操作数据,可能会阻塞UI,除非在构建的时候调用 allowMainThreadQueries()
。如果是返回 LiveData
或者 Flowable
的异步查询例外。
这里只列出几个常用方法
当创建一个DAO方法并使用它的时候,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类型的数组或集合。
关于 @Insert
的详细介绍查看文档 https://developer.android.com/reference/android/arch/persistence/room/Insert.html
Room 会通过每个实体的主键进行查询,然后再进行修改。
返回值可以是一个 int
型的值,返回更新的行数。
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
Room 会数据实体的主键删除相应的数据。
返回值可以是一个 int
型的值,用来表示删除的行数。
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
@Query
是 DAO 中主要使用的注解。它可以执行对数据库的读写操作。每一个 @Query
方法都会在编译时验证,如果出现问题也是在编译时出现问题不会在运行时出现问题。
Room
也会验证方法的返回值,如果返回对象中的字段名称和查询响应中的字段名字不匹配, Room
会通过以下方式给出提示
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
这是一个非常简单的查询所有用户的查询。在编译时,Room会知道是查询用户表的所有列。如果查询包含语法错误或者数据库中不存在这个表。Room会在编译时报错并给出错误信息。
大部分时候查询都是需要过滤参数的。比如要查询一些年龄比较大的用户。
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
在编译时,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);
}
很多时候只需要数据实体的中几个列。例如你可能只想显示用户的姓和名而不是全部的用户信息。只查询需要的列可以节省资源并且查询的更快。
Room
允许返回任何的Java对象。只要查询的结果列能够和Java对象映射上即可。所以我们可以创建一个只包含需要的列的类。
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
知道查询的值并知道怎么映射到对应的NameTuple字段中。所以 Room
会生成正确的代码。如果查询返回的列多了或者少了,Room会给出警告
这里也可以使用@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的实例。
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);
}
更多 Room 和 Rxjava2 的使用 看另一篇文章 https://medium.com/google-developers/room-rxjava-acb0cd4f3757
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
非常不推荐使用Cursor API,因为它不能保证行是否存在或行包含的值。只有当已经拥有需要游标并且无法轻松重构的代码时才使用此功能。
有些时候可能需要查询多个表中的数据来计算结果。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);
}
也可以从这些查询中返回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
数据库原文 https://developer.android.com/training/data-storage/room/migrating-db-versions.html
在APP升级时可能需要更改数据库来策应新的功能。这个时候当然不希望数据库中的数据丢失。
Room
允许我们编写 Migration
,以此来迁移数据。每个迁移类制定一个开始版本和结束版本。
在运行时,Room会运行每个Migration
类的migrate()
方法,并使用正确的顺序将数据库迁移到更高版本。
如果不提供必要的Migration
, 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
发现错误,会抛出一个包含不匹配的异常。
数据迁移是很重要的,一旦迁移失败可能会发生Crash。为了保证程序的稳定性,一定要确认是否否迁移成功。Room 提供了一个测试工件来帮助我们测试,为保证测试工件的正确运行,必须开启导出模式。
编译后,Room将数据库的模式信息导出到JSON文件中。要导出模式,在build.gradle文件中设置room.schemaLocation注解处理器属性,如下面的代码片段所示:
build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
我们应该把导出的 json 文件加入到版本控制中,它记录了数据库的模式历史,它能让Room在测试时创建老版本的数据库。
为了测试迁移,增加 Room 的测试工件依赖,并设置数据库模式文件地址,如下所示:
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 创建数据库时,验证数据库和用户数据的稳定性非常重要。
测试数据库有两种方法
关于测试指定数据库升级的信息 上面已经说过了。
注意:在测试时,Room允许创建Dao的模拟实例。这样的话,如果不是测试数据库本身就不需要创建完整的数据库,这个功能是很好的,Dao不会泄露数据库的任何信息
测试数据库实现的推荐方法是编写在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));
}
}
Room使用SQLite Support Library,它提供了与Android Framework类中的接口相匹配的接口。此支持允许您传递支持库的自定义实现以测试数据库查询。
注意:即使此设置允许您的测试运行速度非常快,也不建议这样做,因为设备上运行的SQLite版本以及用户的设备可能与主机上的版本不匹配
Room提供了原始和包装类型转换的功能,但是不允许实体间对象引用。这里会解释为什么不支持对象引用和怎么使用类型转换器。
有时候你想存储自定义的数据类型在数据库的单个列中。这就需要为自定义类型添加一个类型转换器,这个转换器会将自定类型转换为Room能够认识的原始类型。
例如,我想保存Date类型的实例,我可以编写下面的类型转换器来在数据库中存储等效的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对象持久化。
接下来,在AppDataBase类添加注解 @TypeConverters 这样AppDataBase中的Dao和实体就都能使用这个转换器了。
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
更详细的介绍 请查看文档 https://developer.android.com/reference/android/arch/persistence/room/TypeConverters.html
关键问题:Room不允许实体类之间的对象引用。相反,您必须明确您的应用需要的数据。
将数据库中的关系映射到相应的对象模型是常见的做法,并且在服务器端运行良好。即使程序在访问时加载字段,服务器仍然运行良好。
但是,在客户端,这种延迟加载不可行,因为它通常发生在UI线程上,并且在UI线程中查询磁盘上的信息会产生严重的性能问题。UI线程通常具有约16 ms的时间来计算和绘制活动的更新布局,因此即使查询只需要5 ms,仍然可能您的应用程序将耗尽时间来绘制框架,从而导致明显的视觉干扰。如果有单独的事务并行运行,或者设备正在运行其他磁盘密集型任务,则查询可能需要更多时间才能完成。但是,如果不使用延迟加载,则应用会获取比所需更多的数据,从而导致内存消耗问题。
对象关系映射通常将这个决定留给开发人员,以便他们可以为他们的应用程序的用例做最好的事情。开发人员通常决定在应用程序和用户界面之间共享模型。然而,这种解决方案并不能很好地扩展,因为随着UI的变化,共享模型会产生一些难以让开发人员预测和调试的问题。
例如,考虑加载一个Book对象列表的UI,每个书都有一个Author对象。最初可能会将查询设计为使用延迟加载,以便Book的实例使用getAuthor()方法返回作者。过了一段时间,你意识到你也需要在应用程序的用户界面中显示作者姓名。您可以轻松地添加方法调用,如以下代码片段所示:
authorNameTextView.setText(user.getAuthor().getName());
但是,这个看起来无害的更改会导致在主线程上查询Author表。
如果提前查询作者信息,如果不再需要数据,则很难更改数据的加载方式。例如,如果您的应用程序的用户界面不再需要显示作者信息,则您的应用程序会有效地加载不再显示的数据,从而浪费宝贵的内存空间。如果作者类引用另一个表(如Books),则应用程序的效率会进一步降低。
要使用Room同时引用多个实体,需要创建一个包含每个实体的POJO,然后编写一个查询来加入相应的表。这种结构良好的模型与Room强大的查询验证功能相结合,可让您的应用在加载数据时消耗更少的资源,从而改善应用的性能和用户体验。
end