使用反射+注解封装一个基于Sqlite极简的android数据库框架

数据库

  • 背景
  • GreenDao
  • 封装
    • 创建数据库
    • 对象映射表
    • 数据库操作
    • 扩展
  • 总结

背景

目前市面上已经有比较好用的数据库框架,比如GreenDao和OrmLite,而且功能也很齐全,那为什么还要多费功夫重复造轮子呢?原因无他,就为了装个B,哈哈,开个玩笑;每个框架经过了这么多版本的迭代,可以说设计的已经很周到了,考虑到了开发者能考虑到的问题,但是这也带来了一个问题,就是每个开发者使用这些框架的需求是不同的,有的需要这些功能,有的需要那些功能,这样你就没有必要把整个框架都导入到你的项目中来,特别是一些重量级的框架,对APP造成比较大的侵入性以及增大APP的体积,造成比较大的浪费,所以你就有必要根据自己的需求封装一套符合自己产品的框架,同时在开发过程中也能提高你自己的开发能力,于是就有了这篇文章

既然上面说到了GreenDao和OrmLite,那就对其做个简单的介绍吧

GreenDao

GreenDao官网
GreenDao-Github

至于GreenDao是什么,先看看官网介绍
使用反射+注解封装一个基于Sqlite极简的android数据库框架_第1张图片
简介:greendao是一个开源的Android ORM,这里的ORM全称是Object Relational Mapping,也就是对象关系映射(做java开发的使用mybatis的伙计肯定熟悉mapper),所以这里的意思就是它是一个开源的对象关系映射库;它使Sqlite数据库开发变得有趣,不知道你们感到有趣没,我是没感到有趣,哈哈,生活如此艰难,老婆孩子,还有房贷,没有趣了啊
使用反射+注解封装一个基于Sqlite极简的android数据库框架_第2张图片
继续上面的说,它减轻了开发人员处理低级数据库开发的负担,同时节省了时间,这一点我们需要承认,确实是的,因为不需要你再吭哧吭哧的写sql了,也不用关注数据库怎么创建,怎么创表了;尽管Sqlite是一个非常棒的嵌入式关系型数据库,但是编写Sql以及解析查询结果仍然是一个繁杂且耗时的工作,而Greendao通过将java对象映射到数据库表(也就是orm),将开发者从中解放出来,只需要通过简单的面向对象api进行增删改查就可以了,简单点说就是你可以傻瓜式的对数据库进行增删改查了,这样你会变得有趣吗?

简单翻译结束,再看看官网列举出的一些(吹)优(牛)点(B)

  • 最高性能表现:可能是Android中最快的ORM,官网也给出了例子证明,毕竟没有实践证明的吹牛比那是耍流氓啊,下面是官方给出的关于GreenDao,OrmLite和ActiveAndroid三种ORM解决方案的数据统计图:
    使用反射+注解封装一个基于Sqlite极简的android数据库框架_第3张图片
  • 易于使用的涵盖关系和连接的强大API
  • 最小内存消耗
  • 小于100kb的库体积以保持较低水平的构建时间,同时避免64k方法数限制
  • 支持数据库加密以保障数据安全
  • 强大的社区支持

至于使用也很简单,这里简单叙述下:

第一步是配置:

在项目根目录的build.gradle中添加greendao插件

    dependencies {
        classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
    }

在moudle的build.gradle中添加如下配置

......
apply plugin: 'org.greenrobot.greendao'

android {
	......

}

greendao {
    schemaVersion 1 //指定数据库schema版本号,迁移等操作会用到
    daoPackage 'com.mango.datasave.dao'//通过gradle插件生成的数据库相关文件的包名
    targetGenDir 'src/main/java'//生成数据库文件的目录,比如DaoMaster、DaoSession、Dao目录
//    generateTests false //设置true将自动生成单元测试。
//    targetGenDirTests 'src/main/java' //设置存储生成的单元测试的基本目录。默认为 src / androidTest / java。
}

dependencies {
    ......
    implementation 'org.greenrobot:greendao:3.2.2'
}

第二步是创建实体类:

package com.mango.datasave.entity;

import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.NotNull;
import org.greenrobot.greendao.annotation.Property;
import org.greenrobot.greendao.annotation.Transient;
import org.greenrobot.greendao.annotation.Unique;
import org.greenrobot.greendao.annotation.Generated;

/**
 * Author: mango
 * Time: 2019/8/15 16:56
 * Version:
 * Desc:
 * @Entity:告诉GreenDao该对象为实体,只有被@Entity注解的Bean类才能被dao类操作
 *
 * @Id:对象的Id,也是主键,必须使用Long类型作为Entity的Id,否则会报错。(autoincrement=true)表示主键会自增,如果false就会使用旧值 。
 *
 * @Property:可以自定义字段名,默认是使用字段名,注意外键不能使用该属性
 *
 * @NotNull:表当前列不能为空
 *
 * @Unique:该属性值必须在数据库中是唯一值
 *
 * @Generated:编译后自动生成的构造函数、方法等的注解,提示构造函数、方法等不能被修改
 *
 * @Transient:使用该注解的属性不会被存入数据库的字段中,只是作为一个普通的java类字段
 */
@Entity
public class User {
    
    @Id(autoincrement = true)
    private Long id;
    
    @NotNull   
    @Unique  
    private String name;

    @Property(nameInDb = "userage")
    private int age;

    @Transient
    private String work;
}

构造方法和get/set方法不需要手动创建,直接build工程,greendao插件会帮我们生成

这时候会生成这样三个类
在这里插入图片描述

  • DaoMaster::DaoMaster保存数据库对象(SQLiteDatabase)并管理特定模式的DAO类(而不是对象),它有静态方法来创建或删除表,它的内部类OpenHelper和DevOpenHelper是SQLiteOpenHelper实现,它们在SQLite数据库中创建模式。

  • DaoSession:管理特定模式的所有可用DAO对象,您可以使用其中一个getter方法获取该对象。DaoSession还提供了一些通用的持久性方法,如实体的插入,加载,更新,刷新和删除。

  • XXXDao:数据访问对象(DAO)持久存在并查询实体。对于每个实体,greenDAO生成对应的DAO,它具有比DaoSession更多的持久性方法,例如:count,loadAll和insertInTx。

第三步是初始化Greendao:

public class MyApplication extends Application {

    public static final String DB_NAME = "mango.db";

    private static DaoSession mDaoSession;

    @Override
    public void onCreate() {
        super.onCreate();
        initGreenDao();
    }

    private void initGreenDao() {
        DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, DB_NAME);
        SQLiteDatabase db = helper.getWritableDatabase();
        DaoMaster daoMaster = new DaoMaster(db);
        mDaoSession = daoMaster.newSession();
    }

    public static DaoSession getDaoSession() {
        return mDaoSession;
    }
}

简单使用如下

MyApplication.getDaoSession().getUserDao().insert(new User());

至于其它的就不在这里介绍了,太多了

封装

由于一时没控制住篇幅,这里就不在介绍其它数据库框架了,来讲讲如何封装一个好用的符合自己产品需求的数据库框架

想封装框架肯定对基本的Sqlite的api要会使用,这个可以参考Android开发-教你玩转Android数据存储SQLite 如何加载SD卡数据库

其实不光平时开发中有这个需求,记得之前的头头让我封装一个数据库框架,不要使用第三方的,原因是自己封装的方便调试,加日志记录方便,错误好定位修改,随时都能迭代框架功能,说的这些也确实是真的;不仅仅是这个,在面试一些中大型公司的时候,面试官也基本都会问到数据库这块,比如:如果让你自己设计一个数据框框架,你会怎么做?还有一个原因说起来可能就比较鸡汤了,虽然说现在各个功能都有现成的轮子供开发者使用,但是如果你只是用轮子,而从来不会造轮子,那真的很难提高自己的技术水平,往高级开发方向转

那问题来了,怎么做一个好用的数据库框架呢?要知道开发出来的这个框架是给开发者用的,那你在使用原生Sqlite的api进行数据库开发的时候有哪些痛点呢?是不是要自己去创建数据库和表,以及db文件存放的位置,需要编写繁杂的且容易出错的sql,解析查询出来的数据等,那这些问题就是设计这个框架的需求了

  • 自动创建数据库及表
  • 配置数据库的存放位置
  • 开发者只关注与表映射的对象,而不关注实际sql的编写
  • 完成对数据库的各种操作

整个项目结构如下图,对象映射到表通过一系列的注解完成
使用反射+注解封装一个基于Sqlite极简的android数据库框架_第4张图片

创建数据库

在Sqlite中,SQLiteDatabase代表一个数据库对象,通过其中的静态方法openOrCreateDatabase实例化一个数据库对象

    /**
     * 创建/打开数据库
     * @param dbFile 数据库文件保存位置
     */
    public void initDataBase(File dbFile){
        mDatabase = SQLiteDatabase.openOrCreateDatabase(dbFile,null);
    }

其中的File就是数据库文件,你想放在内部存储还是外部存储由这个文件路径决定,为了安全性考虑建议放到内部存储,如果数据库保存的数据偏大,可以放到外部存储的应用私有目录中,如下

    private void initMangoDao() {
        /*内部存储*/
//        File databasePath = getDatabasePath("mango.db");
        /*外部存储私有目录*/
        File databasePath = FileStorageTools.getInstance(this).getExternalStoragePrivateCache();
        File dbFile = new File(databasePath,"mango.db");
        MangoDaoFactory.getDefault().initDataBase(dbFile);
    }

对象映射表

这里主要通过注解+反射的方式将对象映射到数据库的表中,避免开发者手动编写sql创建表,其定义的注解如下

/**
 * Author: mango
 * Time: 2019/8/16 21:10
 * Version:
 * Desc: 将对象映射到某个表 只有被@Table注解的Bean类才能被dao类操作
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    /**
     * 声明表名
     * @return
     */
    String value();
}

该注解作用于类,声明实体映射的表名

/**
 * Author: mango
 * Time: 2019/8/16 21:10
 * Version:
 * Desc: 将对象属性映射到表字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldName {
    /**
     * 声明字段名
     * @return
     */
    String value();
}

该注解作用于变量,将实体属性映射到表字段

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Key {
    boolean autoincrement() default true;

}

该注解作用于变量,声明实体某个变量作为主键

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {

}

该注解作用于变量,声明实体中某个变量映射到表里的值不能为空

还有其它注解,这里就不一一说明,想了解的可以看源码

接下来就是根据实体来创建表了,主要是帮开发者编写对应的sql,通过mSqLiteDatabase.execSQL方法执行sql语句,生成对应的表

    protected boolean init(SQLiteDatabase sqLiteDatabase,Class entityClz) {
        this.mSqLiteDatabase = sqLiteDatabase;
        this.mEntityClz = entityClz;
        //拿到表名
        mTableName = mEntityClz.getAnnotation(Table.class).value();
        if (TextUtils.isEmpty(mTableName)) {
            return false;
        }
        //如果数据库没创建或者没打开 直接返回
        if (mSqLiteDatabase == null || !mSqLiteDatabase.isOpen()) {
            return false;
        }
        /**
         * 接下来创建表 
         * 只知道表名 不知道字段名,需要通过注解获取
         */
        mSqLiteDatabase.execSQL(getCreateTableSql(entityClz));
        return true;
    }
    /**
     * 获取创表语句
     * @param entityClz
     * @return
     */
    private String getCreateTableSql(Class entityClz){
        StringBuilder sb = new StringBuilder();
        sb.append("create table if not exists ")
                .append(mTableName)
                .append(" (");

        StringBuilder sbUnique = new StringBuilder();

        Field[] declaredFields = entityClz.getDeclaredFields();
        if (declaredFields.length == 0) {
            throw new TableException("该映射对象无有效字段");
        }

        for (Field field : declaredFields) {

            //如果该字段只是作为普通属性,那就跳过
            Irrelevant irr = field.getAnnotation(Irrelevant.class);
            if (irr != null) {
                continue;
            }

            /**
             * 获取 表字段名称
             * 如果没有添加该字段,默认以属性名作为表字段名
             */
            String fieldName ;
            FieldName  name = field.getAnnotation(FieldName.class);
            if (name == null) {
                fieldName = field.getName();
            } else {
                fieldName = name.value();
            }

            //获取成员变量类型
            Class type = field.getType();
            if (type == String.class) {
                sb.append(fieldName);
                sb.append(" TEXT");
                mFieldName.put(fieldName,field);
            } else if (type == int.class) {
                sb.append(fieldName);
                sb.append(" INTEGER");
                mFieldName.put(fieldName,field);
            } else if (type == long.class) {
                sb.append(fieldName);
                sb.append(" LONG");
                mFieldName.put(fieldName,field);
            } else if (type == double.class) {
                sb.append(fieldName);
                sb.append(" DOUBLE");
                mFieldName.put(fieldName,field);
            } else if (type == byte[].class) {
                sb.append(fieldName);
                //存放字节数组,比如将图片转成byte数组保存到数据库
                sb.append(" BLOB");
                mFieldName.put(fieldName,field);
            } else {
                continue;
            }

            //是否添加了主键注解
            Key key = field.getAnnotation(Key.class);
            if (key != null) {
                if (sb.toString().contains("primary")) {
                    throw new TableException("请勿添加多个主键");
                }
                if (type != int.class) {
                    throw new TableException("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY");
                }
                sb.append(" primary key");
                if (key.autoincrement()) {
                    sb.append(" autoincrement");
                }
            }
            //该字段不允许为null
            NotNull notNull = field.getAnnotation(NotNull.class);
            if (notNull != null) {
                sb.append(" not null");
            }

            //字段要求唯一性
            Unique unique = field.getAnnotation(Unique.class);
            if (unique != null) {
                sbUnique.append(fieldName+",");
            }

            sb.append(",");

        }

        //去掉最后的逗号
        if (",".equals(String.valueOf(sbUnique.charAt(sbUnique.length() - 1)))) {
            sbUnique.deleteCharAt(sbUnique.length() - 1);
        }
        sb.append(" UNIQUE ("+ sbUnique.toString() + ")");

        sb.append(")");

        Log.i("MangoDao",""+sb.toString());
        return sb.toString();
    }

在这里拿到实体的所有变量,然后通过拿到它们的注解一一过滤,比如

  • 是否有Irrelevant注解修饰,如果有,说明该字段开发者只是将其作为一个普通java bean的属性,不需要映射到表
  • 是否有FieldName注解修饰,如果有,就获取该注解值作为其映射到表中的字段值,如果没有,就使用该变量名作为字段值
  • 是否有Key注解修饰,如果有,就说明该变量映射到表中是作为唯一主键
  • 是否有NotNull 注解修饰,如果有,就说明该变量映射到表中的该字段值不能为null
  • 是否有Unique 注解修饰,如果有,说明该变量映射到表中的该字段值要求唯一

最后拼成一个创表语句,执行后就可以在数据库生成一个与该实体映射的表了

最后提供一个方法让开发者获取该dao对象

    /**
     * 根据实体对象映射表
     * @param entity
     * @param 
     * @return
     */
    public MangoDao getEntityDao(Class entity) {
        MangoDao mangoDao = mCacheEntityDao.get(entity);
        if (mangoDao != null) {
            return mangoDao;
        }

        try {
            mangoDao = MangoDao.class.newInstance();
            if(mangoDao.init(mDatabase, entity)){
                mCacheEntityDao.put(entity,mangoDao);
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
        return mangoDao;
    }

实体注解的使用如下

@Table("mango_user")
public class User {

    @FieldName("uid")
    @Key
    private int uid;

    @NotNull
    @Unique
    private String name;

    private String sex;

    @Irrelevant
    private String temp;

}

想创建不同的表,只需要在每个实体类中使用这些注解,然后调用getEntityDao方法就可以了,到这里数据库和表的创建就完成了,接下来就是增删改查操作了

数据库操作

这里以插入操作为例进行讲解

这里以SqLiteDatabase的insert(String table, String nullColumnHack, ContentValues values) 方法为例,当然了也可以拼接sql语句执行

    /**
     * 插入数据
     * @param entity 插入对象
     * @return
     */
    @Override
    public long insert(T entity) {
        if (mSqLiteDatabase == null) {
            return -1;
        }
        Map value = bindValue(entity);
        ContentValues values = getContentValues(value);
        long result = mSqLiteDatabase.insert(mTableName, null, values);
        return result;
    }

这里第一步就是要将这个实体对象的属性及对应的属性值获取到

    private Map bindValue(T entity) {

        Map valueMap = new HashMap<>();
        Iterator iterator = mFieldName.values().iterator();
        while (iterator.hasNext()) {
            //遍历每个成员变量
            Field field = iterator.next();
            Irrelevant irr = field.getAnnotation(Irrelevant.class);
            if (irr != null) {
                continue;
            }
            field.setAccessible(true);
            try {
                //拿到变量值
                Object o = field.get(entity);
                if (o == null) {
                    continue;
                }
                String value = o.toString();
                NotNull notNull = field.getAnnotation(NotNull.class);
                if (notNull != null && TextUtils.isEmpty(value)) {
                    throw new TableException("非空字段必须要赋值");
                }

                String fieldName ;
                FieldName  name = field.getAnnotation(FieldName.class);
                if (name == null) {
                    fieldName = field.getName();
                } else {
                    fieldName = name.value();
                }
                valueMap.put(fieldName,value);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }


        }
        return valueMap;
    }

然后就是遍历map将其key、value放到ContentValues中

接下来就可以通过如下方法插入一条数据

MangoDaoFactory.getDefault().getEntityDao(User.class).insert(new User(++uid,"tom"+uid,"1"));

可以看到整个封装过后,开发者只需要简单的两句代码就可以完成创建数据库、创建表、插入数据的操作了,很方便有没有

再以删除为例说明下,要知道一般删除肯定是以表的主键为条件进行删除一行记录,那我们就需要拿到实体中与主键映射的属性及对应的值来删除记录


    @Override
    public long delete(T entity) {
        if (mSqLiteDatabase == null) {
            return -1;
        }
        String key = getKey();
        if (TextUtils.isEmpty(key)) {
            throw new TableException("未找到对应的主键");
        }
        String value = getValue(entity,key);
        if (TextUtils.isEmpty(value)) {
            throw new TableException("主键对应的值不能为null");
        }
        return mSqLiteDatabase.delete(mTableName,key + " = ?",new String[]{value});
    }
   private String getKey(){
        String keyValue = null;
        Iterator iterator = mFieldName.values().iterator();
        while (iterator.hasNext()) {
            Field field = iterator.next();
            Key key = field.getAnnotation(Key.class);
            if (key != null) {
                FieldName  name = field.getAnnotation(FieldName.class);
                if (name == null) {
                    keyValue = field.getName();
                } else {
                    keyValue = name.value();
                }
                break;
            }
        }
        return keyValue;
    }

    private String getValue(T entity,String key){
        Field field = mFieldName.get(key);
        field.setAccessible(true);
        try {
            Object o = field.get(entity);
            String value = o.toString();
            return value;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

至于其它操作由于篇幅原因就不在这里叙述了,感兴趣的可以查看源码

扩展

当然了,如果你有其它操作在MangoDao中没有,你也可以通过继承它扩展功能,比如

public class MyUserDao extends MangoDao {

    public long count(){
        String sql = "select count(1) from " + mTableName;
        Cursor cursor = mSqLiteDatabase.rawQuery(sql, null);
        cursor.moveToFirst();
        //获得某一列的长度
        long count = cursor.getLong(0);
        cursor.close();
        return count;
    }
}
MyUserDao myUserDao = (MyUserDao) MangoDaoFactory.getDefault().getEntityDao(MyUserDao.class, User.class);
Log.e("MainActivity","myUserDao="+myUserDao.count());

这样在不需要开发者创表以及其它繁杂操作的情况下扩展自己的功能

源码:MangoDataSave

总结

通过封装后,同样可以通过简单的调用实现相同的操作,同时更加精简,因为有的时候你在引入第三方框架的时候还是需要考虑下它的方法数的,毕竟有个64k的限制;同时也能提高你的代码能力,架构设计能力等

相比于GreenDao,我这里使用的是运行时注解,就必须在运行时结合反射才能实现功能,不像它是使用编译时注解在编译期生成相应的Java源代码;这样会导致效率会有一些牺牲,编译时注解的开发稍微麻烦点,如果大家有兴趣可以参考仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术,看看编译时注解如何工作

你可能感兴趣的:(【Android常用开发】)