A background.

RxCache is a Local Cache that supports Java and Android.

It was covered in detail in the previous article building a simple responsive Local Cache for Java and Android.

RxCache contains two levels of caching: Memory and Persistence.

Below is a UML class diagram for the RxCache-core module

Durability layer

RxCache’s Persistence layer includes Disk and DB, which abstracts Disk and DB interfaces separately and inherits Persistence.

The DB interface:

package com.safframework.rxcache.persistence.db;

import com.safframework.rxcache.persistence.Persistence;

/** * Created by tony on 2018/10/14. */
public interface DB extends Persistence {}Copy the code

In RxCache persistence layer, try to integrate the common Persistence layer framework of Android.

2.1 integrated greenDAO

GreenDAO is an open source, lightweight, fast ORM framework for Android that maps Java objects to SQLite databases.

First, create a CacheEntity, CacheEntity, which contains id, key, data, timestamp, expireTime. Where data is the object to be cached and converted to a JSON string.

@Entity
public class CacheEntity {

    @Id(autoincrement = true)
    private Long id;

    public String key;

    public String data;// Json string for object conversion

    public Long timestamp;

    publicLong expireTime; ./ / getter and setter
}
Copy the code

Create a singleton DBService with a method to return CacheEntityDao. In fact, the LOGIC of CRUD can also be placed here.

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 = newDBService(context.getApplicationContext()); }}}return defaultInstance;
    }

    public CacheEntityDao getCacheEntityDao(a){
        returndaoSession.getCacheEntityDao(); }}Copy the code

Create GreenDAOImpl to implement the DB interface and implement the real cache logic.

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: the 2018-10-15 11:50 *@version: V1.0 < describes current version features > */
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) { // Cached data never expires

            String json = entity.data;

            result = converter.fromJson(json,type);
        } else {

            if (timestamp + expireTime > System.currentTimeMillis()) {  // The cached data has not expired

                String json = entity.data;

                result = converter.fromJson(json,type);
            } else {        // The cached data has expiredevict(key); }}returnresult ! =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 = newCacheEntity(); entity.setKey(key); entity.setTimestamp(System.currentTimeMillis()); entity.setExpireTime(expireTime); entity.setData(converter.toJson(value)); dao.save(entity); }}@Override
    public List<String> allKeys(a) {

        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(a) { dao.deleteAll(); }}Copy the code

2.2 integrated Room

Room is a SQLite object mapping library developed by Google. Use it to avoid boilerplate code and easily convert SQLite data into Java objects. Room provides compile-time checking of SQLite statements, returning RxJava and LiveData Observable.

Again, a CacheEntity needs to be created first, but the previous CacheEntity cannot be shared. Because Room and greenDAO use different @Entity.

@Entity
public class CacheEntity {

    @PrimaryKey(autoGenerate = true)
    private Long id;

    public String key;

    public String data;// Json string for object conversion

    public Long timestamp;

    publicLong expireTime; ./ / getter and setter
}
Copy the code

Create a CacheEntityDao for cruD implementation.

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: 16:44 * 2018-10-15@version: V1.0 < describes current version features > */
@Dao
public interface CacheEntityDao {

    @Query("SELECT * FROM cacheentity")
    List<CacheEntity> getAll(a);

    @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(a);
}
Copy the code

Create an AppDatabase to represent a database holder.

import androidx.room.Database;
import androidx.room.RoomDatabase;

/ * * *@FileName: com.safframework.rxcache4a.persistence.db.room.AppDatabase
 * @author: Tony Shen
 * @date: the ephod * 2018-10-15@version: V1.0 < describes current version features > */
@Database(entities = {CacheEntity.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {

    public abstract CacheEntityDao cacheEntityDao(a);
}
Copy the code

Finally, create RoomImpl to implement the DB interface to implement the real cache logic.

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: now * 2018-10-15@version: V1.0 < describes current version features > */
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) { // Cached data never expires

            String json = entity.data;

            result = converter.fromJson(json,type);
        } else {

            if (timestamp + expireTime > System.currentTimeMillis()) {  // The cached data has not expired

                String json = entity.data;

                result = converter.fromJson(json,type);
            } else {        // The cached data has expiredevict(key); }}returnresult ! =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 = newCacheEntity(); entity.setKey(key); entity.setTimestamp(System.currentTimeMillis()); entity.setExpireTime(expireTime); entity.setData(converter.toJson(value)); db.cacheEntityDao().insert(entity); }}@Override
    public List<String> allKeys(a) {

        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(a) { db.cacheEntityDao().deleteAll(); }}Copy the code

In both cases, CacheEntity’s data is used to store converted JSON strings of objects. In this way, you can replace it with any persistence layer framework. DB can also be one of the RxCache level cache.

3.

Write unit tests to see the effect of integrating greenDAO.

Test storage of multiple objects and storage with ExpireTime separately.

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: the 2018-10-15 18:51 *@version: V1.0 < describes current version features > */
@RunWith(AndroidJUnit4.class)
public class GreenDAOImplTest {

    Context appContext;
    DBService dbService;

    @Before
    public void setUp(a) {
        appContext = InstrumentationRegistry.getTargetContext();
        dbService = DBService.getInstance(appContext);
    }

    @Test
    public void testWithObject(a) {

        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(a) {

        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); }}Copy the code

Both test cases passed smoothly, indicating that there is no problem in integrating greenDAO. The same goes for integrating Room, of course.

4. To summarize

I created a separate project, RxCache4a, for integrating greenDAO, Room, etc.

Github address: github.com/fengzhizi71…

In the future, we might add annotations to the framework, as well as algorithms for Cache cleaning.

RxCache series

  1. ReentrantReadWriteLock Read/write lock and its use in RxCache
  2. Off-heap memory and its use in RxCache
  3. Retrofit style RxCache and its various cache replacement algorithms
  4. Build a simple responsive Local Cache for Java and Android

Java and Android technology stack: update and push original technical articles every week, welcome to scan the qr code of the public account below and pay attention to, looking forward to growing and progress with you together.