本文主要讲述原生SQLite数据库的一次ORM封装实践 给使用原生数据库操作的业务场景 如:本身是一个SDK
带来一些启示和参考意义以及跟随框架的实现思路对数据库操作 APT 泛型等概念更深一层的理解。
实现思路:通过动态代理获取请求接口参数进行SQL拼凑 并以接口返回值 泛型 类型的RawType和ActualType来
适配调用方式和执行结果以此将实际SQL操作封装在其内部来简化数据库操作的目的。
一、背景
毫无疑问 关于Android数据库现在已经有很多流行好用的ORM框架了 比如:Room GreenDao DBFlow
等都提供了简洁 易用的API 尤其是谷歌开源的Room是目前最主流的框架。
既然已经有了这么多数据库框架了 为什么还要动手封装所谓自己的数据库框架呢?对于普通 APP 的开发确实完全不需要 这些框
架中总有一款可以完全满足你日常需求;但如果你是一个SDK开发者 而且业务是一个比较依赖数据库操作的场景 如果限制不能依
赖第三方SDK(主要考量维护性 问题排查 稳定性 体积大小),那就不得不自己去写原生SQLite操作了 这将是一个既繁琐又容易
出错的过程 数据库升级/降级/打开/关闭 多线程情况 拼凑SQL语句 ContentValues插数据 游标遍历/关闭 Entity转换等。
为了在SDK的开发场景中避免上述繁琐且容易出错的问题 于是就有了接下来的一系列思考和改造。
二、预期目的
•能简化原生的增删改查冗长操作,不要再去写容易出错的中间逻辑步骤
•自动生成数据库的建表、升级/降级逻辑
•易用的调用接口(支持同步/异步 线程切换)
•稳定可靠 无性能问题
三、方案调研
观察我们日常业务代码可以发现:一次数据库查询与一次网络请求在流程上是极为相似的 都是经过构造请求 发起请求中间步骤
获取结果 处理结果等几个步骤。因此感觉可以将数据库操作以网络请求的方式进行抽象和封装 其详细对比如下表所示:
通过上述相似性的对比并综合现有ORM框架来考虑切入口 首先想到的是使用注解:
主流Room使用的是 编译时注解 更有利于性能 但在具体编码实现Processor过程中发现增删改查操作的出参和入
参处理有点过于繁琐 参考 Room实现 不太适用于本身就是一个SDK的场景 最终pass掉了。
运行时注解处理相对更简单一些 接口和参数较容易适配 处理流程也可以直接写我们熟悉的安卓原生代码 而且前面
已经有了大名鼎鼎的网络请求库Retrofit使用运行时注解实现网络请求的典型范例 因此可以依葫芦画瓢尝试实现一
下数据库增删改查操作 也是本次改造最终的实现方案。
相信大部分安卓客户端开发同学都用过Retrofit 网络请求常用库 其大概原理是:使用动态代理获取接口对应的Method
对象为入口并通过该Method对象的各种参数 注解修饰 构造出Request对象抛给okhttp做实际请求返回值则通过Conve
ter和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();
注:
•以上所有操作都支持根据具体的场景进行同步/异步调用。
•增、删、改操作的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轮测试,然后取平均值(四舍五入)
说明:
•表中第4条测试(查出全部10w条数据)差异较大相差79ms其原因是原生接口的Entity对象是直接new出来的而sponsor
内部只能通过Entity的newInstance()接口去反射创建 导致了性能差距 但平均算下来 每newInstance()创建1000个对象才
多了1ms 影响还是很小的。尝试使用Clone的方式优化但效果仍不明显
•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>这种带有泛型的类型 常用方法有:
•Type getRawType()——返回参数化类型中的原始类型 例如List<String>的原始类型为List
•Type[] getActualTypeArguments()——获取参数化类型的类型变量或是实际类型列表
如Map<Integer, String>的实际泛型列表是Integer和String。
TypeVariable:表示的是类型变量如List<T>中的T就是类型变量。
GenericArrayType:表示是数组类型且组成元素是ParameterizedType或TypeVariable 例如List<T>或T[] 常用方法有:
•Type getGenericComponentType()一个方法 它返回数组的组成元素类型。
WildcardType:表示通配符类型 例如? extends Number 和 ? super Integer。常用方法有:
•Type[] getUpperBounds()——返回类型变量的上边界。
•Type[] getLowerBounds()——返回类型变量的下边界。 |