Room使用七步曲

原文地址:7 Steps To Room 

ezgif-3-5d0b67b6b5.jpg

Room是一个数据持久化库,它是 Architecture Component的一部分。它让SQLiteDatabase的使用变得简单,大大减少了重复的代码,并且把SQL查询的检查放在了编译时。

你是否已经有了一个使用了SQLite做持久化的Android项目?如果是的话,你可以迁移到Room!让我们在7步之内使用Room来重构以前的一个项目。

迁移的 sample app显示一个可编辑的用户名,作为User的一部分存储在数据库中。这里我们使用product flavors来展示数据层的不同版本:

  1. sqlite — 使用 SQLiteOpenHelper 以及传统的SQLite接口。

  2. room — 将实现替换成Room并提供迁移

两个flavor使用相同的UI层,使用 Model-View-Presenter 设计模式与UserRepository类一起工作。

在sqlite flavor中,你会看到 UsersDbHelper 和 LocalUserDataSource 类中每个查询数据库的方法之间有许多重复的代码。查询是在ContentValue的帮助下构造的,而Cursor对象返回的数据是一个字段一个字段的读取的。这些代码很容易出错,比如查询的时候忘记一个字段或者构造model对象出错。

让我们来看看Room是如何提高我们的代码的。最开始我们只是把sqlite flavor中的类拷贝过来,然后再逐渐修改它们。

第一步 — 更新gradle dependencies

Room是通过Google的 Maven 仓库来添加的,因此把它添加到根build.gradle文件的仓库列表中:

allprojects {
    repositories {
        google()
        jcenter()
    }
}

在相同的文件中定义Room库的版本。目前还是alpha版本,请关注我们的 开发者页面了解版本更新。

ext {
   ... 
    roomVersion = '1.0.0-alpha4'
}

在 app/build.gradle 文件中,添加Room的依赖。

dependencies{
 …
implementation        
   “android.arch.persistence.room:runtime:$rootProject.roomVersion”
annotationProcessor 
   “android.arch.persistence.room:compiler:$rootProject.roomVersion”
androidTestImplementation 
   “android.arch.persistence.room:testing:$rootProject.roomVersion”
}

要迁移到Room我们需要增加database的版本,为了保住用户数据我们需要实现一个Migration类。要测试迁移,我们需要导出schema,为此在 app/build.gradle中添加如下代码:

android {
    defaultConfig {
        ...
       // used by Room, to test migrations
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = \["room.schemaLocation": 
                                 "$projectDir/schemas".toString()\]
            }
        }
    }
    // used by Room, to test migrations
    sourceSets {
        androidTest.assets.srcDirs += 
                           files("$projectDir/schemas".toString())
    }
...

第二步— 把model类更新为Entity

Room为每个用@Entity注解了的类创建一张表。类的成员对应表中相应的字段。因此,entity类应该是不包含逻辑的轻量的model类。我们的User类代表数据库中的数据模型。让我们修改之,告诉Room它应该基于这个类创建一张表:

  • 用@Entity注解这个类并用tableName属性设置表的名称。

  • 使用 @PrimaryKey 注解把一个成员设置为主键,这里我们的主键是User的ID

  • 使用@ColumnInfo(name = “column_name”) 注解设置成员对应的列名。如果你觉得成员变量名就本身就可以作为列名,也可以不设置。

  • 如果有多个构造方法,使用 @Ignore注解告诉Room哪个用,哪个不用。

@Entity(tableName = "users")
public class User {
    @PrimaryKey
    @ColumnInfo(name = "userid")
    private String mId;
    @ColumnInfo(name = "username")
    private String mUserName;
    @ColumnInfo(name = "last_update")
    private Date mDate;
    @Ignore
    public User(String userName) {
        mId = UUID.randomUUID().toString();
        mUserName = userName;
        mDate = new Date(System.currentTimeMillis());
    }
    public User(String id, String userName, Date date) {
        this.mId = id;
        this.mUserName = userName;
        this.mDate = date;
    }
...
}

第三步 — 创建DAO

DAO负责定义操作数据库的方法。在SQLite实现的版本中,所有的查询都是在LocalUserDataSource文件中完成的,里面主要是 使用了Cursor对象来完成查询的工作。有了Room,我们不再需要Cursor的相关代码,而只需在UserDao类中使用注解来定义查询。

比如,当查询数据库中所有user的时候,我们只需写:

@Query(“SELECT * FROM Users”)
List<User> getUsers();

第四步 — 创建数据库

目前为止,我们已经定义了User表以及对应的查询,但是我们还没有创建数据库来把Room的各个部分联系在一起。为此,我们需要定义一个继承了RoomDatabase的抽象类。这个类使用@Database来注解,列出它所包含的Entity以及操作它们的 DAO 。database version 从最初的值按1递增,因此我们现在的版本应该是2。

@Database(entities = {User.class}, version = 4)
@TypeConverters(DateConverter.class)
public abstract class UsersDatabase extends RoomDatabase {
    private static UsersDatabase INSTANCE;
    public abstract UserDao userDao();

因为我们想保留user数据,所以需要实现一个Migration 类来告诉Room从版本1迁移到2的过程中需要做些什么。而我们这里database schema没有被修改,因此什么也不用做,直接提供一个空的实现。

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
   // 因为表没有发生变化,所以这里什么也不做
    }
};

使用UsersDatabase类创建database对象,定义database的名称以及migration:

database = Room.databaseBuilder(context.getApplicationContext(),
        UsersDatabase.class, "Sample.db")
        .addMigrations(MIGRATION_1_2)
        .build();

更多关于如何实现数据库迁移以及底层原理的知识,请看这篇文章:

    

第五步— 使用Room更新Repository

我们创建了数据库,User表以及查询,那么现在是时候使用它们了!我们将在LocalUserDataSource类中使用UserDao方法。

为此我们首先移除constructor的Context,用UserDao替代。当然任何实例化了LocalUserDataSource的地方都需要更新一遍。

其次,LocalUserDataSource中查询数据库的方法将用UserDao方法来实现。比如,获取所有user的方法现在变成了这样:

public List<User> getUsers() {
   return mUserDao.getUsers();
}

现在到了运行时间!

Room最好的特性之一是如果你在主线程中执行数据库操作,app将崩溃,显示下面的信息。

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

把I/O操作从主线程拿掉的可靠方法之一是为数据库的每一次查询创建一个新的运行在单独线程的的Runnable。我们已经在项目的sqlite flavor中使用了这种方法,无需改变。

第六步 — On-device testing

我们创建了一个新类-UserDao 和 UsersDatabase,同时也更改了LocalUserDataSource以使用Room数据库。现在需要对它们进行测试!

测试 UserDao

要测试UserDao,我们需要创建一个AndroidJUnit4测试类。Room非常酷的一个特性是可以创建一个内存数据库。这样就避免了每次测试之后都需要清理数据。

@Before
public void initDb() throws Exception {
    mDatabase = Room.inMemoryDatabaseBuilder(
                           InstrumentationRegistry.getContext(),
                           UsersDatabase.class)
                    .build();
}

每次测试后,我们还必须保证都关闭了数据库连接。

@After
public void closeDb() throws Exception {
    mDatabase.close();
}

以测试一个User的插入为例,我们先插入user然后再回过头来检查是否真的可以从数据库读出那条User数据。

@Test
public void insertAndGetUser() {
    // When inserting a new user in the data source
    mDatabase.userDao().insertUser(USER);
    //The user can be retrieved
    List<User> users = mDatabase.userDao().getUsers();
    assertThat(users.size(), is(1));
    User dbUser = users.get(0);
    assertEquals(dbUser.getId(), USER.getId());
    assertEquals(dbUser.getUserName(), USER.getUserName());
}

在LocalUserDataSource中测试UserDao

确保LocalUserDataSource是否仍然能正常工作非常简单,因为我们已经测试了这个类的行为。我们只需创建一个in-memory database,从它获取到一个UserDao对象,然后把它作为LocalUserDataSource构造器的一个参数就可以了。

@Before
public void initDb() throws Exception {
    mDatabase = Room.inMemoryDatabaseBuilder(
                           InstrumentationRegistry.getContext(),
                           UsersDatabase.class)
                    .build();
    mDataSource = new LocalUserDataSource(mDatabase.userDao());
}

再次,每次测试之后确保关闭了数据库的连接。

测试数据库 migration

在下面这篇文章中我们详细讨论了如何实现数据库的迁移测试以及MigrationTestHelper的工作原理:

  

更详细的例子见migration sample app

第七步— Cleanup

移除所有被Room替换掉的类和代码。我们的项目中,只需删除继承SQLiteOpenHelper的UsersDbHelper类。

重复易错的代码减少了,现在查询在编译时检查,并且所有的东西都是可测试的。简单的7个步骤就把我们现有的app迁移到了Room。sample app的代码在这里

来自:官方ORM框架Room