Architecture Components之Room初探

简介

Room在SQLite之上提供了一层抽象,能让我们在使用SQLite全部功能的同时还能流畅的对数据库进行访问。Room基于注解对SQLite进行了大量封装,用法十分简洁且功能强大并能与RxJava/LiveData等无缝结合使用,因此官方强烈推荐使用Room来替代SQLite。

导入依赖

导入依赖只需要前2行就够了,可以选择是否需要搭配RxJava使用:

1
2
3
4
implementation "android.arch.persistence.room:runtime:1.1.1"
annotationProcessor "android.arch.persistence.room:compiler:1.1.1"
// optional - RxJava support for Room
implementation "android.arch.persistence.room:rxjava2:1.1.1"

Room的架构

Room里面主要有3个比较重要的组件:

  • Database: 数据库的持有者,它是底层数据库连接的主要接入点。
  • Entity: 表示数据库中的表。
  • DAO: 包含用于访问数据库的方法。

这些组件与程序中其他部分的调用关系如下所示:
room_architecture

基本使用

下面将介绍Room中3个重要组件的基本使用,Room中大量使用注解来帮我们生成实现代码,能让我们更加专注于业务而不是写各种模板代码。有用过Retrofit的童鞋应该对这深有体会~

Entity

Room中,对于每个Entity对象,最终都对应数据库中的一个表。

(1)下面的代码展示了如何定义一个基本的Entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity(tableName = "users")
public class UserInfo {
@NonNull
@PrimaryKey
@ColumnInfo(name = "userId")
private String mUserId;
@ColumnInfo(name = "nickName")
private String mNickName;
@ColumnInfo(name = "phone")
private String mPhone;
@Ignore
private Bitmap picture;
// getter and setter
...
}

  • 使用@Entity标记这个类为Room中的Entity,可以通过tableName属性来指定表名,如果不指定则默认使用类名
  • 默认情况下,Room会为每个实体对象的成员生成数据列,可以用注解@Ignore进行排除
  • 可以通过@PrimaryKey指定主键,使用autoGenerate属性可指定是否需要自动生成主键值
  • 可以通过@ColumnInfo中的name属性指定列名,如果不指定则默认使用变量名

(2)通过@ForeignKey注解可以实现在Entity之间定义外键约束,其中entity属性指定了需要关联的父Entity,parentColumns指定了需要关联到父Entity的字段名,childColumns则是当前Entity对应外键的字段名。我们还可以通过onDeleteonUpdate属性来指定当前Entity要如何响应父Entity的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity(foreignKeys = @ForeignKey(entity = UserInfo.class,
parentColumns = "userId",
childColumns = "uId",
onDelete = CASCADE))
public class Book {
@NonNull
@PrimaryKey
@ColumnInfo(name = "bookId")
private int mBookId;
@ColumnInfo(name = "title")
private String mTitle;
@ColumnInfo(name = "uId")
private int userId;
}

(3)通过@Embedded注解,我们可以在Entity中加入嵌套对象,这样子我们就可以在Entity中直接引用整个POJO了。如下面的例子,在users表中,会新增street, state, city这3个数据列。当我们插入或查询数据的时候,Room将会帮我们处理好字段与数据表的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Address {
@ColumnInfo(name = "street")
private String mStreet;
@ColumnInfo(name = "state")
private String mState;
@ColumnInfo(name = "city")
private String mCity;
...
}
@Entity(tableName = "users")
public class UserInfo {
...
@Embedded
public Address address;
}

DAO(Data Access Object)

Room中,每个DAO对象都包含提供对数据库访问的抽象方法,且DAO对象只能是接口或者抽象类并用@Dao注解进行修饰。下面我们来看一段典型的实现代码,定义好增删查改的接口,就像Retrofit中通过注解声明接口一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Dao
public interface UserDao {
@Query("SELECT * FROM users")
List<UserInfo> getAll();
@Query("SELECT * FROM users WHERE userId = :userId")
UserInfo getUser(String userId);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(UserInfo user);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(List<UserInfo> users);
@Update
public void updateUsers(User... users);
@Delete
void delete(UserInfo user);
@Query("DELETE FROM users")
void deleteAll();
}

(1)通过@Insert注解可以把方法中所有的参数当做一次事务批量插入到数据库中,通过onConflict属性我们可以指定冲突出现时Room该如何操作数据。如果insert()方法只有一个参数,返回值可以修改为long,此时将会返回插入对象的rowId。若是插入多个对象,则返回值应该是long[]或者List<Long>

(2)@Update@Delete比较类似,可以更新/删除多个对象。返回值可以是int,代表了更新/删除Item的总数。

(3)@Query是最重要的DAO注解,它允许你对数据库执行读/写操作。并且每个query方法都在编译时进行验证,如果出现SQL语法问题则会直接编译错误。在高版本的Android Studio中,还提供了表名/字段名的智能补全功能,十分强大。

(4)加入可观察的查询

  • 使用LiveData可使我们的数据在更新后能及时的通知到UI,只需简单包装下方法的返回值即可。

    1
    2
    3
    4
    5
    @Dao
    public interface UserDao {
    @Query("SELECT first_name, last_name FROM users WHERE region IN (:regions)")
    public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
    }
  • 使用RxJava进行响应式查询,并且可以结合Rx强大的操作符做完成各种骚操作~

    1
    2
    3
    4
    5
    @Dao
    public interface UserDao {
    @Query("SELECT * from users where id = :id LIMIT 1")
    public Flowable<User> loadUserById(int id);
    }

注意:在Room中,只支持MaybeSingleFlowable三种返回类型,Observable暂时不支持,会在编译期报错Error: Not sure how to convert a Cursor to this method's return type。关于三种返回类型的具体使用场景,可见官方推荐的这篇文章:Room and RxJava

  • 返回原始的Cursor对象(官方不推荐使用,因为他不保证行是否存在或者行包含了什么数据)
    1
    2
    3
    4
    5
    @Dao
    public interface UserDao {
    @Query("SELECT * FROM users WHERE age > :minAge LIMIT 5")
    public Cursor loadRawUsersOlderThan(int minAge);
    }

Database

Entity与DAO都准备好了,剩下工作就是初始化Database了。在Room中声明我们自定义的Database有几个比较关键的地方:

  • 继承RoomDatabase并且需要是抽象类。
  • 通过@Database注解声明数据库类,并且通过entities属性指定包含哪些数据表,version属性可以指定数据库版本号,用于后续数据库升级。
  • 返回@DAO标记的DAO对象的方法必须是0参数的抽象方法。

一切就绪,调用Room.databaseBuilder()方法即可生成我们的Database实例。一般情况下建议采用单例模式初始化Database对象,因为初始化的代价十分高,并且很少会用到多个实例的情况,参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Database(entities = {UserInfo.class}, version = 1)
public abstract class MainDatabase extends RoomDatabase {
private static final String DB_NAME = "users.db";
private static volatile MainDatabase sInstance;
public abstract UserDao userDao();
public static void init(Context context) {
if (sInstance == null) {
synchronized (MainDatabase.class) {
if (sInstance == null) {
sInstance = Room.databaseBuilder(context.getApplicationContext(),
MainDatabase.class, DB_NAME)
.build();
}
}
}
}
public static MainDatabase getInstance() {
return sInstance;
}
}

注意:在DAO中所有的数据库操作都不能在主线程中进行,否则Room会直接抛异常。当然,你也可以通过RoomDatabase.Builder.allowMainThreadQueries()方法来解除这个限制,但建议还是遵循官方的建议,采用异步的方式进行或者通过LiveDataRxJava等异步框架实现。