RxCache 整合 Android 的持久層框架 greenDAO、Room

一. 背景
ofollow,noindex">RxCache 是一個支援 Java 和 Android 的 Local Cache 。
之前的文章 給 Java 和 Android 構建一個簡單的響應式Local Cache 曾詳細介紹過它。
RxCache 包含了兩級快取: Memory 和 Persistence 。
下圖是 rxcache-core 模組的 uml 類圖

二. 持久層
RxCache 的持久層包括 Disk、DB,分別單獨抽象了 Disk、DB 介面並繼承 Persistence。
DB 介面:
package com.safframework.rxcache.persistence.db; import com.safframework.rxcache.persistence.Persistence; /** * Created by tony on 2018/10/14. */ public interface DB extends Persistence { } 複製程式碼
在 RxCache 的持久層,嘗試整合 Android 常用的持久層框架。
2.1 整合 greenDAO
greenDAO 是一款開源的面向 Android 的輕便、快捷的 ORM 框架,將 Java 物件對映到 SQLite 資料庫。
首先,建立一個快取實體 CacheEntity ,它包含 id、key、data、timestamp、expireTime。其中 data 是待快取的物件並轉換成 json 字串。
@Entity public class CacheEntity { @Id(autoincrement = true) private Long id; public String key; public String data;// 物件轉換的 json 字串 public Long timestamp; public Long expireTime; ...... // getter 、setter } 複製程式碼
建立一個單例的 DBService ,並提供返回 CacheEntityDao 的方法。其實,crud 的邏輯也可以放在此處。
public class DBService { private static final String DB_NAME = "cache.db"; private static volatile DBService defaultInstance; private DaoSession daoSession; private DBService(Context context) { DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(context, DB_NAME); DaoMaster daoMaster = new DaoMaster(helper.getWritableDatabase()); daoSession = daoMaster.newSession(); } public static DBService getInstance(Context context) { if (defaultInstance == null) { synchronized (DBService.class) { if (defaultInstance == null) { defaultInstance = new DBService(context.getApplicationContext()); } } } return defaultInstance; } public CacheEntityDao getCacheEntityDao(){ return daoSession.getCacheEntityDao(); } } 複製程式碼
建立 GreenDAOImpl 實現 DB 介面,實現真正的快取邏輯。
import com.safframework.rxcache.config.Constant; import com.safframework.rxcache.domain.Record; import com.safframework.rxcache.domain.Source; import com.safframework.rxcache.persistence.converter.Converter; import com.safframework.rxcache.persistence.converter.GsonConverter; import com.safframework.rxcache.persistence.db.DB; import com.safframework.tony.common.utils.Preconditions; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; /** * @FileName: com.safframework.rxcache4a.persistence.db.greendao.GreenDAOImpl * @author: Tony Shen * @date: 2018-10-15 11:50 * @version: V1.0 <描述當前版本功能> */ public class GreenDAOImpl implements DB { private CacheEntityDao dao; private Converter converter; public GreenDAOImpl(CacheEntityDao dao) { this(dao,new GsonConverter()); } public GreenDAOImpl(CacheEntityDao dao, Converter converter) { this.dao = dao; this.converter = converter; } @Override public <T> Record<T> retrieve(String key, Type type) { CacheEntity entity = dao.queryBuilder().where(CacheEntityDao.Properties.Key.eq(key)).unique(); if (entity==null) return null; long timestamp = entity.timestamp; long expireTime = entity.expireTime; T result = null; if (expireTime<0) { // 快取的資料從不過期 String json = entity.data; result = converter.fromJson(json,type); } else { if (timestamp + expireTime > System.currentTimeMillis()) {// 快取的資料還沒有過期 String json = entity.data; result = converter.fromJson(json,type); } else {// 快取的資料已經過期 evict(key); } } return result != null ? new Record<>(Source.PERSISTENCE, key, result, timestamp, expireTime) : null; } @Override public <T> void save(String key, T value) { save(key,value, Constant.NEVER_EXPIRE); } @Override public <T> void save(String key, T value, long expireTime) { if (Preconditions.isNotBlanks(key,value)) { CacheEntity entity = new CacheEntity(); entity.setKey(key); entity.setTimestamp(System.currentTimeMillis()); entity.setExpireTime(expireTime); entity.setData(converter.toJson(value)); dao.save(entity); } } @Override public List<String> allKeys() { List<CacheEntity> list = dao.loadAll(); List<String> result = new ArrayList<>(); for (CacheEntity entity:list) { result.add(entity.key); } return result; } @Override public boolean containsKey(String key) { List<String> keys = allKeys(); return Preconditions.isNotBlank(keys) ? keys.contains(key) : false; } @Override public void evict(String key) { CacheEntity entity = dao.queryBuilder().where(CacheEntityDao.Properties.Key.eq(key)).unique(); if (entity!=null) { dao.delete(entity); } } @Override public void evictAll() { dao.deleteAll(); } } 複製程式碼
2.2 整合 Room
Room 是 Google 開發的一個 SQLite 物件對映庫。 使用它來避免樣板程式碼並輕鬆地將 SQLite 資料轉換為 Java 物件。 Room 提供 SQLite 語句的編譯時檢查,可以返回 RxJava 和 LiveData Observable。
同樣,需要先建立一個 CacheEntity,但是不能共用之前的 CacheEntity。因為 Room、greenDAO 使用的 @Entity
不同。
@Entity public class CacheEntity { @PrimaryKey(autoGenerate = true) private Long id; public String key; public String data;// 物件轉換的 json 字串 public Long timestamp; public Long expireTime; ...... // getter 、setter } 複製程式碼
建立一個 CacheEntityDao 用於 crud 的實現。
import java.util.List; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import static androidx.room.OnConflictStrategy.IGNORE; /** * @FileName: com.safframework.rxcache4a.persistence.db.room.CacheEntityDao * @author: Tony Shen * @date: 2018-10-15 16:44 * @version: V1.0 <描述當前版本功能> */ @Dao public interface CacheEntityDao { @Query("SELECT * FROM cacheentity") List<CacheEntity> getAll(); @Query("SELECT * FROM cacheentity WHERE `key` = :key LIMIT 0,1") CacheEntity findByKey(String key); @Insert(onConflict = IGNORE) void insert(CacheEntity entity); @Delete void delete(CacheEntity entity); @Query("DELETE FROM cacheentity") void deleteAll(); } 複製程式碼
建立一個 AppDatabase 表示一個數據庫的持有者。
import androidx.room.Database; import androidx.room.RoomDatabase; /** * @FileName: com.safframework.rxcache4a.persistence.db.room.AppDatabase * @author: Tony Shen * @date: 2018-10-15 16:40 * @version: V1.0 <描述當前版本功能> */ @Database(entities = {CacheEntity.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract CacheEntityDao cacheEntityDao(); } 複製程式碼
最後,建立 RoomImpl 實現 DB 介面,實現真正的快取邏輯。
import android.content.Context; import com.safframework.rxcache.config.Constant; import com.safframework.rxcache.domain.Record; import com.safframework.rxcache.domain.Source; import com.safframework.rxcache.persistence.converter.Converter; import com.safframework.rxcache.persistence.converter.GsonConverter; import com.safframework.rxcache.persistence.db.DB; import com.safframework.tony.common.utils.Preconditions; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import androidx.room.Room; /** * @FileName: com.safframework.rxcache4a.persistence.db.room.RoomImpl * @author: Tony Shen * @date: 2018-10-15 16:46 * @version: V1.0 <描述當前版本功能> */ public class RoomImpl implements DB { private AppDatabase db; private Converter converter; private static final String DB_NAME = "cache"; public RoomImpl(Context context) { this(context,new GsonConverter()); } public RoomImpl(Context context, Converter converter) { this.db = Room.databaseBuilder(context, AppDatabase.class, DB_NAME).build(); this.converter = converter; } @Override public <T> Record<T> retrieve(String key, Type type) { CacheEntity entity = db.cacheEntityDao().findByKey(key); if (entity==null) return null; long timestamp = entity.timestamp; long expireTime = entity.expireTime; T result = null; if (expireTime<0) { // 快取的資料從不過期 String json = entity.data; result = converter.fromJson(json,type); } else { if (timestamp + expireTime > System.currentTimeMillis()) {// 快取的資料還沒有過期 String json = entity.data; result = converter.fromJson(json,type); } else {// 快取的資料已經過期 evict(key); } } return result != null ? new Record<>(Source.PERSISTENCE, key, result, timestamp, expireTime) : null; } @Override public <T> void save(String key, T value) { save(key,value, Constant.NEVER_EXPIRE); } @Override public <T> void save(String key, T value, long expireTime) { if (Preconditions.isNotBlanks(key,value)) { CacheEntity entity = new CacheEntity(); entity.setKey(key); entity.setTimestamp(System.currentTimeMillis()); entity.setExpireTime(expireTime); entity.setData(converter.toJson(value)); db.cacheEntityDao().insert(entity); } } @Override public List<String> allKeys() { List<CacheEntity> list = db.cacheEntityDao().getAll(); List<String> result = new ArrayList<>(); for (CacheEntity entity:list) { result.add(entity.key); } return result; } @Override public boolean containsKey(String key) { List<String> keys = allKeys(); return Preconditions.isNotBlank(keys) ? keys.contains(key) : false; } @Override public void evict(String key) { CacheEntity entity = db.cacheEntityDao().findByKey(key); if (entity!=null) { db.cacheEntityDao().delete(entity); } } @Override public void evictAll() { db.cacheEntityDao().deleteAll(); } } 複製程式碼
這兩種整合方式,都使用 CacheEntity 的 data 來儲存物件轉換後的 json 字串。使用這種方式,可以替換成任何的持久層框架。使得 DB 也可以成為 RxCache 的其中一級快取。
三. 使用
編寫單元測試,看一下整合 greenDAO 的效果。
分別測試多種物件的儲存、帶 ExpireTime 的儲存。
import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import com.safframework.rxcache.RxCache; import com.safframework.rxcache.domain.Record; import com.safframework.rxcache4a.persistence.db.greendao.CacheEntityDao; import com.safframework.rxcache4a.persistence.db.greendao.DBService; import com.safframework.rxcache4a.persistence.db.greendao.GreenDAOImpl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; /** * @FileName: com.safframework.rxcache4a.GreenDAOImplTest * @author: Tony Shen * @date: 2018-10-15 18:51 * @version: V1.0 <描述當前版本功能> */ @RunWith(AndroidJUnit4.class) public class GreenDAOImplTest { Context appContext; DBService dbService; @Before public void setUp() { appContext = InstrumentationRegistry.getTargetContext(); dbService = DBService.getInstance(appContext); } @Test public void testWithObject() { CacheEntityDao dao = dbService.getCacheEntityDao(); GreenDAOImpl impl = new GreenDAOImpl(dao); impl.evictAll(); RxCache.config(new RxCache.Builder().persistence(impl)); RxCache rxCache = RxCache.getRxCache(); Address address = new Address(); address.province = "Jiangsu"; address.city = "Suzhou"; address.area = "Gusu"; address.street = "ren ming road"; User u = new User(); u.name = "tony"; u.password = "123456"; u.address = address; rxCache.save("user",u); Record<User> record = rxCache.get("user", User.class); assertEquals(u.name, record.getData().name); assertEquals(u.password, record.getData().password); assertEquals(address.city, record.getData().address.city); rxCache.save("address",address); Record<Address> record2 = rxCache.get("address", Address.class); assertEquals(address.city, record2.getData().city); } @Test public void testWithExpireTime() { CacheEntityDao dao = dbService.getCacheEntityDao(); GreenDAOImpl impl = new GreenDAOImpl(dao); impl.evictAll(); RxCache.config(new RxCache.Builder().persistence(impl)); RxCache rxCache = RxCache.getRxCache(); User u = new User(); u.name = "tony"; u.password = "123456"; rxCache.save("test",u,2000); try { Thread.sleep(2500); } catch (InterruptedException e) { e.printStackTrace(); } Record<User> record = rxCache.get("test", User.class); assertNull(record); } } 複製程式碼
兩個 test case 都順利通過,表示整合 greenDAO 沒有問題。當然,整合 Room 也是一樣。