今天要介紹Architecture Components中的資料庫library Room,它能讓我們大量減少使用SQLiteDatabase要寫的語法量,並且有兩點優於其他資料庫library:
這幾天我們就將Room加入專案中,儲存從API抓回來的資料達到離線時也能檢視cache資料的效果。
1.加入dependencies
implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
2.建立Entity,即我們的table schema
我們要儲存搜尋的關鍵字及其搜尋結果,先建立儲存關鍵字的Entity:
@Entity
@TypeConverters(GithubTypeConverters.class)
public class RepoSearchResult {
@NonNull
@PrimaryKey
public final String query;
public final List<Integer> repoIds;
public final int totalCount;
public RepoSearchResult(@NonNull String query, List<Integer> repoIds, int totalCount) {
this.query = query;
this.repoIds = repoIds;
this.totalCount = totalCount;
}
}
建立POJO之後標上@Entity
,Room就會將其建成table;@PrimaryKey
標示query為主鍵;@TypeConverters
是用在repoIds這個欄位把多個整數id合起來變成用逗號分隔的字串,透過TypeConverter可以把List存在一個欄位中,待會再看它是怎麼運作的。
repoIds於資料表中的樣子:
另外一個Entity就是我們的Repo,POJO之前就建好了,只要加上annotation:
@Entity(indices = {@Index("id"), @Index("owner_login")},
primaryKeys = {"name", "owner_login"})
public class Repo {
...
@Embedded(prefix = "owner_")
public final Owner owner;
...
}
}
在@Entity
中使用primaryKeys表示要由多欄位當主鍵,@Index
是索引,而其中有個owner_login,那是藉由@Embedded
我們把Owner也加入table欄位中。
Repo資料表完成樣:
最後兩個欄位login和url原本是Owner的欄位而不是Repo的,但藉由@Embedded
我們把它們欄位合併起來,並用(prefix = "owner_")
讓login變成owner_login,url亦同。
最後是剛剛提到的TypeConverter把List轉成String存在同一個欄位,如下:
public class GithubTypeConverters {
@TypeConverter
public static List<Integer> stringToIntList(String data) {
if (data == null) {
return Collections.emptyList();
}
return StringUtil.splitToIntList(data);
}
@TypeConverter
public static String intListToString(List<Integer> ints) {
return StringUtil.joinIntoString(ints);
}
}
將method標上@TypeConverter
,Room就會在insert/query時自動套用。例如要儲存List{1,2,3,4},Room會找出輸入參數型態為List<Integer>
的intListToString
,將List轉成字串"1,2,3,4"並存在repoIds欄位,之後query時因為repoIds的型態是List<Integer>
,就會套用return type為List<Integer>
的 stringToIntList
,把字串再轉回List成為查詢結果。
TypeConverter的部分過兩天講Room的資料關聯會再多說明,先往下進行。
3.建立DAO(Data Access Object)
DAO是包裝SQL語法的class,將CRUD都寫在這邊。
@Dao
public abstract class RepoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void insertRepos(List<Repo> repositories);
@Query("SELECT * FROM Repo WHERE id in (:repoIds)")
protected abstract List<Repo> loadById(List<Integer> repoIds);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void insert(RepoSearchResult result);
@Query("SELECT * FROM RepoSearchResult WHERE query = :query")
public abstract RepoSearchResult search(String query);
}
建立abstract class並標上@Dao
,裡面可以用@Insert
、@Update
、@Delete
以及@Query
寫SQL語法。
4.建立Database
這是最後步驟了,首先建立繼承RoomDatabase的abstract class:
@Database(entities = {RepoSearchResult.class, Repo.class}, version = 1)
public abstract class GithubDb extends RoomDatabase {
abstract public RepoDao repoDao();
}
@Database
處列出Entity和資料庫版本,class內容則須包含要用到的DAO。
接著要初始化,因為我們有使用Dagger所以可以寫在Module裡,如果沒有的話就寫在Application中。
@Module(includes = ViewModelModule.class)
class AppModule {
@Provides
@Singleton
GithubDb provideDb(GithubApp app) {
return Room.databaseBuilder(app, GithubDb.class,"github.db").build();
}
@Provides
@Singleton
RepoDao provideRepoDao(GithubDb db) {
return db.repoDao();
}
...
}
完成,可以開始用DAO來存取資料。
public void insertResult(RepoSearchResult result) {
repoDao.insert(result);
}
public RepoSearchResult searchLocal(String query) {
return repoDao.search(query);
}
不過這樣執行的話會報錯,為了避免資料讀寫造成UI卡頓,Room只允許在background thread進行資料讀寫,所以要用非同步的方式例如Handler或AsyncTask來執行。另外一個不建議使用的方式是在Room的builder加上allowMainThreadQueries()
,不過這樣就違背Room設計的本意了。
使用Room之後,我們的App現在有兩個資料來源:Remote API和Local database,明天會修改Model將兩者整合,讓使用者能最快速的取得資料。
GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day13-initialize-room
Reference:
7 Steps To Room