文章標題

Room使用七步曲
泡在網上的日子 / 文 發表於2017-07-26 10:58 第1009次閱讀 SQLite,持久化,Room

原文地址:7 Steps To Room

Room是一個數據持久化庫,它是 Architecture Component的一部分。它讓SQLiteDatabase的使用變得簡單,大大減少了重複的代碼,並且把SQL查詢的檢查放在了編譯時。

你是否已經有了一個使用了SQLite做持久化的Android項目?如果是的話,你可以遷移到Room!讓我們在7步之內使用Room來重構以前的一個項目。

待遷移的 sample app顯示一個可編輯的用戶名,作爲User的一部分存儲在數據庫中。這裏我們使用product flavors來展示數據層的不同版本:

sqlite — 使用 SQLiteOpenHelper 以及傳統的SQLite接口。
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.roomVersionannotationProcessorandroid.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 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 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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章