前言故事 自從團隊重新打造新 App 開始,小唯就四處查看是否有可用的新技術,Android Jetpack 中的 Room 就是其中之一(話說那時候好像還不是 Part of Jetpack)。Room 是 Google 官方前幾年推出來的技術,官方也建議開發者使用 Room 而不直接使用 SQLite APIs。小唯的團隊當下就開始使用 Room 來取代原本的 SQLite 程式碼,當然途中一定遇到了不少大小問題,也是小唯紀錄這幾篇的主因之一,希望避免再有人浪費時間在這些問題上。
建立 Room DB 要使用 Room,須先定義 database 中要包含哪些 table,裡面有哪些動作可以執行等,例:
AppDatabase.java 1 2 3 4 5 @Database(entities = {SearchHistoryEntity.class}, version = 1) @TypeConverters({RoomConverter.class}) public abstract class AppDatabase extends RoomDatabase { public abstract SearchHistoryDao searchHistoryDao () ; }
@Database(entities = {SearchHistoryEntity.class}, version = 1)
定義這個 database 內含有什麼 table,和 table 中有什麼欄位。
@TypeConverters({RoomConverter.class})
定義當 Room 遇到不能識別的類型,該要如何做轉換。
SearchHistoryDao
定義 database 支援什麼類型的 database 動作,像是新增、刪除、讀取,通常一個動作就是一個 sqlite statement。
定義 Entity 資料 Entity 將相關的資料欄位合併在一起,要比擬的話可以看做 sqlite 中的 table row 。先來看 SearchHistoryEntity.java
,這個 entity 定義了搜尋紀錄的資料,內容很直覺的。這邊有特地舉出使用三個非 sqlite primitive 的欄位:
SearchHistoryEntity.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 @Entity(tableName = "search_history") public class SearchHistoryEntity { public enum TYPE { TYPE_ONE, TYPE_TWO, TYPE_THREE, } @PrimaryKey(autoGenerate = true) private int uid; @ColumnInfo(name = "type") private TYPE type; @ColumnInfo(name = "display_text") private String displayText; @ColumnInfo(name = "query_string") private Map<String, String> queryString; @ColumnInfo(name = "date") private String date; public SearchHistoryEntity ( TYPE type, String displayText, Map<String, String> queryString, String date) { this .type = type; this .displayText = displayText; this .queryString = queryString; this .date = date; } public int getUid () { return uid; } public void setUid (int uid) { this .uid = uid; } public TYPE getType () { return type; } public void setType (TYPE type) { this .type = type; } public String getDisplayText () { return displayText; } public void setDisplayText (String displayText) { this .displayText = displayText; } public Map<String, String> getQueryString () { return queryString; } public void setQueryString (Map<String, String> queryString) { this .queryString = new HashMap<>(queryString); } public String getDate () { return date; } public void setDate (String date) { this .date = date; } }
DAO 定義資料操作方式 有了資料格式與轉換方式,接著需要的就是定義 DAO (Data Access Object),這步將原本 sqlite 中會使用的 sqlite statement 轉為 API。依照 Room 的方式,只需把 sqlite statement 放在 annotation 上:
SearchHistoryDao.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Dao public interface SearchHistoryDao { @Query("SELECT * FROM search_history order by timestamp desc") Flowable<List<SearchHistoryEntity>> getAll(); @Query("SELECT * FROM search_history WHERE uid IN (:ids) order by timestamp desc") Flowable<List<SearchHistoryEntity>> getByIds(int [] ids); @Query("SELECT * FROM search_history WHERE type = :type order by timestamp desc") Flowable<List<SearchHistoryEntity>> getByType(SearchHistoryEntity.TYPE type); @Query("SELECT * FROM search_history WHERE type = :type order by timestamp desc LIMIT :limit") Flowable<List<SearchHistoryEntity>> getByTypeWithLimit(SearchHistoryEntity.TYPE type, int limit); @Insert Long[] insertAll(SearchHistoryEntity... searchHistories); @Delete int delete (SearchHistoryEntity searchHistory) ; @Query("DELETE FROM search_history") int clearTable () ; }
使用 Converter 來互換資料類型 如果資料中有使用到一些非基本的資料類型,像是 Text、Integer、Numeric、Blob 等,就需要用 Converter 協助轉換程式中的物件到 Room DB 的資料結構(上述的那幾種),例如說,程式中可能直接使用 Date 物件,但如果要存到 Room DB 就需要轉換為像 2021-02-22T18:01Z 的 String。
來看看 RoomConverter.java 怎麼樣把 Map 轉 Json 存入、Enum 物件轉為 int 存入、還有 Date 物件轉為 ISO 8601 String 存入:
RoomConverter.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class RoomConverter { private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; @TypeConverter public static String mapToJson (Map<String, String> params) { return new JSONObject(params).toString(); } @TypeConverter public static Map<String, String> jsonToMap (String json) { if (TextUtils.isEmpty(json)) { return Collections.emptyMap(); } Gson gson = new Gson(); Type type = new TypeToken<Map<String, String>>() {}.getType(); return gson.fromJson(json, type); } @TypeConverter public static int searchHistoryTypeToInt (SearchHistoryEntity.TYPE type) { return type.ordinal(); } @TypeConverter public static SearchHistoryEntity.TYPE intToSearchHistoryType (int type) { return SearchHistoryEntity.TYPE.values()[type]; } @TypeConverter public static OffsetDateTime toOffsetDateTime (String offsetDateTime) { if (!TextUtils.isEmpty(offsetDateTime)) { return OffsetDateTime.from(dateTimeFormatter.parse(offsetDateTime)); } else { return null ; } } @TypeConverter public static String fromOffsetDateTime (OffsetDateTime dateTime) { return dateTimeFormatter.format(dateTime); } }
設定 sqlite trigger 以往用 sqlite 時,常會定義一些自動化 trigger 來調整資料內容,但是換作 Room 後,使用的方法就有點不太一樣。新的方式要定義在 AppDatabase.java
中,插入 callback 在 DB 建立時獲取通知並建立 trigger:
AppDatabase.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Database(entities = {SearchHistoryEntity.class}, version = 1) @TypeConverters({RoomConverter.class}) public abstract class AppDatabase extends RoomDatabase { public abstract SearchHistoryDao searchHistoryDao () ; public static AppDatabase getAppDatabase (Context context) { if (sInstance == null ) { RoomDatabase.Builder<AppDatabase> builder = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "userdata" ); builder.addCallback(callback); sInstance = builder.build(); } return sInstance; } private static Callback callback = new Callback() { @Override public void onCreate (@NonNull SupportSQLiteDatabase db) { super .onCreate(db); db.execSQL( "CREATE TRIGGER IF NOT EXISTS truncate_search_history AFTER INSERT ON search_history BEGIN " + "delete from search_history where timestamp < " + " (select timestamp from search_history order by timestamp desc limit 1 offset 200);" + "END;" ); } @Override public void onOpen (@NonNull SupportSQLiteDatabase db) { super .onOpen(db); } }; }
心得 小唯團隊開始使用 Room 時,那時技術還很新,很多功能支援不是很完善,像是不支援 prepolutate 資料庫。但這些日子下來,可以看到 Room 的支援度越來越好,不但加入了 Android Jetpack,更新也很頻繁。下次專案上如有需要,應該還是會推薦團隊使用。
------------- 本文结束 話說為什麼叫做 Room~ -------------