轻量级的利用Annotation方式实现Android SQLite的框架

本文介绍了一款轻量级的Android SQLite框架——LightSQLite,该框架通过注解简化了数据库操作。文章详细展示了如何使用该框架进行数据库的增删改查,并分析了框架的核心代码。此外,还指出了框架的一些不足之处并提供了改进方案。
摘要由CSDN通过智能技术生成

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值