移动架构03-深入解析面向对象数据库框架

移动架构03-数据库框架

一、前言

这是一个比GreenDao更小巧、更易用的面向对象数据库框架。

如果你只需要简单的功能,或者想学习数据库框架,那么这个框架正好适合你。

二、使用

先来说说这个框架怎么用?

1、基本使用

基本的使用就是增删改查了,这里只需要创建BaseDao和实体类,然后随便调方法。

实体类:

//设置表名
@DBTable("tb_person")
public class Person {
    public String name;
    public Integer age;

    public Person() {
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

调用增删改查方法:

public class SqliteActivity extends AppCompatActivity {
    ...
    /**
     * 添加数据
     *
     * @param view
     */
    public void clickInsert(View view) {
        IBaseDao baseDao = BaseDaoFactory.getInstance().getBaseDao(Person.class);
        baseDao.insert(new Person("0", 12));
    }

    /**
     * 删除数据
     *
     * @param view
     */
    public void clickDelete(View view) {
        BaseDao baseDao = BaseDaoFactory.getInstance().getBaseDao(Person.class);
        baseDao.delete(new Person());
    }

    /**
     * 修改数据
     *
     * @param view
     */
    public void clickUpdate(View view) {
        BaseDao baseDao = BaseDaoFactory.getInstance().getBaseDao(Person.class);
        Person user = new Person();
        user.age = 13;
        Person where = new Person();
        where.age = 12;
        baseDao.update(user, where);
    }

    /**
     * 查询数据
     *
     * @param view
     */
    public void clickSelect(View view) {
        IBaseDao baseDao = BaseDaoFactory.getInstance().getBaseDao(Person.class);
        List<Person> list = baseDao.query(null);
    }
}

2、分库设计

一般涉及到多个用户的情况,需要根据用户进行分库设计。

这里只需传入用户名和数据库名就可实现分库。

实体类:

/**
 * 用户名和数据库名
 */
public class DBInfo {
    //用户名
    private String userName;
    //数据库名
    private String dbName;

    /**
     * 判断值是否相等
     *
     * @param info
     * @return
     */
    public boolean equals(DBInfo info) {
        if (info == null || !this.toString().equals(info.toString())) {
            return false;
        }
        return true;
    }
}


/**
 * 用户
 */
//表名
@DBTable("tb_user")
public class BeanUser {
    @DBField("id3")
    private String id;
    private String name;
    private String password;
    ...
}

分库调用:

/**
 * 多用户登录
 *
 * @param view
 */
public void clickLogin(View view) {
    String showResult = "";
    for (int i = 1; i < 4; i++) {
        BeanUser user = new BeanUser(Integer.toString(i), "李涛" + i, "123456");
        DBInfo dbInfo = new DBInfo("litao" + i, "test" + i);
        //根据用户名创建用户目录,根据数据库名创建数据库
        BaseDao baseDao = BaseDaoFactory.getInstance(dbInfo).getBaseDao(BeanUser.class);
        baseDao.delete(null);
        baseDao.insert(user);

        showResult += BaseDaoFactory.getDBPath() + "\n" + baseDao.query().toString() + "\n";
    }
    result.setText(showResult);
}

3、数据库升级

数据库升级也是比较常用的功能。这里使用面向对象的方式实现的,所以调用非常方便,只需要创建一个新的实体类。

新的实体类:

/**
 * 用户2
 */
@DBTable("tb_user")
public class BeanUser2 {
    @DBField("id4")
    private String id;
    private String name;
    private String age;
    private String password;
    ...
}

调用:

/**
 * 升级数据库
 *
 * @param view
 */
public void clickUpdateDB(View view) {
    DBInfo dbInfo = new DBInfo("lili", "test");
    BeanUser user = new BeanUser(Integer.toString(11), "莉莉", "123456");
    BaseDao baseDao = BaseDaoFactory.getInstance(dbInfo).getBaseDao(BeanUser.class);
    baseDao.deleteTable();
    //重启数据库,清除Cursor缓存
    BaseDaoFactory.getInstance(dbInfo).restartDB();
    baseDao = BaseDaoFactory.getInstance(dbInfo).getBaseDao(BeanUser.class);
    baseDao.insert(user);

    String showResult = "";
    showResult += BaseDaoFactory.getDBPath() + "\n 旧表数据:\n" + baseDao.query().toString() + "\n";
    result.setText(showResult);

    //升级数据库
    baseDao.upgrade(dbInfo, BeanUser2.class);
    baseDao = BaseDaoFactory.getInstance(dbInfo).getBaseDao(BeanUser2.class);
    showResult += " 新表数据:\n" + baseDao.query().toString() + "\n";
    result.setText(showResult);
}

三、原理

这个框架的核心还是SQLiteDatabase,只是采用面向对象的思想进行了封装,使其更易用,同时在性能上接近系统原生。

使用SQLiteDatabase时,需要传入ContentValues对象,或者拼接sql语句。使用框架时,通过反射得到ContentValues对象和sql语句,然后再使用SQLiteDatabase操作。

另外,可以通过注解来设置用户名、数据库名、表名和字段名。

四、实现

1、自动建表

一个好用的数据库框架,都需要实现自动建表的功能。

自动建表就是通过反射和注解,拼接出create table if not EXISTS tb_user(_id INTEGER,name TEXT,password TEXT)这样的sql语句,然后调用sqLiteDatabase.execSQL(createTableSql);来完成建表。

/**
 * 基础操作类
 * 能够自动建表
 *
 * @param <T>
 */
public class BaseDao<T> implements IBaseDao<T> {
    ...
    /**
     * 生成自动建表语句
     * 并缓存字段名与成员变量,方便后续操作,减少反射次数
     *
     * @return
     */
    private String getCreateTableSql() {
        cacheMap = new HashMap<>();

        //create table if not EXISTS  tb_user(_id INTEGER,name TEXT,password TEXT)
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("create table if not EXISTS  ");
        stringBuffer.append(tableName + "(");
        //反射得到所有的成员变量
        Field[] fields = entityClass.getDeclaredFields();
        for (Field field : fields) {
            //获取字段名
            String fieldName = field.getName();
            //使用注解信息设置字段名
            if (field.getAnnotation(DBField.class) != null) {
                fieldName = field.getAnnotation(DBField.class).value();
            }
            //拿到成员的类型
            Class type = field.getType();
            //根据成员的类型设置字段的类型
            if (type == String.class) {
                stringBuffer.append(fieldName + " TEXT,");
            } else if (type == Integer.class) {
                stringBuffer.append(fieldName + " INTEGER,");
            } else if (type == Long.class) {
                stringBuffer.append(fieldName + " BIGINT,");
            } else if (type == Double.class) {
                stringBuffer.append(fieldName + " DOUBLE,");
            } else if (type == byte[].class) {
                stringBuffer.append(fieldName + " BLOB,");
            } else {
                continue;
            }

            cacheMap.put(fieldName, field);
        }
        //去掉尾部的','
        if (stringBuffer.charAt(stringBuffer.length() - 1) == ',') {
            stringBuffer.deleteCharAt(stringBuffer.length() - 1);
        }
        stringBuffer.append(")");
        return stringBuffer.toString();
    }
    ...
}

自动建表时,需要缓存字段名与成员变量,方便以后根据传入对象,获取字段名与值,然后进行数据库操作。这样做的好处就是,减少反射,提高性能。

2、增删改

数据库的增、删、改操作比较简单,只需要将实体对象封装成ContentValues,然后调用SQLiteDatabase的API。

这里以Insert操作来说明,首先是转化ContentValues:

public class BaseDao<T> implements IBaseDao<T> {
    ...
    /**
     * 将实体对象转化为ContentValues
     *
     * @param entity
     * @return
     */
    private ContentValues getContentValues(T entity) {
        if (entity == null) {
            return null;
        }
        ContentValues contentValues = new ContentValues();
        for (Map.Entry<String, Field> entry : cacheMap.entrySet()) {
            String fieldName = entry.getKey();
            Field field = entry.getValue();
            String fieldValue = null;
            try {
                field.setAccessible(true);
                Object o = field.get(entity);
                if (o != null) {
                    fieldValue = o.toString();
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            if (!TextUtils.isEmpty(fieldName) && !TextUtils.isEmpty(fieldValue)) {
                contentValues.put(fieldName, fieldValue);
            }
        }
        return contentValues;
    }
    ...
}

然后是调用insert方法:

public class BaseDao<T> implements IBaseDao<T> {
   /**
     * 添加数据
     *
     * @param entity
     * @return
     */
    @Override
    public long insert(T entity) {
//        原始写法
//        ContentValues contentValues = new ContentValues();
//        contentValues.put("_id", "1");//缓存   _id   id变量
//        contentValues.put("name", "jett");
//        sqLiteDatabase.insert(tableName, null, contentValues);

        //准备好ContentValues中需要的数据
        ContentValues values = getContentValues(entity);
        //开始插入
        long result = sqLiteDatabase.insert(tableName, null, values);
        return result;
    }
    ...
}

3、查询数据

查询操作相对复杂一些,因为参数很多,而且都是字符串。

这里我实现了简单的查询功能,首先是封装查询条件:

/**
 * 查询条件
 */
private class Condition {
    private String whereCasue;//"name=? and password=?"
    private String[] whereArgs;//new String[]{"jett"}

    public Condition(Map<String, String> whereCasue) {
        if (whereCasue == null) {
            return;
        }
        ArrayList list = new ArrayList();//whereArgs里面的内容存入list
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("1=1");
        //取所有的字段名
        Set keys = whereCasue.keySet();
        Iterator iterator = keys.iterator();
        while (iterator.hasNext()) {
            String key = (String) iterator.next();
            String value = whereCasue.get(key);
            if (value != null) {
                stringBuilder.append(" and " + key + "=?");
                list.add(value);
            }
        }
        this.whereCasue = stringBuilder.toString();
        this.whereArgs = (String[]) list.toArray(new String[list.size()]);
    }
}

然后传入参数:

public class BaseDao<T> implements IBaseDao<T> {
    /**
     * 按条件查询数据
     *
     * @param where   查询条件
     * @param orderBy 排序方式
     * @param start   返回结果起始行
     * @param end     返回结果结束行(不包括)
     * @return 查询结果
     */
    @Override
    public List<T> query(T where, String orderBy, Integer start, Integer end) {
        //sqLiteDatabase.query(tableName, null, "id=?", new String[], null, null, orderBy, "1,5");

        Map map = getValues(where);
        String limitString = null;
        if (start != null && end != null) {
            limitString = start + " , " + end;
        }
        //封装成指定格式的查询条件
        Condition condition = new Condition(map);
        Cursor cursor = sqLiteDatabase.query(tableName, null, condition.whereCasue, condition.whereArgs, null, null, orderBy, limitString);
        //定义一个用来解析游标的方法
        List<T> result = getResult(cursor, where);
        return result;
    }
}

最后对查询结果封装:

public class BaseDao<T> implements IBaseDao<T> {
   ...
   /**
     * 将游标结果转化为对象集合
     *
     * @param cursor 游标结果
     * @param bean   实体对象
     * @return 对象集合
     */
    private List<T> getResult(Cursor cursor, T bean) {
        List list = new ArrayList();
        Object item = null;
        while (cursor.moveToNext()) {
            try {
                if (bean == null) {
                    item = entityClass.newInstance();
                } else {
                    item = bean.getClass().newInstance();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            for (Map.Entry<String, Field> entry : cacheMap.entrySet()) {
                String columnName = entry.getKey();
                Field field = entry.getValue();
                Class type = field.getType();
                Integer columnIndex = cursor.getColumnIndex(columnName);
                if (columnIndex != -1) {
                    try {
                        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 == Long.class) {
                            field.set(item, cursor.getLong(columnIndex));
                        } else if (type == byte[].class) {
                            field.set(item, cursor.getBlob(columnIndex));
                        } else {
                            continue;
                        }
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
            list.add(item);
        }
        cursor.close();
        return list;
    }
    ...
}

4、分库设计

数据库分库的思想是根据登录的用户创建其专用的数据库。

/**
 * 创建指定数据库
 *
 * @param dbInfo
 */
public BaseDaoFactory(DBInfo dbInfo) {
    if (dbInfo == null) {
        //创建数据库文件的父目录
        dbPath = ToolFile.getSDDirPath(dbDefaultBasePath) + "/" + dbDefaultName;
    } else {
        String path = dbDefaultBasePath;
        String name = dbDefaultName;
        if (!Tool.isEmpty(myDBInfo.getUserName())) {
            path += "/" + myDBInfo.getUserName();
        }
        if (!Tool.isEmpty(myDBInfo.getDbName())) {
            name = myDBInfo.getDbName() + ".db";
        }
        dbPath = ToolFile.getSDDirPath(path) + "/" + name;
    }

    if (sqLiteDatabase != null) {
        sqLiteDatabase.close();
    }
    sqLiteDatabase = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
}

5、数据库升级

这里先说下一般的实现思路:

  1. 修改旧表的表名
  2. 创建新表
  3. 将旧表的数据插入新表:这里使用原生sql语句实现
  4. 删除旧表

我这里使用面向对象的思想实现:

  1. 获取旧表的所有数据
  2. 删除旧表
  3. 创建新表
  4. 将旧表的数据插入新表:这里使用面向对象的方式实现

这样做的好处就是调用方便,非常方便。

更新数据库时,需要传一个新表的实体类,根据这个类来创建新表。

/**
 * 更新数据库
 */
@Override
public <M> int upgrade(DBInfo dbInfo, Class<M> newTable) {
    if (entityClass == null || newTable == null || entityClass.getSimpleName().equals(newTable.getSimpleName())) {
        return UPGRADE_FAIL;
    }
    //查询旧表所有数据
    List<T> dataList = query();
    //删除旧表
    deleteTable();
    BaseDaoFactory baseDaoFactory = BaseDaoFactory.getInstance(dbInfo);
    //创建新表
    BaseDao<M> newDao = baseDaoFactory.getBaseDao(newTable);
    List<ContentValues> newDataList = getNewEntryList(dataList, newDao);
    newDao.insertContentValues(newDataList);
    //重启数据库,是为了清空游标缓存数据(如果不清空,那就会使用旧表的Cursor缓存)
    baseDaoFactory.restartDB();
    return UPGRADE_SUCCESS;
}

向新表插入数据时,需要将旧表的字段和新表的字段一一对应,这里通过比较变量名和注解名来实现。

/**
 * 获取新表数据
 * 如果旧表的字段名或变量名等于新表的字段名或变量名,则把这个数据插入新表
 *
 * @param dataList
 * @return
 */
private List<ContentValues> getNewEntryList(List<T> dataList, BaseDao newDao) {
    if (Tool.isEmpty(dataList)) {
        return null;
    }
    List<ContentValues> newDataList = new ArrayList<>();
    HashMap<String, Field> newCacheMap = newDao.getCacheMap();
    for (T t : dataList) {
        ContentValues values = new ContentValues();
        for (Map.Entry<String, Field> entry : cacheMap.entrySet()) {
            Field value = entry.getValue();
            //字段名
            String columnName = entry.getKey();
            //变量名
            String fieldName = value.getName();
            //变量值
            String fieldValue = null;
            try {
                Object o = value.get(t);
                if (o != null) {
                    fieldValue = o.toString();
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            //判断新、旧表的字段名或变量名是否相等
            for (Map.Entry<String, Field> newEntry : newCacheMap.entrySet()) {
                //新表字段名
                String newColumnName = newEntry.getKey();
                //新表变量名
                String newFieldName = newEntry.getValue().getName();
                if (columnName.equals(newColumnName) || columnName.equals(newFieldName) ||
                        fieldName.equals(newColumnName) || fieldName.equals(newFieldName)) {
                    values.put(newColumnName, fieldValue);
                }
            }
        }
        newDataList.add(values);
    }
    return newDataList;
}

代码已上传gitee,喜欢的同学可以下载,顺便点个赞!

上一篇:移动架构02-Tinker热修复的分析与使用

下一篇:移动架构04-深入解析动态换肤框架)

猜你喜欢

转载自blog.csdn.net/dshdhsd/article/details/80154913