Room是谷歌官方的数据库ORM框架,使用起来非常方便。以下是官网文档的译文:
Room提供了一个SQLite之上的抽象层,使得在充分利用SQLite功能的前提下顺畅的访问数据库。
Room
对于需要处理大量结构化数据的App来说,把这些数据做本地持久化会带来很大的好处。常见的用例是缓存重要数据块。这样当设备无法连网的时候,用户仍然可以浏览内容。而用户对内容做出的任何改动都在网络恢复的时候同步到服务端。
核心framework内置了对SQL的支持。虽然这些API很强大,但是都很低级,使用起来很花时间和精力:
-
没有编译时的SQL查询检查机制。当数据表发生改变的时候,你需要手动更新受影响的SQL查询。这个过程既耗时又容易出错。
-
你需要写很多公式化的代码在SQL查询与Java对象之间转换。
Room为你处理了这些相关的事情,同时提供了SQLite之上的抽象层。
Room中有三个主要的组件:
Database:你可以用这个组件来创建一个database holder。注解定义实体的列表,类的内容定义从数据库中获取数据的对象(DAO)。它也是底层连接的主要入口。
这个被注解的类是一个继承RoomDatabase的抽象类。在运行时,可以通过调用Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()来得到它的实例。
Entity:这个组件代表一个持有数据库的一个表的类。对每一个entity,都会创建一个表来持有这些item。你必须在Database类中的entities数组中引用这些entity类。entity中的每一个field都将被持久化到数据库,[email protected]
DAO:这个组件代表一个作为Data Access Objec的类或者接口。DAO是Room的主要组件,负责定义查询(添加或者删除等)[email protected]0参数的,[email protected]译时生成这个类的实现。
重要:通过DAO而不是query builders或者直接的query语句来处理数据库,可以把数据库的各个部分分离开来。而且DAO还可以让你轻松的使用假的database来测试app。
这些组件以及它们与app其余部分之间的关系如图1:
下面是一个只有一个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(); }
创建了上面的文件之后,你就可以使用下面的代码来得到database的实例了:
AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name").build();
注:在实例化AppDatabase对象的时候你应该遵循单例模式,因为每个Database实例都是相当昂贵的,而且你也很少需要多个实例。
Entities
[email protected]@Database注解中的entities属性所引用,Room就会在数据库中为那个entity创建一张表。
默认Room会为entity中定义的每一个field都创建一个column。如果一个entity中有你不想持久化的field,[email protected],如下面的代码所示:
@Entity class User { @PrimaryKey public int id; public String firstName; public String lastName; @Ignore Bitmap picture; }
要持久化一个field,Room必须有获取它的渠道。你可以把field写成public,也可以为它提供一个setter和getter。如果你使用setter和getter的方式,记住它们要基于Room的Java Bean规范。
Primary key
每个entity必须至少定义一个field作为主键(primary key)。即使只有一个field,[email protected]为entity设置自增ID,[email protected]
@Entity(tableName = "user") public class User { @PrimaryKey(autoGenerate = true) private Integer id; ... }
如果你的entity有一个组合主键,[email protected],具体用法如下:
@Entity(primaryKeys = {"firstName", "lastName"}) class User { public String firstName; public String lastName; @Ignore Bitmap picture; }
Room默认把类名作为数据库的表名。如果你想用其它的名称,[email protected],如下:
@Entity(tableName = "users") class User { ... }
注意:SQLite中的表名是大小写敏感的。
和tableName属性类似,Room默认把field名称作为数据库表的column名。如果你想让column有不一样的名称,[email protected],如下:
@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; }
Indices and uniqueness
为了提高查询的效率,你可能想为特定的字段建立索引。要为一个entity添加索引,[email protected],[email protected]代码片段演示了这个注解的过程:
@Entity(indices = [email protected]("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; }
有时候,某个字段或者几个字段必须是唯一的。你可以通过[email protected][email protected]lastName字段的值相同的情况:
@Entity(indices = [email protected](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库允许entity对象相互引用,但是Room明确禁止了这种行为。详细情况见:Addendum: No object references between entities。
虽然不可以使用直接的关联,Room仍然允许你定义entity之间的外键(Foreign Key)约束。
比如,假设有另外一个entity叫做calledBook,[email protected] entity之间的关联,如下:
@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; }
外键非常强大,因为它允许你指定当被关联的entity更新时做什么操作。例如,[email protected] = CASCADE, 你可以告诉SQLite,如果相应的User实例被删除,那么删除这个User下的所有book。
Note: SQLite handles @Insert(onConflict=REPLACE)
as a set of REMOVE
and REPLACE
operations instead of a single UPDATE
operation. This method of replacing conflicting values could affect your foreign key constraints. For more details, see the SQLite documentation for theON_CONFLICT
clause.
嵌套对象
有时你可能想把一个entity或者一个POJOs作为一个整体看待,即使这个对象包含几个field。这种情况下,[email protected],[email protected]可以像其它独立字段那样查询这些嵌入的字段。
译注:可能不是很好理解,通俗点的说法就是,让带有多个成员的类的每个变量都作为表中的字段。
比如,我们的User类可以包含一个类型为Address的field,Address代表street,city,state, 和postCode字段的组合。为了让这些组合的字段单独存放在这个表中,[email protected],如下面的代码所示:
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。
注:嵌套字段也可以包含其它的嵌套字段。
如果一个entity有多个嵌套字段是相同类型,你可以设置prefix属性保持每个字段的唯一性。Room就会在嵌套对象中的每个字段名的前面添加上这个值。
Data Access Objects (DAOs)
Room中的主要组件是Dao类。DAO抽象出了一种操作数据库的简便方法。
便利的方法
DAO提供了多种简便的查询方式,本文档列出几种常见的例子。
Insert
[email protected],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); }
[email protected],它可以返回一个long,代表新插入元素的rowId,如果参数是一个数组或者集合,那么应该返回long[]或者
List<long>。
[email protected],以及SQLite documentation for rowid tables。
Update
Update是一个更新一系列entity的简便方法。它根据每个entity的主键作为更新的依据。下面的代码演示了如何定义这个方法:
@Dao public interface MyDao { @Update public void updateUsers(User... users); }
你可以让这个方法返回一个int类型的值,表示更新影响的行数,虽然通常并没有这个必要。
Delete
这个API用于删除一系列entity。它使用主键找到要删除的entity。下面的代码演示了如何定义这个方法:
@Dao public interface MyDao { @Delete public void deleteUsers(User... users); }
你可以让这个方法返回一个int类型的值,表示从数据库中被删除的行数,虽然通常并没有这个必要。
@[email protected][email protected]查,因此如果查询存在问题,将出现编译错误,而不是在运行时引起崩溃。
Room还会检查查询的返回值,如果返回的对象的字段名和查询结果的相应字段名不匹配,Room将以下面两种方式提醒你:
-
如果某些字段名不匹配给出警告。
-
如果没有匹配的字段名给出错误提示。
简单的查询
@Dao public interface MyDao { @Query("SELECT * FROM user") public User[] loadAllUsers(); }
这是一个非常简单的查询,加载所有的user。在编译时,Room知道它是查询user表中的所有字段。如果query有语法错误,或者user表不存在,Room将在app编译时显示恰当的错误信息。
向query传递参数
大多数时候,你需要向查询传递参数来执行过滤操作,比如只显示大于某个年龄的user。为此,在Room注解中使用方法参数,如下:
@Dao public interface MyDao { @Query("SELECT * FROM user WHERE age > :minAge") public User[] loadAllUsersOlderThan(int minAge); }
当编译时处理到这个查询的时候,Room把:minAge用方法中的minAge匹配。Room使用参数的名称来匹配。如果有不匹配的情况,app编译的时候就会出现错误。
你也可以传递多个参数或者在查询中多次引用它们,如下面的代码所示:
@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); }
返回字段的子集
大多数时候,我们只需要一个entity的部分字段。比如,你的界面也许只需显示user的first name 和 last name,而不是用户的每个详细信息。只获取UI需要的字段可以节省可观的资源,查询也更快。
只要结果的字段可以和返回的对象匹配,Room允许返回任何的Java对象。比如,你可以创建如下的POJO获取user的first name 和 last name:
public class NameTuple { @ColumnInfo(name="first_name") public String firstName; @ColumnInfo(name="last_name") public String lastName; }
现在你可以在query方法中使用这个POJO:
@Dao public interface MyDao { @Query("SELECT first_name, last_name FROM user") public List<NameTuple> loadFullName(); }
Room知道这个查询返回first_name和last_name字段的值,并且这些值可以被映射到NameTuple类的field中。因此Room可以生成正确的代码。如果查询返回了太多的字段,或者某个字段不在NameTuple类中,Room将显示一个警告。
传入参数集合
一些查询可能需要你传入个数是一个变量的参数,只有在运行时才知道具体的参数个数。比如,你可能想获取一个区间的用户信息。当一个参数代表一个集合的时候Room是知道的,它在运行时自动根据提供的参数个数扩展。
@Dao public interface MyDao { @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)") public List<NameTuple> loadUsersFromRegions(List<String> regions); }
可观察的查询
当执行查询的时候,你通常希望app的UI能自动在数据更新的时候更新。为此,在query方法中使用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); }
注:对于version 1.0,Room使用query中的table列表来决定是否更新LiveData对象。
RxJava
Room还可以让你定义的查询返回RxJava2的Publisher和Flowable对象。要使用这个功能,在Gradle dependencies中添加android.arch.persistence.room:rxjava2。然后你就可以返回RxJava2中定义的对象类型了,如下面的代码所示:
@Dao public interface MyDao { @Query("SELECT * from user where id = :id LIMIT 1") public Flowable<User> loadUserById(int id); }
注:添加android.arch.persistence.room:rxjava2的具体做法是在模块的dependencies中添加:
compile 'android.arch.persistence.room:rxjava2:1.0.0-alpha1'
其实就是要加个版本号而已。
Direct cursor access
如果你的app需要直接获得返回的行,你可以在查询中返回Cursor对象,如下面的代码所示:
@Dao public interface MyDao { @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5") public Cursor loadRawUsersOlderThan(int minAge); }
注:不推荐使用Cursor API,因为它无法保证行是否存在或者行中有哪些值。只有在当前的代码需要一个cursor,而且你又不好重构的时候才使用这个功能。
多表查询
某些查询可能需要根据多个表查询出结果。Room允许你书写任何查询,因此表连接(join)也是可以的。而且如果响应是一个可观察的数据类型,比如Flowable或者LiveData,Room将观察查询中涉及到的所有表。
下面的代码演示了如何执行一个表连接查询来查出借阅图书的user与被借出图书之间的信息。
@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对象。比如你可以写一个如下的查询加载user与它们的宠物名字:
@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; } }
使用类型转换器
类型转换器是一款高质量的ORM必不可少的部分之一。
Room对Java基本数据类型以及其装箱类型都提供了支持,但是有时候你可能使用了一个自定义的数据类型,并且你想将此类型的数据存储到数据库表中的字段里。
为了实现这个功能,你需要提供一个TypeConverter,把自定义的数据类型,转换成Room能够持久化的数据类型。
For example, if we want to persist instances of Date, we can write the following TypeConverter to store the equivalent Unix timestamp in the database:
比如,如果你想持久化一个Date的实例,我们可以编写如下的 TypeConverter 来存储与之等价的 Unix timestamp。
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就可以把你定义的这个converter用到entity和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); }
[email protected],包括单个实体,DAO和DAO方法。有关更多详细信息,[email protected] 注释的参考文档。
数据库迁移
随着app功能的添加和修改,你需要修改entity类来反应这些变化。当一个用户更新了app的最新版本之后,你并不希望它们丢失所有的现有数据,尤其是当你无法通过远程服务器恢复这些数据的时候。
Room让你可以让你写Migration类来保存用户数据。每个Migration类指定from和to版本。运行时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"); } };
注意:为了让迁移的逻辑是可预知的,请使用完整的查询而不是用引用代表查询的constant。
当迁移过程完成之后,Room会检查schema以确保迁移正确进行。如果Room发现了问题,会抛出异常。
测试迁移
写迁移不是一件简单的事情,如果写法不恰当可能导致app的进入崩溃的恶性循环。为了保证app的稳定性,你应该先测试迁移。Room提供了一个Maven测试插件来帮助你完成这个测试过程。但是要让此插件正常工作,需要导出数据库的schema。
导出 schemas
在编译的时候,Room将database的schema信息导出到一个JSON文件中。为此,要在build.gradle 文件中设置room.schemaLocation注解处理器属性,如下面的代码所示:
build.gradle
android { ... defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } } } }
你应该把导出的在这个JSON文件-它代表了你的数据库的schema历史-保存到你的版本管理系统中,这样就可以让Room创建旧版本的数据库来测试。
测试迁移需要把Room的Maven artifact android.arch.persistence.room:testing 添加到你的test dependencies,并且把schema的位置作为 asset folder添加进去,代码如下:
build.gradle
android { ... sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } }
testing package提供了一个MigrationTestHelper类,它可以读出这些schema文件。它同时也是一个 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.getContext(), 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. } }
原文:https://developer.android.com/topic/libraries/architecture/room.html