极光日报
首发于极光日报

从 SQLite 逐步迁移到 Room

简评:Google 工程师的复杂 SQLite 数据库 Room 迁移指南。

Room Persistence Library 是 Google 官方提供的数据持久化类库,提供了 SQLite 的抽象层。官方文档中强烈推荐使用 Room 代替直接操作 SQLite。如果你决定使用 Room,但数据库较大或者有复杂查询时,迁移过程就会比较耗时和麻烦。

这篇文章就是 Google 的一位工程师将 SQLite 迁移到 Room 的步骤拆解成了 PR。

项目场景

想象我们有一个的项目是这样的:

  • 我们的数据库有 10 张表,分别对应一个 model 类。比如,对于 users 表,有一个相应的 User 类。
  • 一个继承自 SQLiteOpenHelper 的 CustomDbHelper。
  • 会在 LocalDataSource 类中通过 CustomDbHelper 进行访问数据库的操作。
  • 一些针对 LocalDataSource 的测试。

First PR

我们的第一个 PR 会包含启用 Room 最低限度的修改。

实现 entity 类

为每张表对应的 model 类增加 @Entity, @PrimaryKey 和 @ColumnInfo 注解。

+ @Entity(tableName = "users")
  public class User {

    + @PrimaryKey
    + @ColumnInfo(name = "userid")
      private int mId;

    + @ColumnInfo(name = "username")
      private String mUserName;

      public User(int id, String userName) {
          this.mId = id;
          this.mUserName = userName;
      }

      public int getId() { return mId; }

      public String getUserName() { return mUserName; }
}

创建 Room 数据库

创建一个继承 RoomDatabase 的抽象类。在 @Database 注解中列出所有创建的 entity 类。再增加数据库版本号并实现一个 Migration

@Database(entities = {<all entity classes>}, 
          version = <incremented_sqlite_version>)
public abstract class AppDatabase extends RoomDatabase {
    private static UsersDatabase INSTANCE;
    static final Migration      MIGRATION_<sqlite_version>_<incremented_sqlite_version> 
= new Migration(<sqlite_version>, <incremented_sqlite_version>) {
         @Override public void migrate(
                    SupportSQLiteDatabase database) {
           // Since we didn’t alter the table, there’s nothing else 
           // to do here.
         }
    };

更新 SQLiteOpenHelper 为 SupportSQLiteOpenHelper

起初,我们是在 LocalDataSource 中使用我们自己实现的 CustomOpenHelper。现在我们要改成使用 SupportSQLiteOpenHelper 了,SupportSQLiteOpenHelper 提供了更加简洁的 API。

public class LocalUserDataSource {
    private SupportSQLiteOpenHelper mDbHelper;

    LocalUserDataSource(@NonNull SupportSQLiteOpenHelper helper) {
       mDbHelper = helper;
    }

对于插入:

@Override
public void insertOrUpdateUser(User user) {
    SupportSQLiteDatabase db = mDbHelper.getWritableDatabase();

    ContentValues values = new ContentValues();
    values.put(COLUMN_NAME_ENTRY_ID, user.getId());
    values.put(COLUMN_NAME_USERNAME, user.getUserName());

    - db.insertWithOnConflict(TABLE_NAME, null, values,
    -        SQLiteDatabase.CONFLICT_REPLACE);
    + db.insert(TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
    db.close();
}

对于查询,SupportSQLiteDatabase 提供了四个方法:

Cursor query(String query);
Cursor query(String query, Object[] bindArgs);
Cursor query(SupportSQLiteQuery query);
Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal);

如果原本的查询操作比较简单,那可以直接使用前两个方法。而如果比较复杂,那就建议通过 SupportSQLiteQueryBuilder 构造一个 SupportSQLiteQuery 来帮助查询。

比如,我们想要获得 users 表中根据用户名排序的第一个用户,来看看 SQLiteDatabase 和 SupportSQLiteDatabase 两者间的实现区别:

public User getFirstUserAlphabetically() {
        User user = null;
        SupportSQLiteDatabase db = mDbHelper.getReadableDatabase();
        String[] projection = {
                COLUMN_NAME_ENTRY_ID,
                COLUMN_NAME_USERNAME
        };
    
        // Get the first user from the table ordered alphabetically
        - Cursor cursor = db.query(TABLE_NAME, projection, null,
        -   null, null, null, COLUMN_NAME_USERNAME +  ASC , 1);
        
        + SupportSQLiteQuery query =
        +  SupportSQLiteQueryBuilder.builder(TABLE_NAME)
        +                           .columns(projection)
        +                           .orderBy(COLUMN_NAME_USERNAME)
        +                           .limit(1)
        +                           .create();
        
        + Cursor cursor = db.query(query);
        
        if (c !=null && c.getCount() > 0){
            // read data from cursor
              ...
        }
        if (c !=null){
            cursor.close();
        }
        db.close();
        return user;
    }

接下来的 PRs

完成以上步骤我们的数据层实现就已经变为 Room 了,之后可以开始逐步创建 DAO(包括测试),并用 DAO 替换 Cursor 和 ContentValue 的代码。

上面我们实现的从 users 表获得按用户名排序的第一个用户方法,应该被定义在 UserDao 接口中:

@Dao
public interface UserDao {
   @Query(SELECT * FROM Users ORDERED BY name ASC LIMIT 1)
    User getFirstUserAlphabetically();
}

并在 LocalDataSource 类中被使用:

public class LocalDataSource {
     private UserDao mUserDao;

     public User getFirstUserAlphabetically() {
        return mUserDao.getFirstUserAlphabetically();
     }
}

以上。如果想了解更多关于 Room 的知识,可以阅读?「扩展阅读」中的文章(需科学上网)。

原文:Incrementally migrate from SQLite to Room

扩展阅读:

文章被以下专栏收录

    简介:每日导读(或翻译)三篇优质英文文章,内容 80% 涉及硅谷/编程/科技/,期待共同成长。