轻量级的利用Annotation方式实现Android SQLite的框架
来源:互联网 发布:银行软件开发必备知识 编辑:程序博客网 时间:2024/06/16 07:23
轻量级的利用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兆多,有利有弊,自行权衡吧。
- 轻量级的利用Annotation方式实现Android SQLite的框架
- 利用Annotation实现Android sqlite框架
- Android 轻量级sqlite orm 框架
- Android数据库框架——GreenDao轻量级的对象关系映射框架,永久告别sqlite
- Android数据库框架——GreenDao轻量级的对象关系映射框架,永久告别sqlite
- Android数据库框架——GreenDao轻量级的对象关系映射框架,永久告别sqlite
- android下的轻量级Sqlite创建数据库
- android轻量级数据库SQlite的工具类
- 快速&轻量级的 Android SQLite ORM 映射框架,尽可能的简化数据库操作。
- 轻量级的ORM框架Peewee访问sqlite数据库
- GreenDao轻量级的对象关系映射框架,永久告别sqlite
- 利用SpringMVC实现基于Http和Json的轻量级RPC框架
- [Android] SharedPreferences(轻量级的存储方式)
- [Android] SharedPreferences(轻量级的存储方式)
- Android操作SQLite轻量级的的ORM工具
- android 超轻量级的ORM框架
- XDroid 轻量级的Android快速开发框架
- 利用 annotation 命令实现图形的标注
- Guava学习——Objects类
- Tomcat插件启动
- 高软实验五
- SSL/TLS安全之——中间人攻击(MITM)浅析
- sql_add_drop_set.html
- 轻量级的利用Annotation方式实现Android SQLite的框架
- 数组,方法,ref和out关键字,字符串的常用方法 strin与StringBuilder
- Wannafly模拟赛4 A Laptop (RMQ)
- AlarmManager-闹钟服务
- Linux(CentOS6.5)安装MySQL5.5
- 中文乱码问题解决方案
- 支付宝支付总结
- linux实验--添加硬盘
- 定时脚本任务列子(crontab)