关于sqlite:Android-原生-SQLite-数据库的一次封装实践

49次阅读

共计 9736 个字符,预计需要花费 25 分钟才能阅读完成。

本文首发于 vivo 互联网技术 微信公众号 
链接:https://mp.weixin.qq.com/s/CL4MsQEsrWS8n7lhXCOQ_g
作者:Li Bingyan

本文次要讲述原生 SQLite 数据库的一次 ORM 封装实际,给应用原生数据库操作的业务场景(如:自身是一个 SDK)带来一些启发和参考意义,以及追随框架的实现思路对数据库操作、APT、泛型等概念更深一层的了解。

实现思路:通过动静代理获取申请接口参数进行 SQL 拼凑,并以接口返回值(泛型)类型的 RawType 和 ActualType 来适配调用形式和执行后果,以此将理论 SQL 操作封装在其外部来简化数据库操作的目标。

一、背景 

毫无疑问,对于 Android 数据库当初曾经有很多风行好用的 ORM 框架了,比方:Room、GreenDao、DBFlow 等都提供了简洁、易用的 API,尤其是谷歌开源的 Room 是目前最支流的框架。

既然曾经有了这么多数据库框架了,为什么还要入手封装所谓本人的数据库框架呢?对于一般 APP 的开发的确齐全不须要,这些框架中总有一款能够齐全满足你日常需要;但如果你是一个 SDK 开发者,而且业务是一个比拟依赖数据库操作的场景,如果限度不能依赖第三方 SDK(次要考量维护性、问题排查、稳定性、体积大小),那就不得不本人去写原生 SQLite 操作了,这将是一个既繁琐又容易出错的过程(数据库降级 / 降级 / 关上 / 敞开、多线程状况、拼凑 SQL 语句、ContentValues 插数据、游标遍历 / 敞开、Entity 转换等)。

为了在 SDK 的开发场景中防止上述繁琐且容易出错的问题,于是就有了接下来的一系列思考和革新。

二、预期目标

  1. 能简化原生的增删改查简短操作,不要再去写容易出错的两头逻辑步骤
  2. 主动生成数据库的建表、降级 / 降级逻辑
  3. 易用的调用接口(反对同步 / 异步、线程切换)
  4. 稳固牢靠,无性能问题

三、计划调研

察看咱们日常业务代码能够发现:一次数据库查问与一次网络申请在流程上是极为类似的,都是通过结构申请、发动申请、两头步骤、获取后果、处理结果等几个步骤。因而感觉能够将数据库操作以网络申请的形式进行形象和封装,其具体比照如下表所示:

通过上述相似性的比照并综合现有 ORM 框架来思考切入口,首先想到的是应用注解:

支流 Room 应用的是 编译时注解(更有利于性能),但在具体编码实现 Processor 过程中发现增删改查操作的出参和入参解决有点过于繁琐(参考 Room 实现),不太实用于自身就是一个 SDK 的场景,最终 pass 掉了。

运行时注解 解决绝对更简略一些(接口和参数较容易适配、解决流程也能够间接写咱们相熟的安卓原生代码),而且后面曾经有了赫赫有名的网络申请库 Retrofit 应用运行时注解实现网络申请的典型范例,因而能够依葫芦画瓢尝试实现一下数据库增删改查操作,也是本次革新最终的实现计划。

置信大部分安卓客户端开发同学都用过 Retrofit(网络申请罕用库),其大略原理是:应用动静代理获取接口对应的 Method 对象为入口,并通过该 Method 对象的各种参数(注解润饰)结构出 Request 对象抛给 okhttp 做理论申请,返回值则通过 Conveter 和 Adapter 适配申请后果(bean 对象)和调用形式,如:Call<List<Bean>>、Observable<List<Bean>> 等。

它以这种形式将网络申请的外部细节封装起来,极大简化了网络申请过程。依据其相似性,数据库操作(增删改查)也能够应用这个机制来进一步封装。

对于数据库的建表、降级、降级等这些容易出错的步骤,最好是不要让使用者本人去手动写这部分逻辑,计划应用编译时注解来实现(Entitiy 类和字段属性、版本号通过注解对应起来),在编译期间主动生成 SQLiteOpenHelper 的实现类。

综合以上两局部根本实现了所有痛点操作不再须要调用者去关注(只需关注传参和返回后果),于是将其独立成一个数据库模块,取名 Sponsor([ˈspɑːnsər] ),寓意一种散发器或调度器计划,目前已在团队外部应用。

四、Sponsor 调用示例

1、Entity 定义:

//Queryable: 示意一个可查问的对象,有办法 bool convert(Cursor cursor),将 cursor 转换为 Entitiy
//Insertable: 示意一个可插入的对象,有办法 ContentValues convert(),将 Entitiy 转换为 ContentValues
public class FooEntity implements Queryable, Insertable {
    /**
     * 数据库自增 id
     */
    private int id;

    /**
     * entitiy id
     */
    private String fooId;

    /**
     * entity 内容
     */
    private String data;
  
    // 其余属性
  
   //getter()/setter()
}

2、接口定义,申明增删改查接口:

/**
 * 插入
 * @return 最初一个 row Id
 */
@Insert(tableName = FooEntity.TABLE)
Call<Integer> insertEntities(List<FooEntity> entities);

/**
 * 查问
 * @return 获取的 entitiy 列表
 */
@Query("SELECT * FROM" + FooEntity.TABLE + "WHERE" + FooEntity.CREATE_TIME + ">"
        + Parameter1.NAME + "AND" + FooEntity.CREATE_TIME + "<" + Parameter2.NAME
        + "ORDER BY" + FooEntity.CREATE_TIME + "ASC LIMIT" + Parameter3.NAME)
Call<List<FooEntity>> queryEntitiesByRange(@Parameter1 long start, @Parameter2 long end, @Parameter3 int limit);


/**
 * 删除
 * @return 删除记录的条数
 */
@Delete(tableName = FooEntity.TABLE, whereClause = FooEntity.ID + ">="
        + Parameter1.NAME + "AND" + FooEntity.ID + "<=" + Parameter2.NAME)
Call<Integer> deleteByIdRange(@Parameter1 int startId, @Parameter2 int endId);

3、创立 FooService 实例:

Sponsor sponsor = new Sponsor.Builder(this)
        .allowMainThreadQueries() // 是否运行在主线程操作,默认不容许
        //.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //rxjava
        //.addCallAdapterFactory(Java8CallAdapterFactory.create()) //java8
        //.addCallAdapterFactory(LiveDataCallAdapterFactory.create()) //livedata
        .logger(new SponsorLogger()) // 日志输入
        .build();

// 调用 create()办法创立 FooService 实例,实际上是返回了 FooService 的动静代理对象
FooService mFooService = sponsor.create(FooService.class);

4、插入 Entitiy 数据:

// 结构 Entity 列表
List<FooEntity> entities = new ArrayList<>();
//add entities

// 同步形式
//rowId 为最终的自增 id(同原生 insert 操作返回值)//final int rowId = mFooService.insertEntities(entities).execute();

// 异步形式
mFooService.insertEntities(entities).enqueue(new Callback<Integer>() {
    @Override
    public void onResponse(Call<Integer> call, Integer rowId) {//success}

    @Override
    public void onFailure(Call<Integer> call, Throwable t) {//failed}
});

5、查问参数指定数据库记录,并转换为 Entitiy 对象列表:

List<FooEntity> entities;

//entities 为查问后果汇合
entities = mFooService.queryEntitiesByRange(1, 200, 100).execute();

6、删除参数指定数据库记录,返回总共删除的记录条数:

//cout 为删除的条数
int count = mFooService.deleteByIdRange(0, 100).execute();

注:

  1. 以上所有操作都反对依据具体的场景进行同步 / 异步调用。
  2. 增、删、改操作的 Call<?> 返回值参数(泛型参数)还能够间接指定为 Throwable,如果外部异样能够通过它返回,胜利则为空

五、外围实现点

基本原理仍是借鉴了 Retrofit 框架的实现,通过动静代理拿到 Method 对象的各种参数进行 SQL 拼凑,并通过 Converter 和 Adapter 适配执行后果,整体框架有如下几 module 形成:

  • sponsor: 主体实现
  • sponsor_annotaiton: 注解定义,包含运行时注解和编译时注解
  • sponsor_compiler: 数据库建表、降级 / 降级等逻辑的 Processor 实现
  • sponsor_java8、sponsor_livedata、sponsor_rxjava2:适配几种支流的调用形式

1、动静代理入口

public <T> T create(final Class<T> daoClass, final Class<? extends DatabaseHelper> helperClass) {final Object obj = Proxy.newProxyInstance(daoClass.getClassLoader(), new Class<?>[]{daoClass},
            new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (method.getDeclaringClass() == Object.class) {return method.invoke(this, args);
            }
            DaoMethod<Object, Object> daoMethod =
                    (DaoMethod<Object, Object>) loadDaoMethod(method);

            final DatabaseHelper helper = loadDatabaseHelper(daoClass, helperClass);
            Call<Object> call = new RealCall<>(helper, mDispatcher, mAllowMainThreadQueries,
                    mLogger, daoMethod, args);

            return daoMethod.adapt(call);
        }
    });
    return (T) obj;
}

2、接口适配

因为动静代理会返回接口的 Method 对象和参数列表 args[],能够通过这两个参数拿到上述标识的所有元素,具体方法如下所示:

获取办法的注解: method.getAnnotations()
获取形参列表:已传过来
获取参数注解和类型:method.getParameterAnnotations() method.getGenericParameterTypes()
获取调用形式:method.getGenericReturnType()后,再调用 Type.getRawType() //Call
获取后果类型:method.getGenericReturnType()后,再调用 Type.getActualTypeArguments() //List<FooEntitiy>

3、返回后果适配

private Converter<Response, ?> createQueryConverter(Type responseType, Class<?> rawType) {
    Converter<Response, ?> converter = null;
    if (Queryable.class.isAssignableFrom(rawType)) { // 返回单个实体对象
        // 其余解决逻辑
        converter = new QueryableConverter((Class<? extends Queryable>) responseType);
    } else if (rawType == List.class) { // 返回一个实体列表
        // 其余解决逻辑
        converter = new ListQueryableConverter((Class<? extends Queryable>) argumentsTypes[0]);
    } else if (rawType == Integer.class) {// 兼容 SELECT COUNT(*) FROM table 的模式
        converter = new IntegerConverter();} else if (rawType == Long.class) {converter = new LongConverter();
    }
    return converter;
}

ListQueryableConverter 实现,次要是遍历 Cursor 构建返回后果列表:

static final class ListQueryableConverter implements Converter<Response, List<? extends Queryable>> {

    @Override
    public List<? extends Queryable> convert(Response value) throws IOException {
        List<Queryable> entities = null;
        Cursor cursor = value.getCursor();
        if (cursor != null && cursor.moveToFirst()) {entities = new ArrayList<>(cursor.getCount());
            try {
                do {
                    try {
                        // 反射创立 entitiy 对象
                        Queryable queryable = convertClass.newInstance();
                        final boolean convert = queryable.convert(cursor);
                        if (convert) {entities.add(queryable);
                        }
                    } catch (Exception e) {e.printStackTrace();
                    }
                } while (cursor.moveToNext());
            } catch (Exception e) {e.printStackTrace();
            }
        }
        /*
         * 防止返回 null
         */
        if (entities == null) {entities = Collections.emptyList();
        }
        return entities;
    }
}

4、执行增删改查操作


final class RealCall<T> implements Call<T> {

    @Override
    public T execute() {
        /**
         * 理论的增删改查操作
         */
        Response response = perform();

        T value = null;
        try {value = mDaoMethod.toResponse(response);
        } catch (Exception e) {e.printStackTrace();
        } finally {
            // 游标敞开
            if (response != null) {Cursor cursor = response.getCursor();
                if (cursor != null) {
                    try {cursor.close();
                    } catch (Exception e) {e.printStackTrace();
                    }
                }
            }
            // 数据库敞开
            if (mDatabaseHelper != null) {
                try {mDatabaseHelper.close();
                } catch (Exception e) {e.printStackTrace();
                }
            }
        }
        return value;
    }

    /**
     * 具体数据库操作方法
     * @return
     */
    private Response perform() {switch (mDaoMethod.getAction()) {
            case Actions.QUERY: {
                //..
              Cursor cursor = query(String sql);
            }
            case Actions.DELETE: {
               //...
              int count =  delete(simple, sql, tableName, whereClause);
            }
            case Actions.INSERT: {//...}
            case Actions.UPDATE: {//...}
        }
        return null;
    }

    /**
     * 具体的查问操作
     */
    private Cursor query(String sql) {
        //...

        SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        final Cursor cursor = db.rawQuery(sql, null);

        //...
        return cursor;
    }

    /**
     * 具体的删除操作
     */
    private int delete(boolean simple, String sql, String tableName, String whereClause) {SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        int result = 0;
        try {db.beginTransaction();
            //...
            result = db.delete(tableName, whereClause, null);
          
            db.setTransactionSuccessful();} finally {
            try {db.endTransaction();
            } catch (Throwable t) {t.printStackTrace();
            }
        }
        return result;
    }
}

六、性能测试比照

  • 测试手机:vivo X23
  • 安卓版本:Android 9
  • 处理器:骁龙 670,2.0GHz,8 核
  • 测试方法:每个比照项测试 5 组数据,每组 5 轮测试,而后取平均值(四舍五入)

阐明:

  1. 表中第 4 条测试(查出全副 10w 条数据)差别较大(相差 79ms),其起因是原生接口的 Entity 对象是间接 new 进去的,而 sponsor 外部只能通过 Entity 的 newInstance()接口去反射创立,导致了性能差距,但均匀算下来,每 newInstance()创立 1000 个对象才多了 1ms,影响还是很小的。(尝试应用 Clone 的形式优化,但成果仍不显著)
  2. sponsor 形式性能均略低于原生形式,起因是其须要动静拼凑 SQL 语句的性能耗费,但耗费极少。

七、在我的项目(SDK)中的利用实际

该我的项目外部应用的数据库是一个多库多表的架构,数据库操作(增删改查、建表、降级 / 降级等)均是调用 SQLiteOpenHelper 原生接口写的代码逻辑,导致相干操作须要写很多的模板代码能力拿到最终后果,逻辑比拟简短;因而,在重构版本咱们应用 sponsor 替换掉了这些原生调用,以此简化这些繁琐易出错操作。目前运行良好,暂没有发现显著重大问题。

八、扩大常识——泛型的类型擦除

对于类型擦除,感觉很多人都有一些误区,特地是客户端开发平时波及较少,感觉都不太了解:

依据咱们的常识都晓得 Java 的泛型在运行时是类型擦除的,编译后就不会有具体的类型信息了(都是 Object 或者某个上界类型)。

那么问题来了,既然类型都擦除了,那 retrofit 又是怎么能在运行时拿到办法泛型参数类型(包含参数类型和返回类型)的呢?比方外部能够依据函数的返回类型将 json 转为对应 bean 对象。

起先也很难了解,于是通过查找材料、技术群交换、写 demo 验证后才根本弄明确,总结为一句话:类型擦除其实只是把泛型的形参擦除了(不便和 1.5 以下版本兼容),原始的字节码中还是会保留类构造(类、办法、字段)的泛型类型信息,具体保留在 Signature 区域,能够应用 Type 的子类接口在运行时获取到泛型的类型信息。

1、retrofit 申请接口个别定义如下:

 

能够看到这个函数的返回类型和参数类型都带有泛型参数。

2、反编译这个 apk,并用 JD-GUI 工具关上能够找到对应办法如下:

很多人看到这里会感觉 泛型的类型信息的确曾经被齐全革除了。不过这个工具只是展现了简略的类构造信息(仅蕴含类、函数、字段)而已,咱们能够更进一步看一下该类对应的字节码来确认下,间接应用 AS 关上 apk,开展 classes.dex 找到对应类,右键 ->”Show ByteCode” 查看:

能够看到在 Signature 区域保留了这个办法的所有参数信息,其中就有泛型的类型信息。

任何类、接口、结构器办法或字段的申明如果蕴含了泛型类型,则会生成 Signature 属性,为它记录泛型签名信息,不过函数内的局部变量泛型信息将不会被记录下来。

3、上面看一下 Type 接口的继承关系,以及提供的接口性能:

Class:最常见的类型,一个 Class 类的对象示意虚拟机中的一个类或接口。

ParameterizedType:示意是参数化类型,如:List<String>、Map<Integer,String> 这种带有泛型的类型,罕用办法有:

  1. Type getRawType()——返回参数化类型中的原始类型,例如 List<String> 的原始类型为 List。
  2. Type[] getActualTypeArguments()——获取参数化类型的类型变量或是理论类型列表,如 Map<Integer, String> 的理论泛型列表是 Integer 和 String。

TypeVariable:示意的是类型变量,如 List<T> 中的 T 就是类型变量。

GenericArrayType:示意是数组类型且组成元素是 ParameterizedType 或 TypeVariable,例如 List<T> 或 T[],罕用办法有:

  1. Type getGenericComponentType()一个办法,它返回数组的组成元素类型。

WildcardType:示意通配符类型,例如? extends Number 和 ? super Integer。罕用办法有:

  1. Type[] getUpperBounds()——返回类型变量的上边界。
  2. Type[] getLowerBounds()——返回类型变量的下边界。

九、参考资料

  1. https://github.com/square/retrofit
  2. https://cs.android.com/androidx/platform/frameworks/support/+/android-room-release:room/compiler/src/main/kotlin/androidx/room/processor/
  3. https://techblog.bozho.net/on-java-generics-and-erasure/
  4. https://blog.csdn.net/u011983531/article/details/80295479

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:Labs2020 分割。

正文完
 0