移动架构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 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
*/
public class BaseDao implements IBaseDao {
...
/**
* 生成自动建表语句
* 并缓存字段名与成员变量,方便后续操作,减少反射次数
*
* @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 implements IBaseDao {
...
/**
* 将实体对象转化为ContentValues
*
* @param entity
* @return
*/
private ContentValues getContentValues(T entity) {
if (entity == null) {
return null;
}
ContentValues contentValues = new ContentValues();
for (Map.Entry 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 implements IBaseDao {
/**
* 添加数据
*
* @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 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 implements IBaseDao {
/**
* 按条件查询数据
*
* @param where 查询条件
* @param orderBy 排序方式
* @param start 返回结果起始行
* @param end 返回结果结束行(不包括)
* @return 查询结果
*/
@Override
public List 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 result = getResult(cursor, where);
return result;
}
}
最后对查询结果封装:
public class BaseDao implements IBaseDao {
...
/**
* 将游标结果转化为对象集合
*
* @param cursor 游标结果
* @param bean 实体对象
* @return 对象集合
*/
private List 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 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、数据库升级
这里先说下一般的实现思路:
- 修改旧表的表名
- 创建新表
- 将旧表的数据插入新表:这里使用原生sql语句实现
- 删除旧表
我这里使用面向对象的思想实现:
- 获取旧表的所有数据
- 删除旧表
- 创建新表
- 将旧表的数据插入新表:这里使用面向对象的方式实现
这样做的好处就是调用方便,非常方便。
更新数据库时,需要传一个新表的实体类,根据这个类来创建新表。
/**
* 更新数据库
*/
@Override
public int upgrade(DBInfo dbInfo, Class newTable) {
if (entityClass == null || newTable == null || entityClass.getSimpleName().equals(newTable.getSimpleName())) {
return UPGRADE_FAIL;
}
//查询旧表所有数据
List dataList = query();
//删除旧表
deleteTable();
BaseDaoFactory baseDaoFactory = BaseDaoFactory.getInstance(dbInfo);
//创建新表
BaseDao newDao = baseDaoFactory.getBaseDao(newTable);
List newDataList = getNewEntryList(dataList, newDao);
newDao.insertContentValues(newDataList);
//重启数据库,是为了清空游标缓存数据(如果不清空,那就会使用旧表的Cursor缓存)
baseDaoFactory.restartDB();
return UPGRADE_SUCCESS;
}
向新表插入数据时,需要将旧表的字段和新表的字段一一对应,这里通过比较变量名和注解名来实现。
/**
* 获取新表数据
* 如果旧表的字段名或变量名等于新表的字段名或变量名,则把这个数据插入新表
*
* @param dataList
* @return
*/
private List getNewEntryList(List dataList, BaseDao newDao) {
if (Tool.isEmpty(dataList)) {
return null;
}
List newDataList = new ArrayList<>();
HashMap newCacheMap = newDao.getCacheMap();
for (T t : dataList) {
ContentValues values = new ContentValues();
for (Map.Entry 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 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;
}
最后
代码地址:https://gitee.com/yanhuo2008/Common/tree/master/ToolSqlite
移动架构专题:https://www.jianshu.com/nb/25128604
喜欢请点赞,谢谢!