Thanks to visit codestin.com
Credit goes to github.com

Skip to content

studyzhxu/zhxuSqlit

Repository files navigation

zhxuSqlit

自己动手撸一个数据库框架

#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,所以通过

<T extends BaseDao

限定了T的类型,就是必须是BaseDao的基类,除了泛型T还有一个泛型M,M代表的是我们需要存入数据库的对象,比如上面讲到的User对象。

这里提到了BaseDao,那我们就来看看其中的具体方法吧,首先来看看init()方法,因为在BaseDaoFactory中调用了这个方法。

##BaseDao 首先看看IBaseDao代码

IBaseDao代码如下 public interface IBaseDao { /** * 插入一个对象到数据库 * @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在路上,欢迎关注

About

自己动手撸一个数据库框架

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages