通过可管理的 PR 将复杂的数据库迁移到 Room
你已经听说过 Room 了吧 或许你已经看过文档, 看过一个或两个视频, 并且决定开始整合 Room 到你的项目中如果你的数据库只有几张表和简单查询的话, 你可以很容易地跟着下面这 7 个步骤, 通过较小改动的类似 pull request 操作迁移到 Room
7 Steps To Room: A step by step guide on how to migrate your app to Room medium.com
不过, 如果你的数据库较大或者有复杂的查询操作的话, 实现所有 entity 类, DAO 类, DAO 的测试类并且替换 SQLiteOpenHelper 的使用就会耗费很多时间你最终会需要一个大改动的 pull request, 去实现这些和检查让我们看看你怎么通过可管理的 PR(pull request), 逐步从 SQLite 迁移到 Room
文长不读的话, 可以看下面的概括点:
第一个 PR: 创建你的 entity 类, RoomDatabase, 并且更新你自定义的 SQLiteOpenHelper 为 SupportSQLiteOpenHelper
其余的 PR: 创建 DAO 类去代替有 Cursor 和 ContentValue 的代码
项目设置
我们考虑有以下这些情况:
我们的数据库有 10 张表, 每张有一个相应的 model 对象例如, 如果有 users 表的话, 我们有相应的 User 对象
一个继承自 SQLiteOpenHelper 的 CustomDbHelper
LocalDataSource 类, 这个是通过 CustomDbHelper 访问数据库的类
我们有一些对 LocalDataSource 类的测试
第一个 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 类现在, 我们就不用再创建 DAO 类了
更新你数据库版本号并生成一个 Migration 对象如果你没改数据库的 schema, 你仍需要生成一个空的 Migration 对象让 Room 保留已有的数据
- @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) {
- // 因为我们并没有对表进行更改,
- // 所以这里没有什么要做的
- }
- };
更新使用 SQLiteOpenHelper 的类
一开始, 我们的 LocalDataSource 类使用 CustomOpenHelper 进行工作, 现在我要把它更新为使用
**SupportSQLiteOpenHelper**
, 这个类可以从 RoomDatabase.getOpenHelper() 获得
- public class LocalUserDataSource {
- private SupportSQLiteOpenHelper mDbHelper;
- LocalUserDataSource(@NonNull SupportSQLiteOpenHelper helper) {
- mDbHelper = helper;
- }
因为
SupportSQLiteOpenHelper
并不是直接继承 SQLiteOpenHelper, 而是对它的一层包装, 我们需要更改获得可写可读数据库的调用方式, 并使用
SupportSQLiteDatabase
而不再是 SQLiteDatabase
- SupportSQLiteDatabase db = mDbHelper.getWritableDatabase();
- SupportSQLiteDatabase
是一个数据库抽象层, 提供类似 SQLiteDatabase 中的方法因为它提供了一个更简洁的 API 去执行插入和查询数据库的操作, 代码相比以前也需要做一些改动
对于插入操作, Room 移除了可选的 nullColumnHack 参数使用 SupportSQLiteDatabase.insert 代替 SQLiteDatabase.insertWithOnConflict
- @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
提供了 4 种方法:
- 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
- };
- // 按字母顺序从表中获取第一个用户
- - 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;
- }
如果你没有对你的 SQLiteOpenHelper 实现类进行测试的话, 那我强烈推荐你先测试下再进行这个迁移的工作, 避免产生相关 bug
其余的 PR
既然你的数据层已经在使用 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();
- }
- }
在单一一个 PR 中, 把 SQLite 迁移一个大型的数据库到 Room 会生成很多新文件和更新过后的文件这需要一定时间去实现, 因此导致 PR 更难检查在最开始的 PR, 先使用 RoomDatabase 提供的 OpenHelper 从而让代码最小程度地改动, 然后在接下来的 PR 中才逐渐创建 DAO 类去替换 Cursor 和 ContentValue 的代码
- 7 Pro-tips for Room: _Learn how you can get the most out of Room_medium.com
- Understanding migrations with Room: _Performing database migrations with the SQLite API always made me feel like I was defusing a bombas if I was one_medium.com
- Testing Room migrations: _In a previous post I explained how database migrations with Room work under the hood. We saw that an incorrect_medium.com
- Room RxJava: _Doing queries in Room with RxJava_medium.com
来源: https://juejin.im/post/5a8c3a2cf265da4e761fd721