今天要介紹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