Android 手写数据库框架

来源:互联网 发布:朗读英语的软件 编辑:程序博客网 时间:2024/05/30 23:35

前言

在Android开发中,一定会遇到数据库sqlit的操作的,如果你的项目中没有用到数据库那么说明你的项目很失败。

一般我们可以直接使用系统提供的sqlit操作完成数据库的操作,同时也可以使用现在比较多的数据库开源框架,比如GreenDAO OrmLitem等数据库框架,都是直接将对象映射到sqlit数据库的ORM框架。

在这篇文章中我们将自己动手写一个ORM框架,自定义一个属于我们自己的ORM数据库框架。

原理分析

在Android中无论我们如何对数据库进行封装,最终操作都离不开sqlit自身对数据的增删改操作,所以我们需要将这些操作封装在底层,上层只需要传入对象调用相关方法即可,不用去管底层是如何做的,包括表的创建等。

好,下面我们来看看分析的图

从图中我们也可以看出来,手写数据库框架的主要内容就在中间部分,主要的有BaseDaoFactory和BaseDao这两个类。

但是在这些之前我们还有两个地方需要关注,就是数据库表的生成。在常用的数据库框架中如GreenDAO和ORMLitem等都是通过注解来生成表和字段的,那么在我们的框架中当然也采用这种方式来完成,下面就来看看代码吧

特此声明,如果是Android studio用户,在使用该库时请关闭Instant Run功能,具体什么原因可以自己手动尝试

注解

生成表的注解

@Target(ElementType.TYPE)    @Retention(RetentionPolicy.RUNTIME)    public @interface DbTable {        String value();    }

生成字段的注解

@Target(ElementType.FIELD)    @Retention(RetentionPolicy.RUNTIME)    public @interface DbFiled {        String value() ;    }

这些注解该如何使用呢?

@DbTable("tb_common_user")    public class User {        @DbFiled("tb_name")        public String name ;        @DbFiled("tb_password")        public String password ;        @DbFiled("tb_age")        public String age ;    }

我们只需要在JavaBean类和变量上标注即可,这样就可以生成对应的表名和字段名,具体如何生成的,我们会在下面讲到,如果对注解知识不是特别了解,那就需要加强一下Java基础了哦。

既然知道了注解生成表和字段并且知道如何使用后,下面我们就来看看Dao层的代码吧

BaseDaoFactory

具体的代码如下

public class BaseDaoFactory {           /** 数据库路径 */        private String sqliteDatabasePath ;        /** 操作数据库 */        private SQLiteDatabase sqLiteDatabase ;         private static BaseDaoFactory instance = null ;        public static BaseDaoFactory getInstance(){            if(instance == null){                synchronized (BaseDaoFactory.class){                    instance = new BaseDaoFactory() ;                }            }            return instance ;        }           private BaseDaoFactory(){            //获取数据库路径            sqliteDatabasePath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/user.db" ;            //打开数据库            openDatabase();        }        /**         * 获取DataHelper         * @param clazz         BaseDao的子类字节码         * @param entityClass   要存入对象的字节码         * @param <T>         * @param <M>         * @return         */        public synchronized <T extends BaseDao<M>,M> T getDataHelper(Class<T> clazz,Class<M> entityClass){            T dao = null ;            //获取对象            try {                dao = clazz.newInstance() ;                dao.init(entityClass,sqLiteDatabase) ;            } catch (InstantiationException e) {                e.printStackTrace();            } catch (IllegalAccessException e) {                e.printStackTrace();            }               return dao;        }        /**         * 打开或创建数据库         */        private void openDatabase() {            this.sqLiteDatabase = SQLiteDatabase.openOrCreateDatabase(sqliteDatabasePath,null) ;        }       }

BaseDaoFactory代码内容不是太多,好,接下来我们就具体分析吧。

可以看出BaseDaoFactory采用单例的方式,用来生成Dao对象的。主要方法有两个openDatabase()和getDataHelper()方法,openDatabase()方法是负责获取sqliteDatabase对象的,因为sqlit底层操作需要这个对象。

getDataHelper()中只做了两件事,创建爱你Dao层对象,并且调用dao的init()方法。所以要想使用Dao我们只需要调用getDataHelper()方法传入我们想要使用的Dao,BaseDaoFactory会帮我们生成。

其中getDataHelper需要两个泛型参数,可能会让人有些费解,那我们就来看看这些泛型参数的含义

public synchronized <T extends BaseDao<M>,M> T getDataHelper(Class<T> clazz,Class<M> entityClass){        .....    }

因为在这个框架中,所有的Dao层都一个基类,就是BaseDao,所以通过

BaseDao

首先看看IBaseDao代码

IBaseDao代码如下

public interface IBaseDao<T> {        /**         * 插入一个对象到数据库         * @param entity 要插入的对象         * @return         */        public Long insert(T entity) ;        /**         * 更新         * @param entity         * @param where         * @return         */        public int update(T entity ,T where) ;        /**         * 删除         * @param where         * @return         */        public int delete(T where);        /**         * 查询         * @param where         * @return         */        public List<T> query(T where) ;        public List<T> query(T where,String orderBy,Integer startIndex,Integer limit) ;    }

BaseDao代码如下

public class BaseDao<T> implements IBaseDao<T> {        /** 持有数据库操作类的引用 */        private SQLiteDatabase database ;        /** 保证实例化一次 */        private boolean isInit = false ;        /** 持有操作数据库表所对应的Java类型 */        private Class<T> entityClass ;        /** 表名 */        private String tableName ;        /** 维护表名与成员变量的映射关系 */        private HashMap<String,Field> cacheMap ;        /** 初始化 */        protected boolean init(Class<T> entity,SQLiteDatabase sqLiteDatabase){            this.entityClass = entity ;            if(!isInit){                this.database = sqLiteDatabase ;                //判断注解是否为null                if(entity.getAnnotation(DbTable.class) == null){                    this.tableName = entity.getClass().getSimpleName() ;                }else {                    this.tableName = entity.getAnnotation(DbTable.class).value();                }                //检查数据库是否打开                if(!database.isOpen()){                    return false ;                }                //执行sql语句创建表                if(!TextUtils.isEmpty(createTable())){                    database.execSQL(createTable());                }                initCacheMap();                isInit = true ;            }            return isInit ;        }        ........    }

通过init()方法我们可以看出来,之前定义的注解这这里得到了使用,通过传入的对象获取注解和值,然后得到表名。这里还调用了两个方法,createTable和initCacheMap方法。

createTable是创建表的方法具体代码如下

/**     * 获取创建数据库表的sql     * @return     */    private String createTable(){        HashMap<String,String> columMap = new HashMap<>();        Field[] fields = entityClass.getFields();        for(Field field : fields){            field.setAccessible(true);            DbFiled dbFiled = field.getAnnotation(DbFiled.class);            if(dbFiled == null){                columMap.put(field.getName(),field.getName());            }else {                columMap.put(field.getName(),dbFiled.value());            }        }        //创建数据库语句        String sql = "create table if not exists "+ tableName + "(" ;        Set<String> keys = columMap.keySet();        StringBuilder sb = new StringBuilder() ;        for(String key : keys){            String value = columMap.get(key);            sb.append(value).append(" varchar(20)").append(",");        }        String s = sb.toString();        s = s.substring(0,s.lastIndexOf(",")) + ")" ;        //拼接sql语句        sql = sql + s ;        return sql ;    }

通过代码我们也可以看出来在createTable()方法中我们通过获取变量上的注解获取到表中的列名然后拼接成sql语句,然后调用这个sql语句创建表。

还有一个initCacheMap()方法代码如下

/** 维护映射关系 */    private void initCacheMap() {        Cursor cursor = null ;        try {            /**             * map集合中             * key  列名             * map  变量对象             *             * 主要功能是找到列名对应的变量对象,便于后续的使用等             */            cacheMap = new HashMap<>();            //1 第一步需要查询一遍表获取列名            String sql = "select * from " + this.tableName ;            cursor = database.rawQuery(sql, null);            //获取表的列名数组            String[] columnNames = cursor.getColumnNames();            //获取Field数组            Field[] columnFields = entityClass.getFields();            for (Field field : columnFields) {                field.setAccessible(true);            }            //查找对应关系            for (String colmunName : columnNames) {                Field columField = null;                for (Field field : columnFields) {                    String fieldName = null;                    //获取注解                    DbFiled dbFiled = field.getAnnotation(DbFiled.class);                    if (dbFiled != null) {                        fieldName = dbFiled.value();                    } else {                        fieldName = field.getName();                    }                    //如果找到对应表的列名对应的成员变量                    if (colmunName.equals(fieldName)) {                        columField = field;                        break;                    }                }                //找到对应关系                if (columField != null) {                    cacheMap.put(colmunName, columField);                }            }        }catch (Exception e){        }finally {            if(cursor != null)                cursor.close() ;        }    }

在initCacheMap()方法中就做了一件事,将列名和对应的变量对象存入到map集合中,在之后会使用到。

下面我们就来看看具体的数据库操作方法吧。

保存数据

首先insert方法代码如下

@Override    public Long insert(T entity) {        Map<String, String> map = getValues(entity);        ContentValues values = getContentValues(map);        long insert = database.insert(tableName, null, values);        return insert;    }

通过代码我们可以看出来getValues()方法是将对象转换成Map集合,getContentValues()方法是将map集合转换成ContentValues,得到ContentValues对象后,我们就可以直接调用database.insert()方法插入数据了。

那我们来看看getValues()方法和getContentValues()方法吧

getValues()代码如下

/** 将对象转换成map集合 */    private Map<String,String> getValues(T entity){        /**         * 集合         * key 列名也是变量上的注解值         * value 变量的具体值         */        HashMap<String,String> result = new HashMap<>() ;        Iterator<Field> fieldIterator = cacheMap.values().iterator();        //循环遍历映射表  遍历cacheMap得到列名和其对应的变量对象(cacheMap中存入的是列名和对象的映射)        while(fieldIterator.hasNext()){            //得到成员变量            Field colmunToField = fieldIterator.next();            //定义变量用于存储变量上注解的值,也就是列名            String cacheKey = null ;            //定义变量用于存储变量的具体值            String cacheValue = null ;            //获取列名            if(colmunToField.getAnnotation(DbFiled.class) != null){                cacheKey = colmunToField.getAnnotation(DbFiled.class).value();            }else {                cacheKey = colmunToField.getName();            }            try {                if(colmunToField.get(entity) == null){                    continue;                }                //得到具体的变量的值                cacheValue = colmunToField.get(entity).toString();            } catch (IllegalAccessException e) {                e.printStackTrace();            }            result.put(cacheKey,cacheValue) ;        }        return result ;    }

具体getValues()是如何将对象转换成Map集合的这里就不再多说了,代码中注释写的比较清楚,就是通过获取注解和反射获取变量的具体值。

getContentValues()方法代码如下

/**     * 将map转换成ContentValues     * @param map     * @return     */    private ContentValues getContentValues(Map<String, String> map) {        ContentValues values = new ContentValues() ;        for(String key : map.keySet()){            values.put(key,map.get(key));        }        return values;    }

这个方法就比较简单了,就是遍历map集合完成操作。

通过上面的分析基本上就可以理清楚思路了,也知道如何完成数据库表的创建和数据的保存了。接下来接看看数据的修改吧

修改数据

@Override    public int update(T entity, T where) {        int result = -1 ;        //将修改的结果转换成Map集合        Map<String, String> map = getValues(entity);        //将修改的条件转换成Map集合        Map<String, String> whereClause = getValues(where);        //得到修改的条件语句        Condition condition = new Condition(whereClause);        ContentValues contentValues = getContentValues(map);        result = database.update(tableName, contentValues, condition.getWhereClause(), condition.getWhereArgs());        return result;    }

修改代码中除了使用了前面讲到了getValues()方法和getContentVlaues()方法外还用到了Condition。

Condition代码如下

/**     * 封装修改的语句     */    class Condition {        private String whereClause ;        private String[] whereArgs ;        public Condition(Map<String, String> whereClause) {            ArrayList<String> list = new ArrayList<>() ;            StringBuilder sb = new StringBuilder() ;            sb.append("1=1") ;            for(String key : whereClause.keySet()){                String value = whereClause.get(key);                if(value != null){                    //拼接条件查询语句                    sb.append(" and ").append(key).append(" =?");                    //查询条件                    list.add(value);                }            }            this.whereClause = sb.toString() ;            this.whereArgs = list.toArray(new String[list.size()]);        }        public String getWhereClause() {            return whereClause;        }        public String[] getWhereArgs() {            return whereArgs;        }    }

Condition是一个队修改语句的封装,类中通过拼接和转换获取到修改的条件语句和参数。

删除数据

@Override    public int delete(T where) {        Map<String, String> map = getValues(where);        Condition condition = new Condition(map) ;        int result = database.delete(tableName, condition.getWhereClause(), condition.getWhereArgs());        return result;    }

删除代码比较简单,也是调用了getValues()方法将条件对象转换成Map集合,然后通过Condition将集合装换成删除的条件语句和参数。

查询数据

@Override    public List<T> query(T where) {        return query(where,null,null,null);    }    @Override    public List<T> query(T where, String orderBy, Integer startIndex, Integer limit) {        Map<String, String> map = getValues(where);        String limitStr = null ;        if(startIndex != null && limit != null){            limitStr = startIndex + " , " + limit ;        }        Condition condition = new Condition(map) ;        Cursor cursor = database.query(tableName, null, condition.getWhereClause(), condition.getWhereArgs(), null, null, orderBy, limitStr);        List<T> result = getResult(cursor,where);        return result;    }

首先上面两个方法主要的是第二个,在代码中首先根据条件获取到了cursor对象,然后通过getResult()方法和cursor得到了最终对象集合

getResult代码如下

/** 获取查询结果 */    private List<T> getResult(Cursor cursor, T where) {        List<T> list = new ArrayList<>() ;        //定义变量用于接收查询到的数据        T item ;        while(cursor.moveToNext()){            try {                //通过反射初始化对象                item = (T) where.getClass().newInstance();                //下面循环对变量名进行赋值                /**                 * cacheMap中缓存的是                 * key      列名                 * value    成员变量名                 *                 */                for(String key : cacheMap.keySet()) {                    //得到数据库表中的列名                    String columnName = key;                    //然后通过列名获取游标的位置                    int columnIndex = cursor.getColumnIndex(columnName);                    //获取到对象中的成员变量名称                    Field field = cacheMap.get(key);                    //获取成员变量的类型                    Class type = field.getType();                    //反射方式给item中的变量赋值                    if (columnIndex != -1){                        if (type == String.class) {                            field.set(item, cursor.getString(columnIndex));                        }else if(type == Double.class){                            field.set(item,cursor.getDouble(columnIndex));                        }else if(type == Integer.class){                            field.set(item,cursor.getInt(columnIndex));                        }else if(type == byte[].class){                            field.set(item,cursor.getBlob(columnIndex));                        }else{                            continue ;                        }                    }                }                //将变量存入到集合中                list.add(item);            } catch (InstantiationException e) {                e.printStackTrace();            } catch (IllegalAccessException e) {                e.printStackTrace();            }        }        return list;    }

首先我们知道数据库中查询到的内容都在cursor中,所以我们只需要遍历cursor就可以获取到我们想要的内容,因为cursor中获取到的值需要赋值给对象,所以我们手动创建了T类型的对象,因为这个对象不确定,所以我们通过泛型表示。在之前的initCacheMap()方法中我们已经获取到了对象内部的变量名和表中的列名,所以可以通过反射获取到变量的类型,并对其进行赋值。

这样就完成了对变量的赋值了,最后将对象存入到list集合中然后返回。

OK完成

使用

上面将框架的各个知识点讲完了还没有具体的使用呢,所以接下里我们就来使用我们手撸的框架

User类代码如下

@DbTable("tb_common_user")    public class User {        @DbFiled("tb_name")        public String name ;        @DbFiled("tb_password")        public String password ;        @DbFiled("tb_age")        public String age ;    }

UserDao代码如下

public class UserDao extends BaseDao<User> {    }

MainActivity代码如下

public class MainActivity extends AppCompatActivity {        @Override        protected void onCreate(Bundle savedInstanceState) {            super.onCreate(savedInstanceState);            setContentView(R.layout.activity_main);        }        //保存        public void save(View view){            Random random = new Random() ;            UserDao userDao = BaseDaoFactory.getInstance().getDataHelper(UserDao.class, User.class);            User user = new User();            user.name = "lilei" ;            user.password = "abc" ;            user.age = random.nextInt() % 2 == 0 ? "男" : "女" ;            userDao.insert(user);        }        //更新        public void update(View view){            UserDao userDao = BaseDaoFactory.getInstance().getDataHelper(UserDao.class, User.class);            //更新条件            User where = new User() ;            where.name = "lilei" ;            //更新为            User user = new User() ;            user.name = "hanmeimei" ;            userDao.update(user,where);        }        //删除        public void delete(View view){            UserDao userDao = BaseDaoFactory.getInstance().getDataHelper(UserDao.class, User.class);            //删除条件            User where = new User() ;            where.name = "hanmeimei";            userDao.delete(where);        }        //查询        public void query(View view){            UserDao userDao = BaseDaoFactory.getInstance().getDataHelper(UserDao.class, User.class);            User where = new User() ;            where.name = "lilei" ;            where.age = "女" ;            List<User> query = userDao.query(where);            for(User user : query){                System.out.println("name:"+user.name+",age:"+user.age+",password:"+user.password);            }        }    }

好了结果我就不展示了,一遍通过。

总结

通过上面的讲解,发现手写一个数据库其实也不是很难,当然这个框架有很多的不足的地方,但是至少让我们了解了如何手动撸一个自己的数据库框架,了解了数据库框架的原理。之后如果有什么想法当然可以在此基础上再添加。

最后代码地址https://github.com/studyzhxu/zhxuSqlit

QQ交流群

微信公众号:Android在路上,欢迎关注