轻量级的利用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兆多,有利有弊,自行权衡吧。

阅读全文
0 0