Android 手写数据库框架

Posted study_zhxu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 手写数据库框架相关的知识,希望对你有一定的参考价值。

前言

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在路上,欢迎关注

以上是关于Android 手写数据库框架的主要内容,如果未能解决你的问题,请参考以下文章

2021最新Android开发者学习路线,源码+原理+手写框架

2021最新Android开发者学习路线,源码+原理+手写框架

2021最新Android开发者学习路线,源码+原理+手写框架

2021最新Android开发者学习路线,源码+原理+手写框架

带你手写基于 Spring 的可插拔式 RPC 框架介绍

框架手写系列---RxJava之从零开始