从 SQLite 逐步迁移到 Room
通过可管理的 PR 将复杂的数据库迁移到 Room
你已经听说过 Room 了吧 —— 或许你已经看过文档,看过一个或两个视频,并且决定开始整合 Room 到你的项目中。如果你的数据库只有几张表和简单查询的话,你可以很容易地跟着下面这 7 个步骤,通过较小改动的类似 pull request 操作迁移到 Room。
不过,如果你的数据库较大或者有复杂的查询操作的话,实现所有 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
的代码。
想了解 Room 的更多相关信息,请阅读下面这些文章:
- 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 bomb — as 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
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于