轻量级的利用Annotation方式实现Android SQLite的框架
8月底离职之后,全职伺候老婆,整个人就浑浑噩噩,很少有时间写代码了,所以这次的博客纯属是弥补一下,减轻我这近两个月来的罪恶感。demo在很久以前就写好了,当时我的领导写了这个SQLite框架让我用的时候,也没发现多少问题,就是过了一下代码,跟他讨论了一下这个框架的功能,就草草了事了。所以当时也没有发现多少问题,当然,这次重新回过头去看,也没有发现特别严重的问题,顶多也就是细节方面的问题需要在码代码的时候注意而已。
这个框架当时他是看到GitHub上面有大量的使用注解的方式开发的框架,比较经典的就是xUtils框架的UI、资源以及事件的绑定,当然数据库的注解框架也很多,但是基本上都比较重量级,很多功能在日常开发中可能无法用上,不可否认它们在健壮性、安全性和可靠性方面肯定要由于我领导写的这个,但是我们依然有必要了解一个轻量级的sqlite框架,因为这样有助于我们更加容易的理解使用注解开发的原理和流程。
源码及DEMO地址:https://github.com/xiaoqi0716/LightSqlite 这个是源码的地址,觉得好用了可以给他star,当然也可以follow他,虽然他很少维护…
本次博客将从以下几个部分描述:
- 框架的使用
- 框架核心部分的代码分析
- 框架的不足及功能的完善
- Android SQLite的坑
LightSqlite框架的使用
我们模拟一个登录注册的需求,不妨我们就以单点登录为例,单点登录时,用户无需输入密码,端可自动完成登录过程,在开发中,我们不可能将用户的密码保存在本地,大家都知道Android系统是非常不安全的,root之后,私有目录的所有文件也都公开了,就算存入数据时,对密码进行过加密,也不能排除加密方式外泄,密钥外泄的情况发生,所以把密码直接存在本地是非常不明智的做法,那么实际在公司项目开发中是怎么做的呢?其实大部分公司使用的多是由服务端下发一个sessionToken,我们称为位密码吧,这个伪密码在特定的时候会变更,比如用户使用了密码登录,伪密码需要变,并且服务端给这个sessionToken设置了一个有效时间,时间过了之后,这个sessionToken也就失效了,还有小部分公司使用了cookie,这里我就不介绍了。
根据前面的分析,我们在设计user表时,需要考虑到的字段大概就需要有loginName、sessionToken、nickName、isLogined这些,当然我们还可以加上性别、个性签名之类的字段。那么我们在写这个JavaBean的时候就需要设计这些字段。
接下来我们先来就可以来讲它的用法了,实现我们需要些一个类继承AbstractDBHelper类,并且实现它的抽象方法
public class UserDB extends AbstractDBHelper {
// 数据库名
private final String DB_NAME = "USER_DB.db";
// 数据库版本
// [VR = 1 数据库初版]
// [VR = 2 版本号说明]
// [VR = 3 版本号说明]
// [...]
private final int DB_VERSION = 1;
public UserDB(Context context) {
super(context);
}
@Override
public String getDataBaseName() {
return DB_NAME;
}
@Override
public int getDataBaseVersion() {
return DB_VERSION;
}
@Override
public List<AbstractTable<?>> getTables() {
List<AbstractTable<?>> list = new ArrayList<>();
list.add(UserTable.getInstance());
return list;
}
}
然后创建一个Javabean:
package com.anzhi.test.testlightsqlitelib.bean;
import com.db.easydao.Column;
/**
* Created by heguowen on 2017/10/18.
*/
public class UserBean {
public static final String FIELD_S_LOGINNAME = "loginname";
public static final String FIELD_S_SEX = "sex";
public static final String FIELD_S_ISLOGING = "islogin";
public static final String FIELD_S_SESSIONTOKEN = "sessiontoken";
public static final String FIELD_S_NICKNAME = "nickname";
public UserBean(String loginName, String sessionToken, String sex) {
this.loginName = loginName;
this.sessionToken = sessionToken;
this.sex = sex;
}
//无参构造器一定要保留,下面我分析代码时我会讲原因
public UserBean() {
}
@Column(name = FIELD_S_LOGINNAME ,notNull = true,type = "varchar(20)")
private String loginName;
@Column(name = FIELD_S_SESSIONTOKEN ,notNull = true,type = "varchar(20)")
private String sessionToken;
private String personalSignature;
@Column(name = FIELD_S_SEX ,notNull = true,type = "varchar(4)")
private String sex;
@Column(name = FIELD_S_ISLOGING ,defaultVal = "1",type = "INTEGER")
private boolean isLogined;
@Column(name = FIELD_S_NICKNAME )
private String nickName;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getSessionToken() {
return sessionToken;
}
public void setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
}
public String getPersonalSignature() {
return personalSignature;
}
public void setPersonalSignature(String personalSignature) {
this.personalSignature = personalSignature;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public boolean isLogined() {
return isLogined;
}
public void setLogined(boolean logined) {
isLogined = logined;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
}
接下来创建用户表对应的类,这个类必须要继承AbstractTable,并且可以给定泛型限定。
public class UserTable extends AbstractTable<UserBean> {
private final static String TABLE_NAME = "user"; // 学生表
private UserTable() {
}
private static UserTable userTable;
public static synchronized UserTable getInstance(){
if(userTable == null){
userTable = new UserTable();
}
return userTable;
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
@Override
public String getTableName() {
return TABLE_NAME;
}
}
接着我们需要在Application里面初始化框架并且建立数据库,我们可以在application的onCreate方法里面加入以下代码:
UserDB db = new UserDB(this.getApplicationContext());
db.open(); //打开MY_DB.db数据库,程序退出不用关闭
这样,我们就已经介绍完了LightSQLite的使用了,那么我们数据库操作的增删改查这四个操作该如何完成呢?非常简单:
UserTable.getInstance().add(……); //插入
UserTable.getInstance().find(……); //查询
UserTable.getInstance().set(……); //修改
UserTable.getInstance().remove(……); //删除
框架核心部分的代码分析
上面讲了使用方法,下面我们分析以下LightSQLite的核心代码:我接下来的源码讲解主要以注释的方式
private void createTable(SQLiteDatabase db, AbstractTable<?> table) {
StringBuilder sql = new StringBuilder();
sql.append("CREATE TABLE IF NOT EXISTS ");
String tableName = table.getTableName();
sql.append(tableName).append(" (");
Class<?> tableCls = null;
// Type是Java 中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。ParameterizedType参数化类型,就是所说的泛型,(Class<?>) type[0]拿到了传入泛型的真实类型,type它是一个数组,由于我们只用到一个参数,如上面看的StudentTable类,所以只取第0个
Type t = table.getClass().getGenericSuperclass();
if (t != null && t instanceof ParameterizedType) {
Type[] type = ((ParameterizedType) t).getActualTypeArguments();
tableCls = (Class<?>) type[0];
}
//反射,得到Javabean对象的字段数组
Field[] fields = tableCls.getDeclaredFields();
for (Field field : fields) {
//设置可见性
field.setAccessible(true);
//field.isAnnotationPresent(Column.class)实际上是判断某些属性是否加上了@Column注解
if (field.isAnnotationPresent(Column.class)) {
//如果有才认为是字段相关的属性
Column ano = field.getAnnotation(Column.class);
String fieldName = ano.name();
sql.append(fieldName).append(" ");
sql.append(getFieldType(ano, field)).append(" ");
if (ano.primaryKey()) {
sql.append("PRIMARY KEY").append(" ");
}
if (ano.autoIncrement()) {
sql.append("AUTOINCREMENT").append(" ");
}
if (ano.unique()) {
sql.append("UNIQUE").append(" ");
}
if (ano.notNull()) {
sql.append("NOT NULL").append(" ");
}
if (!TextUtils.isEmpty(ano.defaultVal())) {
String fieldType = getFieldType(ano, field);
if ("TEXT".equals(fieldType)) {
sql.append("default").append(" ").append("'").append(ano.defaultVal()).append("'")
.append(" ");
} else {
sql.append("default").append(" ").append(ano.defaultVal()).append(" ");
}
}
sql.append(", ");
}
}
sql.deleteCharAt(sql.length() - 2);
sql.append(")");
LogUtils.v("---------------------LightSQLite--------------------------");
LogUtils.v(sql.toString());
LogUtils.v("----------------------------------------------------------");
try {
db.execSQL(sql.toString());
} catch (Exception e) {
e.printStackTrace();
}
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(Column.class)) {
Column an = field.getAnnotation(Column.class);
if (an.index()) {
String sqlStr = "CREATE INDEX IF NOT EXISTS " + table.getTableName() + "_" + an.name()
+ "_index ON " + table.getTableName() + "(" + an.name() + ")";
LogUtils.v("---------------------LightSQLite--------------------------");
LogUtils.v(sqlStr);
LogUtils.v("----------------------------------------------------------");
try {
db.execSQL(sqlStr);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
table.onCreateTrigger(db);
}
接下来我们看下这个自定义注解@Column是如何定义的
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String name();
String type() default "UNKNOW";
boolean primaryKey() default false;
boolean autoIncrement() default false;
boolean unique() default false;
boolean index() default false;
boolean notNull() default false;
String defaultVal() default "";
}
自定义注解的方式我这里不做讲解,我们看到Column里面全是表的约束,主键、唯一性、非空的定义,并且还可以设置默认值之类的。
我们回头看上面创建表的时候调用了一个getFieldType方法
private String getFieldType(Column column, Field field) {
if (!"UNKNOW".equalsIgnoreCase(column.type())) {
return column.type();
}
Class<?> c = field.getType();
if (c == String.class || c == char.class) {
return "TEXT";
} else if (c == int.class || c == long.class || c == byte.class || c == short.class || c == boolean.class) {
return "INTEGER";
} else if (c == float.class || c == double.class) {
return "REAL";
} else if (c == byte[].class) {
return "BLOB";
} else {
throw new RuntimeException(field.getName() + " 非基本数据类型");
}
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
这里主要做的事是根据Java的数据类型,转化为sqlite的表的数据类型存储,我们可以看到boolean类型实际上在表中是一INTEGER整型存储的。我这里单独说boolean类型 是因为后面有一个框架的bug需要指出。
框架的不足及功能的完善
上面我说到boolean类型实际上当时设计的时候是支持的,在表里面转成了整型存储,当时实际使用时,会发现:当存入一个boolean类型数据时,抛异常了:java.lang.RuntimeException: isLogined not allow boolean
我们可以去看源码
private ContentValues createContentValues(T data) throws IllegalAccessException {
ContentValues values = new ContentValues();
Field[] fields = data.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(Column.class)) {
Column an = field.getAnnotation(Column.class);
Class<?> c = field.getType();
if (c == String.class || c == char.class) {
values.put(an.name(), (String) field.get(data));
} else if (c == int.class) {
values.put(an.name(), field.getInt(data));
} else if (c == long.class) {
values.put(an.name(), field.getLong(data));
} else if (c == byte.class) {
values.put(an.name(), field.getByte(data));
} else if (c == short.class) {
values.put(an.name(), field.getShort(data));
} else if (c == float.class) {
values.put(an.name(), field.getFloat(data));
} else if (c == double.class) {
values.put(an.name(), field.getDouble(data));
} else if (c == byte[].class) {
values.put(an.name(), (byte[]) field.get(data));
} else {
throw new RuntimeException(field.getName() + " not allow " + field.getType().getName());
}
}
}
return values;
}
可以看到,异常就是这里抛出去的,这里并没有支持将boolean类型转换成int类型的逻辑代码,我们需要做一下更改:
else if (c == boolean.class) {
values.put(an.name(), field.getBoolean(data) ? 1 : 0);
}
加上一个elseif分支,使之支持boolean类型,相应的,查询的时候也是需要修改的:
private List<T> find(String[] columns, String where, String groupBy, String having, String orderBy, String limit,
String[] whereArgs) {
Cursor cur = null;
List<T> list = new ArrayList<T>();
try {
SQLiteDatabase db = getDataBase().getReadableDatabase();
cur = db.query(false, getTableName(), columns, where, whereArgs, groupBy, having, orderBy, limit);
if (cur == null) {
return list;
}
int count = cur.getCount();
cur.moveToFirst();
for (int i = 0; i < count; i++) {
Object obj = null;
Type t = getClass().getGenericSuperclass();
if (t != null && t instanceof ParameterizedType) {
Type[] type = ((ParameterizedType) t).getActualTypeArguments();
Class<?> c = (Class<?>) type[0];
try {
obj = c.newInstance();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("未找到" + c.getName() + " 默认构造方法,或构造方法非public");
}
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(Column.class)) {
Column an = field.getAnnotation(Column.class);
int index = cur.getColumnIndex(an.name());
if (index != -1) {
Class<?> fieldType = field.getType();
if (fieldType == String.class || fieldType == char.class) {
field.set(obj, cur.getString(index));
} else if (fieldType == int.class) {
field.set(obj, cur.getInt(index));
} else if (fieldType == byte.class) {
field.set(obj, (byte) cur.getInt(index));
} else if (fieldType == long.class) {
field.set(obj, cur.getLong(index));
} else if (fieldType == float.class) {
field.set(obj, cur.getFloat(index));
} else if (fieldType == double.class) {
field.set(obj, cur.getDouble(index));
} else if (c == boolean.class) {
field.set(obj, cur.getInt(index) == 1);
}*else if (fieldType == short.class) {
field.set(obj, cur.getShort(index));
} else if (fieldType == byte[].class) {
field.set(obj, (byte[]) cur.getBlob(index));
}
}
}
}
}
list.add((T) obj);
cur.moveToNext();
}
} catch (Throwable e) {
e.printStackTrace();
return null;
} finally {
if (cur != null) {
cur.close();
}
}
return list;
}
这里我直接贴出我修改之后的代码
我遇到过的Android SQLite的坑
1、无法删除字段
解决方法,新建一个新表,平移表的内容
2、无法一次添加多个字段
解决方法,一次增加一个字段
3、插入不报错,但是数据也没插进去
调用api插入不报错,但是插入不成功,我遇到过一次,忘记了怎么解决的,好像是换为SQL语句插入
当然这次主要讲LightSQLite的使用,说其它就扯远了,但是还有一点需要指出的是,由于Android安全性差的问题,我们单纯的针对数据加密存储是不够的,我们开发时,还需要对整个数据库进行加密,这里我推荐使用SQLCipher,不过使用了之后,安装包的大小会增加4兆多,有利有弊,自行权衡吧。