强大的模型缓存机制

在DBFlow库中,模型缓存被设计得非常简单,同时又具有高度可扩展性、可访问性和可用性。

ModelCache是一个接口,可被用于SQLite检索、FlowQueryListFlowCursorList或者任何需要缓存的地方。

在数据表中开启缓存

通过在@Table注解中设置cachingEnabled = true我们就可以很简单地开启缓存。在复合主键@PrimaryKey的表中,我们还需要定义一个@MultiCacheField对象(之后会进行讲解)

当我们检索数据库,模型数据对象将被缓存下来,DBFlow会高效地管理这些缓存。

这些是被支持的常见的缓存管理方式:

  1. ModelLruCache -> 使用LruCache算法,会按照既定的规则添加和释放缓存。
  2. SimpleMapCache -> 会被缓存到一个预定义大小的Map(默认为HashMap)
  3. SparseArrayBasedCache -> 底层是一个int->object的SparseArray稀疏数组。它用于缓存原生数字类型及其包装类(比如Integer、Double、Long等)、或者返回数字类型的MulitCacheConverter转换器。

NOTE: 如果你执行了SELECT操作,它将会在生成全部副本之前缓存部分数据,在这里推荐载入全部数据,因为缓存能解决大部分的性能问题。
(if you run a SELECT with columns specified, it may cache partial Model classes if they're loaded before the full counterparts. It is highly recommended to just load the full models in this case, since the caching mechanism will make up most efficiency problems.)

默认情况下,缓存使用的是SimpleMapCache,你可以在@Table注解中通过指定cacheSize()参数来设定默认的缓存大小。注意,当我们特别指定了自定义缓存后,这个参数将变得无效。

使用一个自定义缓存,只需要在我们的Model类中添加@ModelCacheField属性:

@ModelCacheField
public static ModelCache<CacheableModel3, ?> modelCache = new SimpleMapCache<>();

@ModelCacheField属性必须是public static

在3.0版本之后,BDFlow对于从缓存中载入的@ForeignKey会进行更加智能的处理。

缓存如何工作

  1. 每个@Tabel/Model类都有自己的缓存,表与表直接、子类与父类直接并不会共享缓存。
    1. 它们会拦截对数据库的检索操作,并将其中的数据缓存下来(后面会详解)
    2. 使用任何的InsertUpdateDelete方法时,其中的对象都会被直接缓存而不会更新(考虑到性能),。如果你想要强制刷新缓存则可以使用FlowManager.getModelAdapter(MyTable.class).getModelCache().clear()方法使缓存失效。
    3. 直接修改从缓存中得到的对象会导致缓存本身被直接修改,由于在使用save()insert()update()等方法之后对象数据才会被写入数据库,所以这可能会导致数据的不一致。

当我们执行一个检索操作,DBFlow将会:

  1. 运行检索语句,并从数据库得到一个Cursor的结果
  2. Cursor中取出主键的值。
  3. 如果这个键值在缓存中,则:
    1. 刷新关系数据(如@ForeignKey)。TIP:将关系所对应的表启动缓存能使这个操作变得更快。
    2. 返回缓存的对象
  4. 如果这个键值不存在,则说明此对象没有被缓存,这时我们会从数据库继续载入此对象的其他数据并将之缓存下来(直到该缓存失效)

复合主键的缓存处理

在3.0版本开始,DBFlow支持对复合主键的缓存。这需要Model类显式定义至少一个@MultiCacheField属性:

@Table(database = TestDatabase.class, cachingEnabled = true)
public class MultipleCacheableModel extends BaseModel {

    @MultiCacheField
    public static IMultiKeyCacheConverter<String> multiKeyCacheModel = new IMultiKeyCacheConverter<String>() {

        @Override
        @NonNull
        public String getCachingKey(@NonNull Object[] values) { // in order of the primary keys defined
            return "(" + values[0] + "," + values[1] + ")";
        }
    };

    @PrimaryKey
    double latitude;

    @PrimaryKey
    double longitude;

    @ForeignKey(references = {@ForeignKeyReference(columnName = "associatedModel",
            columnType = String.class, foreignKeyColumnName = "name", referencedFieldIsPackagePrivate = true)})
    TestModel1 associatedModel;

}

MultiCacheField里的返回值可以是任意类型,只要与你所定义ModelCache的Key类型一致即可。

FlowCursorList + FlowQueryList

FlowCursorListFlowQueryList使用独立的ModelCache缓存,并不与@Tabel/Model类的缓存通用。可以通过重写方法自定义它们的缓存方式:

@Override
protected ModelCache<? extends BaseCacheableModel, ?> getBackingCache() {
        return new MyCustomCache<>();
}

自定义缓存

当然,你甚至可以完全自定义自己的缓存算法。

比如,这是内置的LruCache缓存算法的代码:

public class ModelLruCache<ModelClass extends Model> extends ModelCache<ModelClass, LruCache<Long, ModelClass>>{

    public ModelLruCache(int size) {
        super(new LruCache<Long, ModelClass>(size));
    }

    @Override
    public void addModel(Object id, ModelClass model) {
        if(id instanceof Number) {
            synchronized (getCache()) {
                Number number = ((Number) id);
                getCache().put(number.longValue(), model);
            }
        } else {
            throw new IllegalArgumentException("A ModelLruCache must use an id that can cast to" +
                                               "a Number to convert it into a long");
        }
    }

    @Override
    public ModelClass removeModel(Object id) {
        ModelClass model;
        if(id instanceof Number) {
            synchronized (getCache()) {
                model = getCache().remove(((Number) id).longValue());
            }
        }  else {
            throw new IllegalArgumentException("A ModelLruCache uses an id that can cast to" +
                                               "a Number to convert it into a long");
        }
        return model;
    }

    @Override
    public void clear() {
        synchronized (getCache()) {
            getCache().evictAll();
        }
    }

    @Override
    public void setCacheSize(int size) {
        getCache().resize(size);
    }

    @Override
    public ModelClass get(Object id) {
        if(id instanceof Number) {
            return getCache().get(((Number) id).longValue());
        } else {
            throw new IllegalArgumentException("A ModelLruCache must use an id that can cast to" +
                                               "a Number to convert it into a long");
        }
    }
}