使用Room将数据保存在本地数据库
Room提供了SQLite之上的一层抽象, 既允许流畅地访问数据库, 也充分利用了SQLite.
处理大量结构化数据的应用, 能从在本地持久化数据中极大受益. 最常见的用例是缓存有关联的数据碎片. 以这种方式, 在设备不能访问网络的时候, 用户依然能够浏览离线内容. 任何用户发起的改变, 都应该在设备重新在线之后同步到服务器.
因为Room为你充分消除了这些顾虑, 使用Room而非SQLite是高度推荐的.
添加依赖
Room的依赖添加方式如下:
复制代码
1 dependencies {
2 def room_version = "1.1.1"
3
4 implementation "android.arch.persistence.room:runtime:$room_version"
5 annotationProcessor "android.arch.persistence.room:compiler:$room_version"
6
7 // optional - RxJava support for Room
8 implementation "android.arch.persistence.room:rxjava2:$room_version"
9
10 // optional - Guava support for Room, including Optional and ListenableFuture
11 implementation "android.arch.persistence.room:guava:$room_version"
12
13 // Test helpers
14 testImplementation "android.arch.persistence.room:testing:$room_version"
15 }
复制代码
Room有3个主要构件:
Database: 包含了数据库持有者, 并对于连接应用上持久化的相关数据, 作为一个主要的访问点, 来服务. 注解了@Database的类应该满足以下条件:
继承了RoomDatabase的抽象类;
包含实体列表, 而这些实体与该注解之下数据库关联;
包含一个抽象方法, 无参且返回一个注解了@Dao的类;
在运行时, 你可以通过调用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()方法请求Database实例.
Entity: 表示数据库内的表.
DAO: 包含用于访问数据库的方法.
这些构件, 以及它们与app余下内容的关系, 如下图:
下面的代码片断, 包含了一个数据库配置示例, 有一个实体和一个DAO:
User.java
复制代码
1 @Entity
2 public class User {
3 @PrimaryKey
4 private int uid;
5
6 @ColumnInfo(name = "first_name")
7 private String firstName;
8
9 @ColumnInfo(name = "last_name")
10 private String lastName;
11
12 // Getters and setters are ignored for brevity,
13 // but they're required for Room to work.
14 }
复制代码
UserDao.java
复制代码
1 @Dao
2 public interface UserDao {
3 @Query("SELECT * FROM user")
4 List getAll();
5
6 @Query("SELECT * FROM user WHERE uid IN (:userIds)")
7 List loadAllByIds(int[] userIds);
8
9 @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
10 + "last_name LIKE :last LIMIT 1")
11 User findByName(String first, String last);
12
13 @Insert
14 void insertAll(User... users);
15
16 @Delete
17 void delete(User user);
18 }
复制代码
AppDatabase.java
复制代码
1 @Database(entities = {User.class}, version = 1)
2 public abstract class AppDatabase extends RoomDatabase {
3 public abstract UserDao userDao();
4 }
复制代码
在创建了以上文件之后, 你能够使用以下代码来创建一个database实例:
1 AppDatabase db = Room.databaseBuilder(getApplicationContext(), 2 AppDatabase.class, "database-name").build();
备注: 在实例化AppDatabase对象的时候, 你应该使用单例模式, 因为每一个RoomDatabase实例都是非常耗时的, 而且你也应该很少访问多个实例.
使用Room实体定义数据
在使用Room持久化库的时候, 把相关联的域的集合定义为实体. 对于每一个实体, 数据库都会创建一个表, 该表来持有数据项.
默认情况下, Room会为实体中定义的每个域创建一个列. 如果实体中有你不想持久化的域, 可以使用@Ignore来注解掉. 在Database类中, 你必须通过entities数据来引用实体类.
下面的代码片断展示了如何定义一个实体:
复制代码
1 @Entity
2 public class User {
3 @PrimaryKey
4 public int id;
5
6 public String firstName;
7 public String lastName;
8
9 @Ignore
10 Bitmap picture;
11 }
复制代码
在持久化一个域, Room必须能够访问它. 你可以将域设置为public, 或者你可以提供该的getter/setter. 如果你使用了getter/setter的方式, 一定要记住: 在Room里面, 它们是基于JavaBeans转换的.
备注: 实体要么有个空的构造器(如果相应的DAO类能够访问每一个持久化域的话), 要有构造器里面的参数, 数据类型和名字跟实体里面定义的域相匹配. Room也能够使用包含全部或者部分域的构造器, 例如, 一个构造器只能获取所有域中的几个.
使用主键
每一个实体必须定义至少1个主键. 即使只有一个域, 你依然需要使用@PrimaryKey来注解它. 而且, 如果你想Room分配自动ID给实体的话, 你需要设置@PrimaryKey的autoGenerate属性. 如果实体有一个复合主键的话, 你需要使用注解@Entity的primaryKeys属性, 示例代码如下:
复制代码
1 @Entity(primaryKeys = {"firstName", "lastName"})
2 public class User {
3 public String firstName;
4 public String lastName;
5
6 @Ignore
7 Bitmap picture;
8 }
复制代码
默认情况下, Room使用实体类的名字作为数据库表的名字. 如果你想要表拥有一个不同的名字, 设置@Entity注解的tableName属性, 示例代码如下:
1 @Entity(tableName = "users") 2 public class User { 3 ... 4 }
注意: SQLite中表名是大小写敏感的.
跟tableName属性相似的是, Room使用域的名字作为数据库中列的名字. 如果你想要列有一个不同的名字的话, 给域添加@ColumnInfo注解, 示例代码如下:
复制代码
1 @Entity(tableName = "users")
2 public class User {
3 @PrimaryKey
4 public int id;
5
6 @ColumnInfo(name = "first_name")
7 public String firstName;
8
9 @ColumnInfo(name = "last_name")
10 public String lastName;
11
12 @Ignore
13 Bitmap picture;
14 }
复制代码
注解索引和唯一性
依赖于你如何访问数据, 你也许想要在数据库中建立某些域的索引, 以加速查询速度. 要给实体添加索引, 需要在@Entity中引入indices属性, 并列出你想要在索引或者复合索引中引入的列的名字. 下列代码说明了注解的处理过程:
复制代码
1 @Entity(indices = {@Index("name"),
2 @Index(value = {"last_name", "address"})})
3 public class User {
4 @PrimaryKey
5 public int id;
6
7 public String firstName;
8 public String address;
9
10 @ColumnInfo(name = "last_name")
11 public String lastName;
12
13 @Ignore
14 Bitmap picture;
15 }
复制代码
有些时候, 数据库中的某些域或几组域必须是唯一的. 你可以通过将注解@Index的unique属性设置为true, 强制完成唯一的属性.
下面的代码示例防止表有两行数据在列firstName和lastName拥有相同值:
复制代码
1 @Entity(indices = {@Index(value = {"first_name", "last_name"},
2 unique = true)})
3 public class User {
4 @PrimaryKey
5 public int id;
6
7 @ColumnInfo(name = "first_name")
8 public String firstName;
9
10 @ColumnInfo(name = "last_name")
11 public String lastName;
12
13 @Ignore
14 Bitmap picture;
15 }
复制代码
定义对象之间的关系
因为SQLite是关系型数据库, 你可以指定对象之间的关系. 尽管大多数对象关系的映射允许实体对象引用彼此, 而Room却显式地禁止了这个特性. 要想了解这个讨论背后的原因, 请查看这篇文章. //todo
尽管你不能使用直接的对象关系, Room仍然允许你在实体之间定义外键约束.
比如, 如果有一个实体类Book, 你可以使用@ForeignKey注解定义它和实体User的关系, 示例代码如下:
复制代码
1 @Entity(foreignKeys = @ForeignKey(entity = User.class,
2 parentColumns = "id",
3 childColumns = "user_id"))
4 public class Book {
5 @PrimaryKey
6 public int bookId;
7
8 public String title;
9
10 @ColumnInfo(name = "user_id")
11 public int userId;
12 }
复制代码
外键非常强大, 因为它允许你指定做什么操作, 在引用实体更新的时候. 比如, 你可以告诉SQLite为用户删除所有的书, 在相应的User实例被删除时, 而该User被Book通过在@ForeignKey注解里面声明onDelete = CASCADE而关联.
备注: SQLite将@Insert(onConflict = REPLACE)作为REMOVE和REPLACE的集合来操作, 而非单独的UPDATE操作. 这个取代冲突值的方法能够影响你的外键约束.
创建嵌套对象
有些时候, 在数据库逻辑中, 你想将一个实体或者POJO表示为一个紧密联系的整体, 即使这个对象包含几个域. 在这些情况下, 你能够使用@Embedded注解来表示一个对象, 而你想将这个对象分解为表内的子域. 然后你可以查询这些嵌套域, 就像你查询其它的独立列一样.
举个例子, User类包含一个Address类的域, 这个域表示的是street, city, state, postCode这几个域的复合. 为了在表中单独存储复合的列, 在User类里面, 引入一个注解了@Embedded的Address域, 就像如下代码片断展示的一样:
复制代码
1 public class Address {
2 public String street;
3 public String state;
4 public String city;
5
6 @ColumnInfo(name = "post_code")
7 public int postCode;
8 }
9
10 @Entity
11 public class User {
12 @PrimaryKey
13 public int id;
14
15 public String firstName;
16
17 @Embedded
18 public Address address;
19 }
复制代码
这个表表示User对象包含如下几列: id, firstName, street, state, city和post_code.
备注: 嵌套的域同样可以包含其它的嵌套域.
如果实体拥有多个相同类型的嵌套域, 你可以通过设置prefix属性保留每一列唯一. 然后Room给嵌套对象的每一个列名的起始处添加prefix设置的给定值.
通过Room DAO访问数据
要通过Room持久化库访问应用的数据, 你需要使用数据访问对象(data access objects, 即DAOs). Dao对象集形成了Room的主要构成, 因为每一个DAO对象都引入了提供了抽象访问数据库的方法.
使用DAO对象而非查询构造器或者直接查询来访问数据库, 你可以分开不同的数据库架构组成. 此外, DAO允许你轻易地模拟数据库访问.
DAO要么是接口, 要么是抽象类. 如果DAO是抽象类的话, 它可以随意地拥有一个将RoomDatabase作为唯一参数的构造器. Room在运行时创建DAO的实现.
备注: Room并不支持在主线程访问数据库, 除非在Builder调用allowMainThreadQueries()方法, 因为它很可能将UI锁上较长一段时间. 但是, 异步查询--返回LiveData/Flowable实例的查询--则从此规则中免除, 因为它们在需要的时候会在后台线程异步地运行查询.
方便地定义方法
使用DAO类, 可以非常方便地表示查询.
插入
当你创建了一个DAO方法并注解了@Insert的时候, Room生成了一个实现, 在单个事务中将所有的参数插入数据库.
下面的代码片断展示了几个示例查询:
复制代码
1 @Dao
2 public interface MyDao {
3 @Insert(onConflict = OnConflictStrategy.REPLACE)
4 public void insertUsers(User... users);
5
6 @Insert
7 public void insertBothUsers(User user1, User user2);
8
9 @Insert
10 public void insertUsersAndFriends(User user, List friends);
11 }
复制代码
如果@Insert方法只接收了一个参数, 它可以返回一个long, 表示新插入项的rowId; 如果参数是数组或者集合, 同时地, 它应该返回long[]或者List.
更新
按照惯例, 在数据库中, Update方法修改了作为参数传递的实体集合. 它使用查询来匹配每一个实体的主键.
下面的代码片断展示了如何定义这个方法:
复制代码
1 @Dao
2 public interface MyDao {
3 @Update
4 public void updateUsers(User... users);
5 }
复制代码
尽管通常情况下并不需要, 但是依然可以将这个方法返回int值, 表示在数据库中被修改的行数.
删除
按照惯例, Delete方法从数据库中删除了作为参数传递的实体集合. 它使用主键找到要删除的实体.
下面的代码片断展示了如何定义这个方法:
复制代码
1 @Dao
2 public interface MyDao {
3 @Delete
4 public void deleteUsers(User... users);
5 }
复制代码
尽管通常情况下并不需要, 但是依然可以将这个方法返回int值, 表示从数据库中删除的行数.
查询
@Query是在DAO类中使用的主要的注解. 它允许你在数据库中执行读写操作. 每一个@Query方法都在编译时被证实, 因为, 如果查询有问题出现的话, 会出现编译错误而非运行失败.
Room也证实查询的返回值, 以确定返回对象的域的名字是否跟查询响应中对应列的名字匹配, Room使用如下两种方式提醒你:
如果只有一些域匹配, 它会给予警告;
如果没有域匹配, 它会给予错误;
简单查询
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT * FROM user")
4 public User[] loadAllUsers();
5 }
复制代码
这是一个非常简单的查询, 加载了所有User. 在编译时, Room知晓这是在查询user表中所有列.
如果查询语句包含语法错误, 或者user表在数据库中并不存在, Room会在编译时展示恰当的错误信息.
查询语句中传参
大多数时候, 你需要向查询语句中传参, 以执行过滤操作, 比如, 只展示大于某个年龄的user.
要完成这个任务, 在Room注解中使用方法参数, 如下所示:
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT * FROM user WHERE age > :minAge")
4 public User[] loadAllUsersOlderThan(int minAge);
5 }
复制代码
当这个查询在编译时处理的时候, Room匹配到 :minAge, 并将它跟方法参数minAge绑定. Room使用参数名来执行匹配操作. 如果不匹配的话, app编译时会发生错误.
你也可以在查询中传递多个参数, 或者将参数引用多次, 如下所示:
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
4 public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
5
6 @Query("SELECT * FROM user WHERE first_name LIKE :search "
7 + "OR last_name LIKE :search")
8 public List findUserWithName(String search);
9 }
复制代码
返回列的子集
大多数情况下, 你只需要实体中的几个域. 比如, UI中只需要展示用户的姓和名, 而非用户的每一个细节. 通过只查询UI中展示的列, 将节省宝贵的资源, 查询也更快.
Room允许从查询中返回基于Java的对象, 只要结果列集合能够映射成返回对象. 比如, 你创建了一个POJO来获取用户的名和姓:
复制代码
1 public class NameTuple {
2 @ColumnInfo(name="first_name")
3 public String firstName;
4
5 @ColumnInfo(name="last_name")
6 public String lastName;
7 }
复制代码
现在, 你可以在查询方法中使用这个POJO了:
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT first_name, last_name FROM user")
4 public List loadFullName();
5 }
复制代码
Room明白: 查询返回了列first_name和last_name, 这些值能够映射到NameTuple为的域中.
由此, Room能够产生适当的代码. 如果查询返回了太多列, 或者返回了NameTuple类中并不存在的列, Room将展示警告信息.
备注: POJO也可以使用@Embedded注解.
传递参数集
一些查询可能要求你传入可变数目的参数, 直到运行时才知道精确的参数数量.
比如, 你可能想要搜索地区子集下的所有用户. Room明白参数表示集合的时机, 并在运行时自动地基于提供了参数数目展开它.
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
4 public List loadUsersFromRegions(List regions);
5 }
复制代码
可观察查询
在执行查询的时候, 经常想要在数据发生改变的时候自动更新UI. 要达到这个目的, 需要在查询方法描述中返回LiveData类型的值. 在数据库更新的时候, Room生成所有必要的代码以更新LiveData.
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
4 public LiveData> loadUsersFromRegionsSync(List regions);
5 }
复制代码
备注: 在1.0版本的时候, Room使用查询中访问的表的列表来决定是否更新LiveData实例.
RxJava响应式查询
Room也可以从定义的查询中返回RxJava2中的Publisher和Flowable.
要使用这个功能, 在build.gradle文件中添加依赖: android.arch.persistence.room:rxjava2. 之后, 你可以返回在RxJava2中定义的数据类型, 如下所示:
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT * from user where id = :id LIMIT 1")
4 public Flowable loadUserById(int id);
5 }
复制代码
游标直接访问
如果你的应用逻辑要求直接访问返回的行, 你可以从查询中返回Cursor对象, 如下所示:
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
4 public Cursor loadRawUsersOlderThan(int minAge);
5 }
复制代码
注意: 十分不推荐使用Cursor API. 因为它并不保证行是否存在以及行包含什么值.
除非你有需要Cursor的代码并且并不轻易的修改它的时候, 你才可以使用这个功能.
查询多表
有些查询可能要求访问多个表以计算结果. Room允许你写任何查询, 所以你也可以联接表. 此外, 如果响应是可观测数据类型, 诸如Flowable/LiveData, Room观察并证实查询中引用的所有表.
下面的代码片段展示了如何执行表联接, 以合并包含借书用户的表和包含在借书数据的表的信息:
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT * FROM book "
4 + "INNER JOIN loan ON loan.book_id = book.id "
5 + "INNER JOIN user ON user.id = loan.user_id "
6 + "WHERE user.name LIKE :userName")
7 public List findBooksBorrowedByNameSync(String userName);
8 }
复制代码
你也可以从这些查询中返回POJO. 比如, 你可以写查询加载用户和它的宠物名:
复制代码
1 @Dao
2 public interface MyDao {
3 @Query("SELECT user.name AS userName, pet.name AS petName "
4 + "FROM user, pet "
5 + "WHERE user.id = pet.user_id")
6 public LiveData> loadUserAndPetNames();
7
8
9 // You can also define this class in a separate file, as long as you add the
10 // "public" access modifier.
11 static class UserPet {
12 public String userName;
13 public String petName;
14 }
15 }
复制代码
迁移Room数据库
当应用中添加或者改变特性的时候, 需要修改实体类以反映出这些改变. 当用户升级到最新版本的时候, 你不想用户失去所有数据, 尤其是如果你还不能从远程服务器恢复这些数据的时候.
Room持久化库允许写Migration类来保留用户数据. 每一个Migration类指定了startVersion和endVersion. 在运行时, Room运行每一个Migration类的migrate()方法, 使用正确的顺序迁移数据库到最新版本.
注意: 如果你不提供必要的迁移, Room会重建数据库, 这意味着你会失去原有数据库中的所有数据.
复制代码
1 Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
2 .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
3
4 static final Migration MIGRATION_1_2 = new Migration(1, 2) {
5 @Override
6 public void migrate(SupportSQLiteDatabase database) {
7 database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
8 + "`name` TEXT, PRIMARY KEY(`id`))");
9 }
10 };
11
12 static final Migration MIGRATION_2_3 = new Migration(2, 3) {
13 @Override
14 public void migrate(SupportSQLiteDatabase database) {
15 data